From ef74293b14192b1997043f5caad7017f26ff22d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 09:57:41 +0000 Subject: [PATCH 01/18] MadMatrix: branch-free helicities and single-helicity evaluation via UMAMI_IN_HELICITY_INDEX External wavefunctions (helas.h and the FD-gauge helas_fd.h): the helicity is a fptype_amp_sv (one value per SIMD lane / GPU thread) instead of an int, and every helicity branch or helicity-indexed array lookup is replaced by branchless arithmetic on the truth values tp (nh == +1) / tm (nh == -1), or |hel| = hel*hel and hel0 = 1 - |hel| for vectors. The generated calls pass hel_sv[ipar], which calculate_jamps builds per lane from either one helicity (the good-helicity loop) or a per-event helicity array. sigmaKin (cpu, simd, gpu) takes an optional per-event helicity array: with it the helicity loop becomes a single evaluation in which the events of a SIMD page / GPU warp carry different helicities in lock-step; the colour sum, colour and channel choice run on that one helicity (the GPU jamp buffer stride is now passed explicitly instead of read from dcNGoodHel). Without it everything is as before. UMAMI: UMAMI_IN_HELICITY_INDEX (0-based index into the full helicity table) is supported on all backends; the |M|^2 returned is that helicity's contribution to the helicity-summed |M|^2, and UMAMI_OUT_HELICITY_INDEX echoes it. CPU rejects an out-of-range index with UMAMI_ERROR; the GPU (asynchronous) returns |M|^2 = 0 and helicity -1 for that event. check_sa gets --hel-index, the GPU CI a helicity_index check (sum over helicities, GPU vs CPU per helicity, mixed helicities per page); the FD-gauge IOTest reference is updated and a unit test checks the per-lane helicity in the generated calls. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01RGVSLcRfpx5dH8ovLoLWJ2 --- .github/gpu_ci/madmatrix_checks.py | 75 +++- aloha/template_files/madmatrix/helas.h | 382 ++++++++-------- aloha/template_files/madmatrix/helas_fd.h | 423 ++++++++---------- .../madmatrix/backend/common/Density.cc | 2 +- .../madmatrix/backend/cpu/SigmaKin.cc | 85 +++- .../madmatrix/backend/cpu/SigmaKin.h | 9 +- .../madmatrix/backend/cpu/umami.cc | 27 +- .../madmatrix/backend/gpu/SigmaKin.cc | 68 ++- .../madmatrix/backend/gpu/SigmaKin.h | 6 +- .../madmatrix/backend/gpu/umami.cc | 49 +- .../madmatrix/backend/simd/SigmaKin.cc | 91 ++-- .../madmatrix/backend/simd/SigmaKin.h | 9 +- .../madmatrix/backend/simd/umami.cc | 27 +- .../template_files/madmatrix/check_sa.cc | 28 +- madmatrix/model_handling.py | 4 +- .../%FD_madmatrix%src%HelAmps_sm.h | 423 ++++++++---------- .../iolibs/test_madmatrix_writer.py | 9 + 17 files changed, 950 insertions(+), 767 deletions(-) diff --git a/.github/gpu_ci/madmatrix_checks.py b/.github/gpu_ci/madmatrix_checks.py index 797d9f78fc..fc11077bb4 100644 --- a/.github/gpu_ci/madmatrix_checks.py +++ b/.github/gpu_ci/madmatrix_checks.py @@ -26,6 +26,13 @@ synchronise with the default stream) gives the same |M|^2 and helicities as on the default stream: every memset, kernel and copy of the matrix element must go to the stream umami is given, the helicity filtering of the first call included. +4. helicity_index: ``g g > t t~ z`` (massless and massive vectors, a longitudinal Z, + massive fermions) evaluated for one given helicity per event (UMAMI_IN_HELICITY_INDEX, + --hel-index), on both backends: the |M|^2 of the ncomb helicities, each given to every + event, add up to the helicity-summed |M|^2, the GPU ones equal the CPU ones helicity by + helicity, and with a random helicity per event (the threads of a warp, or the lanes of + a SIMD vector, on different helicities in lock-step) every event gets the |M|^2 of its + helicity and returns it as the chosen helicity. Any CPU backend but scalar (simd_128, ...) works too, to try the script without a GPU; check 3 is then skipped. Writes summary.txt (key=value lines) in the current directory; @@ -46,7 +53,7 @@ GPU_BACKENDS = ('cuda', 'hip') CPU_BACKEND = 'scalar' -CHECKS = ('mssm_gpu_vs_cpu', 'zero_me', 'nonblocking_stream') +CHECKS = ('mssm_gpu_vs_cpu', 'zero_me', 'nonblocking_stream', 'helicity_index') results = {} @@ -66,7 +73,7 @@ def run(cmd, cwd=None, log=None): def write(path, typecode, values): - """A raw binary file of check_sa.exe: 'd' double, 'I' 32-bit unsigned int.""" + """A raw binary file of check_sa.exe: 'd' double, 'I'/'i' 32-bit unsigned/signed int.""" with open(path, 'wb') as f: array.array(typecode, values).tofile(f) return os.path.abspath(path) @@ -267,6 +274,70 @@ def check_nonblocking_stream(backend, repo, ncalls=4): '%d calls of %d events identical to the default stream' % (ncalls, nevt // ncalls)) +# --------------------------------------------------------------------------- +# 4. one given helicity per event +# --------------------------------------------------------------------------- + +def check_helicity_index(backend, repo, nevt=512): + section('g g > t t~ z, one given helicity per event (%s, %s)' % (backend, CPU_BACKEND)) + name = 'ggttxz_hel' + (proc_dir,) = output(repo, name, 'sm', 'g g > t t~ z', (CPU_BACKEND, backend)) + data = open(os.path.join(proc_dir, 'ProcessData.h')).read() + ncomb = int(re.search(r'constexpr int ncomb = (\d+);', data).group(1)) + batch = [1, nevt, 1] + momenta = dump(name, 'momenta') + check_sa(proc_dir, CPU_BACKEND, ['--energy', 2000, '--dump-momenta', momenta] + batch, + name + '_rambo.log') + rng = random.Random(17) + mixed = [rng.randrange(ncomb) for _ in range(nevt)] + problems, details = [], [] + perhel = {} + for run_backend in (CPU_BACKEND, backend): + prefix = '%s_%s' % (name, run_backend) + check_sa(proc_dir, run_backend, ['--momenta', momenta, '--dump-me', dump(prefix, 'me')] + batch, + prefix + '.log') + summed = read(dump(prefix, 'me'), 'd') + perhel[run_backend] = [] + for ihel in range(ncomb): + hprefix = '%s_h%d' % (prefix, ihel) + hels = write(hprefix + '_in.bin', 'i', [ihel] * nevt) + check_sa(proc_dir, run_backend, ['--momenta', momenta, '--hel-index', hels, + '--dump-me', dump(hprefix, 'me'), + '--dump-hel', dump(hprefix, 'hel')] + batch, + hprefix + '.log') + perhel[run_backend].append(read(dump(hprefix, 'me'), 'd')) + if set(read(dump(hprefix, 'hel'), 'i')) != {ihel}: + problems.append('%s: helicity %d not returned as the chosen helicity' % (run_backend, ihel)) + total = [sum(perhel[run_backend][ihel][ievt] for ihel in range(ncomb)) for ievt in range(nevt)] + rels = [abs(a - b) / abs(b) if b != 0 else float('inf') for a, b in zip(total, summed)] + bad = [ievt for ievt, rel in enumerate(rels) if not rel < 1e-10] + if bad or len(summed) != nevt: + problems.append('%s: the sum over the helicities differs from the helicity sum in %d of %d events' + % (run_backend, len(bad), nevt)) + # a random helicity per event, the same events + mprefix = '%s_mixed' % prefix + hels = write(mprefix + '_in.bin', 'i', mixed) + check_sa(proc_dir, run_backend, ['--momenta', momenta, '--hel-index', hels, + '--dump-me', dump(mprefix, 'me'), + '--dump-hel', dump(mprefix, 'hel')] + batch, + mprefix + '.log') + me, hel = read(dump(mprefix, 'me'), 'd'), read(dump(mprefix, 'hel'), 'i') + expected = [perhel[run_backend][ihel][ievt] for ievt, ihel in enumerate(mixed)] + bad = [ievt for ievt, (a, b) in enumerate(zip(me, expected)) if not abs(a - b) <= 1e-12 * abs(b)] + if bad or list(hel) != mixed: + problems.append('%s: a random helicity per event: %d |M|^2 differ from that helicity\'s, ' + '%d helicities not returned' % (run_backend, len(bad), + sum(1 for a, b in zip(hel, mixed) if a != b))) + details.append('%s: max rel diff of the helicity sum %.1e' % (run_backend, max(rels, default=0.))) + print(' ' + details[-1]) + bad = sum(1 for ihel in range(ncomb) for a, b in zip(perhel[backend][ihel], perhel[CPU_BACKEND][ihel]) + if not abs(a - b) <= 1e-9 * max(abs(b), 1e-300)) + if bad: + problems.append('%d (helicity, event) |M|^2 differ between %s and %s' % (bad, backend, CPU_BACKEND)) + check('helicity_index', not problems, '; '.join(problems[:3]) or + '%d helicities x %d events; %s' % (ncomb, nevt, '; '.join(details))) + + def main(): parser = argparse.ArgumentParser(description=__doc__.split('\n')[0]) parser.add_argument('--repo', required=True) diff --git a/aloha/template_files/madmatrix/helas.h b/aloha/template_files/madmatrix/helas.h index a243ce0d96..f33d986f1d 100644 --- a/aloha/template_files/madmatrix/helas.h +++ b/aloha/template_files/madmatrix/helas.h @@ -26,7 +26,7 @@ __host__ __device__ INLINE void ixxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp fmass, // input: fermion mass - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: aloha objects @@ -41,7 +41,7 @@ __host__ __device__ INLINE void ipzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fi, // output: wavefunctions @@ -56,7 +56,7 @@ __host__ __device__ INLINE void imzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fi, // output: wavefunctions @@ -71,7 +71,7 @@ __host__ __device__ INLINE void ixzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fi, // output: wavefunctions @@ -85,7 +85,7 @@ __host__ __device__ INLINE void vxxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp vmass, // input: vector boson mass - const int nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson) + const fptype_amp_sv nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson, per SIMD lane/GPU thread) const int nsv, // input: +1 (final) or -1 (initial) const int flv, // input: flavor index ALOHAOBJ & vc, // output: wavefunctions @@ -113,7 +113,7 @@ __host__ __device__ INLINE void oxxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp fmass, // input: fermion mass - const int nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson) + const fptype_amp_sv nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fo, // output: wavefunctions @@ -128,7 +128,7 @@ __host__ __device__ INLINE void opzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fo, // output: wavefunctions @@ -143,7 +143,7 @@ __host__ __device__ INLINE void omzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fo, // output: wavefunctions @@ -157,7 +157,7 @@ __host__ __device__ INLINE void oxzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fo, // output: wavefunctions @@ -171,7 +171,7 @@ __host__ __device__ void ixxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp fmass, // input: fermion mass - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: wavefunctions @@ -196,7 +196,12 @@ fi.pvec[2] = -pvec2_ * static_cast(nsf); fi.pvec[3] = -pvec3_ * static_cast(nsf); fi.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; if( fmass != 0. ) { #ifndef MGONGPU_CPPSIMD @@ -205,11 +210,9 @@ volatile fptype_amp_sv p2 = pvec1 * pvec1 + pvec2 * pvec2 + pvec3 * pvec3; // volatile fixes #736 const fptype_amp_sv pp = fpmin( pvec0, fpsqrt( p2 ) ); #endif - // In C++ ixxxxx, use a single ip/im numbering that is valid both for pp==0 and pp>0, which have two numbering schemes in Fortran ixxxxx: - // for pp==0, Fortran sqm(0:1) has indexes 0,1 as in C++; but for Fortran pp>0, omega(2) has indexes 1,2 and not 0,1 - // NB: this is only possible in ixxxx, but in oxxxxx two different numbering schemes must be used - const int ip = ( 1 + nh ) / 2; // NB: same as in Fortran pp==0, differs from Fortran pp>0, which is (3+nh)/2 because omega(2) has indexes 1,2 - const int im = ( 1 - nh ) / 2; // NB: same as in Fortran pp==0, differs from Fortran pp>0, which is (3-nh)/2 because omega(2) has indexes 1,2 + // The Fortran ip/im indices ( 1 + nh ) / 2 and ( 1 - nh ) / 2 (for pp > 0, ( 3 +- nh ) / 2 as omega(2) has + // indexes 1,2) are the truth values tp and tm: an array element x[ip] is tm * x[0] + tp * x[1], and + // ip * x[ip] is just tp * x[1] (and likewise for im) #ifndef MGONGPU_CPPSIMD if( pp == 0. ) { @@ -217,10 +220,10 @@ fptype_amp sqm[2] = { fpsqrt( std::abs( fmass ) ), 0. }; // possibility of negative fermion masses //sqm[1] = ( fmass < 0. ? -abs( sqm[0] ) : abs( sqm[0] ) ); // AV: why abs here? sqm[1] = ( fmass < 0. ? -sqm[0] : sqm[0] ); // AV: removed an abs here - w[0] = cxmake( ip * sqm[ip], 0 ); - w[1] = cxmake( im * nsf * sqm[ip], 0 ); - w[2] = cxmake( ip * nsf * sqm[im], 0 ); - w[3] = cxmake( im * sqm[im], 0 ); + w[0] = cxmake( tp * sqm[1], 0 ); + w[1] = cxmake( tm * nsf * sqm[0], 0 ); + w[2] = cxmake( tp * nsf * sqm[0], 0 ); + w[3] = cxmake( tm * sqm[1], 0 ); } else { @@ -228,30 +231,32 @@ fptype_amp( 1 + nsf - ( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; fptype_amp omega[2] = { fpsqrt( pvec0 + pp ), 0. }; omega[1] = fmass / omega[0]; - const fptype_amp sfomega[2] = { sf[0] * omega[ip], sf[1] * omega[im] }; + const fptype_amp sfomega[2] = { sf[0] * ( tm * omega[0] + tp * omega[1] ), sf[1] * ( tp * omega[0] + tm * omega[1] ) }; const fptype_amp pp3 = fpmax( fpternary( fpsignbit( pvec3 ), ( pvec1 * pvec1 + pvec2 * pvec2 ) / ( pp - pvec3 ), pp + pvec3 ), 0. ); const cxtype_amp chi[2] = { cxmake( fpsqrt( pp3 * (fptype_amp)0.5 / pp ), 0. ), ( pp3 == 0. ? cxmake( -nh, 0. ) : cxmake( nh * pvec1, pvec2 ) / fpsqrt( (fptype_amp)2. * pp * pp3 ) ) }; - w[0] = sfomega[0] * chi[im]; - w[1] = sfomega[0] * chi[ip]; - w[2] = sfomega[1] * chi[im]; - w[3] = sfomega[1] * chi[ip]; + const cxtype_amp chi_im = tp * chi[0] + tm * chi[1]; // chi[im] + const cxtype_amp chi_ip = tm * chi[0] + tp * chi[1]; // chi[ip] + w[0] = sfomega[0] * chi_im; + w[1] = sfomega[0] * chi_ip; + w[2] = sfomega[1] * chi_im; + w[3] = sfomega[1] * chi_ip; } #else // Branch A: pp == 0. // NB: Do not use "abs" for floats! It returns an integer with no build warning! Use std::abs! fptype_amp sqm[2] = { fpsqrt( std::abs( fmass ) ), 0 }; // possibility of negative fermion masses (NB: SCALAR!) sqm[1] = ( fmass < 0 ? -sqm[0] : sqm[0] ); // AV: removed an abs here (as above) - const cxtype_amp fiA_2 = ip * sqm[ip]; // scalar cxtype: real part initialised from fptype, imag part = 0 - const cxtype_amp fiA_3 = im * nsf * sqm[ip]; // scalar cxtype: real part initialised from fptype, imag part = 0 - const cxtype_amp fiA_4 = ip * nsf * sqm[im]; // scalar cxtype: real part initialised from fptype, imag part = 0 - const cxtype_amp fiA_5 = im * sqm[im]; // scalar cxtype: real part initialised from fptype, imag part = 0 + const cxtype_amp_v fiA_2 = cxmake( tp * sqm[1], 0 ); + const cxtype_amp_v fiA_3 = cxmake( tm * (fptype_amp)nsf * sqm[0], 0 ); + const cxtype_amp_v fiA_4 = cxmake( tp * (fptype_amp)nsf * sqm[0], 0 ); + const cxtype_amp_v fiA_5 = cxmake( tm * sqm[1], 0 ); // Branch B: pp != 0. - const fptype_amp sf[2] = { fptype_amp( 1 + nsf + ( 1 - nsf ) * nh ) * (fptype_amp)0.5, - fptype_amp( 1 + nsf - ( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; + const fptype_amp_v sf[2] = { ( (fptype_amp)( 1 + nsf ) + (fptype_amp)( 1 - nsf ) * nh ) * (fptype_amp)0.5, + ( (fptype_amp)( 1 + nsf ) - (fptype_amp)( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; fptype_v omega[2] = { fpsqrt( pvec0 + pp ), 0 }; omega[1] = fmass / omega[0]; - const fptype_amp_v sfomega[2] = { sf[0] * omega[ip], sf[1] * omega[im] }; + const fptype_amp_v sfomega[2] = { sf[0] * ( tm * omega[0] + tp * omega[1] ), sf[1] * ( tp * omega[0] + tm * omega[1] ) }; const fptype_amp_v pp3 = fpmax( fpternary( fpsignbit( pvec3 ), ( pvec1 * pvec1 + pvec2 * pvec2 ) / ( pp - pvec3 ), pp + pvec3 ), 0 ); volatile fptype_amp_v ppDENOM = fpternary( pp != 0, pp, 1. ); // hack: ppDENOM[ieppV]=1 if pp[ieppV]==0 volatile fptype_amp_v pp3DENOM = fpternary( pp3 != 0, pp3, 1. ); // hack: pp3DENOM[ieppV]=1 if pp3[ieppV]==0 @@ -259,11 +264,13 @@ const cxtype_amp_v chi[2] = { cxmake( fpsqrt( chi0r2 ), 0 ), // hack: dummy[ieppV] is not used if pp[ieppV]==0 cxternary( ( pp3 == 0. ), cxmake( -nh, 0 ), - cxmake( (fptype_amp)nh * pvec1, pvec2 ) / fpsqrt( (fptype_amp)2. * ppDENOM * pp3DENOM ) ) }; // hack: dummy[ieppV] is not used if pp[ieppV]==0 - const cxtype_amp_v fiB_2 = sfomega[0] * chi[im]; - const cxtype_amp_v fiB_3 = sfomega[0] * chi[ip]; - const cxtype_amp_v fiB_4 = sfomega[1] * chi[im]; - const cxtype_amp_v fiB_5 = sfomega[1] * chi[ip]; + cxmake( nh * pvec1, pvec2 ) / fpsqrt( (fptype_amp)2. * ppDENOM * pp3DENOM ) ) }; // hack: dummy[ieppV] is not used if pp[ieppV]==0 + const cxtype_amp_v chi_im = tp * chi[0] + tm * chi[1]; // chi[im] + const cxtype_amp_v chi_ip = tm * chi[0] + tp * chi[1]; // chi[ip] + const cxtype_amp_v fiB_2 = sfomega[0] * chi_im; + const cxtype_amp_v fiB_3 = sfomega[0] * chi_ip; + const cxtype_amp_v fiB_4 = sfomega[1] * chi_im; + const cxtype_amp_v fiB_5 = sfomega[1] * chi_ip; // Choose between the results from branch A and branch B const bool_v mask = ( pp == 0. ); w[0] = cxternary( mask, fiA_2, fiB_2 ); @@ -289,8 +296,8 @@ volatile fptype_amp_sv sqp0p3DENOM = fpternary( sqp0p3 != 0, (fptype_sv)sqp0p3, 1. ); // hack: dummy sqp0p3DENOM[ieppV]=1 if sqp0p3[ieppV]==0 cxtype_sv chi[2] = { cxmake( (fptype_v)sqp0p3, 0. ), cxternary( sqp0p3 == 0, - cxmake( -(fptype_amp)nhel * fpsqrt( (fptype_amp)2. * pvec0 ), 0. ), - cxmake( (fptype_amp)nh * pvec1, pvec2 ) / (const fptype_v)sqp0p3DENOM ) }; // hack: dummy[ieppV] is not used if sqp0p3[ieppV]==0 + cxmake( -nhel * fpsqrt( (fptype_amp)2. * pvec0 ), 0. ), + cxmake( nh * pvec1, pvec2 ) / (const fptype_v)sqp0p3DENOM ) }; // hack: dummy[ieppV] is not used if sqp0p3[ieppV]==0 #else // pvec0+pvec3 is a cancelling difference of two ~|p| numbers for a // backward-moving massless fermion, and everything below divides by it. @@ -303,22 +310,12 @@ fptype_sv{ 0 }, fpsqrt( p0p3stable ) * (fptype_amp)nsf ); const cxtype_amp_sv chi[2] = { cxmake( sqp0p3, 0. ), - ( sqp0p3 == 0. ? cxmake( -(fptype_amp)nhel * fpsqrt( (fptype_amp)2. * pvec0 ), 0. ) : cxmake( (fptype_amp)nh * pvec1, pvec2 ) / sqp0p3 ) }; + ( sqp0p3 == 0. ? cxmake( -nhel * fpsqrt( (fptype_amp)2. * pvec0 ), 0. ) : cxmake( nh * pvec1, pvec2 ) / sqp0p3 ) }; #endif - if( nh == 1 ) - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi[0]; - w[3] = chi[1]; - } - else - { - w[0] = chi[1]; - w[1] = chi[0]; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } + w[0] = tm * chi[1]; + w[1] = tm * chi[0]; + w[2] = tp * chi[0]; + w[3] = tp * chi[1]; } mgDebug( 1, __FUNCTION__ ); return; @@ -332,7 +329,7 @@ __host__ __device__ void ipzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: wavefunctions @@ -347,20 +344,17 @@ fi.pvec[2] = fptype_momenta_sv{ 0 }; fi.pvec[3] = -pvec3_ * static_cast(nsf); fi.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; const cxtype_amp_sv sqp0p3 = cxmake( fpsqrt( (fptype_amp)2. * pvec3 ) * (fptype_amp)nsf, 0. ); - w[0] = cxmake( fpamp_of_mom( fi.pvec[1] ), fpamp_of_mom( fi.pvec[2] ) ); - if( nh == 1 ) - { - w[1] = cxmake( fpamp_of_mom( fi.pvec[1] ), fpamp_of_mom( fi.pvec[2] ) ); - w[2] = sqp0p3; - } - else - { - w[1] = sqp0p3; - w[2] = cxmake( fpamp_of_mom( fi.pvec[1] ), fpamp_of_mom( fi.pvec[2] ) ); - } - w[3] = cxmake( fpamp_of_mom( fi.pvec[1] ), fpamp_of_mom( fi.pvec[2] ) ); + w[0] = cxzero_sv(); + w[1] = tm * sqp0p3; + w[2] = tp * sqp0p3; + w[3] = cxzero_sv(); mgDebug( 1, __FUNCTION__ ); return; } @@ -373,7 +367,7 @@ __host__ __device__ void imzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: wavefunctions @@ -388,20 +382,17 @@ fi.pvec[2] = fptype_momenta_sv{ 0 }; fi.pvec[3] = -pvec3_ * static_cast(nsf); fi.flv_index = flv; - const int nh = nhel * nsf; - const cxtype_amp_sv chi = cxmake( -(fptype_amp)nhel * fpsqrt( -(fptype_amp)2. * pvec3 ), 0. ); + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; + const cxtype_amp_sv chi = cxmake( -nhel * fpsqrt( -(fptype_amp)2. * pvec3 ), 0. ); + w[0] = tm * chi; w[1] = cxzero_sv(); w[2] = cxzero_sv(); - if( nh == 1 ) - { - w[0] = cxzero_sv(); - w[3] = chi; - } - else - { - w[0] = chi; - w[3] = cxzero_sv(); - } + w[3] = tp * chi; mgDebug( 1, __FUNCTION__ ); return; } @@ -414,7 +405,7 @@ __host__ __device__ void ixzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: wavefunctions @@ -436,7 +427,12 @@ fi.pvec[3] = -pvec3_ * static_cast(nsf); fi.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; //const float sqp0p3 = sqrtf( pvec0 + pvec3 ) * nsf; // AV: why force a float here? // pvec0+pvec3 is a cancelling difference of two ~|p| numbers for a // backward-moving massless fermion, and everything below divides by it. @@ -448,21 +444,11 @@ pvec0 + pvec3 ); const fptype_amp_sv sqp0p3 = fpsqrt( p0p3stable ) * static_cast(nsf); const cxtype_amp_sv chi0 = cxmake( sqp0p3, 0. ); - const cxtype_amp_sv chi1 = cxmake( (fptype_amp)nh * pvec1 / sqp0p3, pvec2 / sqp0p3 ); - if( nh == 1 ) - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi0; - w[3] = chi1; - } - else - { - w[0] = chi1; - w[1] = chi0; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } + const cxtype_amp_sv chi1 = cxmake( nh * pvec1 / sqp0p3, pvec2 / sqp0p3 ); + w[0] = tm * chi1; + w[1] = tm * chi0; + w[2] = tp * chi0; + w[3] = tp * chi1; mgDebug( 1, __FUNCTION__ ); return; } @@ -474,7 +460,7 @@ __host__ __device__ void vxxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp vmass, // input: vector boson mass - const int nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson) + const fptype_amp_sv nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson, per SIMD lane/GPU thread) const int nsv, // input: +1 (final) or -1 (initial) const int flv, // input: flavour ALOHAOBJ & vc, // output: wavefunctions @@ -500,11 +486,14 @@ vc.pvec[3] = pvec3_ * static_cast(nsv); vc.flv_index = flv; const fptype_amp sqh = fpsqrt( 0.5 ); // AV this is > 0! - const fptype_amp hel = nhel; + const fptype_amp_sv hel = nhel; + // BRANCHLESS HELICITY: |hel| is hel * hel for hel in { -1, 0, +1 }, so that the helicity can + // differ from one SIMD lane or GPU thread to the next with no branch and no std::abs + const fptype_amp_sv abshel = hel * hel; if( vmass != 0. ) { - const int nsvahl = nsv * std::abs( hel ); - const fptype_amp hel0 = 1. - std::abs( hel ); + const fptype_amp_sv nsvahl = static_cast( nsv ) * abshel; + const fptype_amp_sv hel0 = 1 - abshel; // truth value of hel == 0 (longitudinal polarisation) #ifndef MGONGPU_CPPSIMD const fptype_amp_sv pt2 = ( pvec1 * pvec1 ) + ( pvec2 * pvec2 ); const fptype_amp_sv pp = fpmin( pvec0, fpsqrt( pt2 + ( pvec3 * pvec3 ) ) ); @@ -545,9 +534,9 @@ const fptype_amp_sv pt = fpmin( pp, fpsqrt( pt2 ) ); // Branch A: pp == 0. const cxtype_amp vcA_2 = cxmake( 0, 0 ); - const cxtype_amp vcA_3 = cxmake( -hel * sqh, 0 ); - const cxtype_amp vcA_4 = cxmake( 0, nsvahl * sqh ); - const cxtype_amp vcA_5 = cxmake( hel0, 0 ); + const cxtype_amp_v vcA_3 = cxmake( -hel * sqh, 0 ); + const cxtype_amp_v vcA_4 = cxmake( 0., nsvahl * sqh ); + const cxtype_amp_v vcA_5 = cxmake( hel0, 0 ); // Branch B: pp != 0. volatile fptype_amp_v ppDENOM = fpternary( pp != 0, pp, 1. ); // hack: ppDENOM[ieppV]=1 if pp[ieppV]==0 const fptype_amp_v emp = pvec0 / ( vmass * ppDENOM ); // hack: dummy[ieppV] is not used if pp[ieppV]==0 @@ -556,11 +545,11 @@ // Branch B1: pp != 0. and pt != 0. volatile fptype_amp_v ptDENOM = fpternary( pt != 0, pt, 1. ); // hack: ptDENOM[ieppV]=1 if pt[ieppV]==0 const fptype_amp_v pzpt = pvec3 / ( ppDENOM * ptDENOM ) * sqh * hel; // hack: dummy[ieppV] is not used if pp[ieppV]==0 - const cxtype_amp_v vcB1_3 = cxmake( hel0 * pvec1 * emp - pvec1 * pzpt, -fpamp_scalar(nsvahl) * pvec2 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 - const cxtype_amp_v vcB1_4 = cxmake( hel0 * pvec2 * emp - pvec2 * pzpt, fpamp_scalar(nsvahl) * pvec1 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 + const cxtype_amp_v vcB1_3 = cxmake( hel0 * pvec1 * emp - pvec1 * pzpt, -nsvahl * pvec2 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 + const cxtype_amp_v vcB1_4 = cxmake( hel0 * pvec2 * emp - pvec2 * pzpt, nsvahl * pvec1 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 // Branch B2: pp != 0. and pt == 0. - const cxtype_amp vcB2_3 = cxmake( -hel * sqh, 0. ); - const cxtype_amp_v vcB2_4 = cxmake( 0., fpamp_scalar(nsvahl) * fpternary( ( pvec3 < 0 ), -sqh, sqh ) ); // AV: removed an abs here + const cxtype_amp_v vcB2_3 = cxmake( -hel * sqh, 0. ); + const cxtype_amp_v vcB2_4 = cxmake( 0., nsvahl * fpternary( ( pvec3 < 0 ), -sqh, sqh ) ); // AV: removed an abs here // Choose between the results from branch A and branch B (and from branch B1 and branch B2) const bool_v mask = ( pp == 0. ); const bool_v maskB = ( pt != 0. ); @@ -599,11 +588,11 @@ // Branch A: pt != 0. volatile fptype_amp_v ptDENOM = fpternary( pt != 0, pt, 1. ); // hack: ptDENOM[ieppV]=1 if pt[ieppV]==0 const fptype_amp_v pzpt = pvec3 / ( pp * ptDENOM ) * sqh * hel; // hack: dummy[ieppV] is not used if pt[ieppV]==0 - const cxtype_amp_v vcA_3 = cxmake( -pvec1 * pzpt, -fpamp_scalar(nsv) * pvec2 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 - const cxtype_amp_v vcA_4 = cxmake( -pvec2 * pzpt, fpamp_scalar(nsv) * pvec1 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 + const cxtype_amp_v vcA_3 = cxmake( -pvec1 * pzpt, -(fptype_amp)nsv * pvec2 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 + const cxtype_amp_v vcA_4 = cxmake( -pvec2 * pzpt, (fptype_amp)nsv * pvec1 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 // Branch B: pt == 0. - const cxtype_amp vcB_3 = cxmake( -(fptype_amp)hel * sqh, 0 ); - const cxtype_amp_v vcB_4 = cxmake( 0, fpamp_scalar(nsv) * fpternary( ( pvec3 < 0 ), -sqh, sqh ) ); // AV: removed an abs here + const cxtype_amp_v vcB_3 = cxmake( -hel * sqh, 0 ); + const cxtype_amp_v vcB_4 = cxmake( 0, (fptype_amp)nsv * fpternary( ( pvec3 < 0 ), -sqh, sqh ) ); // AV: removed an abs here // Choose between the results from branch A and branch B const bool_v mask = ( pt != 0. ); w[1] = cxternary( mask, vcA_3, vcB_3 ); @@ -654,7 +643,7 @@ __host__ __device__ void oxxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp fmass, // input: fermion mass - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) int flv, // input: flavour ALOHAOBJ & fo, // output: wavefunctions @@ -680,7 +669,12 @@ fo.pvec[2] = pvec2_ * static_cast(nsf); fo.pvec[3] = pvec3_ * static_cast(nsf); fo.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; if( fmass != 0. ) { #ifndef MGONGPU_CPPSIMD @@ -691,12 +685,12 @@ fptype_amp sqm[2] = { fpsqrt( std::abs( fmass ) ), 0. }; // possibility of negative fermion masses //sqm[1] = ( fmass < 0. ? -abs( sqm[0] ) : abs( sqm[0] ) ); // AV: why abs here? sqm[1] = ( fmass < 0. ? -sqm[0] : sqm[0] ); // AV: removed an abs here - const int ip = -( ( 1 - nh ) / 2 ) * nhel; // NB: Fortran sqm(0:1) also has indexes 0,1 as in C++ - const int im = ( 1 + nh ) / 2 * nhel; // NB: Fortran sqm(0:1) also has indexes 0,1 as in C++ - w[0] = cxmake( im * sqm[std::abs( ip )], 0 ); - w[1] = cxmake( ip * nsf * sqm[std::abs( ip )], 0 ); - w[2] = cxmake( im * nsf * sqm[std::abs( im )], 0 ); - w[3] = cxmake( ip * sqm[std::abs( im )], 0 ); + // The Fortran ip = -( ( 1 - nh ) / 2 ) * nhel and im = ( 1 + nh ) / 2 * nhel are -tm * nhel and + // tp * nhel, so that |ip| = tm and |im| = tp: sqm[|ip|] is sqm[1] wherever ip != 0, and so on + w[0] = cxmake( tp * nhel * sqm[0], 0 ); + w[1] = cxmake( -tm * nhel * nsf * sqm[1], 0 ); + w[2] = cxmake( tp * nhel * nsf * sqm[1], 0 ); + w[3] = cxmake( -tm * nhel * sqm[0], 0 ); } else { @@ -704,17 +698,18 @@ fptype_amp( 1 + nsf - ( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; fptype_amp omega[2] = { fpsqrt( pvec0 + pp ), 0. }; omega[1] = fmass / omega[0]; - const int ip = ( 1 + nh ) / 2; // NB: Fortran is (3+nh)/2 because omega(2) has indexes 1,2 and not 0,1 - const int im = ( 1 - nh ) / 2; // NB: Fortran is (3-nh)/2 because omega(2) has indexes 1,2 and not 0,1 - const fptype_amp sfomeg[2] = { sf[0] * omega[ip], sf[1] * omega[im] }; + // The Fortran ip/im indices ( 3 +- nh ) / 2 (omega(2) has indexes 1,2) are the truth values tp/tm + const fptype_amp sfomeg[2] = { sf[0] * ( tm * omega[0] + tp * omega[1] ), sf[1] * ( tp * omega[0] + tm * omega[1] ) }; const fptype_amp pp3 = fpmax( fpternary( fpsignbit( pvec3 ), ( pvec1 * pvec1 + pvec2 * pvec2 ) / ( pp - pvec3 ), pp + pvec3 ), 0. ); const cxtype_amp chi[2] = { cxmake( fpsqrt( pp3 * (fptype_amp)0.5 / pp ), 0. ), ( ( pp3 == 0. ) ? cxmake( -nh, 0. ) : cxmake( nh * pvec1, -pvec2 ) / fpsqrt( (fptype_amp)2. * pp * pp3 ) ) }; - w[0] = sfomeg[1] * chi[im]; - w[1] = sfomeg[1] * chi[ip]; - w[2] = sfomeg[0] * chi[im]; - w[3] = sfomeg[0] * chi[ip]; + const cxtype_amp chi_im = tp * chi[0] + tm * chi[1]; // chi[im] + const cxtype_amp chi_ip = tm * chi[0] + tp * chi[1]; // chi[ip] + w[0] = sfomeg[1] * chi_im; + w[1] = sfomeg[1] * chi_ip; + w[2] = sfomeg[0] * chi_im; + w[3] = sfomeg[0] * chi_ip; } #else volatile fptype_amp_sv p2 = pvec1 * pvec1 + pvec2 * pvec2 + pvec3 * pvec3; // volatile fixes #736 @@ -723,20 +718,17 @@ // NB: Do not use "abs" for floats! It returns an integer with no build warning! Use std::abs! fptype_amp sqm[2] = { fpsqrt( std::abs( fmass ) ), 0 }; // possibility of negative fermion masses sqm[1] = ( fmass < 0 ? -sqm[0] : sqm[0] ); // AV: removed an abs here (as above) - const int ipA = -( ( 1 - nh ) / 2 ) * nhel; - const int imA = ( 1 + nh ) / 2 * nhel; - const cxtype_amp foA_2 = imA * sqm[std::abs( ipA )]; - const cxtype_amp foA_3 = ipA * nsf * sqm[std::abs( ipA )]; - const cxtype_amp foA_4 = imA * nsf * sqm[std::abs( imA )]; - const cxtype_amp foA_5 = ipA * sqm[std::abs( imA )]; + // (see the scalar code above for the truth values tp/tm in place of the Fortran ip/im indices) + const cxtype_amp_v foA_2 = cxmake( tp * nhel * sqm[0], 0 ); + const cxtype_amp_v foA_3 = cxmake( -tm * nhel * (fptype_amp)nsf * sqm[1], 0 ); + const cxtype_amp_v foA_4 = cxmake( tp * nhel * (fptype_amp)nsf * sqm[1], 0 ); + const cxtype_amp_v foA_5 = cxmake( -tm * nhel * sqm[0], 0 ); // Branch B: pp != 0. - const fptype_amp sf[2] = { fptype_amp( 1 + nsf + ( 1 - nsf ) * nh ) * (fptype_amp)0.5, - fptype_amp( 1 + nsf - ( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; + const fptype_amp_v sf[2] = { ( (fptype_amp)( 1 + nsf ) + (fptype_amp)( 1 - nsf ) * nh ) * (fptype_amp)0.5, + ( (fptype_amp)( 1 + nsf ) - (fptype_amp)( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; fptype_v omega[2] = { fpsqrt( pvec0 + pp ), 0 }; omega[1] = fmass / omega[0]; - const int ipB = ( 1 + nh ) / 2; - const int imB = ( 1 - nh ) / 2; - const fptype_amp_v sfomeg[2] = { sf[0] * omega[ipB], sf[1] * omega[imB] }; + const fptype_amp_v sfomeg[2] = { sf[0] * ( tm * omega[0] + tp * omega[1] ), sf[1] * ( tp * omega[0] + tm * omega[1] ) }; const fptype_amp_v pp3 = fpmax( fpternary( fpsignbit( pvec3 ), ( pvec1 * pvec1 + pvec2 * pvec2 ) / ( pp - pvec3 ), pp + pvec3 ), 0. ); volatile fptype_amp_v ppDENOM = fpternary( pp != 0, pp, 1. ); // hack: ppDENOM[ieppV]=1 if pp[ieppV]==0 volatile fptype_amp_v pp3DENOM = fpternary( pp3 != 0, pp3, 1. ); // hack: pp3DENOM[ieppV]=1 if pp3[ieppV]==0 @@ -744,11 +736,13 @@ const cxtype_amp_v chi[2] = { cxmake( fpsqrt( chi0r2 ), 0. ), // hack: dummy[ieppV] is not used if pp[ieppV]==0 ( cxternary( ( pp3 == 0. ), cxmake( -nh, 0. ), - cxmake( (fptype_amp)nh * pvec1, -pvec2 ) / fpsqrt( (fptype_amp)2. * ppDENOM * pp3DENOM ) ) ) }; // hack: dummy[ieppV] is not used if pp[ieppV]==0 - const cxtype_amp_v foB_2 = sfomeg[1] * chi[imB]; - const cxtype_amp_v foB_3 = sfomeg[1] * chi[ipB]; - const cxtype_amp_v foB_4 = sfomeg[0] * chi[imB]; - const cxtype_amp_v foB_5 = sfomeg[0] * chi[ipB]; + cxmake( nh * pvec1, -pvec2 ) / fpsqrt( (fptype_amp)2. * ppDENOM * pp3DENOM ) ) ) }; // hack: dummy[ieppV] is not used if pp[ieppV]==0 + const cxtype_amp_v chi_im = tp * chi[0] + tm * chi[1]; // chi[im] + const cxtype_amp_v chi_ip = tm * chi[0] + tp * chi[1]; // chi[ip] + const cxtype_amp_v foB_2 = sfomeg[1] * chi_im; + const cxtype_amp_v foB_3 = sfomeg[1] * chi_ip; + const cxtype_amp_v foB_4 = sfomeg[0] * chi_im; + const cxtype_amp_v foB_5 = sfomeg[0] * chi_ip; // Choose between the results from branch A and branch B const bool_v mask = ( pp == 0. ); w[0] = cxternary( mask, foA_2, foB_2 ); @@ -775,7 +769,7 @@ const cxtype_amp_v chi[2] = { cxmake( (fptype_v)sqp0p3, 0. ), cxternary( ( sqp0p3 == 0. ), cxmake( -nhel, 0. ) * fpsqrt( (fptype_amp)2. * pvec0 ), - cxmake( (fptype_amp)nh * pvec1, -pvec2 ) / (const fptype_amp_sv)sqp0p3DENOM ) }; // hack: dummy[ieppV] is not used if sqp0p3[ieppV]==0 + cxmake( nh * pvec1, -pvec2 ) / (const fptype_amp_sv)sqp0p3DENOM ) }; // hack: dummy[ieppV] is not used if sqp0p3[ieppV]==0 #else // pvec0+pvec3 is a cancelling difference of two ~|p| numbers for a // backward-moving massless fermion, and everything below divides by it. @@ -788,22 +782,12 @@ fptype_sv{ 0 }, fpsqrt( p0p3stable ) * (fptype_amp)nsf ); const cxtype_amp_sv chi[2] = { cxmake( sqp0p3, 0. ), - ( sqp0p3 == 0. ? cxmake( -nhel, 0. ) * fpsqrt( (fptype_amp)2. * pvec0 ) : cxmake( (fptype_amp)nh * pvec1, -pvec2 ) / sqp0p3 ) }; + ( sqp0p3 == 0. ? cxmake( -nhel, 0. ) * fpsqrt( (fptype_amp)2. * pvec0 ) : cxmake( nh * pvec1, -pvec2 ) / sqp0p3 ) }; #endif - if( nh == 1 ) - { - w[0] = chi[0]; - w[1] = chi[1]; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } - else - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi[1]; - w[3] = chi[0]; - } + w[0] = tp * chi[0]; + w[1] = tp * chi[1]; + w[2] = tm * chi[1]; + w[3] = tm * chi[0]; } mgDebug( 1, __FUNCTION__ ); return; @@ -817,7 +801,7 @@ __host__ __device__ void opzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fo, // output: wavefunctions @@ -832,20 +816,17 @@ fo.pvec[2] = fptype_momenta_sv{ 0 }; fo.pvec[3] = pvec3_ * static_cast(nsf); fo.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; const cxtype_amp_sv csqp0p3 = cxmake( fpsqrt( (fptype_amp)2. * pvec3 ) * (fptype_amp)nsf, 0. ); + w[0] = tp * csqp0p3; w[1] = cxzero_sv(); w[2] = cxzero_sv(); - if( nh == 1 ) - { - w[0] = csqp0p3; - w[3] = cxzero_sv(); - } - else - { - w[0] = cxzero_sv(); - w[3] = csqp0p3; - } + w[3] = tm * csqp0p3; mgDebug( 1, __FUNCTION__ ); return; } @@ -858,7 +839,7 @@ __host__ __device__ void omzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fo, // output: wavefunctions @@ -873,23 +854,17 @@ fo.pvec[2] = fptype_momenta_sv{ 0 }; fo.pvec[3] = pvec3_ * static_cast(nsf); fo.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; const cxtype_amp_sv chi1 = cxmake( -nhel, 0. ) * fpsqrt( -(fptype_amp)2. * pvec3 ); - if( nh == 1 ) - { - w[0] = cxzero_sv(); - w[1] = chi1; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } - else - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi1; - //w[3] = chi1; // AV: BUG! - w[3] = cxzero_sv(); // AV: BUG FIX - } + w[0] = cxzero_sv(); + w[1] = tp * chi1; + w[2] = tm * chi1; + w[3] = cxzero_sv(); mgDebug( 1, __FUNCTION__ ); return; } @@ -902,7 +877,7 @@ __host__ __device__ void oxzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fo, // output: wavefunctions @@ -924,7 +899,12 @@ fo.pvec[2] = pvec2_ * static_cast(nsf); fo.pvec[3] = pvec3_ * static_cast(nsf); fo.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; //const float sqp0p3 = sqrtf( pvec0 + pvec3 ) * nsf; // AV: why force a float here? // pvec0+pvec3 is a cancelling difference of two ~|p| numbers for a // backward-moving massless fermion, and everything below divides by it. @@ -936,21 +916,11 @@ pvec0 + pvec3 ); const fptype_amp_sv sqp0p3 = fpsqrt( p0p3stable ) * static_cast(nsf); const cxtype_amp_sv chi0 = cxmake( sqp0p3, 0. ); - const cxtype_amp_sv chi1 = cxmake( (fptype_amp)nh * pvec1 / sqp0p3, -pvec2 / sqp0p3 ); - if( nh == 1 ) - { - w[0] = chi0; - w[1] = chi1; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } - else - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi1; - w[3] = chi0; - } + const cxtype_amp_sv chi1 = cxmake( nh * pvec1 / sqp0p3, -pvec2 / sqp0p3 ); + w[0] = tp * chi0; + w[1] = tp * chi1; + w[2] = tm * chi1; + w[3] = tm * chi0; mgDebug( 1, __FUNCTION__ ); return; } diff --git a/aloha/template_files/madmatrix/helas_fd.h b/aloha/template_files/madmatrix/helas_fd.h index 2aed9342cf..723037a550 100644 --- a/aloha/template_files/madmatrix/helas_fd.h +++ b/aloha/template_files/madmatrix/helas_fd.h @@ -26,7 +26,7 @@ __host__ __device__ INLINE void ixxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp fmass, // input: fermion mass - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: aloha objects @@ -41,7 +41,7 @@ __host__ __device__ INLINE void ipzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fi, // output: wavefunctions @@ -56,7 +56,7 @@ __host__ __device__ INLINE void imzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fi, // output: wavefunctions @@ -71,7 +71,7 @@ __host__ __device__ INLINE void ixzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fi, // output: wavefunctions @@ -85,7 +85,7 @@ __host__ __device__ INLINE void vxxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp vmass, // input: vector boson mass - const int nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson) + const fptype_amp_sv nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson, per SIMD lane/GPU thread) const int nsv, // input: +1 (final) or -1 (initial) const int flv, // input: flavor index ALOHAOBJ & vc, // output: wavefunctions @@ -113,7 +113,7 @@ __host__ __device__ INLINE void oxxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp fmass, // input: fermion mass - const int nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson) + const fptype_amp_sv nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fo, // output: wavefunctions @@ -128,7 +128,7 @@ __host__ __device__ INLINE void opzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fo, // output: wavefunctions @@ -143,7 +143,7 @@ __host__ __device__ INLINE void omzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fo, // output: wavefunctions @@ -157,7 +157,7 @@ __host__ __device__ INLINE void oxzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fo, // output: wavefunctions @@ -198,7 +198,7 @@ __host__ __device__ void ixxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp fmass, // input: fermion mass - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: wavefunctions @@ -223,7 +223,12 @@ fi.pvec[2] = -pvec2_ * static_cast(nsf); fi.pvec[3] = -pvec3_ * static_cast(nsf); fi.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; if( fmass != 0. ) { #ifndef MGONGPU_CPPSIMD @@ -232,11 +237,9 @@ volatile fptype_amp_sv p2 = pvec1 * pvec1 + pvec2 * pvec2 + pvec3 * pvec3; // volatile fixes #736 const fptype_amp_sv pp = fpmin( pvec0, fpsqrt( p2 ) ); #endif - // In C++ ixxxxx, use a single ip/im numbering that is valid both for pp==0 and pp>0, which have two numbering schemes in Fortran ixxxxx: - // for pp==0, Fortran sqm(0:1) has indexes 0,1 as in C++; but for Fortran pp>0, omega(2) has indexes 1,2 and not 0,1 - // NB: this is only possible in ixxxx, but in oxxxxx two different numbering schemes must be used - const int ip = ( 1 + nh ) / 2; // NB: same as in Fortran pp==0, differs from Fortran pp>0, which is (3+nh)/2 because omega(2) has indexes 1,2 - const int im = ( 1 - nh ) / 2; // NB: same as in Fortran pp==0, differs from Fortran pp>0, which is (3-nh)/2 because omega(2) has indexes 1,2 + // The Fortran ip/im indices ( 1 + nh ) / 2 and ( 1 - nh ) / 2 (for pp > 0, ( 3 +- nh ) / 2 as omega(2) has + // indexes 1,2) are the truth values tp and tm: an array element x[ip] is tm * x[0] + tp * x[1], and + // ip * x[ip] is just tp * x[1] (and likewise for im) #ifndef MGONGPU_CPPSIMD if( pp == 0. ) { @@ -244,10 +247,10 @@ fptype_amp sqm[2] = { fpsqrt( std::abs( fmass ) ), 0. }; // possibility of negative fermion masses //sqm[1] = ( fmass < 0. ? -abs( sqm[0] ) : abs( sqm[0] ) ); // AV: why abs here? sqm[1] = ( fmass < 0. ? -sqm[0] : sqm[0] ); // AV: removed an abs here - w[0] = cxmake( ip * sqm[ip], 0 ); - w[1] = cxmake( im * nsf * sqm[ip], 0 ); - w[2] = cxmake( ip * nsf * sqm[im], 0 ); - w[3] = cxmake( im * sqm[im], 0 ); + w[0] = cxmake( tp * sqm[1], 0 ); + w[1] = cxmake( tm * nsf * sqm[0], 0 ); + w[2] = cxmake( tp * nsf * sqm[0], 0 ); + w[3] = cxmake( tm * sqm[1], 0 ); } else { @@ -255,30 +258,32 @@ fptype_amp( 1 + nsf - ( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; fptype_amp omega[2] = { fpsqrt( pvec0 + pp ), 0. }; omega[1] = fmass / omega[0]; - const fptype_amp sfomega[2] = { sf[0] * omega[ip], sf[1] * omega[im] }; + const fptype_amp sfomega[2] = { sf[0] * ( tm * omega[0] + tp * omega[1] ), sf[1] * ( tp * omega[0] + tm * omega[1] ) }; const fptype_amp pp3 = fpmax( fpternary( fpsignbit( pvec3 ), ( pvec1 * pvec1 + pvec2 * pvec2 ) / ( pp - pvec3 ), pp + pvec3 ), 0. ); const cxtype_amp chi[2] = { cxmake( fpsqrt( pp3 * (fptype_amp)0.5 / pp ), 0. ), ( pp3 == 0. ? cxmake( -nh, 0. ) : cxmake( nh * pvec1, pvec2 ) / fpsqrt( (fptype_amp)2. * pp * pp3 ) ) }; - w[0] = sfomega[0] * chi[im]; - w[1] = sfomega[0] * chi[ip]; - w[2] = sfomega[1] * chi[im]; - w[3] = sfomega[1] * chi[ip]; + const cxtype_amp chi_im = tp * chi[0] + tm * chi[1]; // chi[im] + const cxtype_amp chi_ip = tm * chi[0] + tp * chi[1]; // chi[ip] + w[0] = sfomega[0] * chi_im; + w[1] = sfomega[0] * chi_ip; + w[2] = sfomega[1] * chi_im; + w[3] = sfomega[1] * chi_ip; } #else // Branch A: pp == 0. // NB: Do not use "abs" for floats! It returns an integer with no build warning! Use std::abs! fptype_amp sqm[2] = { fpsqrt( std::abs( fmass ) ), 0 }; // possibility of negative fermion masses (NB: SCALAR!) sqm[1] = ( fmass < 0 ? -sqm[0] : sqm[0] ); // AV: removed an abs here (as above) - const cxtype_amp fiA_2 = ip * sqm[ip]; // scalar cxtype: real part initialised from fptype, imag part = 0 - const cxtype_amp fiA_3 = im * nsf * sqm[ip]; // scalar cxtype: real part initialised from fptype, imag part = 0 - const cxtype_amp fiA_4 = ip * nsf * sqm[im]; // scalar cxtype: real part initialised from fptype, imag part = 0 - const cxtype_amp fiA_5 = im * sqm[im]; // scalar cxtype: real part initialised from fptype, imag part = 0 + const cxtype_amp_v fiA_2 = cxmake( tp * sqm[1], 0 ); + const cxtype_amp_v fiA_3 = cxmake( tm * (fptype_amp)nsf * sqm[0], 0 ); + const cxtype_amp_v fiA_4 = cxmake( tp * (fptype_amp)nsf * sqm[0], 0 ); + const cxtype_amp_v fiA_5 = cxmake( tm * sqm[1], 0 ); // Branch B: pp != 0. - const fptype_amp sf[2] = { fptype_amp( 1 + nsf + ( 1 - nsf ) * nh ) * (fptype_amp)0.5, - fptype_amp( 1 + nsf - ( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; + const fptype_amp_v sf[2] = { ( (fptype_amp)( 1 + nsf ) + (fptype_amp)( 1 - nsf ) * nh ) * (fptype_amp)0.5, + ( (fptype_amp)( 1 + nsf ) - (fptype_amp)( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; fptype_v omega[2] = { fpsqrt( pvec0 + pp ), 0 }; omega[1] = fmass / omega[0]; - const fptype_amp_v sfomega[2] = { sf[0] * omega[ip], sf[1] * omega[im] }; + const fptype_amp_v sfomega[2] = { sf[0] * ( tm * omega[0] + tp * omega[1] ), sf[1] * ( tp * omega[0] + tm * omega[1] ) }; const fptype_amp_v pp3 = fpmax( fpternary( fpsignbit( pvec3 ), ( pvec1 * pvec1 + pvec2 * pvec2 ) / ( pp - pvec3 ), pp + pvec3 ), 0 ); volatile fptype_amp_v ppDENOM = fpternary( pp != 0, pp, 1. ); // hack: ppDENOM[ieppV]=1 if pp[ieppV]==0 volatile fptype_amp_v pp3DENOM = fpternary( pp3 != 0, pp3, 1. ); // hack: pp3DENOM[ieppV]=1 if pp3[ieppV]==0 @@ -286,11 +291,13 @@ const cxtype_amp_v chi[2] = { cxmake( fpsqrt( chi0r2 ), 0 ), // hack: dummy[ieppV] is not used if pp[ieppV]==0 cxternary( ( pp3 == 0. ), cxmake( -nh, 0 ), - cxmake( (fptype_amp)nh * pvec1, pvec2 ) / fpsqrt( (fptype_amp)2. * ppDENOM * pp3DENOM ) ) }; // hack: dummy[ieppV] is not used if pp[ieppV]==0 - const cxtype_amp_v fiB_2 = sfomega[0] * chi[im]; - const cxtype_amp_v fiB_3 = sfomega[0] * chi[ip]; - const cxtype_amp_v fiB_4 = sfomega[1] * chi[im]; - const cxtype_amp_v fiB_5 = sfomega[1] * chi[ip]; + cxmake( nh * pvec1, pvec2 ) / fpsqrt( (fptype_amp)2. * ppDENOM * pp3DENOM ) ) }; // hack: dummy[ieppV] is not used if pp[ieppV]==0 + const cxtype_amp_v chi_im = tp * chi[0] + tm * chi[1]; // chi[im] + const cxtype_amp_v chi_ip = tm * chi[0] + tp * chi[1]; // chi[ip] + const cxtype_amp_v fiB_2 = sfomega[0] * chi_im; + const cxtype_amp_v fiB_3 = sfomega[0] * chi_ip; + const cxtype_amp_v fiB_4 = sfomega[1] * chi_im; + const cxtype_amp_v fiB_5 = sfomega[1] * chi_ip; // Choose between the results from branch A and branch B const bool_v mask = ( pp == 0. ); w[0] = cxternary( mask, fiA_2, fiB_2 ); @@ -316,8 +323,8 @@ volatile fptype_amp_sv sqp0p3DENOM = fpternary( sqp0p3 != 0, (fptype_sv)sqp0p3, 1. ); // hack: dummy sqp0p3DENOM[ieppV]=1 if sqp0p3[ieppV]==0 cxtype_sv chi[2] = { cxmake( (fptype_v)sqp0p3, 0. ), cxternary( sqp0p3 == 0, - cxmake( -(fptype_amp)nhel * fpsqrt( (fptype_amp)2. * pvec0 ), 0. ), - cxmake( (fptype_amp)nh * pvec1, pvec2 ) / (const fptype_v)sqp0p3DENOM ) }; // hack: dummy[ieppV] is not used if sqp0p3[ieppV]==0 + cxmake( -nhel * fpsqrt( (fptype_amp)2. * pvec0 ), 0. ), + cxmake( nh * pvec1, pvec2 ) / (const fptype_v)sqp0p3DENOM ) }; // hack: dummy[ieppV] is not used if sqp0p3[ieppV]==0 #else // pvec0+pvec3 is a cancelling difference of two ~|p| numbers for a // backward-moving massless fermion, and everything below divides by it. @@ -330,22 +337,12 @@ fptype_sv{ 0 }, fpsqrt( p0p3stable ) * (fptype_amp)nsf ); const cxtype_amp_sv chi[2] = { cxmake( sqp0p3, 0. ), - ( sqp0p3 == 0. ? cxmake( -(fptype_amp)nhel * fpsqrt( (fptype_amp)2. * pvec0 ), 0. ) : cxmake( (fptype_amp)nh * pvec1, pvec2 ) / sqp0p3 ) }; + ( sqp0p3 == 0. ? cxmake( -nhel * fpsqrt( (fptype_amp)2. * pvec0 ), 0. ) : cxmake( nh * pvec1, pvec2 ) / sqp0p3 ) }; #endif - if( nh == 1 ) - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi[0]; - w[3] = chi[1]; - } - else - { - w[0] = chi[1]; - w[1] = chi[0]; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } + w[0] = tm * chi[1]; + w[1] = tm * chi[0]; + w[2] = tp * chi[0]; + w[3] = tp * chi[1]; } mgDebug( 1, __FUNCTION__ ); return; @@ -359,7 +356,7 @@ __host__ __device__ void ipzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: wavefunctions @@ -374,20 +371,17 @@ fi.pvec[2] = fptype_momenta_sv{ 0 }; fi.pvec[3] = -pvec3_ * static_cast(nsf); fi.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; const cxtype_amp_sv sqp0p3 = cxmake( fpsqrt( (fptype_amp)2. * pvec3 ) * (fptype_amp)nsf, 0. ); - w[0] = cxmake( fpamp_of_mom( fi.pvec[1] ), fpamp_of_mom( fi.pvec[2] ) ); - if( nh == 1 ) - { - w[1] = cxmake( fpamp_of_mom( fi.pvec[1] ), fpamp_of_mom( fi.pvec[2] ) ); - w[2] = sqp0p3; - } - else - { - w[1] = sqp0p3; - w[2] = cxmake( fpamp_of_mom( fi.pvec[1] ), fpamp_of_mom( fi.pvec[2] ) ); - } - w[3] = cxmake( fpamp_of_mom( fi.pvec[1] ), fpamp_of_mom( fi.pvec[2] ) ); + w[0] = cxzero_sv(); + w[1] = tm * sqp0p3; + w[2] = tp * sqp0p3; + w[3] = cxzero_sv(); mgDebug( 1, __FUNCTION__ ); return; } @@ -400,7 +394,7 @@ __host__ __device__ void imzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: wavefunctions @@ -415,20 +409,17 @@ fi.pvec[2] = fptype_momenta_sv{ 0 }; fi.pvec[3] = -pvec3_ * static_cast(nsf); fi.flv_index = flv; - const int nh = nhel * nsf; - const cxtype_amp_sv chi = cxmake( -(fptype_amp)nhel * fpsqrt( -(fptype_amp)2. * pvec3 ), 0. ); + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; + const cxtype_amp_sv chi = cxmake( -nhel * fpsqrt( -(fptype_amp)2. * pvec3 ), 0. ); + w[0] = tm * chi; w[1] = cxzero_sv(); w[2] = cxzero_sv(); - if( nh == 1 ) - { - w[0] = cxzero_sv(); - w[3] = chi; - } - else - { - w[0] = chi; - w[3] = cxzero_sv(); - } + w[3] = tp * chi; mgDebug( 1, __FUNCTION__ ); return; } @@ -441,7 +432,7 @@ __host__ __device__ void ixzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: wavefunctions @@ -463,7 +454,12 @@ fi.pvec[3] = -pvec3_ * static_cast(nsf); fi.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; //const float sqp0p3 = sqrtf( pvec0 + pvec3 ) * nsf; // AV: why force a float here? // pvec0+pvec3 is a cancelling difference of two ~|p| numbers for a // backward-moving massless fermion, and everything below divides by it. @@ -475,21 +471,11 @@ pvec0 + pvec3 ); const fptype_amp_sv sqp0p3 = fpsqrt( p0p3stable ) * static_cast(nsf); const cxtype_amp_sv chi0 = cxmake( sqp0p3, 0. ); - const cxtype_amp_sv chi1 = cxmake( (fptype_amp)nh * pvec1 / sqp0p3, pvec2 / sqp0p3 ); - if( nh == 1 ) - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi0; - w[3] = chi1; - } - else - { - w[0] = chi1; - w[1] = chi0; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } + const cxtype_amp_sv chi1 = cxmake( nh * pvec1 / sqp0p3, pvec2 / sqp0p3 ); + w[0] = tm * chi1; + w[1] = tm * chi0; + w[2] = tp * chi0; + w[3] = tp * chi1; mgDebug( 1, __FUNCTION__ ); return; } @@ -501,7 +487,7 @@ __host__ __device__ void vxxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp vmass, // input: vector boson mass - const int nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson) + const fptype_amp_sv nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson, per SIMD lane/GPU thread) const int nsv, // input: +1 (final) or -1 (initial) const int flv, // input: flavour ALOHAOBJ & vc, // output: wavefunctions @@ -527,7 +513,10 @@ vc.pvec[3] = pvec3_ * static_cast(nsv); vc.flv_index = flv; const fptype_amp sqh = fpsqrt( 0.5 ); // AV this is > 0! - const fptype_amp hel = nhel; + const fptype_amp_sv hel = nhel; + // BRANCHLESS HELICITY: |hel| is hel * hel for hel in { -1, 0, +1 }, so that the helicity can + // differ from one SIMD lane or GPU thread to the next with no branch and no std::abs + const fptype_amp_sv abshel = hel * hel; // FD gauge const cxtype_amp_sv cI = cxmake( 0. + fptype_amp_sv{ 0. }, 1. + fptype_amp_sv{ 0. } ); @@ -538,8 +527,8 @@ if( vmass != 0. ) { - const int nsvahl = nsv * std::abs( hel ); - const fptype_amp hel0 = 1. - std::abs( hel ); + const fptype_amp_sv nsvahl = static_cast( nsv ) * abshel; + const fptype_amp_sv hel0 = 1 - abshel; // truth value of hel == 0 (longitudinal polarisation) #ifndef MGONGPU_CPPSIMD const fptype_amp_sv pt2 = ( pvec1 * pvec1 ) + ( pvec2 * pvec2 ); const fptype_amp_sv pp = fpmin( pvec0, fpsqrt( pt2 + ( pvec3 * pvec3 ) ) ); @@ -594,17 +583,14 @@ nk = n[0]*pvec0 - n[1]*pvec1 - n[2]*pvec2 - n[3]*pvec3; - if ( abs(nhel) == 1) - { - w[4] = cxzero_sv(); - } - else{ - w[0] = cxmake( -vmass/nk * n[0], zero ); - w[1] = cxmake( -vmass/nk * n[1], zero ); - w[2] = cxmake( -vmass/nk * n[2], zero ); - w[3] = cxmake( -vmass/nk * n[3], zero ); - w[4] = -static_cast(nsv)*cI; - } + // BRANCHLESS HELICITY: |hel| = 1 keeps w[0..3] with w[4] = 0, hel = 0 (hel0 = 1) takes the + // FD-gauge longitudinal polarisation instead (nk > 0 for a massive momentum, so the unused + // alternative is finite and vanishes when multiplied by its zero truth value) + w[0] = abshel * w[0] + hel0 * cxmake( -vmass / nk * n[0], zero ); + w[1] = abshel * w[1] + hel0 * cxmake( -vmass / nk * n[1], zero ); + w[2] = abshel * w[2] + hel0 * cxmake( -vmass / nk * n[2], zero ); + w[3] = abshel * w[3] + hel0 * cxmake( -vmass / nk * n[3], zero ); + w[4] = hel0 * ( -static_cast( nsv ) * cI ); #else volatile fptype_amp_sv pt2 = ( pvec1 * pvec1 ) + ( pvec2 * pvec2 ); @@ -613,9 +599,9 @@ const fptype_amp_sv pt = fpmin( pp, fpsqrt( pt2 ) ); // Branch A: pp == 0. const cxtype_amp vcA_2 = cxmake( 0, 0 ); - const cxtype_amp vcA_3 = cxmake( -hel * sqh, 0 ); - const cxtype_amp vcA_4 = cxmake( 0, nsvahl * sqh ); - const cxtype_amp vcA_5 = cxmake( hel0, 0 ); + const cxtype_amp_v vcA_3 = cxmake( -hel * sqh, 0 ); + const cxtype_amp_v vcA_4 = cxmake( 0., nsvahl * sqh ); + const cxtype_amp_v vcA_5 = cxmake( hel0, 0 ); // Branch B: pp != 0. volatile fptype_amp_v ppDENOM = fpternary( pp != 0, pp, 1. ); // hack: ppDENOM[ieppV]=1 if pp[ieppV]==0 const fptype_amp_v emp = pvec0 / ( vmass * ppDENOM ); // hack: dummy[ieppV] is not used if pp[ieppV]==0 @@ -624,11 +610,11 @@ // Branch B1: pp != 0. and pt != 0. volatile fptype_amp_v ptDENOM = fpternary( pt != 0, pt, 1. ); // hack: ptDENOM[ieppV]=1 if pt[ieppV]==0 const fptype_amp_v pzpt = pvec3 / ( ppDENOM * ptDENOM ) * sqh * hel; // hack: dummy[ieppV] is not used if pp[ieppV]==0 - const cxtype_amp_v vcB1_3 = cxmake( hel0 * pvec1 * emp - pvec1 * pzpt, -fpamp_scalar(nsvahl) * pvec2 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 - const cxtype_amp_v vcB1_4 = cxmake( hel0 * pvec2 * emp - pvec2 * pzpt, fpamp_scalar(nsvahl) * pvec1 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 + const cxtype_amp_v vcB1_3 = cxmake( hel0 * pvec1 * emp - pvec1 * pzpt, -nsvahl * pvec2 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 + const cxtype_amp_v vcB1_4 = cxmake( hel0 * pvec2 * emp - pvec2 * pzpt, nsvahl * pvec1 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 // Branch B2: pp != 0. and pt == 0. - const cxtype_amp vcB2_3 = cxmake( -hel * sqh, 0. ); - const cxtype_amp_v vcB2_4 = cxmake( 0., fpamp_scalar(nsvahl) * fpternary( ( pvec3 < 0 ), -sqh, sqh ) ); // AV: removed an abs here + const cxtype_amp_v vcB2_3 = cxmake( -hel * sqh, 0. ); + const cxtype_amp_v vcB2_4 = cxmake( 0., nsvahl * fpternary( ( pvec3 < 0 ), -sqh, sqh ) ); // AV: removed an abs here // Choose between the results from branch A and branch B (and from branch B1 and branch B2) const bool_v mask = ( pp == 0. ); const bool_v maskB = ( pt != 0. ); @@ -650,20 +636,14 @@ nk = n[0]*pvec0 - n[1]*pvec1 - n[2]*pvec2 - n[3]*pvec3; - // nhel is a scalar: no need for a per-lane mask here (and a bool_v built - // from a single value would only set the first lane) - if ( abs(nhel) == 1 ) - { - w[4] = zero; - } - else - { - w[0] = cxmake( -vmass/nk * n[0], zero ); - w[1] = cxmake( -vmass/nk * n[1], zero ); - w[2] = cxmake( -vmass/nk * n[2], zero ); - w[3] = cxmake( -vmass/nk * n[3], zero ); - w[4] = -static_cast(nsv)*cI; - } + // BRANCHLESS HELICITY: |hel| = 1 keeps w[0..3] with w[4] = 0, hel = 0 (hel0 = 1) takes the + // FD-gauge longitudinal polarisation instead (nk > 0 for a massive momentum, so the unused + // alternative is finite and vanishes when multiplied by its zero truth value) + w[0] = abshel * w[0] + hel0 * cxmake( -vmass / nk * n[0], zero ); + w[1] = abshel * w[1] + hel0 * cxmake( -vmass / nk * n[1], zero ); + w[2] = abshel * w[2] + hel0 * cxmake( -vmass / nk * n[2], zero ); + w[3] = abshel * w[3] + hel0 * cxmake( -vmass / nk * n[3], zero ); + w[4] = hel0 * ( -static_cast( nsv ) * cI ); #endif } else @@ -695,11 +675,11 @@ // Branch A: pt != 0. volatile fptype_amp_v ptDENOM = fpternary( pt != 0, pt, 1. ); // hack: ptDENOM[ieppV]=1 if pt[ieppV]==0 const fptype_amp_v pzpt = pvec3 / ( pp * ptDENOM ) * sqh * hel; // hack: dummy[ieppV] is not used if pt[ieppV]==0 - const cxtype_amp_v vcA_3 = cxmake( -pvec1 * pzpt, -fpamp_scalar(nsv) * pvec2 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 - const cxtype_amp_v vcA_4 = cxmake( -pvec2 * pzpt, fpamp_scalar(nsv) * pvec1 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 + const cxtype_amp_v vcA_3 = cxmake( -pvec1 * pzpt, -(fptype_amp)nsv * pvec2 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 + const cxtype_amp_v vcA_4 = cxmake( -pvec2 * pzpt, (fptype_amp)nsv * pvec1 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 // Branch B: pt == 0. - const cxtype_amp vcB_3 = cxmake( -(fptype_amp)hel * sqh, 0 ); - const cxtype_amp_v vcB_4 = cxmake( 0, fpamp_scalar(nsv) * fpternary( ( pvec3 < 0 ), -sqh, sqh ) ); // AV: removed an abs here + const cxtype_amp_v vcB_3 = cxmake( -hel * sqh, 0 ); + const cxtype_amp_v vcB_4 = cxmake( 0, (fptype_amp)nsv * fpternary( ( pvec3 < 0 ), -sqh, sqh ) ); // AV: removed an abs here // Choose between the results from branch A and branch B const bool_v mask = ( pt != 0. ); w[1] = cxternary( mask, vcA_3, vcB_3 ); @@ -758,7 +738,7 @@ __host__ __device__ void oxxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp fmass, // input: fermion mass - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) int flv, // input: flavour ALOHAOBJ & fo, // output: wavefunctions @@ -784,7 +764,12 @@ fo.pvec[2] = pvec2_ * static_cast(nsf); fo.pvec[3] = pvec3_ * static_cast(nsf); fo.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; if( fmass != 0. ) { #ifndef MGONGPU_CPPSIMD @@ -795,12 +780,12 @@ fptype_amp sqm[2] = { fpsqrt( std::abs( fmass ) ), 0. }; // possibility of negative fermion masses //sqm[1] = ( fmass < 0. ? -abs( sqm[0] ) : abs( sqm[0] ) ); // AV: why abs here? sqm[1] = ( fmass < 0. ? -sqm[0] : sqm[0] ); // AV: removed an abs here - const int ip = -( ( 1 - nh ) / 2 ) * nhel; // NB: Fortran sqm(0:1) also has indexes 0,1 as in C++ - const int im = ( 1 + nh ) / 2 * nhel; // NB: Fortran sqm(0:1) also has indexes 0,1 as in C++ - w[0] = cxmake( im * sqm[std::abs( ip )], 0 ); - w[1] = cxmake( ip * nsf * sqm[std::abs( ip )], 0 ); - w[2] = cxmake( im * nsf * sqm[std::abs( im )], 0 ); - w[3] = cxmake( ip * sqm[std::abs( im )], 0 ); + // The Fortran ip = -( ( 1 - nh ) / 2 ) * nhel and im = ( 1 + nh ) / 2 * nhel are -tm * nhel and + // tp * nhel, so that |ip| = tm and |im| = tp: sqm[|ip|] is sqm[1] wherever ip != 0, and so on + w[0] = cxmake( tp * nhel * sqm[0], 0 ); + w[1] = cxmake( -tm * nhel * nsf * sqm[1], 0 ); + w[2] = cxmake( tp * nhel * nsf * sqm[1], 0 ); + w[3] = cxmake( -tm * nhel * sqm[0], 0 ); } else { @@ -808,17 +793,18 @@ fptype_amp( 1 + nsf - ( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; fptype_amp omega[2] = { fpsqrt( pvec0 + pp ), 0. }; omega[1] = fmass / omega[0]; - const int ip = ( 1 + nh ) / 2; // NB: Fortran is (3+nh)/2 because omega(2) has indexes 1,2 and not 0,1 - const int im = ( 1 - nh ) / 2; // NB: Fortran is (3-nh)/2 because omega(2) has indexes 1,2 and not 0,1 - const fptype_amp sfomeg[2] = { sf[0] * omega[ip], sf[1] * omega[im] }; + // The Fortran ip/im indices ( 3 +- nh ) / 2 (omega(2) has indexes 1,2) are the truth values tp/tm + const fptype_amp sfomeg[2] = { sf[0] * ( tm * omega[0] + tp * omega[1] ), sf[1] * ( tp * omega[0] + tm * omega[1] ) }; const fptype_amp pp3 = fpmax( fpternary( fpsignbit( pvec3 ), ( pvec1 * pvec1 + pvec2 * pvec2 ) / ( pp - pvec3 ), pp + pvec3 ), 0. ); const cxtype_amp chi[2] = { cxmake( fpsqrt( pp3 * (fptype_amp)0.5 / pp ), 0. ), ( ( pp3 == 0. ) ? cxmake( -nh, 0. ) : cxmake( nh * pvec1, -pvec2 ) / fpsqrt( (fptype_amp)2. * pp * pp3 ) ) }; - w[0] = sfomeg[1] * chi[im]; - w[1] = sfomeg[1] * chi[ip]; - w[2] = sfomeg[0] * chi[im]; - w[3] = sfomeg[0] * chi[ip]; + const cxtype_amp chi_im = tp * chi[0] + tm * chi[1]; // chi[im] + const cxtype_amp chi_ip = tm * chi[0] + tp * chi[1]; // chi[ip] + w[0] = sfomeg[1] * chi_im; + w[1] = sfomeg[1] * chi_ip; + w[2] = sfomeg[0] * chi_im; + w[3] = sfomeg[0] * chi_ip; } #else volatile fptype_amp_sv p2 = pvec1 * pvec1 + pvec2 * pvec2 + pvec3 * pvec3; // volatile fixes #736 @@ -827,20 +813,17 @@ // NB: Do not use "abs" for floats! It returns an integer with no build warning! Use std::abs! fptype_amp sqm[2] = { fpsqrt( std::abs( fmass ) ), 0 }; // possibility of negative fermion masses sqm[1] = ( fmass < 0 ? -sqm[0] : sqm[0] ); // AV: removed an abs here (as above) - const int ipA = -( ( 1 - nh ) / 2 ) * nhel; - const int imA = ( 1 + nh ) / 2 * nhel; - const cxtype_amp foA_2 = imA * sqm[std::abs( ipA )]; - const cxtype_amp foA_3 = ipA * nsf * sqm[std::abs( ipA )]; - const cxtype_amp foA_4 = imA * nsf * sqm[std::abs( imA )]; - const cxtype_amp foA_5 = ipA * sqm[std::abs( imA )]; + // (see the scalar code above for the truth values tp/tm in place of the Fortran ip/im indices) + const cxtype_amp_v foA_2 = cxmake( tp * nhel * sqm[0], 0 ); + const cxtype_amp_v foA_3 = cxmake( -tm * nhel * (fptype_amp)nsf * sqm[1], 0 ); + const cxtype_amp_v foA_4 = cxmake( tp * nhel * (fptype_amp)nsf * sqm[1], 0 ); + const cxtype_amp_v foA_5 = cxmake( -tm * nhel * sqm[0], 0 ); // Branch B: pp != 0. - const fptype_amp sf[2] = { fptype_amp( 1 + nsf + ( 1 - nsf ) * nh ) * (fptype_amp)0.5, - fptype_amp( 1 + nsf - ( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; + const fptype_amp_v sf[2] = { ( (fptype_amp)( 1 + nsf ) + (fptype_amp)( 1 - nsf ) * nh ) * (fptype_amp)0.5, + ( (fptype_amp)( 1 + nsf ) - (fptype_amp)( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; fptype_v omega[2] = { fpsqrt( pvec0 + pp ), 0 }; omega[1] = fmass / omega[0]; - const int ipB = ( 1 + nh ) / 2; - const int imB = ( 1 - nh ) / 2; - const fptype_amp_v sfomeg[2] = { sf[0] * omega[ipB], sf[1] * omega[imB] }; + const fptype_amp_v sfomeg[2] = { sf[0] * ( tm * omega[0] + tp * omega[1] ), sf[1] * ( tp * omega[0] + tm * omega[1] ) }; const fptype_amp_v pp3 = fpmax( fpternary( fpsignbit( pvec3 ), ( pvec1 * pvec1 + pvec2 * pvec2 ) / ( pp - pvec3 ), pp + pvec3 ), 0. ); volatile fptype_amp_v ppDENOM = fpternary( pp != 0, pp, 1. ); // hack: ppDENOM[ieppV]=1 if pp[ieppV]==0 volatile fptype_amp_v pp3DENOM = fpternary( pp3 != 0, pp3, 1. ); // hack: pp3DENOM[ieppV]=1 if pp3[ieppV]==0 @@ -848,11 +831,13 @@ const cxtype_amp_v chi[2] = { cxmake( fpsqrt( chi0r2 ), 0. ), // hack: dummy[ieppV] is not used if pp[ieppV]==0 ( cxternary( ( pp3 == 0. ), cxmake( -nh, 0. ), - cxmake( (fptype_amp)nh * pvec1, -pvec2 ) / fpsqrt( (fptype_amp)2. * ppDENOM * pp3DENOM ) ) ) }; // hack: dummy[ieppV] is not used if pp[ieppV]==0 - const cxtype_amp_v foB_2 = sfomeg[1] * chi[imB]; - const cxtype_amp_v foB_3 = sfomeg[1] * chi[ipB]; - const cxtype_amp_v foB_4 = sfomeg[0] * chi[imB]; - const cxtype_amp_v foB_5 = sfomeg[0] * chi[ipB]; + cxmake( nh * pvec1, -pvec2 ) / fpsqrt( (fptype_amp)2. * ppDENOM * pp3DENOM ) ) ) }; // hack: dummy[ieppV] is not used if pp[ieppV]==0 + const cxtype_amp_v chi_im = tp * chi[0] + tm * chi[1]; // chi[im] + const cxtype_amp_v chi_ip = tm * chi[0] + tp * chi[1]; // chi[ip] + const cxtype_amp_v foB_2 = sfomeg[1] * chi_im; + const cxtype_amp_v foB_3 = sfomeg[1] * chi_ip; + const cxtype_amp_v foB_4 = sfomeg[0] * chi_im; + const cxtype_amp_v foB_5 = sfomeg[0] * chi_ip; // Choose between the results from branch A and branch B const bool_v mask = ( pp == 0. ); w[0] = cxternary( mask, foA_2, foB_2 ); @@ -879,7 +864,7 @@ const cxtype_amp_v chi[2] = { cxmake( (fptype_v)sqp0p3, 0. ), cxternary( ( sqp0p3 == 0. ), cxmake( -nhel, 0. ) * fpsqrt( (fptype_amp)2. * pvec0 ), - cxmake( (fptype_amp)nh * pvec1, -pvec2 ) / (const fptype_amp_sv)sqp0p3DENOM ) }; // hack: dummy[ieppV] is not used if sqp0p3[ieppV]==0 + cxmake( nh * pvec1, -pvec2 ) / (const fptype_amp_sv)sqp0p3DENOM ) }; // hack: dummy[ieppV] is not used if sqp0p3[ieppV]==0 #else // pvec0+pvec3 is a cancelling difference of two ~|p| numbers for a // backward-moving massless fermion, and everything below divides by it. @@ -892,22 +877,12 @@ fptype_sv{ 0 }, fpsqrt( p0p3stable ) * (fptype_amp)nsf ); const cxtype_amp_sv chi[2] = { cxmake( sqp0p3, 0. ), - ( sqp0p3 == 0. ? cxmake( -nhel, 0. ) * fpsqrt( (fptype_amp)2. * pvec0 ) : cxmake( (fptype_amp)nh * pvec1, -pvec2 ) / sqp0p3 ) }; + ( sqp0p3 == 0. ? cxmake( -nhel, 0. ) * fpsqrt( (fptype_amp)2. * pvec0 ) : cxmake( nh * pvec1, -pvec2 ) / sqp0p3 ) }; #endif - if( nh == 1 ) - { - w[0] = chi[0]; - w[1] = chi[1]; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } - else - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi[1]; - w[3] = chi[0]; - } + w[0] = tp * chi[0]; + w[1] = tp * chi[1]; + w[2] = tm * chi[1]; + w[3] = tm * chi[0]; } mgDebug( 1, __FUNCTION__ ); return; @@ -921,7 +896,7 @@ __host__ __device__ void opzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fo, // output: wavefunctions @@ -936,20 +911,17 @@ fo.pvec[2] = fptype_momenta_sv{ 0 }; fo.pvec[3] = pvec3_ * static_cast(nsf); fo.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; const cxtype_amp_sv csqp0p3 = cxmake( fpsqrt( (fptype_amp)2. * pvec3 ) * (fptype_amp)nsf, 0. ); + w[0] = tp * csqp0p3; w[1] = cxzero_sv(); w[2] = cxzero_sv(); - if( nh == 1 ) - { - w[0] = csqp0p3; - w[3] = cxzero_sv(); - } - else - { - w[0] = cxzero_sv(); - w[3] = csqp0p3; - } + w[3] = tm * csqp0p3; mgDebug( 1, __FUNCTION__ ); return; } @@ -962,7 +934,7 @@ __host__ __device__ void omzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fo, // output: wavefunctions @@ -977,23 +949,17 @@ fo.pvec[2] = fptype_momenta_sv{ 0 }; fo.pvec[3] = pvec3_ * static_cast(nsf); fo.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; const cxtype_amp_sv chi1 = cxmake( -nhel, 0. ) * fpsqrt( -(fptype_amp)2. * pvec3 ); - if( nh == 1 ) - { - w[0] = cxzero_sv(); - w[1] = chi1; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } - else - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi1; - //w[3] = chi1; // AV: BUG! - w[3] = cxzero_sv(); // AV: BUG FIX - } + w[0] = cxzero_sv(); + w[1] = tp * chi1; + w[2] = tm * chi1; + w[3] = cxzero_sv(); mgDebug( 1, __FUNCTION__ ); return; } @@ -1006,7 +972,7 @@ __host__ __device__ void oxzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fo, // output: wavefunctions @@ -1028,7 +994,12 @@ fo.pvec[2] = pvec2_ * static_cast(nsf); fo.pvec[3] = pvec3_ * static_cast(nsf); fo.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; //const float sqp0p3 = sqrtf( pvec0 + pvec3 ) * nsf; // AV: why force a float here? // pvec0+pvec3 is a cancelling difference of two ~|p| numbers for a // backward-moving massless fermion, and everything below divides by it. @@ -1040,21 +1011,11 @@ pvec0 + pvec3 ); const fptype_amp_sv sqp0p3 = fpsqrt( p0p3stable ) * static_cast(nsf); const cxtype_amp_sv chi0 = cxmake( sqp0p3, 0. ); - const cxtype_amp_sv chi1 = cxmake( (fptype_amp)nh * pvec1 / sqp0p3, -pvec2 / sqp0p3 ); - if( nh == 1 ) - { - w[0] = chi0; - w[1] = chi1; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } - else - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi1; - w[3] = chi0; - } + const cxtype_amp_sv chi1 = cxmake( nh * pvec1 / sqp0p3, -pvec2 / sqp0p3 ); + w[0] = tp * chi0; + w[1] = tp * chi1; + w[2] = tm * chi1; + w[3] = tm * chi0; mgDebug( 1, __FUNCTION__ ); return; } diff --git a/madgraph/iolibs/template_files/madmatrix/backend/common/Density.cc b/madgraph/iolibs/template_files/madmatrix/backend/common/Density.cc index f0a04883ba..0b8870a3d7 100644 --- a/madgraph/iolibs/template_files/madmatrix/backend/common/Density.cc +++ b/madgraph/iolibs/template_files/madmatrix/backend/common/Density.cc @@ -199,7 +199,7 @@ namespace madmatrix for( int n = 0; n < nChanging; n++ ) row[pos0[n]] = allowHel[icomb * nChanging + n]; cxtype_amp_sv* jamp_sv = &s.jamps[(size_t)icomb * nParity * njampso]; for( int i = 0; i < nParity * njampso; i++ ) jamp_sv[i] = cxzero_sv(); - calculate_jamps( row, allmomenta, allcouplings, allflavors, jamp_sv, false, numerators, denominators, nullptr, ievt00 ); + calculate_jamps( row, nullptr, allmomenta, allcouplings, allflavors, jamp_sv, false, numerators, denominators, nullptr, ievt00 ); } bool nonzero = false; for( int iParity = 0; iParity < nParity; iParity++ ) diff --git a/madgraph/iolibs/template_files/madmatrix/backend/cpu/SigmaKin.cc b/madgraph/iolibs/template_files/madmatrix/backend/cpu/SigmaKin.cc index 79fe87609f..1ab1bbcb8c 100644 --- a/madgraph/iolibs/template_files/madmatrix/backend/cpu/SigmaKin.cc +++ b/madgraph/iolibs/template_files/madmatrix/backend/cpu/SigmaKin.cc @@ -193,6 +193,29 @@ namespace madmatrix //-------------------------------------------------------------------------- + // The helicities of the npar external legs for the events of one page (neppV events, + // one SIMD lane each, or one event without SIMD), as the external wavefunctions take + // them: the helicities hel[npar] for every event, or the helicity allihel[ievt] of each event. + static inline void + fillHelicities( fptype_amp_sv* hel_sv, // output: hel_sv[npar] helicities of the external legs + const short* hel, // input: helicity of each leg, hel[npar], of all events (if allihel == nullptr) + const int* allihel, // input: helicity index [0,ncomb) of each event[nevt] (nullptr: hel for all events) + const int ievt0 ) // input: first event number in current C++ event page + { +#ifdef MGONGPU_CPPSIMD + for( int ieppV = 0; ieppV < neppV; ieppV++ ) + { + const short* heli = ( allihel != nullptr ? cHel[allihel[ievt0 + ieppV]] : hel ); + for( int ipar = 0; ipar < npar; ipar++ ) hel_sv[ipar][ieppV] = heli[ipar]; + } +#else + const short* heli = ( allihel != nullptr ? cHel[allihel[ievt0]] : hel ); + for( int ipar = 0; ipar < npar; ipar++ ) hel_sv[ipar] = heli[ipar]; +#endif + } + + //-------------------------------------------------------------------------- + // Accumulate a multichannel numerator contribution in place. In C++ each // event page is processed serially within the helicity loop, so a plain // sum suffices (CUDA needs atomicAdd instead: see backend/gpu/SigmaKin.cc). @@ -204,9 +227,12 @@ namespace madmatrix // moved to a separate function/kernel). This function processes a single event "page" // or SIMD vector (or for two in "mixed" precision mode, nParity=2). Accepts a SCALAR // channelId because it is GUARANTEED that all events in a SIMD vector have the same - // channelId #898. + // channelId #898. The helicity, on the other hand, may differ from one event (SIMD lane) + // to the next: the external wavefunctions are branch-free in the helicity (see helas.h), + // so that the events of a page with different helicities are computed in lock-step. void - calculate_jamps( const short* hel, // input: helicity of each leg, hel[npar] (a row of cHel, or not: see Density.cc) + calculate_jamps( const short* hel, // input: helicity of each leg, hel[npar], of all events (if allihel == nullptr): a row of cHel, or not (see Density.cc) + const int* allihel, // input: helicity index [0,ncomb) of each event[nevt] (nullptr: hel for all events) const fptype_momenta* allmomenta, // input: momenta[nevt*npar*4] const fptype* allcouplings, // input: couplings[nevt*ndcoup*2] const unsigned int* iflavorVec, // input: indices of the flavor combinations @@ -227,16 +253,6 @@ namespace madmatrix using NUM_ACCESS = HostAccessNumerators; // non-trivial access: buffer includes all events mgDebug( 0, __FUNCTION__ ); - // EvaluateDiagrams.inc reads the helicities as cHel[ihel][ipar]. Shadow the - // helicity table with the one row this call evaluates, so that the row need - // not be one of cHel at all: the density matrix builds combinations that a - // polarised process leaves out of its table (sigmaKin_density, Density.cc). -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wshadow" - const short( *cHel )[npar] = reinterpret_cast( hel ); -#pragma GCC diagnostic pop - constexpr int ihel = 0; - // Local TEMPORARY variables for a subset of Feynman diagrams in the given C++ event // page (ipagV) [NB these variables are reused several times (and re-initialised each // time) within the same event or event page]. Create memory for both momenta and @@ -292,6 +308,10 @@ namespace madmatrix for( size_t iicoup = 0; iicoup < nIPC; iicoup++ ) COUPs[ndcoup + iicoup] = allCOUPs[ndcoup + iicoup]; // independent couplings, fixed for all events fptype_amp* numerators = NUM_ACCESS::ieventAccessRecord( allNumerators, ievt0 * ndiagrams ); + // The helicities of the external legs for the events of this page, one per SIMD lane: + // the same helicities hel for all of them, or the helicity allihel of each event + fptype_amp_sv hel_sv[npar]; + fillHelicities( hel_sv, hel, allihel, ievt0 ); // Create an array of views over the Flavor Couplings FLV_COUPLING_ARRAY flvCOUPs{ cIPF_partner1, cIPF_partner2, cIPF_value }; @@ -414,7 +434,7 @@ namespace madmatrix #else cxtype_amp_sv jamp_sv[njampso] = {}; // all zeros #endif - calculate_jamps( cHel[ihel], allmomenta, allcouplings, hgFlavorVec, jamp_sv, false, allNumerators, allDenominators, jamp2_sv, ievt00 ); + calculate_jamps( cHel[ihel], nullptr, allmomenta, allcouplings, hgFlavorVec, jamp_sv, false, allNumerators, allDenominators, jamp2_sv, ievt00 ); color_sum_cpu( allMEs, jamp_sv, ievt00 ); for( int ieppV = 0; ieppV < neppV; ++ieppV ) { @@ -569,7 +589,8 @@ namespace madmatrix unsigned int* allDiagramIdsOut, // output: multichannel channelIds[nevt] (1 to #diagrams) bool mulChannelWeight, // if true, multiply channel weight to ME output const int nevt, // input: #events (for cuda: nevt == ndim == gpublocks*gputhreads) - const bool sampleSignedHelicity ) // input: choose the helicity on |T_i| and return sign(T_i)*sum|T| (split orders only) + const bool sampleSignedHelicity, // input: choose the helicity on |T_i| and return sign(T_i)*sum|T| (split orders only) + const int* allihel ) // input: helicity index [0,ncomb) of each event[nevt]; nullptr to sum over the good helicities { mgDebugInitialise(); @@ -606,6 +627,10 @@ namespace madmatrix // === PART 1 - HELICITY LOOP: CALCULATE WAVEFUNCTIONS === // (using precomputed good helicities) + // With allihel, each event has a single helicity of its own instead: the "loop" is one + // evaluation in which the events of a SIMD page carry different helicities in lock-step, + // and everything below (color sum, color and channel choice) runs on that one helicity. + const int nhelEval = ( allihel != nullptr ? 1 : cNGoodHel ); #if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT // Mixed fptypes #537: float for color algebra and double elsewhere // Delay color algebra and ME updates (only on even pages) @@ -616,7 +641,7 @@ namespace madmatrix #endif #ifdef _OPENMP // OMP multithreading #575 (NB: tested only with gcc11 so far) -#define _OMPLIST0 allcouplings, allMEs, allmomenta, allrndcol, allrndhel, allselcol, allselhel, cGoodHel, cNGoodHel, npagV2, sampleSignedHelicity +#define _OMPLIST0 allcouplings, allMEs, allmomenta, allrndcol, allrndhel, allselcol, allselhel, allihel, cGoodHel, nhelEval, npagV2, sampleSignedHelicity #define _OMPLIST1 , allDenominators, allNumerators, allChannelIds, mgOnGpu::icolamp, mgOnGpu::channel2iconfig #pragma omp parallel for default( none ) shared( _OMPLIST0 _OMPLIST1 ) #undef _OMPLIST0 @@ -645,31 +670,35 @@ namespace madmatrix if( ColorMatrixData::shouldUseBlas ) { static thread_local std::vector ghelJamp_sv( (size_t)ncomb * nParity * ncolor ); - for( int ighel = 0; ighel < cNGoodHel; ighel++ ) + for( int ighel = 0; ighel < nhelEval; ighel++ ) { - const int ihel = cGoodHel[ighel]; + const int ihel = ( allihel != nullptr ? -1 : cGoodHel[ighel] ); // unused with allihel cxtype_sv* jamp_sv = ghelJamp_sv.data() + (size_t)ighel * nParity * ncolor; for( int i = 0; i < nParity * ncolor; i++ ) jamp_sv[i] = cxzero_sv(); // calculate_jamps accumulates into jamp_sv // **NB! in "mixed" precision, using SIMD, calculate_jamps computes MEs for TWO neppV pages with a single channelId! #924 bool storeChannelWeights = allChannelIds != nullptr || allrnddiagram != nullptr; - calculate_jamps( cHel[ihel], allmomenta, allcouplings, iflavorVec, jamp_sv, storeChannelWeights, allNumerators, allDenominators, jamp2_sv, ievt00 ); + calculate_jamps( ( allihel != nullptr ? nullptr : cHel[ihel] ), allihel, allmomenta, allcouplings, iflavorVec, jamp_sv, storeChannelWeights, allNumerators, allDenominators, jamp2_sv, ievt00 ); } + // nhelEval may be 0 (no good helicity): the color sum of no helicity leaves allMEs at 0 + if( nhelEval > 0 ) + { #if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT - color_sum_cpu_blas( allMEs, MEs_ighel, MEs_ighel2, ghelJamp_sv.data(), cNGoodHel, ievt00 ); + color_sum_cpu_blas( allMEs, MEs_ighel, MEs_ighel2, ghelJamp_sv.data(), nhelEval, ievt00 ); #else - color_sum_cpu_blas( allMEs, MEs_ighel, nullptr, ghelJamp_sv.data(), cNGoodHel, ievt00 ); + color_sum_cpu_blas( allMEs, MEs_ighel, nullptr, ghelJamp_sv.data(), nhelEval, ievt00 ); #endif + } } else #endif // MGONGPU_CPP_HAS_BLAS { - for( int ighel = 0; ighel < cNGoodHel; ighel++ ) + for( int ighel = 0; ighel < nhelEval; ighel++ ) { - const int ihel = cGoodHel[ighel]; + const int ihel = ( allihel != nullptr ? -1 : cGoodHel[ighel] ); // unused with allihel cxtype_amp_sv jamp_sv[nParity * njampso] = {}; // fixed nasty bug (omitting 'nParity' caused memory corruptions after calling calculate_jamps) // **NB! in "mixed" precision, using SIMD, calculate_jamps computes MEs for TWO neppV pages with a single channelId! #924 bool storeChannelWeights = allChannelIds != nullptr || allrnddiagram != nullptr; - calculate_jamps( cHel[ihel], allmomenta, allcouplings, iflavorVec, jamp_sv, storeChannelWeights, allNumerators, allDenominators, jamp2_sv, ievt00 ); + calculate_jamps( ( allihel != nullptr ? nullptr : cHel[ihel] ), allihel, allmomenta, allcouplings, iflavorVec, jamp_sv, storeChannelWeights, allNumerators, allDenominators, jamp2_sv, ievt00 ); color_sum_cpu( allMEs, jamp_sv, ievt00 ); MEs_ighel[ighel] = E_ACCESS::kernelAccess( E_ACCESS::ieventAccessRecord( allMEs, ievt00 ) ); #if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT @@ -682,6 +711,16 @@ namespace madmatrix for( int ieppV = 0; ieppV < neppV; ++ieppV ) { const int ievt = ievt00 + ieppV; + if( allihel != nullptr ) + { + // No choice: each event was evaluated for its given helicity only, whose |M|^2 (signed, + // with an interference) is the color sum above + allselhel[ievt] = allihel[ievt] + 1; // NB Fortran [1,ncomb], cudacpp [0,ncomb-1] +#if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT + allselhel[ievt + neppV] = allihel[ievt + neppV] + 1; +#endif + continue; + } if constexpr( nampso > 1 ) { if( sampleSignedHelicity ) diff --git a/madgraph/iolibs/template_files/madmatrix/backend/cpu/SigmaKin.h b/madgraph/iolibs/template_files/madmatrix/backend/cpu/SigmaKin.h index 6549e30940..7db8e7e00f 100644 --- a/madgraph/iolibs/template_files/madmatrix/backend/cpu/SigmaKin.h +++ b/madgraph/iolibs/template_files/madmatrix/backend/cpu/SigmaKin.h @@ -56,7 +56,11 @@ namespace madmatrix // input: when the |M|^2 keeps an interference (squared split orders, nampso > 1), // choose the helicity on |T_i| and return sign(T_i) * sum_j |T_j| as madevent does, // so that the chosen helicity is exact; false returns sum_j T_j (no effect otherwise) - const bool sampleSignedHelicity = false ); + const bool sampleSignedHelicity = false, + // input: the helicity index [0,ncomb) of each event, which is then evaluated for that + // helicity alone (and returns its contribution to the helicity-summed |M|^2, see + // umami_matrix_element); nullptr, the default, sums over the good helicities + const int* allihel = nullptr ); // Setters: called once by CPPProcess (P1-generated) to populate this file's // otherwise-internal storage, since it can no longer be written directly @@ -74,7 +78,8 @@ namespace madmatrix // helicity table cHel[ncomb][npar] (row major) the combinations are built // from, and the identical-particle factor of a flavor combination. void - calculate_jamps( const short* hel, // input: helicity of each leg, hel[npar] + calculate_jamps( const short* hel, // input: helicity of each leg, hel[npar], of all events (if allihel == nullptr) + const int* allihel, // input: helicity index [0,ncomb) of each event[nevt] (nullptr: hel for all events) const fptype_momenta* allmomenta, // input: momenta[nevt*npar*4] const fptype* allcouplings, // input: couplings[nevt*ndcoup*2] const unsigned int* iflavorVec, // input: indices of the flavor combinations diff --git a/madgraph/iolibs/template_files/madmatrix/backend/cpu/umami.cc b/madgraph/iolibs/template_files/madmatrix/backend/cpu/umami.cc index 419c0310e0..e9d591dc4b 100644 --- a/madgraph/iolibs/template_files/madmatrix/backend/cpu/umami.cc +++ b/madgraph/iolibs/template_files/madmatrix/backend/cpu/umami.cc @@ -245,8 +245,8 @@ extern "C" UmamiStatus umami_supported_inputs( bool const** supported, int* count ) { // MOMENTA, ALPHA_S, FLAVOR_INDEX, RANDOM_COLOR, RANDOM_HELICITY, RANDOM_DIAGRAM, - // HELICITY_INDEX=false, DIAGRAM_INDEX=true, CHANNEL_INDEX=false - static const bool data[UMAMI_INPUT_KEY_COUNT] = { true, true, true, true, true, true, false, true }; + // HELICITY_INDEX, DIAGRAM_INDEX=true, CHANNEL_INDEX=false + static const bool data[UMAMI_INPUT_KEY_COUNT] = { true, true, true, true, true, true, true, true }; *supported = data; *count = UMAMI_INPUT_KEY_COUNT; return UMAMI_SUCCESS; @@ -338,6 +338,7 @@ extern "C" const double* random_color_in = nullptr; const double* random_helicity_in = nullptr; const double* random_diagram_in = nullptr; + const int* helicity_in = nullptr; [[maybe_unused]] const int* diagram_in = nullptr; // TODO: unused for( std::size_t i = 0; i < input_count; ++i ) @@ -364,7 +365,8 @@ extern "C" random_diagram_in = static_cast( input ); break; case UMAMI_IN_HELICITY_INDEX: - return UMAMI_ERROR_UNSUPPORTED_INPUT; + helicity_in = static_cast( input ); + break; case UMAMI_IN_DIAGRAM_INDEX: diagram_in = static_cast( input ); break; @@ -373,6 +375,20 @@ extern "C" } } if( !momenta_in ) return UMAMI_ERROR_MISSING_INPUT; + // UMAMI_IN_HELICITY_INDEX: evaluate each event for the one helicity it is given, an index + // into the full helicity table [0,ncomb) (the UMAMI_OUT_HELICITY_INDEX convention), instead + // of summing over the helicities. The |M|^2 is then the contribution of that helicity to + // the helicity-summed (and averaged) |M|^2, so that the sum over all ncomb helicities gives + // back the |M|^2 without this input; UMAMI_OUT_HELICITY_INDEX returns the given helicity, + // the color and diagram are chosen for it, and UMAMI_IN_RANDOM_HELICITY is not used. + if( helicity_in ) + { + for( std::size_t i_event = 0; i_event < count; ++i_event ) + { + const int ihel = helicity_in[i_event + offset]; + if( ihel < 0 || ihel >= ProcessData::ncomb ) return UMAMI_ERROR; + } + } double* m2_out = nullptr; double* amp2_out = nullptr; @@ -459,6 +475,7 @@ extern "C" HostBufferBase denominators( rounded_count ); HostBufferBase helicity_index( rounded_count ); HostBufferBase color_index( rounded_count ); + HostBufferBase helicity_given( rounded_count ); // as the other inputs, the padding lanes copy their source event // The input event of each lane. The padding lanes, which complete the last page // of a flavor, are copies of the first event of their page, as on GPUs. Left at // zero (momenta and g_s), they evaluated to nan, and the good-helicity filter of @@ -485,6 +502,7 @@ extern "C" diagram_random[lane] = random_diagram_in ? random_diagram_in[i_event + offset] : 0.5; g_s[lane] = alpha_s_in ? sqrt( 4 * M_PI * alpha_s_in[i_event + offset] ) : 1.2177157847767195; flavor_indices[lane] = flavor_indices_in ? flavor_indices_in[i_event + offset] : 0; + if( helicity_in ) helicity_given[lane] = helicity_in[i_event + offset]; } computeDependentCouplings( g_s.data(), couplings.data(), rounded_count ); @@ -522,7 +540,8 @@ extern "C" // an interference |M|^2, see select_helicity_signed, unless the instance was asked // to keep the helicity sum (interference_helicity_summed); without the random number // the |M|^2 stays the helicity sum (systematics re-evaluation, standalone checks) - random_helicity_in != nullptr && !instance->summed_helicity ); + random_helicity_in != nullptr && !instance->summed_helicity, + helicity_in ? helicity_given.data() : nullptr ); if ( sort_flavors ) { diff --git a/madgraph/iolibs/template_files/madmatrix/backend/gpu/SigmaKin.cc b/madgraph/iolibs/template_files/madmatrix/backend/gpu/SigmaKin.cc index 4fb7d8e33c..e9418f333f 100644 --- a/madgraph/iolibs/template_files/madmatrix/backend/gpu/SigmaKin.cc +++ b/madgraph/iolibs/template_files/madmatrix/backend/gpu/SigmaKin.cc @@ -183,18 +183,22 @@ namespace madmatrix // Also compute running sums over helicities adding jamp2, numerator, denominator // (NB: this function no longer handles matrix elements as the color sum has now been // moved to a separate function/kernel). This is a kernel function: it processes a - // single event (the CUDA thread), and takes a channelId array as input. + // single event (the CUDA thread), and takes a channelId array as input. The helicity + // may differ from one thread to the next (allihel): the external wavefunctions are + // branch-free in the helicity (see helas.h), so the threads of a warp do not diverge. __global__ void /* clang-format off */ - calculate_jamps( int ihel, + calculate_jamps( int ihel, // input: helicity index [0,ncomb) of all events (if !processAllHelicities and allihel == nullptr) + const int* allihel, // input: helicity index [0,ncomb) of each event[nevt] (nullptr: ihel, or the good helicity blockIdx.y) const fptype_momenta* allmomenta, // input: momenta[nevt*npar*4] const fptype* allcouplings, // input: couplings[nevt*ndcoup*2] const unsigned int* iflavorVec, // input: indices of the flavor combinations - fptype_amp* allJamps, // output: jamp[2*njampso*nevt] buffer for one helicity _within a super-buffer for dcNGoodHel helicities_ + fptype_amp* allJamps, // output: jamp[2*njampso*nevt] buffer for one helicity _within a super-buffer for nhel helicities_ bool storeChannelWeights, fptype_amp* allNumerators, // input/output: multichannel numerators[nevt], add helicity ihel fptype_amp* allDenominators, // input/output: multichannel denominators[nevt], add helicity ihel fptype_amp* colAllJamp2s, // output: allJamp2s[ncolor_flow][nevt] super-buffer, sum over col/hel (nullptr to disable) const int nevt, // input: #events (nevt == ndim == gpublocks*gputhreads) + const int nhel, // input: #helicities in the allJamps super-buffer (nGoodHel, or 1 with allihel) const bool processAllHelicities ) // input: if true, use blockIdx.y to index helicities /* clang-format on */ { @@ -208,15 +212,21 @@ namespace madmatrix using NUM_ACCESS = DeviceAccessNumerators; // non-trivial access: buffer includes all events mgDebug( 0, __FUNCTION__ ); + const int ievt = blockDim.x * blockIdx.x + threadIdx.x; // index of event (thread) in grid if( processAllHelicities ) { int ighel = blockIdx.y; - ihel = dcGoodHel[ighel]; + if( allihel == nullptr ) ihel = dcGoodHel[ighel]; allJamps = allJamps + ighel * nevt; // NB: the numerators buffer has NO helicity dimension anymore: all good-helicity blocks // for a given event accumulate in place into the same [nevt][ndiagrams] slot via atomicAdd. // The denominators are no longer accumulated here (derived as the sum of numerators later). } + // One helicity per event (nhel == 1 and gridDim.y == 1): the given helicity of this event + if( allihel != nullptr ) ihel = allihel[ievt]; + // The helicities of the external legs for this event, as the external wavefunctions take them + fptype_amp_sv hel_sv[npar]; + for( int ipar = 0; ipar < npar; ipar++ ) hel_sv[ipar] = cHel[ihel][ipar]; // Local TEMPORARY variables for a subset of Feynman diagrams in the given CUDA event // (ievt) [NB these variables are reused several times (and re-initialised each time) @@ -271,7 +281,6 @@ namespace madmatrix const fptype_momenta* momenta = allmomenta; const fptype* COUPs[nxcoup > 0 ? nxcoup : 1]; for( size_t ixcoup = 0; ixcoup < nxcoup; ixcoup++ ) COUPs[ixcoup] = allCOUPs[ixcoup]; - const int ievt = blockDim.x * blockIdx.x + threadIdx.x; // index of event (thread) in grid fptype_amp* numerators = &allNumerators[ievt * ndiagrams]; // Create an array of views over the Flavor Couplings FLV_COUPLING_ARRAY flvCOUPs{ cIPF_partner1, cIPF_partner2, cIPF_value }; @@ -323,13 +332,13 @@ namespace madmatrix // *** PREPARE OUTPUT JAMPS *** // allJamps already points at this helicity's slot (ighel * nevt) in the - // [2][ncolor][dcNGoodHel][nevt] super-buffer, hence ihel0 = 0; nhel stays - // dcNGoodHel because it is the color stride, and color_sum reads with it. + // [2][ncolor][nhel][nevt] super-buffer, hence ihel0 = 0; nhel stays the + // number of helicities because it is the color stride, and color_sum reads with it. { constexpr int ihel0 = 0; using J_ACCESS = DeviceAccessJamp; for( int icol = 0; icol < njampso; icol++ ) - J_ACCESS::kernelAccessIcolIhelNhel( allJamps, icol, ihel0, dcNGoodHel ) = jamp_sv[icol]; + J_ACCESS::kernelAccessIcolIhelNhel( allJamps, icol, ihel0, nhel ) = jamp_sv[icol]; } mgDebug( 1, __FUNCTION__ ); @@ -370,12 +379,11 @@ namespace madmatrix const int gpublocks = 1; const int gputhreads = maxtry; constexpr int nOneHel = 1; // use a jamp buffer for a single helicity - gpuMemcpyToSymbol( dcNGoodHel, &nOneHel, sizeof( int ) ); // NEW IMPLEMENTATION OF GETGOODHEL (#630): RESET THE RUNNING SUM OVER HELICITIES TO 0 BEFORE ADDING A NEW HELICITY gpuMemset( allMEs, 0, maxtry * sizeof( fptype ) ); // NB: color_sum ADDS |M|^2 for one helicity to the running sum of |M|^2 over helicities for the given event(s) constexpr fptype_amp_sv* allJamp2s = nullptr; // no need for color selection during helicity filtering - gpuLaunchKernel( calculate_jamps, gpublocks, gputhreads, ihel, allmomenta, allcouplings, devFlavorVec, allJamps, false, allNumerators, allDenominators, allJamp2s, gpublocks * gputhreads, false ); + gpuLaunchKernel( calculate_jamps, gpublocks, gputhreads, ihel, nullptr, allmomenta, allcouplings, devFlavorVec, allJamps, false, allNumerators, allDenominators, allJamp2s, gpublocks * gputhreads, nOneHel, false ); gpuLaunchKernel( color_sum_kernel, gpublocks, gputhreads, allMEs, allJamps, nOneHel, 0 ); gpuMemcpy( hstMEs, allMEs, maxtry * sizeof( fptype ), gpuMemcpyDeviceToHost ); for( int ievt = 0; ievt < maxtry; ++ievt ) @@ -518,9 +526,18 @@ namespace madmatrix fptype* ghelAllMEs, // input/tmp: allMEs for nGoodHel <= ncomb individual/runningsum helicities (index is ighel) fptype* allMEs, // output: allMEs[nevt], final sum over helicities const int nevt, // input: #events (for cuda: nevt == ndim == gpublocks*gputhreads) - const bool sampleSignedHelicity ) // input: see sigmaKin + const bool sampleSignedHelicity, // input: see sigmaKin + const int* allihel ) // input: helicity index [0,ncomb) of each event[nevt] (nullptr: choose one) { const int ievt = blockDim.x * blockIdx.x + threadIdx.x; // index of event (thread) + if( allihel != nullptr ) + { + // No sum and no choice: each event was evaluated for its given helicity only, whose + // |M|^2 (signed, with an interference) is the one entry of ghelAllMEs (nhel == 1) + allMEs[ievt] = ghelAllMEs[ievt]; + allselhel[ievt] = allihel[ievt] + 1; // NB Fortran [1,ncomb], cudacpp [0,ncomb-1] + return; + } // no helicity (0) unless one is chosen below: a vanishing or NaN |M|^2 chooses none, and // allselhel is not cleared beforehand (the CPU buffers are zero-initialised) allselhel[ievt] = 0; @@ -690,7 +707,8 @@ namespace madmatrix const bool async, const int gpublocks, // input: cuda gpublocks const int gputhreads, // input: cuda gputhreads - const bool sampleSignedHelicity ) // input: choose the helicity on |T_i| and return sign(T_i)*sum|T| (split orders only) + const bool sampleSignedHelicity, // input: choose the helicity on |T_i| and return sign(T_i)*sum|T| (split orders only) + const int* allihel ) // input: helicity index [0,ncomb) of each event[nevt]; nullptr to sum over the good helicities /* clang-format on */ { mgDebugInitialise(); @@ -711,15 +729,19 @@ namespace madmatrix // Reset the "matrix elements" - running sums of |M|^2 over helicities for the given event // (the sizes are computed in size_t, from sizeof: the int product could overflow) const int nevt = gpublocks * gputhreads; + // With allihel, each event has a single helicity of its own instead of the sum over the good + // helicities: one evaluation (nhel == 1) in which the threads carry different helicities in + // lock-step, and the color sum, the color and channel choice all run on that one helicity. + const int nhel = ( allihel != nullptr ? 1 : cNGoodHel ); gpuMemsetAsync( allMEs, 0, sizeof( fptype ) * nevt, stream ); - gpuMemsetAsync( ghelAllJamps, 0, sizeof( fptype_amp ) * cNGoodHel * njampso * mgOnGpu::nx2 * nevt, stream ); + gpuMemsetAsync( ghelAllJamps, 0, sizeof( fptype_amp ) * nhel * njampso * mgOnGpu::nx2 * nevt, stream ); gpuMemsetAsync( colAllJamp2s, 0, sizeof( fptype_amp ) * ncolor_flow * nevt, stream ); // The numerators buffer has NO helicity dimension: all good helicities accumulate in place via // atomicAdd, so it is zeroed once as [nevt][ndiagrams]. The denominators are derived from the // numerators in normalise_output, so the buffer is just [nevt]. gpuMemsetAsync( ghelAllNumerators, 0, sizeof( fptype_amp ) * ndiagrams * nevt, stream ); gpuMemsetAsync( ghelAllDenominators, 0, sizeof( fptype_amp ) * nevt, stream ); - gpuMemsetAsync( ghelAllMEs, 0, sizeof( fptype ) * cNGoodHel * nevt, stream ); + gpuMemsetAsync( ghelAllMEs, 0, sizeof( fptype ) * nhel * nevt, stream ); // === PART 1 - HELICITY LOOP: CALCULATE WAVEFUNCTIONS (one event per GPU thread) === @@ -730,28 +752,28 @@ namespace madmatrix // every Wilson coefficient at zero): nothing to compute, the |M|^2 stays 0 as on the CPU (a // launch with gridDim.y = 0 is an invalid configuration) bool storeChannelWeights = allChannelIds != nullptr || allrnddiagram != nullptr; - if( cNGoodHel > 0 && async ) + if( nhel > 0 && async ) { - gpuLaunchKernel2D( calculate_jamps, gpublocks, cNGoodHel, gputhreads, ghelStreams[0], 0, allmomenta, allcouplings, iflavorVec, ghelAllJamps, storeChannelWeights, ghelAllNumerators, ghelAllDenominators, colAllJamp2s, nevt, true ); - color_sum_gpu( ghelAllMEs, ghelAllJamps, ghelAllBlasTmp, pBlasHandle, ghelStreams, cNGoodHel, gpublocks, gputhreads, true ); + gpuLaunchKernel2D( calculate_jamps, gpublocks, nhel, gputhreads, ghelStreams[0], 0, allihel, allmomenta, allcouplings, iflavorVec, ghelAllJamps, storeChannelWeights, ghelAllNumerators, ghelAllDenominators, colAllJamp2s, nevt, nhel, true ); + color_sum_gpu( ghelAllMEs, ghelAllJamps, ghelAllBlasTmp, pBlasHandle, ghelStreams, nhel, gpublocks, gputhreads, true ); } - else if( cNGoodHel > 0 ) + else if( nhel > 0 ) { - for( int ighel = 0; ighel < cNGoodHel; ighel++ ) + for( int ighel = 0; ighel < nhel; ighel++ ) { - const int ihel = cGoodHel[ighel]; + const int ihel = ( allihel != nullptr ? 0 : cGoodHel[ighel] ); // unused with allihel fptype_amp* hAllJamps = ghelAllJamps + ighel * nevt; // HACK: bypass DeviceAccessJamp (consistent with layout defined there) // NB: the numerators buffer has no helicity dimension: every helicity stream accumulates in place // into the same [nevt][ndiagrams] slot via atomicAdd. The denominators are derived later. - gpuLaunchKernelStream( calculate_jamps, gpublocks, gputhreads, ghelStreams[ighel], ihel, allmomenta, allcouplings, iflavorVec, hAllJamps, storeChannelWeights, ghelAllNumerators, ghelAllDenominators, colAllJamp2s, nevt, false ); + gpuLaunchKernelStream( calculate_jamps, gpublocks, gputhreads, ghelStreams[ighel], ihel, allihel, allmomenta, allcouplings, iflavorVec, hAllJamps, storeChannelWeights, ghelAllNumerators, ghelAllDenominators, colAllJamp2s, nevt, nhel, false ); } // (2) Then compute the ME for that helicity from the color sum of QCD partial amplitudes jamps - color_sum_gpu( ghelAllMEs, ghelAllJamps, ghelAllBlasTmp, pBlasHandle, ghelStreams, cNGoodHel, gpublocks, gputhreads, false ); + color_sum_gpu( ghelAllMEs, ghelAllJamps, ghelAllBlasTmp, pBlasHandle, ghelStreams, nhel, gpublocks, gputhreads, false ); checkGpu( gpuDeviceSynchronize() ); // do not start helicity/color selection until the loop over helicities has completed // (3) Wait for all helicity streams to complete, then finally compute the ME sum over all helicities and choose one helicity and one color } // Event-by-event random choice of helicity #403 and ME sum over helicities (defer this after the helicity loop to avoid breaking streams parallelism) - gpuLaunchKernelStream( add_and_select_hel, gpublocks, gputhreads, stream, allselhel, allrndhel, ghelAllMEs, allMEs, gpublocks * gputhreads, sampleSignedHelicity ); + gpuLaunchKernelStream( add_and_select_hel, gpublocks, gputhreads, stream, allselhel, allrndhel, ghelAllMEs, allMEs, gpublocks * gputhreads, sampleSignedHelicity, allihel ); gpuLaunchKernelStream( normalise_output, gpublocks, gputhreads, stream, allMEs, iflavorVec, ghelAllNumerators, ghelAllDenominators, allChannelIds, storeChannelWeights, mulChannelWeight, helcolDenominators[0] ); diff --git a/madgraph/iolibs/template_files/madmatrix/backend/gpu/SigmaKin.h b/madgraph/iolibs/template_files/madmatrix/backend/gpu/SigmaKin.h index dad3d88bbb..83fb696625 100644 --- a/madgraph/iolibs/template_files/madmatrix/backend/gpu/SigmaKin.h +++ b/madgraph/iolibs/template_files/madmatrix/backend/gpu/SigmaKin.h @@ -65,7 +65,11 @@ namespace madmatrix // input: when the |M|^2 keeps an interference (squared split orders, nampso > 1), // choose the helicity on |T_i| and return sign(T_i) * sum_j |T_j| as madevent does, // so that the chosen helicity is exact; false returns sum_j T_j (no effect otherwise) - const bool sampleSignedHelicity = false ); + const bool sampleSignedHelicity = false, + // input: the helicity index [0,ncomb) of each event (a device array), which is then + // evaluated for that helicity alone (and returns its contribution to the helicity-summed + // |M|^2, see umami_matrix_element); nullptr, the default, sums over the good helicities + const int* allihel = nullptr ); // Setters: called once by CPPProcess (P1-generated) to populate this file's // otherwise-internal storage, since it can no longer be written directly diff --git a/madgraph/iolibs/template_files/madmatrix/backend/gpu/umami.cc b/madgraph/iolibs/template_files/madmatrix/backend/gpu/umami.cc index 74335b434b..a77515f358 100644 --- a/madgraph/iolibs/template_files/madmatrix/backend/gpu/umami.cc +++ b/madgraph/iolibs/template_files/madmatrix/backend/gpu/umami.cc @@ -120,12 +120,14 @@ namespace const double* diagram_random_in, const double* alpha_s_in, const unsigned int* flavor_indices_in, + const int* helicity_in, fptype_momenta* momenta, fptype* helicity_random, fptype* color_random, fptype* diagram_random, fptype* g_s, unsigned int* flavor_indices, + int* helicity_given, std::size_t count, std::size_t stride, std::size_t offset ) @@ -144,6 +146,14 @@ namespace color_random[i_event] = color_random_in ? color_random_in[i_in + offset] : 0.5; g_s[i_event] = alpha_s_in ? sqrt( 4 * M_PI * alpha_s_in[i_in + offset] ) : 1.2177157847767195; flavor_indices[i_event] = flavor_indices_in ? flavor_indices_in[i_in + offset] : 0; + if( helicity_in ) + { + // an index out of [0,ncomb) cannot be reported from here (umami_matrix_element returns + // before the kernels run): evaluate helicity 0 instead, never out of the helicity table, + // and let copy_outputs flag the event (see umami_matrix_element) + const int ihel = helicity_in[i_in + offset]; + helicity_given[i_event] = ( ihel >= 0 && ihel < ProcessData::ncomb ? ihel : 0 ); + } } __global__ void copy_outputs( @@ -158,12 +168,24 @@ namespace int* diagram_out, int* color_out, int* helicity_out, + const int* helicity_in, std::size_t count, std::size_t stride, std::size_t offset ) { std::size_t i_event = blockDim.x * blockIdx.x + threadIdx.x; if( i_event >= count ) return; + if( helicity_in ) + { + const int ihel = helicity_in[i_event + offset]; + if( ihel < 0 || ihel >= ProcessData::ncomb ) + { + // an invalid given helicity: no |M|^2 and no helicity, see umami_matrix_element + if( m2_out ) m2_out[i_event + offset] = 0; + if( helicity_out ) helicity_out[i_event + offset] = -1; + return; + } + } if( m2_out ) m2_out[i_event + offset] = matrix_elements[i_event]; if( amp2_out ) @@ -333,8 +355,8 @@ extern "C" UmamiStatus umami_supported_inputs( bool const** supported, int* count ) { // MOMENTA, ALPHA_S, FLAVOR_INDEX, RANDOM_COLOR, RANDOM_HELICITY, RANDOM_DIAGRAM, - // HELICITY_INDEX=false, DIAGRAM_INDEX=true, CHANNEL_INDEX=false - static const bool data[UMAMI_INPUT_KEY_COUNT] = { true, true, true, true, true, true, false, true }; + // HELICITY_INDEX, DIAGRAM_INDEX=true, CHANNEL_INDEX=false + static const bool data[UMAMI_INPUT_KEY_COUNT] = { true, true, true, true, true, true, true, true }; *supported = data; *count = UMAMI_INPUT_KEY_COUNT; return UMAMI_SUCCESS; @@ -427,6 +449,7 @@ extern "C" const double* random_color_in = nullptr; const double* random_helicity_in = nullptr; const double* random_diagram_in = nullptr; + const int* helicity_in = nullptr; [[maybe_unused]] const int* diagram_in = nullptr; // TODO: unused for( std::size_t i = 0; i < input_count; ++i ) @@ -453,7 +476,8 @@ extern "C" random_diagram_in = static_cast( input ); break; case UMAMI_IN_HELICITY_INDEX: - return UMAMI_ERROR_UNSUPPORTED_INPUT; + helicity_in = static_cast( input ); + break; case UMAMI_IN_DIAGRAM_INDEX: diagram_in = static_cast( input ); break; @@ -462,6 +486,14 @@ extern "C" } } if( !momenta_in ) return UMAMI_ERROR_MISSING_INPUT; + // UMAMI_IN_HELICITY_INDEX: evaluate each event for the one helicity it is given, an index + // into the full helicity table [0,ncomb) (the UMAMI_OUT_HELICITY_INDEX convention), instead + // of summing over the helicities. The |M|^2 is then the contribution of that helicity to + // the helicity-summed (and averaged) |M|^2, so that the sum over all ncomb helicities gives + // back the |M|^2 without this input; UMAMI_OUT_HELICITY_INDEX returns the given helicity, + // the color and diagram are chosen for it, and UMAMI_IN_RANDOM_HELICITY is not used. The + // indices live on the device and are only read by the kernels, so that an index out of + // [0,ncomb) cannot fail the call as on the CPU: that event gets |M|^2 = 0 and helicity -1. gpuStream_t gpu_stream = nullptr; double* m2_out = nullptr; @@ -507,11 +539,11 @@ extern "C" fptype *couplings, *g_s, *helicity_random, *color_random, *diagram_random; fptype *matrix_elements, *ghel_matrix_elements; fptype_amp *color_jamps, *ghel_jamps; - int *helicity_index, *color_index; + int *helicity_index, *color_index, *helicity_given; unsigned int *flavor_indices, *diagram_index; std::size_t n_coup = madmatrix::Parameters_dependentCouplings::ndcoup; - std::array, 16> ptrs_and_sizes = {{ + std::array, 17> ptrs_and_sizes = {{ {reinterpret_cast(&momenta), rounded_count * ProcessData::npar * 4 * sizeof( fptype_momenta )}, {reinterpret_cast(&couplings), rounded_count * n_coup * 2 * sizeof( fptype )}, {reinterpret_cast(&g_s), rounded_count * sizeof( fptype )}, @@ -530,6 +562,7 @@ extern "C" {reinterpret_cast(&denominators), rounded_count * sizeof( fptype_amp )}, {reinterpret_cast(&helicity_index), rounded_count * sizeof( int )}, {reinterpret_cast(&color_index), rounded_count * sizeof( int )}, + {reinterpret_cast(&helicity_given), rounded_count * sizeof( int )}, {reinterpret_cast(&ghel_matrix_elements), rounded_count * ProcessData::ncomb * sizeof( fptype )}, // njampso = ncolor per amplitude split order (just ncolor without split orders) {reinterpret_cast(&ghel_jamps), rounded_count * ProcessData::ncomb * ProcessData::njampso * mgOnGpu::nx2 * sizeof( fptype_amp )}, @@ -557,12 +590,14 @@ extern "C" random_diagram_in, alpha_s_in, flavor_indices_in, + helicity_in, momenta, helicity_random, color_random, diagram_random, g_s, flavor_indices, + helicity_given, count, stride, offset ); @@ -611,7 +646,8 @@ extern "C" // an interference |M|^2 (see add_and_select_hel), unless the instance was asked to // keep the helicity sum (interference_helicity_summed); without the random number // the |M|^2 stays the helicity sum (systematics re-evaluation, standalone checks) - random_helicity_in != nullptr && !instance->summed_helicity ); + random_helicity_in != nullptr && !instance->summed_helicity, + helicity_in ? helicity_given : nullptr ); copy_outputs<<>>( denominators, @@ -625,6 +661,7 @@ extern "C" diagram_out, color_out, helicity_out, + helicity_in, count, stride, offset ); diff --git a/madgraph/iolibs/template_files/madmatrix/backend/simd/SigmaKin.cc b/madgraph/iolibs/template_files/madmatrix/backend/simd/SigmaKin.cc index 21bdda6bfe..994338c5b9 100644 --- a/madgraph/iolibs/template_files/madmatrix/backend/simd/SigmaKin.cc +++ b/madgraph/iolibs/template_files/madmatrix/backend/simd/SigmaKin.cc @@ -222,21 +222,21 @@ namespace madmatrix template inline void - ixxxxx( const fptype_momenta momenta[], const fptype_amp_v fmass, const int nhel, const int nsf, const int flv, ALOHAOBJ& fi, const int ipar ) + ixxxxx( const fptype_momenta momenta[], const fptype_amp_v fmass, const fptype_amp_sv nhel, const int nsf, const int flv, ALOHAOBJ& fi, const int ipar ) { wavefunction_per_lane_mass( fmass, fi, [&]( const fptype_amp m, ALOHAOBJ& out ) { ixxxxx( momenta, m, nhel, nsf, flv, out, ipar ); } ); } template inline void - oxxxxx( const fptype_momenta momenta[], const fptype_amp_v fmass, const int nhel, const int nsf, const int flv, ALOHAOBJ& fo, const int ipar ) + oxxxxx( const fptype_momenta momenta[], const fptype_amp_v fmass, const fptype_amp_sv nhel, const int nsf, const int flv, ALOHAOBJ& fo, const int ipar ) { wavefunction_per_lane_mass( fmass, fo, [&]( const fptype_amp m, ALOHAOBJ& out ) { oxxxxx( momenta, m, nhel, nsf, flv, out, ipar ); } ); } template inline void - vxxxxx( const fptype_momenta momenta[], const fptype_amp_v vmass, const int nhel, const int nsv, const int flv, ALOHAOBJ& vc, const int ipar ) + vxxxxx( const fptype_momenta momenta[], const fptype_amp_v vmass, const fptype_amp_sv nhel, const int nsv, const int flv, ALOHAOBJ& vc, const int ipar ) { wavefunction_per_lane_mass( vmass, vc, [&]( const fptype_amp m, ALOHAOBJ& out ) { vxxxxx( momenta, m, nhel, nsv, flv, out, ipar ); } ); } @@ -257,6 +257,29 @@ namespace madmatrix //-------------------------------------------------------------------------- + // The helicities of the npar external legs for the events of one page (neppV events, + // one SIMD lane each, or one event without SIMD), as the external wavefunctions take + // them: the helicities hel[npar] for every event, or the helicity allihel[ievt] of each event. + static inline void + fillHelicities( fptype_amp_sv* hel_sv, // output: hel_sv[npar] helicities of the external legs + const short* hel, // input: helicity of each leg, hel[npar], of all events (if allihel == nullptr) + const int* allihel, // input: helicity index [0,ncomb) of each event[nevt] (nullptr: hel for all events) + const int ievt0 ) // input: first event number in current C++ event page + { +#ifdef MGONGPU_CPPSIMD + for( int ieppV = 0; ieppV < neppV; ieppV++ ) + { + const short* heli = ( allihel != nullptr ? cHel[allihel[ievt0 + ieppV]] : hel ); + for( int ipar = 0; ipar < npar; ipar++ ) hel_sv[ipar][ieppV] = heli[ipar]; + } +#else + const short* heli = ( allihel != nullptr ? cHel[allihel[ievt0]] : hel ); + for( int ipar = 0; ipar < npar; ipar++ ) hel_sv[ipar] = heli[ipar]; +#endif + } + + //-------------------------------------------------------------------------- + // Accumulate a multichannel numerator contribution in place. In C++ each // event page is processed serially within the helicity loop, so a plain // sum suffices (CUDA needs atomicAdd instead: see backend/gpu/SigmaKin.cc). @@ -268,9 +291,12 @@ namespace madmatrix // moved to a separate function/kernel). This function processes a single event "page" // or SIMD vector (or for two in "mixed" precision mode, nParity=2). Accepts a SCALAR // channelId because it is GUARANTEED that all events in a SIMD vector have the same - // channelId #898. + // channelId #898. The helicity, on the other hand, may differ from one event (SIMD lane) + // to the next: the external wavefunctions are branch-free in the helicity (see helas.h), + // so that the events of a page with different helicities are computed in lock-step. void - calculate_jamps( const short* hel, // input: helicity of each leg, hel[npar] (a row of cHel, or not: see Density.cc) + calculate_jamps( const short* hel, // input: helicity of each leg, hel[npar], of all events (if allihel == nullptr): a row of cHel, or not (see Density.cc) + const int* allihel, // input: helicity index [0,ncomb) of each event[nevt] (nullptr: hel for all events) const fptype_momenta* allmomenta, // input: momenta[nevt*npar*4] const fptype* allcouplings, // input: couplings[nevt*ndcoup*2] const unsigned int* iflavorVec, // input: indices of the flavor combinations @@ -291,16 +317,6 @@ namespace madmatrix using NUM_ACCESS = HostAccessNumerators; // non-trivial access: buffer includes all events mgDebug( 0, __FUNCTION__ ); - // EvaluateDiagrams.inc reads the helicities as cHel[ihel][ipar]. Shadow the - // helicity table with the one row this call evaluates, so that the row need - // not be one of cHel at all: the density matrix builds combinations that a - // polarised process leaves out of its table (sigmaKin_density, Density.cc). -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wshadow" - const short( *cHel )[npar] = reinterpret_cast( hel ); -#pragma GCC diagnostic pop - constexpr int ihel = 0; - // Local TEMPORARY variables for a subset of Feynman diagrams in the given C++ event // page (ipagV) [NB these variables are reused several times (and re-initialised each // time) within the same event or event page]. Create memory for both momenta and @@ -356,6 +372,10 @@ namespace madmatrix for( size_t iicoup = 0; iicoup < nIPC; iicoup++ ) COUPs[ndcoup + iicoup] = allCOUPs[ndcoup + iicoup]; // independent couplings, fixed for all events fptype_amp* numerators = NUM_ACCESS::ieventAccessRecord( allNumerators, ievt0 * ndiagrams ); + // The helicities of the external legs for the events of this page, one per SIMD lane: + // the same helicities hel for all of them, or the helicity allihel of each event + fptype_amp_sv hel_sv[npar]; + fillHelicities( hel_sv, hel, allihel, ievt0 ); // Create an array of views over the Flavor Couplings FLV_COUPLING_ARRAY flvCOUPs{ cIPF_partner1, cIPF_partner2, cIPF_value }; @@ -478,7 +498,7 @@ namespace madmatrix #else cxtype_amp_sv jamp_sv[njampso] = {}; // all zeros #endif - calculate_jamps( cHel[ihel], allmomenta, allcouplings, hgFlavorVec, jamp_sv, false, allNumerators, allDenominators, jamp2_sv, ievt00 ); + calculate_jamps( cHel[ihel], nullptr, allmomenta, allcouplings, hgFlavorVec, jamp_sv, false, allNumerators, allDenominators, jamp2_sv, ievt00 ); color_sum_cpu( allMEs, jamp_sv, ievt00 ); for( int ieppV = 0; ieppV < neppV; ++ieppV ) { @@ -633,7 +653,8 @@ namespace madmatrix unsigned int* allDiagramIdsOut, // output: multichannel channelIds[nevt] (1 to #diagrams) bool mulChannelWeight, // if true, multiply channel weight to ME output const int nevt, // input: #events (for cuda: nevt == ndim == gpublocks*gputhreads) - const bool sampleSignedHelicity ) // input: choose the helicity on |T_i| and return sign(T_i)*sum|T| (split orders only) + const bool sampleSignedHelicity, // input: choose the helicity on |T_i| and return sign(T_i)*sum|T| (split orders only) + const int* allihel ) // input: helicity index [0,ncomb) of each event[nevt]; nullptr to sum over the good helicities { mgDebugInitialise(); @@ -670,6 +691,10 @@ namespace madmatrix // === PART 1 - HELICITY LOOP: CALCULATE WAVEFUNCTIONS === // (using precomputed good helicities) + // With allihel, each event has a single helicity of its own instead: the "loop" is one + // evaluation in which the events of a SIMD page carry different helicities in lock-step, + // and everything below (color sum, color and channel choice) runs on that one helicity. + const int nhelEval = ( allihel != nullptr ? 1 : cNGoodHel ); #if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT // Mixed fptypes #537: float for color algebra and double elsewhere // Delay color algebra and ME updates (only on even pages) @@ -680,7 +705,7 @@ namespace madmatrix #endif #ifdef _OPENMP // OMP multithreading #575 (NB: tested only with gcc11 so far) -#define _OMPLIST0 allcouplings, allMEs, allmomenta, allrndcol, allrndhel, allselcol, allselhel, cGoodHel, cNGoodHel, npagV2, sampleSignedHelicity +#define _OMPLIST0 allcouplings, allMEs, allmomenta, allrndcol, allrndhel, allselcol, allselhel, allihel, cGoodHel, nhelEval, npagV2, sampleSignedHelicity #define _OMPLIST1 , allDenominators, allNumerators, allChannelIds, mgOnGpu::icolamp, mgOnGpu::channel2iconfig #pragma omp parallel for default( none ) shared( _OMPLIST0 _OMPLIST1 ) #undef _OMPLIST0 @@ -709,31 +734,35 @@ namespace madmatrix if( ColorMatrixData::shouldUseBlas ) { static thread_local std::vector ghelJamp_sv( (size_t)ncomb * nParity * ncolor ); - for( int ighel = 0; ighel < cNGoodHel; ighel++ ) + for( int ighel = 0; ighel < nhelEval; ighel++ ) { - const int ihel = cGoodHel[ighel]; + const int ihel = ( allihel != nullptr ? -1 : cGoodHel[ighel] ); // unused with allihel cxtype_sv* jamp_sv = ghelJamp_sv.data() + (size_t)ighel * nParity * ncolor; for( int i = 0; i < nParity * ncolor; i++ ) jamp_sv[i] = cxzero_sv(); // calculate_jamps accumulates into jamp_sv // **NB! in "mixed" precision, using SIMD, calculate_jamps computes MEs for TWO neppV pages with a single channelId! #924 bool storeChannelWeights = allChannelIds != nullptr || allrnddiagram != nullptr; - calculate_jamps( cHel[ihel], allmomenta, allcouplings, iflavorVec, jamp_sv, storeChannelWeights, allNumerators, allDenominators, jamp2_sv, ievt00 ); + calculate_jamps( ( allihel != nullptr ? nullptr : cHel[ihel] ), allihel, allmomenta, allcouplings, iflavorVec, jamp_sv, storeChannelWeights, allNumerators, allDenominators, jamp2_sv, ievt00 ); } + // nhelEval may be 0 (no good helicity): the color sum of no helicity leaves allMEs at 0 + if( nhelEval > 0 ) + { #if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT - color_sum_cpu_blas( allMEs, MEs_ighel, MEs_ighel2, ghelJamp_sv.data(), cNGoodHel, ievt00 ); + color_sum_cpu_blas( allMEs, MEs_ighel, MEs_ighel2, ghelJamp_sv.data(), nhelEval, ievt00 ); #else - color_sum_cpu_blas( allMEs, MEs_ighel, nullptr, ghelJamp_sv.data(), cNGoodHel, ievt00 ); + color_sum_cpu_blas( allMEs, MEs_ighel, nullptr, ghelJamp_sv.data(), nhelEval, ievt00 ); #endif + } } else #endif // MGONGPU_CPP_HAS_BLAS { - for( int ighel = 0; ighel < cNGoodHel; ighel++ ) + for( int ighel = 0; ighel < nhelEval; ighel++ ) { - const int ihel = cGoodHel[ighel]; + const int ihel = ( allihel != nullptr ? -1 : cGoodHel[ighel] ); // unused with allihel cxtype_amp_sv jamp_sv[nParity * njampso] = {}; // fixed nasty bug (omitting 'nParity' caused memory corruptions after calling calculate_jamps) // **NB! in "mixed" precision, using SIMD, calculate_jamps computes MEs for TWO neppV pages with a single channelId! #924 bool storeChannelWeights = allChannelIds != nullptr || allrnddiagram != nullptr; - calculate_jamps( cHel[ihel], allmomenta, allcouplings, iflavorVec, jamp_sv, storeChannelWeights, allNumerators, allDenominators, jamp2_sv, ievt00 ); + calculate_jamps( ( allihel != nullptr ? nullptr : cHel[ihel] ), allihel, allmomenta, allcouplings, iflavorVec, jamp_sv, storeChannelWeights, allNumerators, allDenominators, jamp2_sv, ievt00 ); color_sum_cpu( allMEs, jamp_sv, ievt00 ); MEs_ighel[ighel] = E_ACCESS::kernelAccess( E_ACCESS::ieventAccessRecord( allMEs, ievt00 ) ); #if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT @@ -746,6 +775,16 @@ namespace madmatrix for( int ieppV = 0; ieppV < neppV; ++ieppV ) { const int ievt = ievt00 + ieppV; + if( allihel != nullptr ) + { + // No choice: each event was evaluated for its given helicity only, whose |M|^2 (signed, + // with an interference) is the color sum above + allselhel[ievt] = allihel[ievt] + 1; // NB Fortran [1,ncomb], cudacpp [0,ncomb-1] +#if defined MGONGPU_CPPSIMD and defined MGONGPU_FPTYPE_DOUBLE and defined MGONGPU_FPTYPE2_FLOAT + allselhel[ievt + neppV] = allihel[ievt + neppV] + 1; +#endif + continue; + } if constexpr( nampso > 1 ) { if( sampleSignedHelicity ) diff --git a/madgraph/iolibs/template_files/madmatrix/backend/simd/SigmaKin.h b/madgraph/iolibs/template_files/madmatrix/backend/simd/SigmaKin.h index 60a5af11bc..8e0d0f94ab 100644 --- a/madgraph/iolibs/template_files/madmatrix/backend/simd/SigmaKin.h +++ b/madgraph/iolibs/template_files/madmatrix/backend/simd/SigmaKin.h @@ -57,7 +57,11 @@ namespace madmatrix // input: when the |M|^2 keeps an interference (squared split orders, nampso > 1), // choose the helicity on |T_i| and return sign(T_i) * sum_j |T_j| as madevent does, // so that the chosen helicity is exact; false returns sum_j T_j (no effect otherwise) - const bool sampleSignedHelicity = false ); + const bool sampleSignedHelicity = false, + // input: the helicity index [0,ncomb) of each event, which is then evaluated for that + // helicity alone (and returns its contribution to the helicity-summed |M|^2, see + // umami_matrix_element); nullptr, the default, sums over the good helicities + const int* allihel = nullptr ); // Setters: called once by CPPProcess (P1-generated) to populate this file's // otherwise-internal storage, since it can no longer be written directly @@ -75,7 +79,8 @@ namespace madmatrix // helicity table cHel[ncomb][npar] (row major) the combinations are built // from, and the identical-particle factor of a flavor combination. void - calculate_jamps( const short* hel, // input: helicity of each leg, hel[npar] + calculate_jamps( const short* hel, // input: helicity of each leg, hel[npar], of all events (if allihel == nullptr) + const int* allihel, // input: helicity index [0,ncomb) of each event[nevt] (nullptr: hel for all events) const fptype_momenta* allmomenta, // input: momenta[nevt*npar*4] const fptype* allcouplings, // input: couplings[nevt*ndcoup*2] const unsigned int* iflavorVec, // input: indices of the flavor combinations diff --git a/madgraph/iolibs/template_files/madmatrix/backend/simd/umami.cc b/madgraph/iolibs/template_files/madmatrix/backend/simd/umami.cc index 6ca9d8a37a..1b1df7b4ec 100644 --- a/madgraph/iolibs/template_files/madmatrix/backend/simd/umami.cc +++ b/madgraph/iolibs/template_files/madmatrix/backend/simd/umami.cc @@ -246,8 +246,8 @@ extern "C" UmamiStatus umami_supported_inputs( bool const** supported, int* count ) { // MOMENTA, ALPHA_S, FLAVOR_INDEX, RANDOM_COLOR, RANDOM_HELICITY, RANDOM_DIAGRAM, - // HELICITY_INDEX=false, DIAGRAM_INDEX=true, CHANNEL_INDEX=false - static const bool data[UMAMI_INPUT_KEY_COUNT] = { true, true, true, true, true, true, false, true }; + // HELICITY_INDEX, DIAGRAM_INDEX=true, CHANNEL_INDEX=false + static const bool data[UMAMI_INPUT_KEY_COUNT] = { true, true, true, true, true, true, true, true }; *supported = data; *count = UMAMI_INPUT_KEY_COUNT; return UMAMI_SUCCESS; @@ -339,6 +339,7 @@ extern "C" const double* random_color_in = nullptr; const double* random_helicity_in = nullptr; const double* random_diagram_in = nullptr; + const int* helicity_in = nullptr; [[maybe_unused]] const int* diagram_in = nullptr; // TODO: unused for( std::size_t i = 0; i < input_count; ++i ) @@ -365,7 +366,8 @@ extern "C" random_diagram_in = static_cast( input ); break; case UMAMI_IN_HELICITY_INDEX: - return UMAMI_ERROR_UNSUPPORTED_INPUT; + helicity_in = static_cast( input ); + break; case UMAMI_IN_DIAGRAM_INDEX: diagram_in = static_cast( input ); break; @@ -374,6 +376,20 @@ extern "C" } } if( !momenta_in ) return UMAMI_ERROR_MISSING_INPUT; + // UMAMI_IN_HELICITY_INDEX: evaluate each event for the one helicity it is given, an index + // into the full helicity table [0,ncomb) (the UMAMI_OUT_HELICITY_INDEX convention), instead + // of summing over the helicities. The |M|^2 is then the contribution of that helicity to + // the helicity-summed (and averaged) |M|^2, so that the sum over all ncomb helicities gives + // back the |M|^2 without this input; UMAMI_OUT_HELICITY_INDEX returns the given helicity, + // the color and diagram are chosen for it, and UMAMI_IN_RANDOM_HELICITY is not used. + if( helicity_in ) + { + for( std::size_t i_event = 0; i_event < count; ++i_event ) + { + const int ihel = helicity_in[i_event + offset]; + if( ihel < 0 || ihel >= ProcessData::ncomb ) return UMAMI_ERROR; + } + } double* m2_out = nullptr; double* amp2_out = nullptr; @@ -460,6 +476,7 @@ extern "C" HostBufferBase denominators( rounded_count ); HostBufferBase helicity_index( rounded_count ); HostBufferBase color_index( rounded_count ); + HostBufferBase helicity_given( rounded_count ); // as the other inputs, the padding lanes copy their source event // The input event of each lane. The padding lanes, which complete the last page // of a flavor, are copies of the first event of their page, as on GPUs. Left at // zero (momenta and g_s), they evaluated to nan, and the good-helicity filter of @@ -486,6 +503,7 @@ extern "C" diagram_random[lane] = random_diagram_in ? random_diagram_in[i_event + offset] : 0.5; g_s[lane] = alpha_s_in ? sqrt( 4 * M_PI * alpha_s_in[i_event + offset] ) : 1.2177157847767195; flavor_indices[lane] = flavor_indices_in ? flavor_indices_in[i_event + offset] : 0; + if( helicity_in ) helicity_given[lane] = helicity_in[i_event + offset]; } computeDependentCouplings( g_s.data(), couplings.data(), rounded_count ); @@ -523,7 +541,8 @@ extern "C" // an interference |M|^2, see select_helicity_signed, unless the instance was asked // to keep the helicity sum (interference_helicity_summed); without the random number // the |M|^2 stays the helicity sum (systematics re-evaluation, standalone checks) - random_helicity_in != nullptr && !instance->summed_helicity ); + random_helicity_in != nullptr && !instance->summed_helicity, + helicity_in ? helicity_given.data() : nullptr ); if ( sort_flavors ) { diff --git a/madgraph/iolibs/template_files/madmatrix/check_sa.cc b/madgraph/iolibs/template_files/madmatrix/check_sa.cc index c69de545fb..49ecb6de5a 100644 --- a/madgraph/iolibs/template_files/madmatrix/check_sa.cc +++ b/madgraph/iolibs/template_files/madmatrix/check_sa.cc @@ -176,7 +176,7 @@ namespace return true; } - // Raw binary per-event values (--alphas , --flavors, --rnd-hel, --rnd-col): one + // Raw binary per-event values (--alphas , --flavors, --rnd-hel, --rnd-col, --hel-index): one // value of type T per event, with no header, as array.tofile writes them in Python. template bool read_binary_values( const std::string& path, const char* what, std::vector& values ) @@ -206,6 +206,7 @@ namespace std::string flavorsFile; // --flavors: a file of 32-bit unsigned ints std::string rndHelFile; // --rnd-hel: a file of doubles std::string rndColFile; // --rnd-col: a file of doubles + std::string helIndexFile; // --hel-index: a file of 32-bit ints std::string dumpHelFile; // --dump-hel: write the helicity indices std::string dumpColFile; // --dump-col: write the color indices std::vector> parameters; // --param = @@ -254,6 +255,8 @@ namespace << " --rnd-hel Random numbers of the helicity choice (umami otherwise uses 0.5\n" << " and returns the helicity sum for an interference |M|^2).\n" << " --rnd-col Random numbers of the color choice (otherwise 0.5).\n" + << " --hel-index Helicity index [0,ncomb) of every event: the |M|^2 of that one\n" + << " helicity instead of the helicity sum (UMAMI_IN_HELICITY_INDEX).\n" << " --dump-hel Write the chosen helicity index of every event (-1: none).\n" << " --dump-col Write the chosen color index of every event (-1: none).\n" << " --param = umami_set_parameter after umami_initialize (repeatable).\n" @@ -647,6 +650,7 @@ namespace double alphasValue = param_card_alpha_s(); std::vector alphasIn, rndHelIn, rndColIn; std::vector flavorsIn; + std::vector helIndexIn; if( is_float( io.alphas.c_str() ) ) alphasValue = atof( io.alphas.c_str() ); else if( !io.alphas.empty() && !read_binary_values( io.alphas, "alpha_s", alphasIn ) ) @@ -654,11 +658,13 @@ namespace if( !io.flavorsFile.empty() && !read_binary_values( io.flavorsFile, "flavor", flavorsIn ) ) return 2; if( !io.rndHelFile.empty() && !read_binary_values( io.rndHelFile, "helicity random number", rndHelIn ) ) return 2; if( !io.rndColFile.empty() && !read_binary_values( io.rndColFile, "color random number", rndColIn ) ) return 2; + if( !io.helIndexFile.empty() && !read_binary_values( io.helIndexFile, "helicity index", helIndexIn ) ) return 2; const std::size_t nevtRun = eventsFromFile ? lheEvents.size() : (std::size_t)niter * nevt; for( auto [size, what] : { std::make_pair( alphasIn.size(), "alpha_s" ), std::make_pair( flavorsIn.size(), "flavor" ), std::make_pair( rndHelIn.size(), "helicity random number" ), - std::make_pair( rndColIn.size(), "color random number" ) } ) + std::make_pair( rndColIn.size(), "color random number" ), + std::make_pair( helIndexIn.size(), "helicity index" ) } ) { if( size != 0 && size < nevtRun ) { @@ -683,6 +689,7 @@ namespace std::vector alphasVec( nevt, alphasValue ); std::vector rndHelVec( nevt, 0.5 ), rndColVec( nevt, 0.5 ); std::vector helVec( nevt ), colVec( nevt ); + std::vector helInVec( nevt, 0 ); #ifdef MGONGPUCPP_GPUIMPL timermap.start( "00 GpuInit" ); GpuRuntime gpuRuntime( false ); @@ -699,6 +706,7 @@ namespace DeviceBufferBase devAlphaS( nevt ); DeviceBufferBase devRndHel( nevt ), devRndCol( nevt ); DeviceBufferBase devHel( nevt ), devCol( nevt ); + DeviceBufferBase devHelIn( nevt ); std::vector hstUmamiMEs( nevt ); // the inputs that are the same for every event are copied to the device once gpuMemcpy( devFlv.data(), flvVec.data(), nevt * sizeof( unsigned int ), gpuMemcpyHostToDevice ); @@ -711,6 +719,7 @@ namespace double* rndColPtr = devRndCol.data(); int* helPtr = devHel.data(); int* colPtr = devCol.data(); + int* helInPtr = devHelIn.data(); gpuStream_t stream = nullptr; // nullptr: the default stream if( io.nonblockingStream ) gpuStreamCreateNonBlocking( &stream ); #else @@ -727,6 +736,7 @@ namespace double* rndColPtr = rndColVec.data(); int* helPtr = helVec.data(); int* colPtr = colVec.data(); + int* helInPtr = helInVec.data(); #endif // The keys and buffers of the umami call, the same for every batch @@ -744,6 +754,11 @@ namespace in_keys.push_back( UMAMI_IN_RANDOM_COLOR ); inputs.push_back( rndColPtr ); } + if( !helIndexIn.empty() ) + { + in_keys.push_back( UMAMI_IN_HELICITY_INDEX ); + inputs.push_back( helInPtr ); + } if( dumpHel.is_open() ) { out_keys.push_back( UMAMI_OUT_HELICITY_INDEX ); @@ -916,11 +931,17 @@ namespace for( unsigned int ievt = 0; ievt < nevt; ++ievt ) flvVec[ievt] = flavorsIn[base + std::min( ievt, nreal - 1 )]; } + if( !helIndexIn.empty() ) + { + for( unsigned int ievt = 0; ievt < nevt; ++ievt ) + helInVec[ievt] = helIndexIn[base + std::min( ievt, nreal - 1 )]; + } #ifdef MGONGPUCPP_GPUIMPL if( !alphasIn.empty() ) gpuMemcpy( alphasPtr, alphasVec.data(), nevt * sizeof( double ), gpuMemcpyHostToDevice ); if( !rndHelIn.empty() ) gpuMemcpy( rndHelPtr, rndHelVec.data(), nevt * sizeof( double ), gpuMemcpyHostToDevice ); if( !rndColIn.empty() ) gpuMemcpy( rndColPtr, rndColVec.data(), nevt * sizeof( double ), gpuMemcpyHostToDevice ); if( !flavorsIn.empty() ) gpuMemcpy( flvPtr, flvVec.data(), nevt * sizeof( unsigned int ), gpuMemcpyHostToDevice ); + if( !helIndexIn.empty() ) gpuMemcpy( helInPtr, helInVec.data(), nevt * sizeof( int ), gpuMemcpyHostToDevice ); // A non-blocking stream does not wait for the inputs copied and transposed on the // default stream: finish them first, as any caller on such a stream must if( stream != nullptr ) checkGpu( gpuDeviceSynchronize() ); @@ -1139,10 +1160,11 @@ int main( int argc, char** argv ) else if( arg == "--energy" && argn + 1 < argc && is_float( argv[argn + 1] ) ) perfEnergy = atof( argv[++argn] ); else if( ( arg == "--alphas" || arg == "--flavors" || arg == "--rnd-hel" || arg == "--rnd-col" || - arg == "--dump-hel" || arg == "--dump-col" ) && argn + 1 < argc ) + arg == "--hel-index" || arg == "--dump-hel" || arg == "--dump-col" ) && argn + 1 < argc ) { std::string& value = ( arg == "--alphas" ? io.alphas : arg == "--flavors" ? io.flavorsFile : arg == "--rnd-hel" ? io.rndHelFile : arg == "--rnd-col" ? io.rndColFile : + arg == "--hel-index" ? io.helIndexFile : arg == "--dump-hel" ? io.dumpHelFile : io.dumpColFile ); value = argv[++argn]; mode = MODE_PERF; diff --git a/madmatrix/model_handling.py b/madmatrix/model_handling.py index fa557f9e6f..5e97d644bd 100644 --- a/madmatrix/model_handling.py +++ b/madmatrix/model_handling.py @@ -3386,10 +3386,10 @@ def get_external_line(self, wf, argument): # part of the call key, but the leg number and the mass are wf's own: # argument is whichever wavefunction first had this key. if argument.get('offshell') and str(wf.get('mass')).upper() != 'ZERO': - call = call + 'offshell_mass( m_pars->%%s, momenta, %d ), cHel[ihel][%%d],' \ + call = call + 'offshell_mass( m_pars->%%s, momenta, %d ), hel_sv[%%d],' \ % (wf.get('number_external') - 1) else: - call = call + 'm_pars->%s, cHel[ihel][%d],' + call = call + 'm_pars->%s, hel_sv[%d],' else: # AV This seems to be for scalars (spin==1???), pass neither mass nor helicity (#351) ###call = call + 'm_pars->%s,' diff --git a/tests/input_files/IOTestsComparison/IOTestFDGauge/FDgauge_madmatrix/%FD_madmatrix%src%HelAmps_sm.h b/tests/input_files/IOTestsComparison/IOTestFDGauge/FDgauge_madmatrix/%FD_madmatrix%src%HelAmps_sm.h index 63f0eac4b4..436e64a059 100644 --- a/tests/input_files/IOTestsComparison/IOTestFDGauge/FDgauge_madmatrix/%FD_madmatrix%src%HelAmps_sm.h +++ b/tests/input_files/IOTestsComparison/IOTestFDGauge/FDgauge_madmatrix/%FD_madmatrix%src%HelAmps_sm.h @@ -141,7 +141,7 @@ namespace madmatrix __host__ __device__ INLINE void ixxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp fmass, // input: fermion mass - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: aloha objects @@ -156,7 +156,7 @@ namespace madmatrix __host__ __device__ INLINE void ipzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fi, // output: wavefunctions @@ -171,7 +171,7 @@ namespace madmatrix __host__ __device__ INLINE void imzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fi, // output: wavefunctions @@ -186,7 +186,7 @@ namespace madmatrix __host__ __device__ INLINE void ixzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fi, // output: wavefunctions @@ -200,7 +200,7 @@ namespace madmatrix __host__ __device__ INLINE void vxxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp vmass, // input: vector boson mass - const int nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson) + const fptype_amp_sv nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson, per SIMD lane/GPU thread) const int nsv, // input: +1 (final) or -1 (initial) const int flv, // input: flavor index ALOHAOBJ & vc, // output: wavefunctions @@ -228,7 +228,7 @@ namespace madmatrix __host__ __device__ INLINE void oxxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp fmass, // input: fermion mass - const int nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson) + const fptype_amp_sv nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fo, // output: wavefunctions @@ -243,7 +243,7 @@ namespace madmatrix __host__ __device__ INLINE void opzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fo, // output: wavefunctions @@ -258,7 +258,7 @@ namespace madmatrix __host__ __device__ INLINE void omzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fo, // output: wavefunctions @@ -272,7 +272,7 @@ namespace madmatrix __host__ __device__ INLINE void oxzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavor index ALOHAOBJ & fo, // output: wavefunctions @@ -313,7 +313,7 @@ namespace madmatrix __host__ __device__ void ixxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp fmass, // input: fermion mass - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: wavefunctions @@ -338,7 +338,12 @@ namespace madmatrix fi.pvec[2] = -pvec2_ * static_cast(nsf); fi.pvec[3] = -pvec3_ * static_cast(nsf); fi.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; if( fmass != 0. ) { #ifndef MGONGPU_CPPSIMD @@ -347,11 +352,9 @@ namespace madmatrix volatile fptype_amp_sv p2 = pvec1 * pvec1 + pvec2 * pvec2 + pvec3 * pvec3; // volatile fixes #736 const fptype_amp_sv pp = fpmin( pvec0, fpsqrt( p2 ) ); #endif - // In C++ ixxxxx, use a single ip/im numbering that is valid both for pp==0 and pp>0, which have two numbering schemes in Fortran ixxxxx: - // for pp==0, Fortran sqm(0:1) has indexes 0,1 as in C++; but for Fortran pp>0, omega(2) has indexes 1,2 and not 0,1 - // NB: this is only possible in ixxxx, but in oxxxxx two different numbering schemes must be used - const int ip = ( 1 + nh ) / 2; // NB: same as in Fortran pp==0, differs from Fortran pp>0, which is (3+nh)/2 because omega(2) has indexes 1,2 - const int im = ( 1 - nh ) / 2; // NB: same as in Fortran pp==0, differs from Fortran pp>0, which is (3-nh)/2 because omega(2) has indexes 1,2 + // The Fortran ip/im indices ( 1 + nh ) / 2 and ( 1 - nh ) / 2 (for pp > 0, ( 3 +- nh ) / 2 as omega(2) has + // indexes 1,2) are the truth values tp and tm: an array element x[ip] is tm * x[0] + tp * x[1], and + // ip * x[ip] is just tp * x[1] (and likewise for im) #ifndef MGONGPU_CPPSIMD if( pp == 0. ) { @@ -359,10 +362,10 @@ namespace madmatrix fptype_amp sqm[2] = { fpsqrt( std::abs( fmass ) ), 0. }; // possibility of negative fermion masses //sqm[1] = ( fmass < 0. ? -abs( sqm[0] ) : abs( sqm[0] ) ); // AV: why abs here? sqm[1] = ( fmass < 0. ? -sqm[0] : sqm[0] ); // AV: removed an abs here - w[0] = cxmake( ip * sqm[ip], 0 ); - w[1] = cxmake( im * nsf * sqm[ip], 0 ); - w[2] = cxmake( ip * nsf * sqm[im], 0 ); - w[3] = cxmake( im * sqm[im], 0 ); + w[0] = cxmake( tp * sqm[1], 0 ); + w[1] = cxmake( tm * nsf * sqm[0], 0 ); + w[2] = cxmake( tp * nsf * sqm[0], 0 ); + w[3] = cxmake( tm * sqm[1], 0 ); } else { @@ -370,30 +373,32 @@ namespace madmatrix fptype_amp( 1 + nsf - ( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; fptype_amp omega[2] = { fpsqrt( pvec0 + pp ), 0. }; omega[1] = fmass / omega[0]; - const fptype_amp sfomega[2] = { sf[0] * omega[ip], sf[1] * omega[im] }; + const fptype_amp sfomega[2] = { sf[0] * ( tm * omega[0] + tp * omega[1] ), sf[1] * ( tp * omega[0] + tm * omega[1] ) }; const fptype_amp pp3 = fpmax( fpternary( fpsignbit( pvec3 ), ( pvec1 * pvec1 + pvec2 * pvec2 ) / ( pp - pvec3 ), pp + pvec3 ), 0. ); const cxtype_amp chi[2] = { cxmake( fpsqrt( pp3 * (fptype_amp)0.5 / pp ), 0. ), ( pp3 == 0. ? cxmake( -nh, 0. ) : cxmake( nh * pvec1, pvec2 ) / fpsqrt( (fptype_amp)2. * pp * pp3 ) ) }; - w[0] = sfomega[0] * chi[im]; - w[1] = sfomega[0] * chi[ip]; - w[2] = sfomega[1] * chi[im]; - w[3] = sfomega[1] * chi[ip]; + const cxtype_amp chi_im = tp * chi[0] + tm * chi[1]; // chi[im] + const cxtype_amp chi_ip = tm * chi[0] + tp * chi[1]; // chi[ip] + w[0] = sfomega[0] * chi_im; + w[1] = sfomega[0] * chi_ip; + w[2] = sfomega[1] * chi_im; + w[3] = sfomega[1] * chi_ip; } #else // Branch A: pp == 0. // NB: Do not use "abs" for floats! It returns an integer with no build warning! Use std::abs! fptype_amp sqm[2] = { fpsqrt( std::abs( fmass ) ), 0 }; // possibility of negative fermion masses (NB: SCALAR!) sqm[1] = ( fmass < 0 ? -sqm[0] : sqm[0] ); // AV: removed an abs here (as above) - const cxtype_amp fiA_2 = ip * sqm[ip]; // scalar cxtype: real part initialised from fptype, imag part = 0 - const cxtype_amp fiA_3 = im * nsf * sqm[ip]; // scalar cxtype: real part initialised from fptype, imag part = 0 - const cxtype_amp fiA_4 = ip * nsf * sqm[im]; // scalar cxtype: real part initialised from fptype, imag part = 0 - const cxtype_amp fiA_5 = im * sqm[im]; // scalar cxtype: real part initialised from fptype, imag part = 0 + const cxtype_amp_v fiA_2 = cxmake( tp * sqm[1], 0 ); + const cxtype_amp_v fiA_3 = cxmake( tm * (fptype_amp)nsf * sqm[0], 0 ); + const cxtype_amp_v fiA_4 = cxmake( tp * (fptype_amp)nsf * sqm[0], 0 ); + const cxtype_amp_v fiA_5 = cxmake( tm * sqm[1], 0 ); // Branch B: pp != 0. - const fptype_amp sf[2] = { fptype_amp( 1 + nsf + ( 1 - nsf ) * nh ) * (fptype_amp)0.5, - fptype_amp( 1 + nsf - ( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; + const fptype_amp_v sf[2] = { ( (fptype_amp)( 1 + nsf ) + (fptype_amp)( 1 - nsf ) * nh ) * (fptype_amp)0.5, + ( (fptype_amp)( 1 + nsf ) - (fptype_amp)( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; fptype_v omega[2] = { fpsqrt( pvec0 + pp ), 0 }; omega[1] = fmass / omega[0]; - const fptype_amp_v sfomega[2] = { sf[0] * omega[ip], sf[1] * omega[im] }; + const fptype_amp_v sfomega[2] = { sf[0] * ( tm * omega[0] + tp * omega[1] ), sf[1] * ( tp * omega[0] + tm * omega[1] ) }; const fptype_amp_v pp3 = fpmax( fpternary( fpsignbit( pvec3 ), ( pvec1 * pvec1 + pvec2 * pvec2 ) / ( pp - pvec3 ), pp + pvec3 ), 0 ); volatile fptype_amp_v ppDENOM = fpternary( pp != 0, pp, 1. ); // hack: ppDENOM[ieppV]=1 if pp[ieppV]==0 volatile fptype_amp_v pp3DENOM = fpternary( pp3 != 0, pp3, 1. ); // hack: pp3DENOM[ieppV]=1 if pp3[ieppV]==0 @@ -401,11 +406,13 @@ namespace madmatrix const cxtype_amp_v chi[2] = { cxmake( fpsqrt( chi0r2 ), 0 ), // hack: dummy[ieppV] is not used if pp[ieppV]==0 cxternary( ( pp3 == 0. ), cxmake( -nh, 0 ), - cxmake( (fptype_amp)nh * pvec1, pvec2 ) / fpsqrt( (fptype_amp)2. * ppDENOM * pp3DENOM ) ) }; // hack: dummy[ieppV] is not used if pp[ieppV]==0 - const cxtype_amp_v fiB_2 = sfomega[0] * chi[im]; - const cxtype_amp_v fiB_3 = sfomega[0] * chi[ip]; - const cxtype_amp_v fiB_4 = sfomega[1] * chi[im]; - const cxtype_amp_v fiB_5 = sfomega[1] * chi[ip]; + cxmake( nh * pvec1, pvec2 ) / fpsqrt( (fptype_amp)2. * ppDENOM * pp3DENOM ) ) }; // hack: dummy[ieppV] is not used if pp[ieppV]==0 + const cxtype_amp_v chi_im = tp * chi[0] + tm * chi[1]; // chi[im] + const cxtype_amp_v chi_ip = tm * chi[0] + tp * chi[1]; // chi[ip] + const cxtype_amp_v fiB_2 = sfomega[0] * chi_im; + const cxtype_amp_v fiB_3 = sfomega[0] * chi_ip; + const cxtype_amp_v fiB_4 = sfomega[1] * chi_im; + const cxtype_amp_v fiB_5 = sfomega[1] * chi_ip; // Choose between the results from branch A and branch B const bool_v mask = ( pp == 0. ); w[0] = cxternary( mask, fiA_2, fiB_2 ); @@ -431,8 +438,8 @@ namespace madmatrix volatile fptype_amp_sv sqp0p3DENOM = fpternary( sqp0p3 != 0, (fptype_sv)sqp0p3, 1. ); // hack: dummy sqp0p3DENOM[ieppV]=1 if sqp0p3[ieppV]==0 cxtype_sv chi[2] = { cxmake( (fptype_v)sqp0p3, 0. ), cxternary( sqp0p3 == 0, - cxmake( -(fptype_amp)nhel * fpsqrt( (fptype_amp)2. * pvec0 ), 0. ), - cxmake( (fptype_amp)nh * pvec1, pvec2 ) / (const fptype_v)sqp0p3DENOM ) }; // hack: dummy[ieppV] is not used if sqp0p3[ieppV]==0 + cxmake( -nhel * fpsqrt( (fptype_amp)2. * pvec0 ), 0. ), + cxmake( nh * pvec1, pvec2 ) / (const fptype_v)sqp0p3DENOM ) }; // hack: dummy[ieppV] is not used if sqp0p3[ieppV]==0 #else // pvec0+pvec3 is a cancelling difference of two ~|p| numbers for a // backward-moving massless fermion, and everything below divides by it. @@ -445,22 +452,12 @@ namespace madmatrix fptype_sv{ 0 }, fpsqrt( p0p3stable ) * (fptype_amp)nsf ); const cxtype_amp_sv chi[2] = { cxmake( sqp0p3, 0. ), - ( sqp0p3 == 0. ? cxmake( -(fptype_amp)nhel * fpsqrt( (fptype_amp)2. * pvec0 ), 0. ) : cxmake( (fptype_amp)nh * pvec1, pvec2 ) / sqp0p3 ) }; + ( sqp0p3 == 0. ? cxmake( -nhel * fpsqrt( (fptype_amp)2. * pvec0 ), 0. ) : cxmake( nh * pvec1, pvec2 ) / sqp0p3 ) }; #endif - if( nh == 1 ) - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi[0]; - w[3] = chi[1]; - } - else - { - w[0] = chi[1]; - w[1] = chi[0]; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } + w[0] = tm * chi[1]; + w[1] = tm * chi[0]; + w[2] = tp * chi[0]; + w[3] = tp * chi[1]; } mgDebug( 1, __FUNCTION__ ); return; @@ -474,7 +471,7 @@ namespace madmatrix __host__ __device__ void ipzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: wavefunctions @@ -489,20 +486,17 @@ namespace madmatrix fi.pvec[2] = fptype_momenta_sv{ 0 }; fi.pvec[3] = -pvec3_ * static_cast(nsf); fi.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; const cxtype_amp_sv sqp0p3 = cxmake( fpsqrt( (fptype_amp)2. * pvec3 ) * (fptype_amp)nsf, 0. ); - w[0] = cxmake( fpamp_of_mom( fi.pvec[1] ), fpamp_of_mom( fi.pvec[2] ) ); - if( nh == 1 ) - { - w[1] = cxmake( fpamp_of_mom( fi.pvec[1] ), fpamp_of_mom( fi.pvec[2] ) ); - w[2] = sqp0p3; - } - else - { - w[1] = sqp0p3; - w[2] = cxmake( fpamp_of_mom( fi.pvec[1] ), fpamp_of_mom( fi.pvec[2] ) ); - } - w[3] = cxmake( fpamp_of_mom( fi.pvec[1] ), fpamp_of_mom( fi.pvec[2] ) ); + w[0] = cxzero_sv(); + w[1] = tm * sqp0p3; + w[2] = tp * sqp0p3; + w[3] = cxzero_sv(); mgDebug( 1, __FUNCTION__ ); return; } @@ -515,7 +509,7 @@ namespace madmatrix __host__ __device__ void imzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: wavefunctions @@ -530,20 +524,17 @@ namespace madmatrix fi.pvec[2] = fptype_momenta_sv{ 0 }; fi.pvec[3] = -pvec3_ * static_cast(nsf); fi.flv_index = flv; - const int nh = nhel * nsf; - const cxtype_amp_sv chi = cxmake( -(fptype_amp)nhel * fpsqrt( -(fptype_amp)2. * pvec3 ), 0. ); + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; + const cxtype_amp_sv chi = cxmake( -nhel * fpsqrt( -(fptype_amp)2. * pvec3 ), 0. ); + w[0] = tm * chi; w[1] = cxzero_sv(); w[2] = cxzero_sv(); - if( nh == 1 ) - { - w[0] = cxzero_sv(); - w[3] = chi; - } - else - { - w[0] = chi; - w[3] = cxzero_sv(); - } + w[3] = tp * chi; mgDebug( 1, __FUNCTION__ ); return; } @@ -556,7 +547,7 @@ namespace madmatrix __host__ __device__ void ixzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fi, // output: wavefunctions @@ -578,7 +569,12 @@ namespace madmatrix fi.pvec[3] = -pvec3_ * static_cast(nsf); fi.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; //const float sqp0p3 = sqrtf( pvec0 + pvec3 ) * nsf; // AV: why force a float here? // pvec0+pvec3 is a cancelling difference of two ~|p| numbers for a // backward-moving massless fermion, and everything below divides by it. @@ -590,21 +586,11 @@ namespace madmatrix pvec0 + pvec3 ); const fptype_amp_sv sqp0p3 = fpsqrt( p0p3stable ) * static_cast(nsf); const cxtype_amp_sv chi0 = cxmake( sqp0p3, 0. ); - const cxtype_amp_sv chi1 = cxmake( (fptype_amp)nh * pvec1 / sqp0p3, pvec2 / sqp0p3 ); - if( nh == 1 ) - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi0; - w[3] = chi1; - } - else - { - w[0] = chi1; - w[1] = chi0; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } + const cxtype_amp_sv chi1 = cxmake( nh * pvec1 / sqp0p3, pvec2 / sqp0p3 ); + w[0] = tm * chi1; + w[1] = tm * chi0; + w[2] = tp * chi0; + w[3] = tp * chi1; mgDebug( 1, __FUNCTION__ ); return; } @@ -616,7 +602,7 @@ namespace madmatrix __host__ __device__ void vxxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp vmass, // input: vector boson mass - const int nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson) + const fptype_amp_sv nhel, // input: -1, 0 (only if vmass!=0) or +1 (helicity of vector boson, per SIMD lane/GPU thread) const int nsv, // input: +1 (final) or -1 (initial) const int flv, // input: flavour ALOHAOBJ & vc, // output: wavefunctions @@ -642,7 +628,10 @@ namespace madmatrix vc.pvec[3] = pvec3_ * static_cast(nsv); vc.flv_index = flv; const fptype_amp sqh = fpsqrt( 0.5 ); // AV this is > 0! - const fptype_amp hel = nhel; + const fptype_amp_sv hel = nhel; + // BRANCHLESS HELICITY: |hel| is hel * hel for hel in { -1, 0, +1 }, so that the helicity can + // differ from one SIMD lane or GPU thread to the next with no branch and no std::abs + const fptype_amp_sv abshel = hel * hel; // FD gauge const cxtype_amp_sv cI = cxmake( 0. + fptype_amp_sv{ 0. }, 1. + fptype_amp_sv{ 0. } ); @@ -653,8 +642,8 @@ namespace madmatrix if( vmass != 0. ) { - const int nsvahl = nsv * std::abs( hel ); - const fptype_amp hel0 = 1. - std::abs( hel ); + const fptype_amp_sv nsvahl = static_cast( nsv ) * abshel; + const fptype_amp_sv hel0 = 1 - abshel; // truth value of hel == 0 (longitudinal polarisation) #ifndef MGONGPU_CPPSIMD const fptype_amp_sv pt2 = ( pvec1 * pvec1 ) + ( pvec2 * pvec2 ); const fptype_amp_sv pp = fpmin( pvec0, fpsqrt( pt2 + ( pvec3 * pvec3 ) ) ); @@ -709,17 +698,14 @@ namespace madmatrix nk = n[0]*pvec0 - n[1]*pvec1 - n[2]*pvec2 - n[3]*pvec3; - if ( abs(nhel) == 1) - { - w[4] = cxzero_sv(); - } - else{ - w[0] = cxmake( -vmass/nk * n[0], zero ); - w[1] = cxmake( -vmass/nk * n[1], zero ); - w[2] = cxmake( -vmass/nk * n[2], zero ); - w[3] = cxmake( -vmass/nk * n[3], zero ); - w[4] = -static_cast(nsv)*cI; - } + // BRANCHLESS HELICITY: |hel| = 1 keeps w[0..3] with w[4] = 0, hel = 0 (hel0 = 1) takes the + // FD-gauge longitudinal polarisation instead (nk > 0 for a massive momentum, so the unused + // alternative is finite and vanishes when multiplied by its zero truth value) + w[0] = abshel * w[0] + hel0 * cxmake( -vmass / nk * n[0], zero ); + w[1] = abshel * w[1] + hel0 * cxmake( -vmass / nk * n[1], zero ); + w[2] = abshel * w[2] + hel0 * cxmake( -vmass / nk * n[2], zero ); + w[3] = abshel * w[3] + hel0 * cxmake( -vmass / nk * n[3], zero ); + w[4] = hel0 * ( -static_cast( nsv ) * cI ); #else volatile fptype_amp_sv pt2 = ( pvec1 * pvec1 ) + ( pvec2 * pvec2 ); @@ -728,9 +714,9 @@ namespace madmatrix const fptype_amp_sv pt = fpmin( pp, fpsqrt( pt2 ) ); // Branch A: pp == 0. const cxtype_amp vcA_2 = cxmake( 0, 0 ); - const cxtype_amp vcA_3 = cxmake( -hel * sqh, 0 ); - const cxtype_amp vcA_4 = cxmake( 0, nsvahl * sqh ); - const cxtype_amp vcA_5 = cxmake( hel0, 0 ); + const cxtype_amp_v vcA_3 = cxmake( -hel * sqh, 0 ); + const cxtype_amp_v vcA_4 = cxmake( 0., nsvahl * sqh ); + const cxtype_amp_v vcA_5 = cxmake( hel0, 0 ); // Branch B: pp != 0. volatile fptype_amp_v ppDENOM = fpternary( pp != 0, pp, 1. ); // hack: ppDENOM[ieppV]=1 if pp[ieppV]==0 const fptype_amp_v emp = pvec0 / ( vmass * ppDENOM ); // hack: dummy[ieppV] is not used if pp[ieppV]==0 @@ -739,11 +725,11 @@ namespace madmatrix // Branch B1: pp != 0. and pt != 0. volatile fptype_amp_v ptDENOM = fpternary( pt != 0, pt, 1. ); // hack: ptDENOM[ieppV]=1 if pt[ieppV]==0 const fptype_amp_v pzpt = pvec3 / ( ppDENOM * ptDENOM ) * sqh * hel; // hack: dummy[ieppV] is not used if pp[ieppV]==0 - const cxtype_amp_v vcB1_3 = cxmake( hel0 * pvec1 * emp - pvec1 * pzpt, -fpamp_scalar(nsvahl) * pvec2 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 - const cxtype_amp_v vcB1_4 = cxmake( hel0 * pvec2 * emp - pvec2 * pzpt, fpamp_scalar(nsvahl) * pvec1 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 + const cxtype_amp_v vcB1_3 = cxmake( hel0 * pvec1 * emp - pvec1 * pzpt, -nsvahl * pvec2 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 + const cxtype_amp_v vcB1_4 = cxmake( hel0 * pvec2 * emp - pvec2 * pzpt, nsvahl * pvec1 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 // Branch B2: pp != 0. and pt == 0. - const cxtype_amp vcB2_3 = cxmake( -hel * sqh, 0. ); - const cxtype_amp_v vcB2_4 = cxmake( 0., fpamp_scalar(nsvahl) * fpternary( ( pvec3 < 0 ), -sqh, sqh ) ); // AV: removed an abs here + const cxtype_amp_v vcB2_3 = cxmake( -hel * sqh, 0. ); + const cxtype_amp_v vcB2_4 = cxmake( 0., nsvahl * fpternary( ( pvec3 < 0 ), -sqh, sqh ) ); // AV: removed an abs here // Choose between the results from branch A and branch B (and from branch B1 and branch B2) const bool_v mask = ( pp == 0. ); const bool_v maskB = ( pt != 0. ); @@ -765,20 +751,14 @@ namespace madmatrix nk = n[0]*pvec0 - n[1]*pvec1 - n[2]*pvec2 - n[3]*pvec3; - // nhel is a scalar: no need for a per-lane mask here (and a bool_v built - // from a single value would only set the first lane) - if ( abs(nhel) == 1 ) - { - w[4] = zero; - } - else - { - w[0] = cxmake( -vmass/nk * n[0], zero ); - w[1] = cxmake( -vmass/nk * n[1], zero ); - w[2] = cxmake( -vmass/nk * n[2], zero ); - w[3] = cxmake( -vmass/nk * n[3], zero ); - w[4] = -static_cast(nsv)*cI; - } + // BRANCHLESS HELICITY: |hel| = 1 keeps w[0..3] with w[4] = 0, hel = 0 (hel0 = 1) takes the + // FD-gauge longitudinal polarisation instead (nk > 0 for a massive momentum, so the unused + // alternative is finite and vanishes when multiplied by its zero truth value) + w[0] = abshel * w[0] + hel0 * cxmake( -vmass / nk * n[0], zero ); + w[1] = abshel * w[1] + hel0 * cxmake( -vmass / nk * n[1], zero ); + w[2] = abshel * w[2] + hel0 * cxmake( -vmass / nk * n[2], zero ); + w[3] = abshel * w[3] + hel0 * cxmake( -vmass / nk * n[3], zero ); + w[4] = hel0 * ( -static_cast( nsv ) * cI ); #endif } else @@ -810,11 +790,11 @@ namespace madmatrix // Branch A: pt != 0. volatile fptype_amp_v ptDENOM = fpternary( pt != 0, pt, 1. ); // hack: ptDENOM[ieppV]=1 if pt[ieppV]==0 const fptype_amp_v pzpt = pvec3 / ( pp * ptDENOM ) * sqh * hel; // hack: dummy[ieppV] is not used if pt[ieppV]==0 - const cxtype_amp_v vcA_3 = cxmake( -pvec1 * pzpt, -fpamp_scalar(nsv) * pvec2 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 - const cxtype_amp_v vcA_4 = cxmake( -pvec2 * pzpt, fpamp_scalar(nsv) * pvec1 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 + const cxtype_amp_v vcA_3 = cxmake( -pvec1 * pzpt, -(fptype_amp)nsv * pvec2 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 + const cxtype_amp_v vcA_4 = cxmake( -pvec2 * pzpt, (fptype_amp)nsv * pvec1 / ptDENOM * sqh ); // hack: dummy[ieppV] is not used if pt[ieppV]==0 // Branch B: pt == 0. - const cxtype_amp vcB_3 = cxmake( -(fptype_amp)hel * sqh, 0 ); - const cxtype_amp_v vcB_4 = cxmake( 0, fpamp_scalar(nsv) * fpternary( ( pvec3 < 0 ), -sqh, sqh ) ); // AV: removed an abs here + const cxtype_amp_v vcB_3 = cxmake( -hel * sqh, 0 ); + const cxtype_amp_v vcB_4 = cxmake( 0, (fptype_amp)nsv * fpternary( ( pvec3 < 0 ), -sqh, sqh ) ); // AV: removed an abs here // Choose between the results from branch A and branch B const bool_v mask = ( pt != 0. ); w[1] = cxternary( mask, vcA_3, vcB_3 ); @@ -873,7 +853,7 @@ namespace madmatrix __host__ __device__ void oxxxxx( const fptype_momenta momenta[], // input: momenta const fptype_amp fmass, // input: fermion mass - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) int flv, // input: flavour ALOHAOBJ & fo, // output: wavefunctions @@ -899,7 +879,12 @@ namespace madmatrix fo.pvec[2] = pvec2_ * static_cast(nsf); fo.pvec[3] = pvec3_ * static_cast(nsf); fo.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; if( fmass != 0. ) { #ifndef MGONGPU_CPPSIMD @@ -910,12 +895,12 @@ namespace madmatrix fptype_amp sqm[2] = { fpsqrt( std::abs( fmass ) ), 0. }; // possibility of negative fermion masses //sqm[1] = ( fmass < 0. ? -abs( sqm[0] ) : abs( sqm[0] ) ); // AV: why abs here? sqm[1] = ( fmass < 0. ? -sqm[0] : sqm[0] ); // AV: removed an abs here - const int ip = -( ( 1 - nh ) / 2 ) * nhel; // NB: Fortran sqm(0:1) also has indexes 0,1 as in C++ - const int im = ( 1 + nh ) / 2 * nhel; // NB: Fortran sqm(0:1) also has indexes 0,1 as in C++ - w[0] = cxmake( im * sqm[std::abs( ip )], 0 ); - w[1] = cxmake( ip * nsf * sqm[std::abs( ip )], 0 ); - w[2] = cxmake( im * nsf * sqm[std::abs( im )], 0 ); - w[3] = cxmake( ip * sqm[std::abs( im )], 0 ); + // The Fortran ip = -( ( 1 - nh ) / 2 ) * nhel and im = ( 1 + nh ) / 2 * nhel are -tm * nhel and + // tp * nhel, so that |ip| = tm and |im| = tp: sqm[|ip|] is sqm[1] wherever ip != 0, and so on + w[0] = cxmake( tp * nhel * sqm[0], 0 ); + w[1] = cxmake( -tm * nhel * nsf * sqm[1], 0 ); + w[2] = cxmake( tp * nhel * nsf * sqm[1], 0 ); + w[3] = cxmake( -tm * nhel * sqm[0], 0 ); } else { @@ -923,17 +908,18 @@ namespace madmatrix fptype_amp( 1 + nsf - ( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; fptype_amp omega[2] = { fpsqrt( pvec0 + pp ), 0. }; omega[1] = fmass / omega[0]; - const int ip = ( 1 + nh ) / 2; // NB: Fortran is (3+nh)/2 because omega(2) has indexes 1,2 and not 0,1 - const int im = ( 1 - nh ) / 2; // NB: Fortran is (3-nh)/2 because omega(2) has indexes 1,2 and not 0,1 - const fptype_amp sfomeg[2] = { sf[0] * omega[ip], sf[1] * omega[im] }; + // The Fortran ip/im indices ( 3 +- nh ) / 2 (omega(2) has indexes 1,2) are the truth values tp/tm + const fptype_amp sfomeg[2] = { sf[0] * ( tm * omega[0] + tp * omega[1] ), sf[1] * ( tp * omega[0] + tm * omega[1] ) }; const fptype_amp pp3 = fpmax( fpternary( fpsignbit( pvec3 ), ( pvec1 * pvec1 + pvec2 * pvec2 ) / ( pp - pvec3 ), pp + pvec3 ), 0. ); const cxtype_amp chi[2] = { cxmake( fpsqrt( pp3 * (fptype_amp)0.5 / pp ), 0. ), ( ( pp3 == 0. ) ? cxmake( -nh, 0. ) : cxmake( nh * pvec1, -pvec2 ) / fpsqrt( (fptype_amp)2. * pp * pp3 ) ) }; - w[0] = sfomeg[1] * chi[im]; - w[1] = sfomeg[1] * chi[ip]; - w[2] = sfomeg[0] * chi[im]; - w[3] = sfomeg[0] * chi[ip]; + const cxtype_amp chi_im = tp * chi[0] + tm * chi[1]; // chi[im] + const cxtype_amp chi_ip = tm * chi[0] + tp * chi[1]; // chi[ip] + w[0] = sfomeg[1] * chi_im; + w[1] = sfomeg[1] * chi_ip; + w[2] = sfomeg[0] * chi_im; + w[3] = sfomeg[0] * chi_ip; } #else volatile fptype_amp_sv p2 = pvec1 * pvec1 + pvec2 * pvec2 + pvec3 * pvec3; // volatile fixes #736 @@ -942,20 +928,17 @@ namespace madmatrix // NB: Do not use "abs" for floats! It returns an integer with no build warning! Use std::abs! fptype_amp sqm[2] = { fpsqrt( std::abs( fmass ) ), 0 }; // possibility of negative fermion masses sqm[1] = ( fmass < 0 ? -sqm[0] : sqm[0] ); // AV: removed an abs here (as above) - const int ipA = -( ( 1 - nh ) / 2 ) * nhel; - const int imA = ( 1 + nh ) / 2 * nhel; - const cxtype_amp foA_2 = imA * sqm[std::abs( ipA )]; - const cxtype_amp foA_3 = ipA * nsf * sqm[std::abs( ipA )]; - const cxtype_amp foA_4 = imA * nsf * sqm[std::abs( imA )]; - const cxtype_amp foA_5 = ipA * sqm[std::abs( imA )]; + // (see the scalar code above for the truth values tp/tm in place of the Fortran ip/im indices) + const cxtype_amp_v foA_2 = cxmake( tp * nhel * sqm[0], 0 ); + const cxtype_amp_v foA_3 = cxmake( -tm * nhel * (fptype_amp)nsf * sqm[1], 0 ); + const cxtype_amp_v foA_4 = cxmake( tp * nhel * (fptype_amp)nsf * sqm[1], 0 ); + const cxtype_amp_v foA_5 = cxmake( -tm * nhel * sqm[0], 0 ); // Branch B: pp != 0. - const fptype_amp sf[2] = { fptype_amp( 1 + nsf + ( 1 - nsf ) * nh ) * (fptype_amp)0.5, - fptype_amp( 1 + nsf - ( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; + const fptype_amp_v sf[2] = { ( (fptype_amp)( 1 + nsf ) + (fptype_amp)( 1 - nsf ) * nh ) * (fptype_amp)0.5, + ( (fptype_amp)( 1 + nsf ) - (fptype_amp)( 1 - nsf ) * nh ) * (fptype_amp)0.5 }; fptype_v omega[2] = { fpsqrt( pvec0 + pp ), 0 }; omega[1] = fmass / omega[0]; - const int ipB = ( 1 + nh ) / 2; - const int imB = ( 1 - nh ) / 2; - const fptype_amp_v sfomeg[2] = { sf[0] * omega[ipB], sf[1] * omega[imB] }; + const fptype_amp_v sfomeg[2] = { sf[0] * ( tm * omega[0] + tp * omega[1] ), sf[1] * ( tp * omega[0] + tm * omega[1] ) }; const fptype_amp_v pp3 = fpmax( fpternary( fpsignbit( pvec3 ), ( pvec1 * pvec1 + pvec2 * pvec2 ) / ( pp - pvec3 ), pp + pvec3 ), 0. ); volatile fptype_amp_v ppDENOM = fpternary( pp != 0, pp, 1. ); // hack: ppDENOM[ieppV]=1 if pp[ieppV]==0 volatile fptype_amp_v pp3DENOM = fpternary( pp3 != 0, pp3, 1. ); // hack: pp3DENOM[ieppV]=1 if pp3[ieppV]==0 @@ -963,11 +946,13 @@ namespace madmatrix const cxtype_amp_v chi[2] = { cxmake( fpsqrt( chi0r2 ), 0. ), // hack: dummy[ieppV] is not used if pp[ieppV]==0 ( cxternary( ( pp3 == 0. ), cxmake( -nh, 0. ), - cxmake( (fptype_amp)nh * pvec1, -pvec2 ) / fpsqrt( (fptype_amp)2. * ppDENOM * pp3DENOM ) ) ) }; // hack: dummy[ieppV] is not used if pp[ieppV]==0 - const cxtype_amp_v foB_2 = sfomeg[1] * chi[imB]; - const cxtype_amp_v foB_3 = sfomeg[1] * chi[ipB]; - const cxtype_amp_v foB_4 = sfomeg[0] * chi[imB]; - const cxtype_amp_v foB_5 = sfomeg[0] * chi[ipB]; + cxmake( nh * pvec1, -pvec2 ) / fpsqrt( (fptype_amp)2. * ppDENOM * pp3DENOM ) ) ) }; // hack: dummy[ieppV] is not used if pp[ieppV]==0 + const cxtype_amp_v chi_im = tp * chi[0] + tm * chi[1]; // chi[im] + const cxtype_amp_v chi_ip = tm * chi[0] + tp * chi[1]; // chi[ip] + const cxtype_amp_v foB_2 = sfomeg[1] * chi_im; + const cxtype_amp_v foB_3 = sfomeg[1] * chi_ip; + const cxtype_amp_v foB_4 = sfomeg[0] * chi_im; + const cxtype_amp_v foB_5 = sfomeg[0] * chi_ip; // Choose between the results from branch A and branch B const bool_v mask = ( pp == 0. ); w[0] = cxternary( mask, foA_2, foB_2 ); @@ -994,7 +979,7 @@ namespace madmatrix const cxtype_amp_v chi[2] = { cxmake( (fptype_v)sqp0p3, 0. ), cxternary( ( sqp0p3 == 0. ), cxmake( -nhel, 0. ) * fpsqrt( (fptype_amp)2. * pvec0 ), - cxmake( (fptype_amp)nh * pvec1, -pvec2 ) / (const fptype_amp_sv)sqp0p3DENOM ) }; // hack: dummy[ieppV] is not used if sqp0p3[ieppV]==0 + cxmake( nh * pvec1, -pvec2 ) / (const fptype_amp_sv)sqp0p3DENOM ) }; // hack: dummy[ieppV] is not used if sqp0p3[ieppV]==0 #else // pvec0+pvec3 is a cancelling difference of two ~|p| numbers for a // backward-moving massless fermion, and everything below divides by it. @@ -1007,22 +992,12 @@ namespace madmatrix fptype_sv{ 0 }, fpsqrt( p0p3stable ) * (fptype_amp)nsf ); const cxtype_amp_sv chi[2] = { cxmake( sqp0p3, 0. ), - ( sqp0p3 == 0. ? cxmake( -nhel, 0. ) * fpsqrt( (fptype_amp)2. * pvec0 ) : cxmake( (fptype_amp)nh * pvec1, -pvec2 ) / sqp0p3 ) }; + ( sqp0p3 == 0. ? cxmake( -nhel, 0. ) * fpsqrt( (fptype_amp)2. * pvec0 ) : cxmake( nh * pvec1, -pvec2 ) / sqp0p3 ) }; #endif - if( nh == 1 ) - { - w[0] = chi[0]; - w[1] = chi[1]; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } - else - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi[1]; - w[3] = chi[0]; - } + w[0] = tp * chi[0]; + w[1] = tp * chi[1]; + w[2] = tm * chi[1]; + w[3] = tm * chi[0]; } mgDebug( 1, __FUNCTION__ ); return; @@ -1036,7 +1011,7 @@ namespace madmatrix __host__ __device__ void opzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fo, // output: wavefunctions @@ -1051,20 +1026,17 @@ namespace madmatrix fo.pvec[2] = fptype_momenta_sv{ 0 }; fo.pvec[3] = pvec3_ * static_cast(nsf); fo.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; const cxtype_amp_sv csqp0p3 = cxmake( fpsqrt( (fptype_amp)2. * pvec3 ) * (fptype_amp)nsf, 0. ); + w[0] = tp * csqp0p3; w[1] = cxzero_sv(); w[2] = cxzero_sv(); - if( nh == 1 ) - { - w[0] = csqp0p3; - w[3] = cxzero_sv(); - } - else - { - w[0] = cxzero_sv(); - w[3] = csqp0p3; - } + w[3] = tm * csqp0p3; mgDebug( 1, __FUNCTION__ ); return; } @@ -1077,7 +1049,7 @@ namespace madmatrix __host__ __device__ void omzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fo, // output: wavefunctions @@ -1092,23 +1064,17 @@ namespace madmatrix fo.pvec[2] = fptype_momenta_sv{ 0 }; fo.pvec[3] = pvec3_ * static_cast(nsf); fo.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; const cxtype_amp_sv chi1 = cxmake( -nhel, 0. ) * fpsqrt( -(fptype_amp)2. * pvec3 ); - if( nh == 1 ) - { - w[0] = cxzero_sv(); - w[1] = chi1; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } - else - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi1; - //w[3] = chi1; // AV: BUG! - w[3] = cxzero_sv(); // AV: BUG FIX - } + w[0] = cxzero_sv(); + w[1] = tp * chi1; + w[2] = tm * chi1; + w[3] = cxzero_sv(); mgDebug( 1, __FUNCTION__ ); return; } @@ -1121,7 +1087,7 @@ namespace madmatrix __host__ __device__ void oxzxxx( const fptype_momenta momenta[], // input: momenta //const fptype_amp fmass, // [skip: ASSUME fermion mass==0] - const int nhel, // input: -1 or +1 (helicity of fermion) + const fptype_amp_sv nhel, // input: -1 or +1 (helicity of fermion, per SIMD lane/GPU thread) const int nsf, // input: +1 (particle) or -1 (antiparticle) const int flv, // input: flavour ALOHAOBJ & fo, // output: wavefunctions @@ -1143,7 +1109,12 @@ namespace madmatrix fo.pvec[2] = pvec2_ * static_cast(nsf); fo.pvec[3] = pvec3_ * static_cast(nsf); fo.flv_index = flv; - const int nh = nhel * nsf; + const fptype_amp_sv nh = nhel * static_cast( nsf ); + // BRANCHLESS HELICITY: the helicity is not a branch condition but enters as the truth values + // tp (nh == +1) and tm (nh == -1), 1 or 0, which multiply the two alternatives. Different + // SIMD lanes or GPU threads can then carry different helicities and still run in lock-step. + const fptype_amp_sv tp = ( 1 + nh ) * (fptype_amp)0.5; + const fptype_amp_sv tm = 1 - tp; //const float sqp0p3 = sqrtf( pvec0 + pvec3 ) * nsf; // AV: why force a float here? // pvec0+pvec3 is a cancelling difference of two ~|p| numbers for a // backward-moving massless fermion, and everything below divides by it. @@ -1155,21 +1126,11 @@ namespace madmatrix pvec0 + pvec3 ); const fptype_amp_sv sqp0p3 = fpsqrt( p0p3stable ) * static_cast(nsf); const cxtype_amp_sv chi0 = cxmake( sqp0p3, 0. ); - const cxtype_amp_sv chi1 = cxmake( (fptype_amp)nh * pvec1 / sqp0p3, -pvec2 / sqp0p3 ); - if( nh == 1 ) - { - w[0] = chi0; - w[1] = chi1; - w[2] = cxzero_sv(); - w[3] = cxzero_sv(); - } - else - { - w[0] = cxzero_sv(); - w[1] = cxzero_sv(); - w[2] = chi1; - w[3] = chi0; - } + const cxtype_amp_sv chi1 = cxmake( nh * pvec1 / sqp0p3, -pvec2 / sqp0p3 ); + w[0] = tp * chi0; + w[1] = tp * chi1; + w[2] = tm * chi1; + w[3] = tm * chi0; mgDebug( 1, __FUNCTION__ ); return; } diff --git a/tests/unit_tests/iolibs/test_madmatrix_writer.py b/tests/unit_tests/iolibs/test_madmatrix_writer.py index a9756dea07..1fa72f32b9 100644 --- a/tests/unit_tests/iolibs/test_madmatrix_writer.py +++ b/tests/unit_tests/iolibs/test_madmatrix_writer.py @@ -168,3 +168,12 @@ def test_no_optimised_call_anywhere(self): for call in self.external_calls(ids): for name in self.OPTIMISED: self.assertNotIn(name, call, ids) + + def test_helicity_is_per_lane(self): + """The external wavefunctions take the helicity of each SIMD lane/GPU + thread (hel_sv, built in calculate_jamps), not one helicity for all + events, so that events with different helicities run in lock-step.""" + calls = self.external_calls([81, -81, 23, 21]) # q q~ > z g + for ipar, call in enumerate(calls): + self.assertIn('hel_sv[%d]' % ipar, call) + self.assertNotIn('cHel[', call) From c17c87f7dc75e3d9609c3505a3fe82715054887f Mon Sep 17 00:00:00 2001 From: Zenny Date: Fri, 23 Oct 2026 11:28:38 +0200 Subject: [PATCH 02/18] MadMatrix: UMAMI parameter setters/getters and model/fptype-tagged libraries - umami_set_parameter / umami_get_parameter: runtime access to independent model parameters (by name or SLHA location) and to parton masses, for the MadMatrix backends (cpu/simd/gpu umami.cc) and the mg7 C++ api; the exporters register every scalar parameter/coupling in a name -> pointer table. Couplings and parameters are registered into separate maps so that FPTYPE=f builds compile (couplings are cxtype, parameters stay double). - Process and common libraries now carry the model name and FPTYPE (libmadmatrix____.so), so builds for different models/precisions out of the same directory never collide; me_path in subprocesses.json gains an {fptype} placeholder, resolved by mg7 launch.py and gridpack.py from [run] precision. - madmatrix.mk/madmatrix_src.mk pass FPTYPE through to the common library build and clean targets. - The MadSpin density libraries follow the new names: LIBTAG comes right after 'madmatrix'/'madmatrix_common', madmatrix_processes.json records the common and process library names (model and '{fptype}' included), and madmatrix/density.py builds and loads them with an explicit FPTYPE. Co-Authored-By: Claude Opus 5.5 --- MadSpin/interface_madspin.py | 2 +- madgraph/iolibs/export_cpp.py | 139 +++++++++++++++++- .../cpp_model_parameters_cc.inc | 66 ++++++++- .../template_files/cpp_model_parameters_h.inc | 43 ++++++ .../template_files/cpp_process_class.inc | 6 + .../cpp_process_function_definitions.inc | 13 +- .../madmatrix/backend/cpu/umami.cc | 36 +++++ .../madmatrix/backend/gpu/umami.cc | 36 +++++ .../madmatrix/backend/simd/umami.cc | 36 +++++ .../madmatrix/cpp_model_parameters_cc.inc | 93 ++++++++++++ .../madmatrix/cpp_model_parameters_h.inc | 56 +++++++ .../template_files/madmatrix/madmatrix.mk | 39 ++++- .../template_files/madmatrix/madmatrix_src.mk | 14 +- .../madmatrix/process_class.inc | 15 ++ .../process_function_definitions.inc | 23 +++ madgraph/iolibs/template_files/mg7/api.cpp | 35 ++++- madgraph/iolibs/template_files/mg7/api.h | 28 +++- .../iolibs/template_files/mg7/gridpack.py | 17 ++- madgraph/iolibs/template_files/mg7/launch.py | 16 +- madmatrix/density.py | 33 ++++- madmatrix/model_handling.py | 62 +++++++- madmatrix/output.py | 25 +++- madspace/include/madspace/umami.h | 10 +- tests/unit_tests/iolibs/test_export_cpp.py | 1 + .../iolibs/test_madmatrix_density.py | 21 ++- 25 files changed, 812 insertions(+), 53 deletions(-) diff --git a/MadSpin/interface_madspin.py b/MadSpin/interface_madspin.py index 1449e51cac..bc15fdcf9f 100755 --- a/MadSpin/interface_madspin.py +++ b/MadSpin/interface_madspin.py @@ -11937,7 +11937,7 @@ def _density_libtag(self, tree, prod_or_decay): that build's tag.""" tag = madspin.density_libtag(self._ms_run_id, 'p' if prod_or_decay == 'prod' else 'd') - if os.path.exists(pjoin(tree, 'lib', 'libmadmatrix_common%s_scalar.so' % tag)): + if os.path.exists(madmatrix_density.common_library_path(tree, tag)): return tag try: return madmatrix_density.find_libtag(tree) diff --git a/madgraph/iolibs/export_cpp.py b/madgraph/iolibs/export_cpp.py index e70cbfe98c..0574b64378 100755 --- a/madgraph/iolibs/export_cpp.py +++ b/madgraph/iolibs/export_cpp.py @@ -121,7 +121,15 @@ def __init__(self, model, output_path, wanted_lorentz = [], self.params_dep = [] # base_objects.ModelVariable self.params_indep = [] # base_objects.ModelVariable self.coups_flv_dep = [] # (name, object, [couplings]) - self.coups_flv_indep = [] # (name, object, [couplings]) + self.coups_flv_indep = [] # (name, object, [couplings]) + self.params_ext_names = [] # names of independent parameters read directly + # from the SLHA card (as opposed to internal + # parameters/couplings derived from them); + # these are the only ones settable by name + # at runtime (e.g. via the UMAMI interface) + self.params_ext_slha = [] # (name, lhablock.lower(), lhacode) for the same + # parameters, so they can also be addressed by + # their SLHA card location (block + index) self.p_to_cpp = parsers.UFOExpressionParserCPP() # Prepare parameters and couplings for writeout in C++ @@ -184,6 +192,18 @@ def prepare_parameters(self): % (param.name, param.lhablock.lower(), param.value.real) else: raise MadGraph5Error("Only support for SLHA blocks with 1 or 2 indices") + # Track which independent parameters were read directly from the + # SLHA card (as opposed to internal parameters derived from them): + # these are the only ones settable by name at runtime (e.g. via + # the UMAMI set_parameter interface). NB: this list is consumed + # only by write_set_parameters() below, not by param.expr itself, + # since other UFOModelConverterCPP subclasses (e.g. the MadMatrix/ + # cudacpp backend) read param.expr directly for purposes (like + # generating hardcoded/constexpr parameter mirrors) that require + # it to remain a single plain assignment. + self.params_ext_names.append(param.name) + self.params_ext_slha.append((param.name, param.lhablock.lower(), + list(param.lhacode))) self.params_indep.insert(0, base_objects.ModelVariable(param.name, expression, @@ -314,6 +334,14 @@ def generate_parameters_class_files(self): replace_dict['set_flv_couplings'] = \ self.write_flv_couplings(self.coups_flv_indep) + replace_dict['register_parameters'] = \ + self.write_register_parameters( + self.params_indep + self.params_dep + \ + self.coups_indep + list(self.coups_dep.values()), + self.params_ext_names) + replace_dict['register_slha_keys'] = \ + self.write_register_slha_keys(self.params_ext_slha) + replace_dict['print_independent_parameters'] = \ self.write_print_parameters(self.params_indep) replace_dict['print_independent_couplings'] = \ @@ -360,11 +388,21 @@ def write_parameters(self, params): def write_set_parameters(self, params): """Write out the lines of independent parameters""" - # For each parameter, write name = expr; + # For each parameter, write name = expr;, unless a runtime override + # was set by name for this (necessarily independent, SLHA-card-level) + # parameter (e.g. via the UMAMI set_parameter interface), in which + # case the override takes precedence over the value read from the + # param card res_strings = [] for param in params: - res_strings.append("%s" % param.expr) + if param.name in self.params_ext_names: + res_strings.append( + "if (m_param_overrides.find(\"%s\") != m_param_overrides.end())\n" + "%s = m_param_overrides[\"%s\"];\nelse\n%s" % + (param.name, param.name, param.name, param.expr)) + else: + res_strings.append("%s" % param.expr) # Correct width sign for Majorana particles (where the width # and mass need to have the same sign) @@ -377,6 +415,55 @@ def write_set_parameters(self, params): return "\n".join(res_strings) + def write_register_parameters(self, params, ext_names, complex_map_name='m_complex_params'): + """Write out the code that registers name->pointer entries for every + scalar parameter and coupling (independent or dependent), and marks + which independent parameters were read directly from the SLHA card + (as opposed to being derived internally). This lookup-by-name table + backs runtime access to parameters/couplings, e.g. through the UMAMI + set_parameter/get_parameter interface. FLV_COUPLING entries are + skipped: they are not simple scalar values. + + complex_map_name: which C++ map a complex-valued param's pointer + gets registered into (default 'm_complex_params'). Model parameters + (mdl_*) and derived couplings (GC_*) are *not* interchangeable + here in the mg7/madmatrix backend: couplings are declared cxtype + (which the caller may have templated to a non-double precision -- + see model_handling.py's own 'cxsmpl' -> 'cxtype' rewrite + for independent couplings specifically), while parameters always + stay plain cxsmpl for numerical accuracy (matching + m_real_params' own always-double real-parameter pointers) -- so a + caller registering both groups needs two separate calls, one per + map, each with its own C++ declaration of matching pointer type; + passing both groups into a single call/map only compiles by + accident whenever cxtype happens to resolve to cxsmpl too + (FPTYPE=m/d), and fails to compile at FPTYPE=f.""" + + lines = [] + for param in params: + if not hasattr(param, 'type'): + continue + map_name = 'm_real_params' if param.type == 'real' \ + else complex_map_name + lines.append('%s["%s"] = &%s;' % (map_name, param.name, param.name)) + for name in ext_names: + lines.append('m_settable_params.insert("%s");' % name) + + return "\n".join(lines) + + def write_register_slha_keys(self, params_ext_slha): + """Write out the code that registers a lookup from SLHA (block, index[, + index2]) key strings to parameter names, so that a settable parameter + can also be addressed by its location in the SLHA param card (e.g. "mass + 6") rather than only by its model name (e.g. "mdl_MT").""" + + lines = [] + for name, block, lhacode in params_ext_slha: + key = block + "".join(" %d" % i for i in lhacode) + lines.append('m_slha_key_to_name["%s"] = "%s";' % (key, name)) + + return "\n".join(lines) + def _assert_flv_couplings_supported(self, params): """Refuse, with a clear and actionable message, the merged-flavor coupling structures the C++ (mg7/standalone) backend cannot yet @@ -1053,6 +1140,8 @@ def get_process_function_definitions(self, write=True): replace_dict['initProc_lines'] = \ self.get_initProc_lines(self.matrix_elements[0], color_amplitudes) + replace_dict['refreshMasses_lines'] = \ + self.get_refresh_masses_lines(self.matrix_elements[0]) replace_dict['reset_jamp_lines'] = \ self.get_reset_jamp_lines(color_amplitudes) replace_dict['sigmaKin_lines'], other_replace = \ @@ -1138,6 +1227,17 @@ def get_initProc_lines(self, matrix_element, color_amplitudes): return "\n".join(initProc_lines) + # Companion to get_initProc_lines: the same external-mass push_back list, without the + # jamp2 allocation (jamp2 is allocated once in the constructor and must not be + # reallocated every time CPPProcess::refreshMasses() re-populates mME). + def get_refresh_masses_lines(self, matrix_element): + """Get refreshMasses_lines for function definition for CPPProcess::refreshMasses""" + + lines = [] + for part in matrix_element.get_external_wavefunctions(): + lines.append("mME.push_back(pars.%s);" % part.get('mass')) + return "\n".join(lines) + def get_reset_jamp_lines(self, color_amplitudes): """Get lines to reset jamps""" @@ -3251,9 +3351,20 @@ class ProcessExporterMG7(ProcessExporterCPP): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.me_lib_format = args[1].get("me_lib_format", None) + self.me_lib_format = args[1].get("me_lib_format", None) or pjoin("SubProcesses", "{process_id}", "api.so") self.process_info = [] self.merged_subprocesses = defaultdict(list) + # Lazy import: madgraph.iolibs.export_mg7 imports export_cpp itself, so a top-level + # import here would be circular. Without this, oneprocessclass silently falls back + # to the base ProcessExporterCPP.oneprocessclass (OneProcessExporterCPP), which lacks + # the .name attribute (and other MG7-specific behaviour) generate_subprocess_directory + # needs. Only apply the fallback when nothing more specific was set: subclasses such + # as madmatrix.output.ProcessExporterMadMatrix set their own oneprocessclass (with the + # coloramps.h/epoch_process_id.h/color_sum.cc/makefile-symlink logic in + # OneProcessExporterMadMatrix.generate_process_files()) and that must not be clobbered. + if self.oneprocessclass is OneProcessExporterCPP: + from madgraph.iolibs import export_mg7 + self.oneprocessclass = export_mg7.OneProcessExporterMG7 def generate_subprocess_directory( self, matrix_element, cpp_helas_call_writer, proc_number=None @@ -3268,6 +3379,13 @@ def generate_subprocess_directory( # Create the directory PN_xx_xxxxx in the specified path proc_dir_name = process_exporter_mg7.name dirpath = pjoin(self.dir_path, 'SubProcesses', proc_dir_name) + # self.model/self.model_name aren't reliably set on this class by the + # time subprocess directories are generated (see eg + # write_check_sa_cpp()'s own "self.model may not be set yet" fallback + # above); the matrix element's own model is always available though, + # and is what %(model)s in template_Sub_make/template_src_make is + # substituted from too (via copy_template()'s model parameter). + model = matrix_element.get('processes')[0].get('model') try: os.mkdir(dirpath) @@ -3284,7 +3402,6 @@ def generate_subprocess_directory( # Generate SVG Feynman diagrams (diagrams.svg + diagrams.json) if not self.opt.get('output_options', {}).get('noeps') == 'True': svg_stem = pjoin(dirpath, 'diagrams') - model = matrix_element.get('processes')[0].get('model') diagrams = matrix_element.get('base_amplitude').get('diagrams') logger.info('Generating Feynman diagrams for %s' % matrix_element.get('processes')[0].nice_string()) @@ -3292,7 +3409,17 @@ def generate_subprocess_directory( model=model, amplitude=True) plot.draw() - me_lib_path = self.me_lib_format.format(process_id = proc_dir_name) + # me_lib_format may (eg madmatrix/output.py's ProcessExporterMadMatrix) + # carry a {model} placeholder alongside {process_id}, resolved here to + # the same sanitized model name template_Sub_make's %(model)s uses; + # harmless (str.format ignores unused kwargs) for exporters whose + # me_lib_format has no {model} placeholder at all. + # kept for the exporters that name the libraries elsewhere too (the + # process table of madmatrix/output.py's standalone exporter) + self.lib_model_name = self.get_model_name(model.get('name')) + me_lib_path = self.me_lib_format.format( + process_id=proc_dir_name, model=self.lib_model_name + ) # Store the path relative to the process directory (like me_path): an # absolute path would break as soon as the process directory is moved. rel_dirpath = pjoin('SubProcesses', proc_dir_name) diff --git a/madgraph/iolibs/template_files/cpp_model_parameters_cc.inc b/madgraph/iolibs/template_files/cpp_model_parameters_cc.inc index 26ba3a3668..c3b0222785 100644 --- a/madgraph/iolibs/template_files/cpp_model_parameters_cc.inc +++ b/madgraph/iolibs/template_files/cpp_model_parameters_cc.inc @@ -3,16 +3,21 @@ %(info_lines)s //========================================================================== +#include +#include #include #include +#include #include "Parameters_%(model_name)s.h" void Parameters_%(model_name)s::setIndependentParameters(SLHAReader& slha){ // Define "zero" zero=0; ZERO=0; - // Prepare a vector for indices + // Prepare a vector for indices vector indices(2,0); + m_slha = slha; + registerParameters(); %(set_independent_parameters)s } void Parameters_%(model_name)s::setIndependentCouplings(){ @@ -44,3 +49,62 @@ %(print_dependent_couplings)s } + void Parameters_%(model_name)s::registerParameters(){ + %(register_parameters)s + %(register_slha_keys)s + } + + bool Parameters_%(model_name)s::resolveSlhaKey(const std::string& name, std::string& resolved) const { + std::istringstream iss(name); + std::string block; + if (!(iss >> block)) return false; + std::transform(block.begin(), block.end(), block.begin(), + [](unsigned char c){ return std::tolower(c); }); + std::string key = block; + bool any_index = false; + int index; + while (iss >> index) { + key += " " + std::to_string(index); + any_index = true; + } + if (!any_index) return false; + std::map::const_iterator it = m_slha_key_to_name.find(key); + if (it == m_slha_key_to_name.end()) return false; + resolved = it->second; + return true; + } + + bool Parameters_%(model_name)s::setParameterByName(std::string name, double real, double imag){ + (void)imag; // settable (SLHA-card-level) parameters are always real + std::string resolved; + if (resolveSlhaKey(name, resolved)) name = resolved; + if (m_settable_params.find(name) == m_settable_params.end()) + return false; + m_param_overrides[name] = real; + setIndependentParameters(m_slha); + setIndependentCouplings(); + return true; + } + + bool Parameters_%(model_name)s::getParameterByName(std::string name, double* real, double* imag){ + std::string resolved; + if (resolveSlhaKey(name, resolved)) name = resolved; + map::iterator real_it = m_real_params.find(name); + if (real_it != m_real_params.end()){ + *real = *(real_it->second); + if (imag) *imag = 0.; + return true; + } + map *>::iterator complex_it = m_complex_params.find(name); + if (complex_it != m_complex_params.end()){ + *real = complex_it->second->real(); + if (imag) *imag = complex_it->second->imag(); + return true; + } + return false; + } + + void Parameters_%(model_name)s::resetParameterOverrides(){ + m_param_overrides.clear(); + } + diff --git a/madgraph/iolibs/template_files/cpp_model_parameters_h.inc b/madgraph/iolibs/template_files/cpp_model_parameters_h.inc index 7fa3fb27e8..4f044b9f04 100644 --- a/madgraph/iolibs/template_files/cpp_model_parameters_h.inc +++ b/madgraph/iolibs/template_files/cpp_model_parameters_h.inc @@ -7,6 +7,9 @@ #define Parameters_%(model_name)s_H #include +#include +#include +#include #include "read_slha.h" using namespace std; @@ -48,6 +51,46 @@ void printIndependentCouplings(); void printDependentParameters(); // Print couplings that are changed event by event void printDependentCouplings(); + +// Set the value of an independent, SLHA-card-level parameter by name at +// runtime (overriding the value read from the param card), and recompute +// every parameter/coupling that depends on it. Returns false if "name" is +// not the name of a settable independent parameter (e.g. it names an +// internal parameter derived from others, or a coupling, neither of which +// can be set directly since they would just be recomputed from the +// underlying parameters on the next call). "name" may be either the +// parameter's model name (e.g. "mdl_MT") or its SLHA card location, given as +// " []" (e.g. "mass 6"); the block name is matched +// case-insensitively. +bool setParameterByName(std::string name, double real, double imag); +// Retrieve the current value of any parameter or coupling by name (real or +// complex, independent or dependent). As with setParameterByName, "name" may +// be either the parameter's model name or its SLHA card location. Returns +// false if "name" is unknown. +bool getParameterByName(std::string name, double* real, double* imag); +// Drop all overrides previously set via setParameterByName(), so that the +// next setIndependentParameters(slha) call starts from a clean slate (e.g. +// when (re-)initializing from a param card). Does not itself recompute +// anything; the caller is expected to call setIndependentParameters()/ +// setIndependentCouplings() afterward (as CPPProcess's constructor does). +void resetParameterOverrides(); + +private: + +// If "name" is of the form " []", resolve it (via +// m_slha_key_to_name) to the corresponding parameter's model name and return +// true; otherwise leave "resolved" untouched and return false. +bool resolveSlhaKey(const std::string& name, std::string& resolved) const; + +// Name-keyed lookup tables populated by registerParameters(), backing +// setParameterByName()/getParameterByName() above. +void registerParameters(); +std::map m_real_params; +std::map *> m_complex_params; +std::set m_settable_params; +std::map m_param_overrides; +std::map m_slha_key_to_name; +SLHAReader m_slha; }; #endif // Parameters_%(model_name)s_H diff --git a/madgraph/iolibs/template_files/cpp_process_class.inc b/madgraph/iolibs/template_files/cpp_process_class.inc index b64dfd4f2f..3739efb726 100644 --- a/madgraph/iolibs/template_files/cpp_process_class.inc +++ b/madgraph/iolibs/template_files/cpp_process_class.inc @@ -29,6 +29,12 @@ public: const vector& getMasses() const {return mME;} + // Recompute mME (external particle on-shell masses) from the current model parameters + // (pars). mME is only populated once, in the constructor; call this after overriding a + // mass via pars.setParameterByName() (e.g. through umami_set_parameter) so getMasses() + // reflects the new value. + void refreshMasses(); + const double* getAmp2() const { return amp2; } Parameters_%(model_name)s& getParameters() { return pars; } diff --git a/madgraph/iolibs/template_files/cpp_process_function_definitions.inc b/madgraph/iolibs/template_files/cpp_process_function_definitions.inc index db3a44fee6..79a3be1664 100644 --- a/madgraph/iolibs/template_files/cpp_process_function_definitions.inc +++ b/madgraph/iolibs/template_files/cpp_process_function_definitions.inc @@ -10,13 +10,22 @@ CPPProcess::CPPProcess(string param_card_name) : { // Instantiate the model class and set parameters that stay fixed during run SLHAReader slha(param_card_name, false); + pars.resetParameterOverrides(); pars.setIndependentParameters(slha); pars.setIndependentCouplings(); %(initProc_lines)s -} +} + +//-------------------------------------------------------------------------- +// Re-populate mME from the current model parameters (pars). See declaration in CPPProcess.h. + +void CPPProcess::refreshMasses() { + mME.clear(); + %(refreshMasses_lines)s +} //-------------------------------------------------------------------------- -// Evaluate |M|^2, part independent of incoming flavour. +// Evaluate |M|^2, part independent of incoming flavour. double CPPProcess::sigmaKin(int flavor_id) { // Set the parameters which change event by event diff --git a/madgraph/iolibs/template_files/madmatrix/backend/cpu/umami.cc b/madgraph/iolibs/template_files/madmatrix/backend/cpu/umami.cc index 419c0310e0..75d9846d84 100644 --- a/madgraph/iolibs/template_files/madmatrix/backend/cpu/umami.cc +++ b/madgraph/iolibs/template_files/madmatrix/backend/cpu/umami.cc @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -308,7 +309,36 @@ extern "C" static_cast( handle )->summed_helicity = parameter_real != 0; return UMAMI_SUCCESS; } + // Parameters are process-wide (Parameters::getInstance()), not per-handle, + // matching how umami_initialize itself never stores the CPPProcess it + // constructs: initProc() only ever populates the Parameters singleton. +#ifdef MGONGPU_HARDCODE_PARAM + // Hardcoded parameters are compile-time constants: there is nothing to set. return UMAMI_ERROR_NOT_IMPLEMENTED; +#else + if( !Parameters::getInstance()->setParameterByName( name, parameter_real, parameter_imag ) ) + return UMAMI_ERROR_UNKNOWN_PARAMETER; + // sigmaKin() and friends read cached copies of the independent parameters/couplings + // (cIPD/cIPC/cIPF/bsmIndepParam), not the Parameters singleton itself, for + // performance; refresh those caches now so the override actually takes effect. + CPPProcess::refreshIndependentParams(); + // Keep g_externalMasses in sync with the (possibly updated) singleton. + // Emit a one-time warning if a mass parameter was changed, since any phase-space + // generator that cached masses from a prior umami_get_meta(UMAMI_META_MASSES,...) call + // will need to re-query them. + { + std::vector updatedMasses; + CPPProcess::refreshExternalMasses( updatedMasses ); + bool changed = ( updatedMasses.size() != g_externalMasses.size() ); + for( size_t i = 0; !changed && i < updatedMasses.size(); ++i ) + changed = ( static_cast( updatedMasses[i] ) != g_externalMasses[i] ); + if( changed ) + { + g_externalMasses.assign( updatedMasses.begin(), updatedMasses.end() ); + } + } + return UMAMI_SUCCESS; +#endif } UmamiStatus umami_get_parameter( @@ -317,7 +347,13 @@ extern "C" [[maybe_unused]] double* parameter_real, [[maybe_unused]] double* parameter_imag ) { +#ifdef MGONGPU_HARDCODE_PARAM return UMAMI_ERROR_NOT_IMPLEMENTED; +#else + if( !Parameters::getInstance()->getParameterByName( name, parameter_real, parameter_imag ) ) + return UMAMI_ERROR_UNKNOWN_PARAMETER; + return UMAMI_SUCCESS; +#endif } UmamiStatus umami_matrix_element( diff --git a/madgraph/iolibs/template_files/madmatrix/backend/gpu/umami.cc b/madgraph/iolibs/template_files/madmatrix/backend/gpu/umami.cc index 74335b434b..3203ebb88f 100644 --- a/madgraph/iolibs/template_files/madmatrix/backend/gpu/umami.cc +++ b/madgraph/iolibs/template_files/madmatrix/backend/gpu/umami.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -397,7 +398,36 @@ extern "C" static_cast( handle )->summed_helicity = parameter_real != 0; return UMAMI_SUCCESS; } + // Parameters are process-wide (Parameters::getInstance()), not per-handle, + // matching how umami_initialize itself never stores the CPPProcess it + // constructs: initProc() only ever populates the Parameters singleton. +#ifdef MGONGPU_HARDCODE_PARAM + // Hardcoded parameters are compile-time constants: there is nothing to set. return UMAMI_ERROR_NOT_IMPLEMENTED; +#else + if( !Parameters::getInstance()->setParameterByName( name, parameter_real, parameter_imag ) ) + return UMAMI_ERROR_UNKNOWN_PARAMETER; + // sigmaKin() and friends read cached copies of the independent parameters/couplings + // (cIPD/cIPC/cIPF/bsmIndepParam), not the Parameters singleton itself, for + // performance; refresh those caches now so the override actually takes effect. + CPPProcess::refreshIndependentParams(); + // Keep g_externalMasses in sync with the (possibly updated) singleton. + // Emit a one-time warning if a mass parameter was changed, since any phase-space + // generator that cached masses from a prior umami_get_meta(UMAMI_META_MASSES,...) call + // will need to re-query them. + { + std::vector updatedMasses; + CPPProcess::refreshExternalMasses( updatedMasses ); + bool changed = ( updatedMasses.size() != g_externalMasses.size() ); + for( size_t i = 0; !changed && i < updatedMasses.size(); ++i ) + changed = ( static_cast( updatedMasses[i] ) != g_externalMasses[i] ); + if( changed ) + { + g_externalMasses.assign( updatedMasses.begin(), updatedMasses.end() ); + } + } + return UMAMI_SUCCESS; +#endif } UmamiStatus umami_get_parameter( @@ -406,7 +436,13 @@ extern "C" [[maybe_unused]] double* parameter_real, [[maybe_unused]] double* parameter_imag ) { +#ifdef MGONGPU_HARDCODE_PARAM return UMAMI_ERROR_NOT_IMPLEMENTED; +#else + if( !Parameters::getInstance()->getParameterByName( name, parameter_real, parameter_imag ) ) + return UMAMI_ERROR_UNKNOWN_PARAMETER; + return UMAMI_SUCCESS; +#endif } UmamiStatus umami_matrix_element( diff --git a/madgraph/iolibs/template_files/madmatrix/backend/simd/umami.cc b/madgraph/iolibs/template_files/madmatrix/backend/simd/umami.cc index 6ca9d8a37a..b2d6be465f 100644 --- a/madgraph/iolibs/template_files/madmatrix/backend/simd/umami.cc +++ b/madgraph/iolibs/template_files/madmatrix/backend/simd/umami.cc @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -309,7 +310,36 @@ extern "C" static_cast( handle )->summed_helicity = parameter_real != 0; return UMAMI_SUCCESS; } + // Parameters are process-wide (Parameters::getInstance()), not per-handle, + // matching how umami_initialize itself never stores the CPPProcess it + // constructs: initProc() only ever populates the Parameters singleton. +#ifdef MGONGPU_HARDCODE_PARAM + // Hardcoded parameters are compile-time constants: there is nothing to set. return UMAMI_ERROR_NOT_IMPLEMENTED; +#else + if( !Parameters::getInstance()->setParameterByName( name, parameter_real, parameter_imag ) ) + return UMAMI_ERROR_UNKNOWN_PARAMETER; + // sigmaKin() and friends read cached copies of the independent parameters/couplings + // (cIPD/cIPC/cIPF/bsmIndepParam), not the Parameters singleton itself, for + // performance; refresh those caches now so the override actually takes effect. + CPPProcess::refreshIndependentParams(); + // Keep g_externalMasses in sync with the (possibly updated) singleton. + // Emit a one-time warning if a mass parameter was changed, since any phase-space + // generator that cached masses from a prior umami_get_meta(UMAMI_META_MASSES,...) call + // will need to re-query them. + { + std::vector updatedMasses; + CPPProcess::refreshExternalMasses( updatedMasses ); + bool changed = ( updatedMasses.size() != g_externalMasses.size() ); + for( size_t i = 0; !changed && i < updatedMasses.size(); ++i ) + changed = ( static_cast( updatedMasses[i] ) != g_externalMasses[i] ); + if( changed ) + { + g_externalMasses.assign( updatedMasses.begin(), updatedMasses.end() ); + } + } + return UMAMI_SUCCESS; +#endif } UmamiStatus umami_get_parameter( @@ -318,7 +348,13 @@ extern "C" [[maybe_unused]] double* parameter_real, [[maybe_unused]] double* parameter_imag ) { +#ifdef MGONGPU_HARDCODE_PARAM return UMAMI_ERROR_NOT_IMPLEMENTED; +#else + if( !Parameters::getInstance()->getParameterByName( name, parameter_real, parameter_imag ) ) + return UMAMI_ERROR_UNKNOWN_PARAMETER; + return UMAMI_SUCCESS; +#endif } UmamiStatus umami_matrix_element( diff --git a/madgraph/iolibs/template_files/madmatrix/cpp_model_parameters_cc.inc b/madgraph/iolibs/template_files/madmatrix/cpp_model_parameters_cc.inc index 5d465f831e..094964fea4 100644 --- a/madgraph/iolibs/template_files/madmatrix/cpp_model_parameters_cc.inc +++ b/madgraph/iolibs/template_files/madmatrix/cpp_model_parameters_cc.inc @@ -13,8 +13,11 @@ #include "Parameters.h" +#include +#include #include #include +#include using namespace madmatrix; @@ -38,6 +41,8 @@ Parameters::setIndependentParameters( SLHAReader& slha ) zero = 0; // define "zero" ZERO = 0; // define "zero" std::vector indices( 2, 0 ); // prepare a vector for indices + m_slha = slha; + registerParameters(); %(set_independent_parameters)s } @@ -48,6 +53,94 @@ Parameters::setIndependentCouplings() %(set_flv_couplings)s } +void +Parameters::registerParameters() +{ + %(register_parameters)s + %(register_slha_keys)s +} + +bool +Parameters::resolveSlhaKey( const std::string& name, std::string& resolved ) const +{ + std::istringstream iss( name ); + std::string block; + if( !( iss >> block ) ) return false; + std::transform( block.begin(), block.end(), block.begin(), + []( unsigned char c ) { return std::tolower( c ); } ); + std::string key = block; + bool any_index = false; + int index; + while( iss >> index ) + { + key += " " + std::to_string( index ); + any_index = true; + } + if( !any_index ) return false; + std::map::const_iterator it = m_slha_key_to_name.find( key ); + if( it == m_slha_key_to_name.end() ) return false; + resolved = it->second; + return true; +} + +bool +Parameters::setParameterByName( std::string name, double real, double imag ) +{ + (void)imag; // settable (SLHA-card-level) parameters are always real + std::string resolved; + if( resolveSlhaKey( name, resolved ) ) name = resolved; + if( m_settable_params.find( name ) == m_settable_params.end() ) + return false; + m_param_overrides[name] = real; + setIndependentParameters( m_slha ); + setIndependentCouplings(); + return true; +} + +bool +Parameters::getParameterByName( std::string name, double* real, double* imag ) +{ + std::string resolved; + if( resolveSlhaKey( name, resolved ) ) name = resolved; + std::map::iterator real_it = m_real_params.find( name ); + if( real_it != m_real_params.end() ) + { + *real = *( real_it->second ); + if( imag ) *imag = 0.; + return true; + } + // cxtype (not hardcoded cxsmpl): must match m_complex_params' + // own value_type (see its declaration's own comment) -- real()/imag() + // below still convert safely into *real/*imag (always plain double* + // here) whatever cxtype's own underlying precision is. + std::map::iterator complex_it = m_complex_params.find( name ); + if( complex_it != m_complex_params.end() ) + { + *real = complex_it->second->real(); + if( imag ) *imag = complex_it->second->imag(); + return true; + } + // Independent model parameters (as opposed to derived couplings just + // above) -- see m_complex_model_params' own declaration comment for why + // this is a second, separate map/type rather than sharing + // m_complex_params. + std::map*>::iterator complex_model_it = + m_complex_model_params.find( name ); + if( complex_model_it != m_complex_model_params.end() ) + { + *real = complex_model_it->second->real(); + if( imag ) *imag = complex_model_it->second->imag(); + return true; + } + return false; +} + +void +Parameters::resetParameterOverrides() +{ + m_param_overrides.clear(); +} + /* void Parameters::setDependentParameters() // now computed event-by-event (running alphas #373) diff --git a/madgraph/iolibs/template_files/madmatrix/cpp_model_parameters_h.inc b/madgraph/iolibs/template_files/madmatrix/cpp_model_parameters_h.inc index ded7bf84d5..8bcfa3e425 100644 --- a/madgraph/iolibs/template_files/madmatrix/cpp_model_parameters_h.inc +++ b/madgraph/iolibs/template_files/madmatrix/cpp_model_parameters_h.inc @@ -34,6 +34,10 @@ #include "read_slha.h" +#include +#include +#include + //One namespace. Split ber backend. namespace madmatrix { @@ -105,8 +109,60 @@ namespace madmatrix static constexpr int nBsmIndepParam = %(nbsmip)i; %(hasbsmip)sdouble mdl_bsmIndepParam[nBsmIndepParam]; + // Set the value of an independent, SLHA-card-level parameter by name at + // runtime (overriding the value read from the param card), and recompute + // every parameter/coupling that depends on it. Returns false if "name" is + // not the name of a settable independent parameter (e.g. it names an + // internal parameter derived from others, or a coupling, neither of which + // can be set directly since they would just be recomputed from the + // underlying parameters on the next call). + // "name" may be either the parameter's model name (e.g. "mdl_MT") or its + // SLHA card location, given as " []" (e.g. "mass + // 6"); the block name is matched case-insensitively. + bool setParameterByName( std::string name, double real, double imag ); + // Retrieve the current value of any independent parameter or coupling by + // name (real or complex; "name" accepts the same two forms as + // setParameterByName). NB: dependent (aS-dependent) parameters and + // couplings are not stored here at all in this backend (they are + // recomputed fresh every event batch by computeDependentCouplings()), so + // they cannot be retrieved by name; only independent ones can. Returns + // false if "name" is unknown. + bool getParameterByName( std::string name, double* real, double* imag ); + // Drop all overrides previously set via setParameterByName(), so that the + // next setIndependentParameters(slha) call starts from a clean slate (e.g. + // when (re-)initializing from a param card). Does not itself recompute + // anything; the caller is expected to call setIndependentParameters()/ + // setIndependentCouplings() afterward (as initProc() does). + void resetParameterOverrides(); + private: + // If "name" is of the form " []", resolve it (via + // m_slha_key_to_name) to the corresponding parameter's model name and + // return true; otherwise leave "resolved" untouched and return false. + bool resolveSlhaKey( const std::string& name, std::string& resolved ) const; + + // Name-keyed lookup tables populated by registerParameters(), backing + // setParameterByName()/getParameterByName() above. + void registerParameters(); + std::map m_real_params; + // Two separate maps, not one: independent *couplings* (eg GC_72 below) + // are declared cxtype, which resolves to cxsmpl at FPTYPE=f, so + // this map's value_type has to be cxtype too (a mismatched hardcoded + // cxsmpl here fails to compile the moment any coupling is + // actually registered below, eg complex effective couplings like + // ggH's) -- while independent model *parameters* (eg mdl_I1x33) always + // stay plain cxsmpl, matching m_real_params' own always-double + // real parameters, and go into m_complex_model_params below instead; + // see write_register_parameters()'s own doc comment (export_cpp.py) + // for why these can't share one map/type. + std::map m_complex_params; + std::map*> m_complex_model_params; + std::set m_settable_params; + std::map m_param_overrides; + std::map m_slha_key_to_name; + SLHAReader m_slha; + static Parameters* instance; }; diff --git a/madgraph/iolibs/template_files/madmatrix/madmatrix.mk b/madgraph/iolibs/template_files/madmatrix/madmatrix.mk index ef0f449331..b6ea6f5bf2 100644 --- a/madgraph/iolibs/template_files/madmatrix/madmatrix.mk +++ b/madgraph/iolibs/template_files/madmatrix/madmatrix.mk @@ -409,6 +409,9 @@ else endif export BACKENDDIR +# Export FPTYPE (used e.g. to name the common library, see MADMATRIX_COMMONLIB below) +export FPTYPE + #------------------------------------------------------------------------------- #=== Configure ccache for C++ and CUDA/HIP builds @@ -469,11 +472,16 @@ ifeq ($(PCH),1) endif # Dependency on src directory -# The common library name carries the full BACKEND suffix so each vectorisation/GPU variant is distinct. +# The common library name carries the model name and the full BACKEND suffix -- see the +# matching comment in madmatrix_src.mk for why the model name is required (dlopen +# collisions across process directories regenerated in different models) -- and, purely +# for naming consistency with MADMATRIX_LIB, the FPTYPE too; unlike MADMATRIX_LIB this +# isn't consumed by anything outside these madmatrix makefiles (subprocesses.json never +# references it directly), so no further propagation is needed for this one. # LIBTAG (empty by default) goes into the name of every library of this output: the # dynamic loader identifies a library by its name, so two outputs loaded into one # process (MadSpin's production and decay matrix elements) need different ones. -MADMATRIX_COMMONLIB = madmatrix_common$(LIBTAG)_$(BACKEND) +MADMATRIX_COMMONLIB = madmatrix_common$(LIBTAG)_%(model)s_$(BACKEND)_$(FPTYPE) LIBFLAGS = -L$(LIBDIR) -l$(MADMATRIX_COMMONLIB) INCFLAGS += -I$(SRC) -I$(SRC)/rambo @@ -814,7 +822,16 @@ override RUNTIME = processid_short=$(shell basename $(CURDIR)) ###$(info processid_short=$(processid_short)) -MADMATRIX_LIB = madmatrix$(LIBTAG)_$(processid_short)_$(BACKEND) +# The process library name carries LIBTAG (see MADMATRIX_COMMONLIB above), the model +# name (same %(model)s substitution, and the same dlopen-collision rationale, as +# MADMATRIX_COMMONLIB above), the process id, the hardware backend, and the +# floating-point type -- so that libraries built for different models/precisions/backends +# out of the same process directory never collide, and so that a loader can tell, from +# the filename alone, exactly which of those a given library on disk was built with +# (subprocesses.json itself stays agnostic to BACKEND and FPTYPE -- see me_lib_format in +# madmatrix/output.py -- since a given process directory can be compiled for several of +# each without regenerating the JSON). +MADMATRIX_LIB = madmatrix$(LIBTAG)_%(model)s_$(processid_short)_$(BACKEND)_$(FPTYPE) objects_lib=$(BUILDDIR)/CPPProcess.o $(BUILDDIR)/color_sum.o $(BUILDDIR)/MatrixElementKernels.o $(BUILDDIR)/CrossSectionKernels.o $(BUILDDIR)/umami.o $(BUILDDIR)/SigmaKin.o # The spin-density matrices for MadSpin (backend/common/Density.h): C++ backends, # and only in the builds madmatrix/density.py asks for (DENSITY=1, the scalar @@ -917,7 +934,7 @@ ifeq ($(MADMATRIX_COMMONLIB_EXTERNAL),1) $(LIBDIR)/lib$(MADMATRIX_COMMONLIB).so: ; else $(LIBDIR)/lib$(MADMATRIX_COMMONLIB).so: $(SRC)/*.h $(SRC)/*.cc $(BUILDDIR)/.build.$(TAG) - $(MAKE) -C $(SRC) BACKEND=$(BACKEND) LIBDIR=$(LIBDIR) + $(MAKE) -C $(SRC) BACKEND=$(BACKEND) FPTYPE=$(FPTYPE) LIBDIR=$(LIBDIR) endif #------------------------------------------------------------------------------- @@ -1020,20 +1037,26 @@ $(addprefix commonlib.,$(BLDBACKENDS)): commonlib.%%: clean: ifeq ($(USEBUILDDIR),1) rm -rf $(BUILDDIR) +ifneq ($(MADMATRIX_COMMONLIB_EXTERNAL),1) + $(MAKE) -C $(SRC) clean BACKEND=$(BACKEND) FPTYPE=$(FPTYPE) LIBDIR=$(LIBDIR) +endif else rm -f $(BUILDDIR)/.build.* $(BUILDDIR)/*.o rm -f $(LIBDIR)/lib$(MADMATRIX_LIB).so rm -f $(BACKEND_LOG) ifneq ($(MADMATRIX_COMMONLIB_EXTERNAL),1) - $(MAKE) -C $(SRC) clean BACKEND=$(BACKEND) LIBDIR=$(LIBDIR) + $(MAKE) -C $(SRC) clean BACKEND=$(BACKEND) FPTYPE=$(FPTYPE) LIBDIR=$(LIBDIR) endif endif -# cleanall: remove objects and libraries for ALL backends. +# cleanall: remove objects and libraries for ALL backends and FPTYPEs. +# (NB: MADMATRIX_LIB itself only names one BACKEND/FPTYPE combination -- see +# its own definition above -- so this globs for all of them explicitly, +# rather than just removing $(MADMATRIX_LIB) as 'clean' above does.) cleanall: rm -rf build.* rm -f ./.build.* ./*.o - rm -f $(LIBDIR)/libmadmatrix_$(processid_short)_*.so + rm -f $(LIBDIR)/libmadmatrix_%(model)s_$(processid_short)_*.so ifneq ($(MADMATRIX_COMMONLIB_EXTERNAL),1) $(MAKE) -C $(SRC) cleanall LIBDIR=$(LIBDIR) endif @@ -1042,7 +1065,7 @@ endif # for the SubProcesses dispatcher, which owns the common library while the P* # directories are cleaned in parallel with MADMATRIX_COMMONLIB_EXTERNAL=1. cleancommon: - $(MAKE) -C $(SRC) clean BACKEND=$(BACKEND) LIBDIR=$(LIBDIR) + $(MAKE) -C $(SRC) clean BACKEND=$(BACKEND) FPTYPE=$(FPTYPE) LIBDIR=$(LIBDIR) cleanallcommon: $(MAKE) -C $(SRC) cleanall LIBDIR=$(LIBDIR) diff --git a/madgraph/iolibs/template_files/madmatrix/madmatrix_src.mk b/madgraph/iolibs/template_files/madmatrix/madmatrix_src.mk index 1a68e606a5..f918e2a6c7 100644 --- a/madgraph/iolibs/template_files/madmatrix/madmatrix_src.mk +++ b/madgraph/iolibs/template_files/madmatrix/madmatrix_src.mk @@ -66,9 +66,17 @@ endif #=== Makefile TARGETS and build rules below #=============================================================================== -# The common library name carries the full BACKEND suffix so each vectorisation/GPU variant is distinct -# (and LIBTAG, see madmatrix.mk). -MADMATRIX_COMMONLIB = madmatrix_common$(LIBTAG)_$(BACKEND) +# The common library name carries the model name and the full BACKEND suffix, so each +# vectorisation/GPU variant is distinct AND libraries from different regenerated model +# directories (eg the same process regenerated in several models and loaded into one +# program) never collide: dlopen resolves a bare NEEDED entry by matching any +# already-loaded library of that name anywhere in the process, regardless of RUNPATH, +# so two "libmadmatrix_common_.so" from different models would otherwise +# silently alias -- the second model's process code ends up bound to the first +# model's Parameters implementation. FPTYPE is appended too, purely for naming +# consistency with MADMATRIX_LIB (SubProcesses/madmatrix.mk); FPTYPE is exported from +# there, same as BACKEND. LIBTAG as well, see madmatrix.mk. +MADMATRIX_COMMONLIB = madmatrix_common$(LIBTAG)_%(model)s_$(BACKEND)_$(FPTYPE) # Name recorded in the library itself, and hence in whatever links against it. # Without it the linker bakes in the absolute build path (LIBDIR), so the process diff --git a/madgraph/iolibs/template_files/madmatrix/process_class.inc b/madgraph/iolibs/template_files/madmatrix/process_class.inc index 51aecbc4dd..c43d0876e0 100644 --- a/madgraph/iolibs/template_files/madmatrix/process_class.inc +++ b/madgraph/iolibs/template_files/madmatrix/process_class.inc @@ -25,6 +25,21 @@ // Initialize process (read model parameters from file) virtual void initProc( const std::string& param_card_name ); +#ifndef MGONGPU_HARDCODE_PARAM + // Re-synchronize the cached independent parameters/couplings (cIPD/cIPC/cIPF) and the + // BSM-independent-parameter buffer (bsmIndepParam) with the current values in the + // Parameters singleton. sigmaKin() and friends read these cached copies, not the + // singleton itself, for performance, so updating the singleton alone (e.g. via + // Parameters::setParameterByName()) has no effect until this is called again; it is + // called once automatically by initProc(). + static void refreshIndependentParams(); + // Re-populate a caller-supplied mass vector from the current Parameters singleton, + // mirroring what initProc() stores in m_masses. Call after umami_set_parameter + // changes a mass parameter so callers of umami_get_meta(UMAMI_META_MASSES,...) see + // the updated values. + static void refreshExternalMasses( std::vector& masses ); +#endif + // PDG id for a given flavor combination index and particle index (0-based). // Useful for tools that need to enumerate flavor combinations without going through UMAMI. static int flavorPDG( int iflavor, int ipar ); diff --git a/madgraph/iolibs/template_files/madmatrix/process_function_definitions.inc b/madgraph/iolibs/template_files/madmatrix/process_function_definitions.inc index 2f2a273d02..3e3875927c 100644 --- a/madgraph/iolibs/template_files/madmatrix/process_function_definitions.inc +++ b/madgraph/iolibs/template_files/madmatrix/process_function_definitions.inc @@ -65,6 +65,12 @@ namespace madmatrix { // Instantiate the model class and set parameters that stay fixed during run m_pars = Parameters::getInstance(); + // Parameters is a process-wide singleton (not owned per-instance), so a fresh + // initProc() must explicitly drop any overrides left by a previous + // setParameterByName() call -- otherwise re-initializing from a (possibly + // different) param card would silently carry forward stale overrides instead + // of giving a clean slate. + m_pars->resetParameterOverrides(); SLHAReader slha( param_card_name, m_verbose ); m_pars->setIndependentParameters( slha ); m_pars->setIndependentCouplings(); @@ -79,6 +85,14 @@ namespace madmatrix } %(initProc_lines)s createNormalizedColorMatrix(); // no-op on cpu/simd, pushes to device memory on gpu + refreshIndependentParams(); + } + + // See declaration in CPPProcess.h for why this exists and when it must be called. + void + CPPProcess::refreshIndependentParams() + { + Parameters* m_pars = Parameters::getInstance(); // Read physics parameters like masses and couplings from user configuration files, // then hand them to the backend-owned storage via the setters (see SigmaKin.cc). %(cipdassign)s @@ -86,6 +100,15 @@ namespace madmatrix %(cipfassign)s %(bsmassign)s } + + // See declaration in CPPProcess.h for why this exists and when it must be called. + void + CPPProcess::refreshExternalMasses( std::vector& masses ) + { + Parameters* m_pars = Parameters::getInstance(); + masses.clear(); +%(refreshExternalMasses_lines)s + } #else // Initialize process (with hardcoded parameters) void diff --git a/madgraph/iolibs/template_files/mg7/api.cpp b/madgraph/iolibs/template_files/mg7/api.cpp index 6961afc7bd..d8198ce285 100644 --- a/madgraph/iolibs/template_files/mg7/api.cpp +++ b/madgraph/iolibs/template_files/mg7/api.cpp @@ -1,7 +1,17 @@ #include "CPPProcess.h" #include "api.h" +#include #include +namespace { + // Masses reported by umami_get_meta(UMAMI_META_MASSES, ...), which (unlike every other + // UMAMI entry point in this file) takes no handle: model parameters are stored per- + // instance in this backend (CPPProcess::pars), so this can only ever reflect the most + // recently initialized-or-modified instance, not a specific one. See the caveat on + // umami_get_meta() in api.h. + std::vector g_masses; +} + extern "C" { UmamiStatus umami_get_meta(UmamiMetaKey meta_key, void* result) { @@ -20,6 +30,12 @@ UmamiStatus umami_get_meta(UmamiMetaKey meta_key, void* result) { break; case UMAMI_META_COLOR_COUNT: return UMAMI_ERROR_UNSUPPORTED_META; + case UMAMI_META_MASSES: { + if (g_masses.size() != (size_t)CPPProcess::nexternal) return UMAMI_ERROR_UNINITIALIZED_META; + for (int i = 0; i < CPPProcess::nexternal; ++i) + static_cast(result)[i] = g_masses[i]; + break; + } default: return UMAMI_ERROR_UNSUPPORTED_META; } @@ -54,6 +70,7 @@ UmamiStatus umami_initialize(UmamiHandle* handle, char const* param_card_path) { CPPProcess* process = new CPPProcess(param_card_path); std::vector& momenta = process->getMomenta(); for (int i = 0; i < CPPProcess::nexternal; ++i) momenta.push_back(new double[4]()); + g_masses.assign(process->getMasses().begin(), process->getMasses().end()); *handle = process; return UMAMI_SUCCESS; } @@ -65,7 +82,18 @@ UmamiStatus umami_set_parameter( double parameter_real, double parameter_imag ) { - return UMAMI_ERROR_NOT_IMPLEMENTED; + CPPProcess* process = static_cast(handle); + if (!process->getParameters().setParameterByName(name, parameter_real, parameter_imag)) + return UMAMI_ERROR_UNKNOWN_PARAMETER; + process->refreshMasses(); + const std::vector& masses = process->getMasses(); + bool changed = (masses.size() != g_masses.size()); + for (size_t i = 0; !changed && i < masses.size(); ++i) + changed = (masses[i] != g_masses[i]); + if (changed) { + g_masses.assign(masses.begin(), masses.end()); + } + return UMAMI_SUCCESS; } @@ -75,7 +103,10 @@ UmamiStatus umami_get_parameter( double* parameter_real, double* parameter_imag ) { - return UMAMI_ERROR_NOT_IMPLEMENTED; + CPPProcess* process = static_cast(handle); + if (!process->getParameters().getParameterByName(name, parameter_real, parameter_imag)) + return UMAMI_ERROR_UNKNOWN_PARAMETER; + return UMAMI_SUCCESS; } UmamiStatus umami_matrix_element( diff --git a/madgraph/iolibs/template_files/mg7/api.h b/madgraph/iolibs/template_files/mg7/api.h index 426a342000..16fe3d514b 100644 --- a/madgraph/iolibs/template_files/mg7/api.h +++ b/madgraph/iolibs/template_files/mg7/api.h @@ -40,6 +40,8 @@ typedef enum { UMAMI_ERROR_UNSUPPORTED_OUTPUT, UMAMI_ERROR_UNSUPPORTED_META, UMAMI_ERROR_MISSING_INPUT, + UMAMI_ERROR_UNKNOWN_PARAMETER, + UMAMI_ERROR_UNINITIALIZED_META, } UmamiStatus; typedef enum { @@ -54,6 +56,7 @@ typedef enum { UMAMI_META_DIAGRAM_COUNT, UMAMI_META_HELICITY_COUNT, UMAMI_META_COLOR_COUNT, + UMAMI_META_MASSES, } UmamiMetaKey; typedef enum { @@ -97,7 +100,12 @@ typedef void* UmamiHandle; * pointer to an instance of the subprocess. Has to be cleaned up by * the caller with `free_subprocess`. * @return - * UMAMI_SUCCESS on success, error code otherwise + * UMAMI_SUCCESS on success, error code otherwise. UMAMI_META_MASSES returns + * UMAMI_ERROR_UNINITIALIZED_META if no instance has been created yet with + * umami_initialize(). NB: unlike other meta keys, UMAMI_META_MASSES can vary between + * instances (parameters are stored per-instance in this backend, and are settable via + * umami_set_parameter); since this function takes no handle, it always reports the + * masses of the most recently initialized-or-modified instance, not a specific one. */ UmamiStatus umami_get_meta(UmamiMetaKey meta_key, void* result); @@ -143,18 +151,24 @@ UmamiStatus umami_supported_outputs(bool const** supported, int* count); UmamiStatus umami_initialize(UmamiHandle* handle, char const* param_card_path); /** - * Sets the value of a model parameter + * Sets the value of a model parameter. Only independent, SLHA-card-level + * parameters (i.e. those normally read from the param card) can be set this + * way; parameters or couplings derived from them are recomputed + * automatically and cannot be overridden directly. * * @param handle * handle of a matrix element instance * @param name - * name of the parameter + * name of the parameter, given either as its model name (e.g. "mdl_MT") + * or as its SLHA card location, " []" (e.g. + * "mass 6"); the block name is matched case-insensitively * @param parameter_real * real part of the parameter value * @param parameter_imag * imaginary part of the parameter value. Ignored for real valued parameters. * @return - * UMAMI_SUCCESS on success, error code otherwise + * UMAMI_SUCCESS on success, UMAMI_ERROR_UNKNOWN_PARAMETER if "name" is not + * a settable parameter, error code otherwise */ UmamiStatus umami_set_parameter( UmamiHandle handle, @@ -169,14 +183,16 @@ UmamiStatus umami_set_parameter( * @param handle * handle of a matrix element instance * @param name - * name of the parameter + * name of the parameter; accepts the same two forms as + * umami_set_parameter (model name, or SLHA " []") * @param parameter_real * pointer to double to return real part of the parameter value * @param parameter_imag * pointer to double to return imaginary part of the parameter value. Ignored * for real-valued parameters (i.e. you may pass a null pointer) * @return - * UMAMI_SUCCESS on success, error code otherwise + * UMAMI_SUCCESS on success, UMAMI_ERROR_UNKNOWN_PARAMETER if "name" is not + * a known parameter or coupling, error code otherwise */ UmamiStatus umami_get_parameter( UmamiHandle handle, diff --git a/madgraph/iolibs/template_files/mg7/gridpack.py b/madgraph/iolibs/template_files/mg7/gridpack.py index faa066fee6..a23a74e563 100644 --- a/madgraph/iolibs/template_files/mg7/gridpack.py +++ b/madgraph/iolibs/template_files/mg7/gridpack.py @@ -36,6 +36,15 @@ import tomllib import argparse +_PRECISION_2_FPTYPE = {"all64": "d", "all32": "f", "color32": "m", "denom64": "v"} + + +def fptype_of(precision: str) -> str: + """[run] precision -> FPTYPE the matrix elements were compiled with (as + fptype_of() in launch.py, which this standalone script can't import).""" + return _PRECISION_2_FPTYPE.get(str(precision).lower(), str(precision).lower()) + + def resolve_verbosity(verbosity: str) -> str: """Resolve the run_card "auto" verbosity to "pretty"/"log" depending on whether stdout is attached to a terminal; other values pass through @@ -279,6 +288,8 @@ def resolve_dir(cli_value, card_value): # run-time matrix-element parameters (bwcutoff) of the run that made the # gridpack; gridpacks written before they were recorded used the default me_parameters = madspace_data.get("me_parameters", {}) + # the FPTYPE the matrix elements were compiled with ({fptype} in me_path) + fptype = fptype_of(run_args["precision"]) globals_path = os.path.join("data", "globals.tar") for context in contexts: context.load_globals(globals_path) @@ -292,7 +303,7 @@ def resolve_dir(cli_value, card_value): for context, backend in zip(contexts, backends): for me_path in madspace_data["matrix_elements"]: context.load_matrix_element( - me_path.format(device=backend), param_card_path, me_parameters + me_path.format(device=backend, fptype=fptype), param_card_path, me_parameters ) # set up generators @@ -448,7 +459,9 @@ def load_systematics(run_card, backends=(), param_card_path=None, if i not in need: matrix_elements.append(None) continue - lib = me_path.format(device=backend) + lib = me_path.format( + device=backend, fptype=fptype_of(run_card["run"]["precision"]) + ) if not os.path.exists(lib): print("WARNING systematics: %s not found, mu_R variations of the " "mixed-order subprocesses are dropped" % lib) diff --git a/madgraph/iolibs/template_files/mg7/launch.py b/madgraph/iolibs/template_files/mg7/launch.py index 2fcb9abd38..8e6406dbc5 100644 --- a/madgraph/iolibs/template_files/mg7/launch.py +++ b/madgraph/iolibs/template_files/mg7/launch.py @@ -1175,7 +1175,8 @@ def build_systematics_matrix_elements(self, args, context): matrix_elements.append(None) continue api = context.load_matrix_element( - meta["me_path"].format(device=backend), self.param_card_path, + meta["me_path"].format(device=backend, fptype=self.fptype), + self.param_card_path, me_parameters(self.run_card)) matrix_elements.append(ms.MatrixElement( api, @@ -1451,14 +1452,14 @@ def compile_matrix_elements(self) -> list[str]: device_names = [device_names] cpu_mode = self.run_card["run"]["cpu_mode"] backends = [backend_of(name, cpu_mode) for name in device_names] + # FPTYPE is part of the library names ({fptype} in me_path) + fptype = self.fptype = fptype_of(self.run_card["run"]["precision"]) if not self.subprocess_data: return backends first_proc_path = self.subprocess_data[0]["path"] subproc_path = os.path.dirname(first_proc_path) - fptype = fptype_of(self.run_card["run"]["precision"]) - # Resolve cpu_mode='auto' once (the build rules pick the best SIMD # backend available here), so all subprocesses agree on the library names. auto_backend = None @@ -1479,7 +1480,9 @@ def compile_matrix_elements(self) -> list[str]: # by library: the flavours of a split subprocess share one missing = list({ meta["me_path"]: meta for meta in self.subprocess_data - if not os.path.isfile(meta["me_path"].format(device=backend)) + if not os.path.isfile( + meta["me_path"].format(device=backend, fptype=fptype) + ) }.values()) if not missing: continue @@ -2687,7 +2690,10 @@ def __init__( # The libraries were all built up front by MadgraphProcess.compile_matrix_elements all_api_paths = [ - [api_path_format.format(device=backend) for backend in self.process.backends] + [ + api_path_format.format(device=backend, fptype=self.process.fptype) + for backend in self.process.backends + ] for api_path_format in api_path_formats ] diff --git a/madmatrix/density.py b/madmatrix/density.py index 09aa7d0117..b23b05804b 100644 --- a/madmatrix/density.py +++ b/madmatrix/density.py @@ -38,6 +38,10 @@ PROCESSES_FILE = 'madmatrix_processes.json' +# The FPTYPE of the libraries this module builds and loads: the default of +# madmatrix.mk, made explicit since it is part of the library names +FPTYPE = 'm' + class MadDensityError(Exception): pass @@ -65,7 +69,7 @@ def make_variables(backend='scalar', libtag=''): the standalone check_sa.exe) when make runs in SubProcesses. DENSITY=1 adds Density.o, which only this module calls: other builds of the same output (check_sa, mg7) leave it out.""" - variables = ['PGOAL=lib', 'BACKEND=%s' % backend, 'DENSITY=1'] + variables = ['PGOAL=lib', 'BACKEND=%s' % backend, 'FPTYPE=%s' % FPTYPE, 'DENSITY=1'] if libtag: variables.append('LIBTAG=%s' % libtag) return variables @@ -76,11 +80,30 @@ def build_command(nb_core=1, backend='scalar', libtag=''): return ['make', '-j%d' % max(1, int(nb_core))] + make_variables(backend, libtag) +def common_library(output_dir): + """The name of the common library of output_dir, with '{libtag}', '{backend}' + and '{fptype}' left open, as its process table gives it (the name without the + model and FPTYPE for an output whose table predates them).""" + try: + with open(os.path.join(output_dir, 'SubProcesses', PROCESSES_FILE)) as fsock: + data = json.load(fsock) + except (IOError, OSError, ValueError): + data = {} + return data.get('common_library', 'libmadmatrix_common{libtag}_{backend}.so') + + +def common_library_path(output_dir, libtag, backend='scalar'): + """The path of the common library of output_dir built with libtag.""" + return os.path.join(output_dir, 'lib', common_library(output_dir).format( + libtag=libtag, backend=backend, fptype=FPTYPE)) + + def find_libtag(output_dir, backend='scalar'): """The LIBTAG the libraries of output_dir were built with, read off the name of its common library.""" lib = os.path.join(output_dir, 'lib') - prefix, suffix = 'libmadmatrix_common', '_%s.so' % backend + prefix, suffix = common_library(output_dir).format( + libtag='{libtag}', backend=backend, fptype=FPTYPE).split('{libtag}') found = sorted(name[len(prefix):-len(suffix)] for name in (os.listdir(lib) if os.path.isdir(lib) else []) if name.startswith(prefix) and name.endswith(suffix)) @@ -224,8 +247,7 @@ def __init__(self, output_dir, backend='scalar', libtag=None): self.libtag = libtag # the common library first, by its path: the process libraries only name # it, and the check of _load has to see it - _load(os.path.join(output_dir, 'lib', - 'libmadmatrix_common%s_%s.so' % (libtag, backend))) + _load(common_library_path(output_dir, libtag, backend)) self.subprocesses = [] # raw pdg combination (leg order of the process) -> (subprocess, flavor index) self._by_pdgs = {} @@ -236,7 +258,8 @@ def __init__(self, output_dir, backend='scalar', libtag=None): self._process_slots = [] for entry in entries: lib_path = os.path.join(output_dir, 'lib', - entry['library'].format(libtag=libtag, backend=backend)) + entry['library'].format(libtag=libtag, backend=backend, + fptype=FPTYPE)) if not os.path.exists(lib_path): raise MadDensityError('missing %s: build the libraries first (%s in %s)' % (lib_path, ' '.join(build_command(backend=backend, diff --git a/madmatrix/model_handling.py b/madmatrix/model_handling.py index fa557f9e6f..55d5cbfe9e 100644 --- a/madmatrix/model_handling.py +++ b/madmatrix/model_handling.py @@ -1359,7 +1359,29 @@ def write_flv_couplings(self, params): # AV - overload export_cpp.UFOModelConverterCPP method (improve formatting) def write_set_parameters(self, params): - res = self.super_write_set_parameters_donotfixMajorana(params) + # Independent, SLHA-card-level parameters (self.params_ext_names) get + # an inline check for a runtime override (e.g. set by name via the + # UMAMI set_parameter interface) ahead of their normal SLHA-driven + # assignment; every other parameter/coupling is unaffected. NB: this + # must NOT be folded into super_write_set_parameters_donotfixMajorana, + # since write_hardcoded_parameters() calls that method directly and + # requires each parameter's expr to remain a single plain assignment. + res_strings = [] + for param in params: + # 'aS' is excluded here even though it is SLHA-card-level: its + # assignment is entirely commented out below (by name-matching on + # a plain " aS = ..." line), since this backend always retrieves + # alphaS event-by-event (as G) rather than from the param card. + # Wrapping it in an override check would break that name-match. + if param.name in self.params_ext_names and param.name != 'aS': + res_strings.append( + 'if (m_param_overrides.find("%s") != m_param_overrides.end())\n' + '%s = m_param_overrides["%s"];\nelse\n%s' % + (param.name, param.name, param.name, param.expr)) + else: + res_strings.append(param.expr) + res = "\n".join(res_strings) + res = res.replace('ABS(','std::abs(') # for SMEFT #614 and #616 res = res.replace('(','( ') res = res.replace(')',' )') res = res.replace('+',' + ') @@ -1598,6 +1620,31 @@ def super_generate_parameters_class_files(self): replace_dict['set_independent_couplings'] = self.write_set_parameters(self.coups_indep) replace_dict['set_dependent_parameters'] = self.write_set_parameters(self.params_dep) replace_dict['set_dependent_couplings'] = self.write_set_parameters(list(self.coups_dep.values())) + # NB: unlike the plain (mg7_v5) backend, dependent (aS-dependent) parameters + # and couplings are NOT persistent members of this Parameters class at all + # (their declarations are commented out above): they are recomputed fresh + # every event batch by computeDependentCouplings(), bypassing the Parameters + # singleton entirely. Only independent parameters/couplings (and never 'aS', + # whose declaration is likewise commented out) can be registered here. + # Two separate calls/maps, not one combined list into one map: model + # parameters (mdl_*) always stay plain cxsmpl (registered + # into m_complex_model_params below), while independent couplings + # (GC_*) are cxtype -- FPTYPE-templated, so possibly cxsmpl + # -- and go into m_complex_params instead. A single combined + # registration into one map only compiled by accident at FPTYPE=m/d + # (where cxtype happens to also be cxsmpl) and failed to + # compile at FPTYPE=f -- see write_register_parameters()'s own doc + # comment. + replace_dict['register_parameters'] = \ + self.write_register_parameters( + [p for p in self.params_indep if p.name != 'aS'], + [n for n in self.params_ext_names if n != 'aS'], + complex_map_name='m_complex_model_params') + \ + '\n' + \ + self.write_register_parameters(self.coups_indep, []) + replace_dict['register_slha_keys'] = \ + self.write_register_slha_keys( + [e for e in self.params_ext_slha if e[0] != 'aS']) print_params_indep = [ line.replace('std::cout','//std::cout') + ' // now retrieved event-by-event (as G) from Fortran (running alphas #373)' if '"aS =' in line else line for line in self.write_print_parameters(self.params_indep).split('\n') ] @@ -1985,6 +2032,8 @@ def get_process_function_definitions(self, write=True): # process-specific count (ProcessTables::nb_tmp_jamp) rather than # hardcoded per-process like the rest of calculate_jamps. self._nb_tmp_jamp = getattr(self.helas_call_writer, 'nb_tmp_jamp', 0) + replace_dict['refreshExternalMasses_lines'] = \ + self.get_refresh_external_masses_lines(self.matrix_elements[0]) replace_dict['nparams'] = len(self.params2order) replace_dict['coupling_list'] = ' ' replace_dict['hel_amps_cc'] = '#include \"HelAmps_%s.cc\"' % self.model_name # AV @@ -2656,6 +2705,17 @@ def get_hardcoded_initProc_lines(self, matrix_element): initProc_lines.append(' m_masses.push_back( Parameters::%s );' % part.get('mass')) return '\n'.join(initProc_lines) + # Companion to get_initProc_lines: the same external-mass list, but pushed into an + # arbitrary "masses" vector instead of m_masses, for CPPProcess::refreshExternalMasses() + # (called after umami_set_parameter() overrides a mass so UMAMI_META_MASSES stays in sync; + # see madgraph/iolibs/template_files/madmatrix/umami.cc). + def get_refresh_external_masses_lines(self, matrix_element): + """Get refreshExternalMasses_lines for function definition for CPPProcess::refreshExternalMasses""" + lines = [' // Set external particle masses for this matrix element'] + for part in matrix_element.get_external_wavefunctions(): + lines.append(' masses.push_back( m_pars->%s );' % part.get('mass')) + return '\n'.join(lines) + # AV - replace the export_cpp.OneProcessExporterCPP method (fix helicity order and improve formatting) def get_helicity_matrix(self, matrix_element): """Return the Helicity matrix definition lines for this matrix element""" diff --git a/madmatrix/output.py b/madmatrix/output.py index 27ef9a6c35..e5911deb63 100644 --- a/madmatrix/output.py +++ b/madmatrix/output.py @@ -188,7 +188,18 @@ class ProcessExporterMadMatrix(export_cpp.ProcessExporterMG7): # AV (default from OM's tutorial) - add a debug printout def __init__(self, *args, **kwargs): self.in_madevent_mode = False # see MR #747 - args[1]["me_lib_format"] = pjoin("lib", "libmadmatrix_{process_id}_{{device}}.so") + # {model} is resolved by ProcessExporterMG7.generate_subprocess_directory() + # alongside {process_id}, from the matrix element's own model -- same + # sanitized name (get_model_name()) used for MADMATRIX_LIB/ + # MADMATRIX_COMMONLIB's %(model)s in madmatrix.mk. {{device}}/{{fptype}} + # stay literal ("{device}"/"{fptype}") in the resulting me_path: which + # concrete BACKEND/FPTYPE a given process directory was compiled for is + # a per-build choice made when the libraries are compiled and loaded + # (see compile_matrix_elements() in mg7/launch.py), not something + # subprocesses.json commits to at generation time. + args[1]["me_lib_format"] = pjoin( + "lib", "libmadmatrix_{model}_{process_id}_{{device}}_{{fptype}}.so" + ) super().__init__(*args, **kwargs) # Honor the output command's --mask=True|False (flavor-mask # optimization for grouped/merged flavors). Default: enabled. @@ -415,8 +426,9 @@ def finalize(self, *args, **kwargs): def write_processes_file(self): """SubProcesses/madmatrix_processes.json: per subprocess, its library - (with the make variables LIBTAG and BACKEND left as '{libtag}' and - '{backend}'), its legs, the processes it evaluates, its off-shell legs + (with the make variables LIBTAG, BACKEND and FPTYPE left as '{libtag}', + '{backend}' and '{fptype}', as for the common library, common_library), + its legs, the processes it evaluates, its off-shell legs and, per flavor index, every pdg combination (in the leg order of the process) that this index stands for, and what of it depends on the renormalisation scale beyond alpha_s (scale_dependence).""" @@ -432,7 +444,8 @@ def write_processes_file(self): flavors[flavor['index']].extend(list(pdgs) for pdgs in flavor['options']) subprocesses.append({ 'path': path, - 'library': 'libmadmatrix{libtag}_%s_{backend}.so' % path, + 'library': 'libmadmatrix{libtag}_%s_%s_{backend}_{fptype}.so' + % (self.lib_model_name, path), 'ninitial': len(info['incoming']), 'legs': list(info['incoming']) + list(info['outgoing']), 'processes': processes, @@ -441,4 +454,6 @@ def write_processes_file(self): 'scale_dependence': scale, 'flavors': flavors}) with open(pjoin(self.dir_path, 'SubProcesses', self.processes_file), 'w') as fsock: - json.dump({'subprocesses': subprocesses}, fsock, indent=1) + json.dump({'common_library': 'libmadmatrix_common{libtag}_%s_{backend}_{fptype}.so' + % self.lib_model_name, + 'subprocesses': subprocesses}, fsock, indent=1) diff --git a/madspace/include/madspace/umami.h b/madspace/include/madspace/umami.h index 91f3adc413..8510845910 100644 --- a/madspace/include/madspace/umami.h +++ b/madspace/include/madspace/umami.h @@ -51,6 +51,8 @@ typedef enum { UMAMI_ERROR_UNSUPPORTED_META, /** a mandatory matrix element input was not provided */ UMAMI_ERROR_MISSING_INPUT, + /** the provided parameter name is not a known, settable parameter */ + UMAMI_ERROR_UNKNOWN_PARAMETER, } UmamiStatus; typedef enum { @@ -222,7 +224,7 @@ UmamiStatus umami_supported_outputs(bool const** supported, int* count); * * @param handle * pointer to an instance of the subprocess. Has to be cleaned up by - * the caller with `free_subprocess`. + * the caller with `umami_free`. * @param param_card_path * path to the parameter file * @return @@ -242,7 +244,8 @@ UmamiStatus umami_initialize(UmamiHandle* handle, char const* param_card_path); * @param parameter_imag * imaginary part of the parameter value. Ignored for real valued parameters. * @return - * UMAMI_SUCCESS on success, error code otherwise + * UMAMI_SUCCESS on success, UMAMI_ERROR_UNKNOWN_PARAMETER if "name" is not + * a settable parameter, error code otherwise */ UmamiStatus umami_set_parameter( UmamiHandle handle, char const* name, double parameter_real, double parameter_imag @@ -261,7 +264,8 @@ UmamiStatus umami_set_parameter( * pointer to double to return imaginary part of the parameter value. Ignored * for real-valued parameters (i.e. you may pass a null pointer) * @return - * UMAMI_SUCCESS on success, error code otherwise + * UMAMI_SUCCESS on success, UMAMI_ERROR_UNKNOWN_PARAMETER if "name" is not + * a known parameter, error code otherwise */ UmamiStatus umami_get_parameter( UmamiHandle handle, char const* name, double* parameter_real, double* parameter_imag diff --git a/tests/unit_tests/iolibs/test_export_cpp.py b/tests/unit_tests/iolibs/test_export_cpp.py index 1f5369b615..f6961727b3 100755 --- a/tests/unit_tests/iolibs/test_export_cpp.py +++ b/tests/unit_tests/iolibs/test_export_cpp.py @@ -911,6 +911,7 @@ def test_cpp_export_decay_chain_broken_symmetry_metadata(self): 'process_lines': '', 'model_name': 'sm', 'initProc_lines': '', + 'refreshMasses_lines': '', 'reset_jamp_lines': '', 'sigmaKin_lines': '', 'sigmaHat_lines': 'return 0.;', diff --git a/tests/unit_tests/iolibs/test_madmatrix_density.py b/tests/unit_tests/iolibs/test_madmatrix_density.py index b7fa245559..625519a59a 100644 --- a/tests/unit_tests/iolibs/test_madmatrix_density.py +++ b/tests/unit_tests/iolibs/test_madmatrix_density.py @@ -102,10 +102,10 @@ def touch(self, name): def test_make_variables(self): self.assertEqual(density.make_variables(), - ['PGOAL=lib', 'BACKEND=scalar', 'DENSITY=1']) + ['PGOAL=lib', 'BACKEND=scalar', 'FPTYPE=m', 'DENSITY=1']) self.assertEqual(density.build_command(4, 'simd_128', '_ms2d'), - ['make', '-j4', 'PGOAL=lib', 'BACKEND=simd_128', 'DENSITY=1', - 'LIBTAG=_ms2d']) + ['make', '-j4', 'PGOAL=lib', 'BACKEND=simd_128', 'FPTYPE=m', + 'DENSITY=1', 'LIBTAG=_ms2d']) def test_find_libtag(self): self.assertRaises(density.MadDensityError, density.find_libtag, self.tmp) @@ -116,6 +116,21 @@ def test_find_libtag(self): self.touch('libmadmatrix_common_scalar.so') self.assertRaises(density.MadDensityError, density.find_libtag, self.tmp) + def test_find_libtag_model_fptype(self): + """An output whose process table names the common library (with the + model and FPTYPE): the tag is read off that name.""" + with open(os.path.join(self.tmp, 'SubProcesses', density.PROCESSES_FILE), 'w') as f: + json.dump({'common_library': 'libmadmatrix_common{libtag}_sm_{backend}_{fptype}.so', + 'subprocesses': []}, f) + self.assertRaises(density.MadDensityError, density.find_libtag, self.tmp) + self.touch('libmadmatrix_common_ms1p_sm_scalar_m.so') + self.touch('libmadmatrix_common_ms1p_sm_scalar_d.so') + self.assertEqual(density.find_libtag(self.tmp), '_ms1p') + self.assertEqual(density.common_library_path(self.tmp, '_ms1p'), + os.path.join(self.tmp, 'lib', 'libmadmatrix_common_ms1p_sm_scalar_m.so')) + self.touch('libmadmatrix_common_sm_scalar_m.so') + self.assertRaises(density.MadDensityError, density.find_libtag, self.tmp) + def test_is_output(self): self.assertFalse(density.is_output(self.tmp)) with open(os.path.join(self.tmp, 'SubProcesses', density.PROCESSES_FILE), 'w') as f: From a4dbd2f2e0d3b33b45383f82e8da53822009d622 Mon Sep 17 00:00:00 2001 From: Zenny Date: Mon, 26 Oct 2026 10:01:11 +0100 Subject: [PATCH 03/18] madspace: set and read UMAMI parameters through MatrixElementApi The madspace side of umami_set_parameter/umami_get_parameter: - MatrixElementApi::set_parameter()/get_parameter(), through the optional umami_set_parameter/umami_get_parameter symbols (UMAMI_ERROR_NOT_IMPLEMENTED when a library lacks them), with supports_set_parameter()/ supports_get_parameter() to check first; UMAMI_ERROR_UNKNOWN_PARAMETER gets its own message. - MatrixElementApi::device(), the device the library was loaded on, for callers that stage umami_matrix_element inputs/outputs themselves. - Python bindings for these and for file_name(), supported_inputs(), required_inputs() and supported_outputs(). - Install umami.h with the public headers, for libraries and callers built against an installed madspace. Co-Authored-By: Claude Opus 5.5 (1M context) --- madspace/CMakeLists.txt | 5 +++- madspace/include/madspace/driver/context.hpp | 24 ++++++++++++++++++++ madspace/src/driver/context.cpp | 21 ++++++++++++++++- madspace/src/python/madspace.cpp | 16 ++++++++++++- 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/madspace/CMakeLists.txt b/madspace/CMakeLists.txt index d1f3927d46..fbe5244703 100644 --- a/madspace/CMakeLists.txt +++ b/madspace/CMakeLists.txt @@ -334,7 +334,10 @@ install(TARGETS madspace DESTINATION madspace/lib) install( DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/" DESTINATION "madspace/include" - FILES_MATCHING PATTERN "*.hpp*" + # *.h (not just *.hpp*) so umami.h -- the public UMAMI ABI header needed by + # anyone implementing or consuming a MatrixElementApi-compatible shared + # library -- is installed too. + FILES_MATCHING PATTERN "*.hpp*" PATTERN "*.h" ) # Generated public headers: installed alongside function.hpp/instruction.hpp # so a downstream #include "function_builder_mixin.inc" resolves the same diff --git a/madspace/include/madspace/driver/context.hpp b/madspace/include/madspace/driver/context.hpp index 0d1ed0eeb5..43eb5acab2 100644 --- a/madspace/include/madspace/driver/context.hpp +++ b/madspace/include/madspace/driver/context.hpp @@ -3,9 +3,11 @@ #include #include #include +#include #include #include #include +#include #include "madspace/compgraphs.hpp" #include "madspace/driver/tensor.hpp" @@ -34,6 +36,7 @@ struct UmamiFunctions { supported_outputs; std::function> initialize; std::function> set_parameter; + std::function> get_parameter; std::function> matrix_element; std::function> free; @@ -174,6 +177,26 @@ class MatrixElementApi { /// Opaque per-thread UMAMI process handle, initialized on first use. void* process_instance() const { return _instances.get().get(); } + // The Device this api's library was loaded on (ie the owning Context's + // device). Callers driving umami_matrix_element() need this to know + // whether the inputs/outputs it exchanges have to be staged through + // device-resident buffers (cuda/hip) or can be passed as plain host + // pointers (cpu). + DevicePtr device() const noexcept { return _device; } + + // Whether the loaded library actually implements umami_set_parameter/ + // umami_get_parameter (both optional symbols; a library built without + // them still loads fine, it just can't be parameter-reweighted in place + // and set_parameter()/get_parameter() will throw UMAMI_ERROR_NOT_IMPLEMENTED + // if called anyway). + bool supports_set_parameter() const noexcept { return bool(_functions.set_parameter); } + bool supports_get_parameter() const noexcept { return bool(_functions.get_parameter); } + + // Directly sets/reads physics parameters according to whatever + // convention the library has implemented. + void set_parameter(const std::string& name, double real, double imag = 0.0) const; + std::pair get_parameter(const std::string& name) const; + private: MatrixElementApi( const std::string& file, @@ -208,6 +231,7 @@ class MatrixElementApi { ThreadResource _instances; std::string _file_name; std::size_t _index; + DevicePtr _device; friend class Context; }; diff --git a/madspace/src/driver/context.cpp b/madspace/src/driver/context.cpp index 398fe70cc5..2c25ae9975 100644 --- a/madspace/src/driver/context.cpp +++ b/madspace/src/driver/context.cpp @@ -36,7 +36,7 @@ MatrixElementApi::MatrixElementApi( std::size_t index, const std::unordered_map& parameters ) : - _file_name(file), _index(index) { + _file_name(file), _index(index), _device(device) { _shared_lib = std::unique_ptr>( dlopen(file.c_str(), RTLD_NOW), [file](void* lib) { dlclose(lib); } ); @@ -53,6 +53,7 @@ MatrixElementApi::MatrixElementApi( resolve_symbol(lib, "umami_supported_outputs", _functions.supported_outputs); resolve_symbol(lib, "umami_initialize", _functions.initialize); resolve_symbol(lib, "umami_set_parameter", _functions.set_parameter); + resolve_symbol(lib, "umami_get_parameter", _functions.get_parameter); resolve_symbol(lib, "umami_matrix_element", _functions.matrix_element); resolve_symbol(lib, "umami_free", _functions.free); init_functions(parameters); @@ -148,6 +149,8 @@ void MatrixElementApi::check_umami_status(UmamiStatus status) const { throw_error("unsupported metadata key"); case UMAMI_ERROR_MISSING_INPUT: throw_error("missing input"); + case UMAMI_ERROR_UNKNOWN_PARAMETER: + throw_error("unknown or non-settable parameter"); default: throw_error("unknown error"); } @@ -159,6 +162,22 @@ void MatrixElementApi::throw_error(const std::string& message) const { ); } +void MatrixElementApi::set_parameter(const std::string& name, double real, double imag) const { + if (!_functions.set_parameter) { + check_umami_status(UMAMI_ERROR_NOT_IMPLEMENTED); + } + check_umami_status(_functions.set_parameter(process_instance(), name.c_str(), real, imag)); +} + +std::pair MatrixElementApi::get_parameter(const std::string& name) const { + if (!_functions.get_parameter) { + check_umami_status(UMAMI_ERROR_NOT_IMPLEMENTED); + } + double real = 0.0, imag = 0.0; + check_umami_status(_functions.get_parameter(process_instance(), name.c_str(), &real, &imag)); + return {real, imag}; +} + const MatrixElementApi& Context::load_matrix_element( const std::string& file, const std::string& param_card, diff --git a/madspace/src/python/madspace.cpp b/madspace/src/python/madspace.cpp index 3e3e90326d..f086ad33e0 100644 --- a/madspace/src/python/madspace.cpp +++ b/madspace/src/python/madspace.cpp @@ -337,7 +337,21 @@ PYBIND11_MODULE(_madspace_py, m) { &MatrixElementApi::helicity_count, pydoc::doc("MatrixElementApi::helicity_count") ) - .def("index", &MatrixElementApi::index, pydoc::doc("MatrixElementApi::index")); + .def("index", &MatrixElementApi::index, pydoc::doc("MatrixElementApi::index")) + .def("file_name", &MatrixElementApi::file_name) + .def("supported_inputs", &MatrixElementApi::supported_inputs) + .def("required_inputs", &MatrixElementApi::required_inputs) + .def("supported_outputs", &MatrixElementApi::supported_outputs) + .def("supports_set_parameter", &MatrixElementApi::supports_set_parameter) + .def("supports_get_parameter", &MatrixElementApi::supports_get_parameter) + .def( + "set_parameter", + &MatrixElementApi::set_parameter, + py::arg("name"), + py::arg("real"), + py::arg("imag") = 0.0 + ) + .def("get_parameter", &MatrixElementApi::get_parameter, py::arg("name")); py::classh(m, "Tensor", py::dynamic_attr(), pydoc::doc("Tensor")) .def( From 98e0f40796fcb522b032298eeb0cbe2f2d108988 Mon Sep 17 00:00:00 2001 From: Zenny Date: Mon, 26 Oct 2026 15:02:56 +0100 Subject: [PATCH 04/18] Add MadtRex: matrix-element and PDF reweighting for MG7, beside the classic module MadtRex reweights MG7 events with the MG7 matrix-element libraries (UMAMI) and the Rex/teaRex event library. It lives in its own top-level madtrex/ directory and depends on madspace; madspace does not depend on it. madspace (generic, no MadtRex dependency): - event-file helpers, npy 2.0/3.0 headers (large headers no longer corrupt the file), header/particle-count checks fixed, EventFile load mode read-only, a failed backend dlopen() raises instead of segfaulting, mixmax_skip_N17.c installed with the headers. madtrex/ (new): - TupperWare/WareHouse (MatrixElementApi -> REX::tea weightors), Driver (subprocesses.json, device/fptype priorities, normaliser fptype), ParamHandler, PdfDriver (PDF and scale reweighting), binary (.npy) event I/O, the _madtrex_py bindings, the Python package, CMake + install.py. - Rex/: vendored copy of github.com/zeniheisser/Rex (94b4ed4, pyrex), with the header-parsing fix and process::helicity_ for events without a fixed helicity. - helicity reweighting (default on): matrix elements at each event's own helicity via UMAMI_IN_HELICITY_INDEX, beam-swapped events in library leg order, helicity-map checks per subprocess, error for samples without helicity information; tools/check_helicity_map.py. - reads lhe_npy events that carry the systematics' rwgt_ columns. MadGraph / MG7: - 'install madtrex' (madgraph_interface), [gridpack] include_madtrex / include_madtrex_source: gridpacks ship madtrex and bin/reweight. - mg7/madtrex_driver.py: standalone driver (madspace + madtrex only; the gridpack's bin/reweight), mg7/madtrex.py: MadGraph side ('change model' regeneration), run_reweighting() routing parameter/PDF/generic cards. - Reweighting backend switch: madgraph/various/reweight_backend.py chooses the classic module or MadtRex for Cards/reweight_card.dat (--reweight-backend > launch switch classic/madtrex/density > card 'change backend' > auto, which prefers MadtRex when it can run the card; every reason given when a choice cannot run, before the generation). Reweighting runs once, after the systematics; --reweight=CARD switches it on with that card. The launch question offers OFF/ON/classic/madtrex/ density for MG7 outputs (madevent/aMC@NLO unchanged). The decision is logged and recorded in Events//reweighting.json. - One card vocabulary (classic): 'change' for options (backend, helicity, mode LO|pdf, fptype, driver and PDF options), 'set' for parameters, including 'set MT 175' by param_card name; former MadtRex 'set' spellings deprecated; directives of the other backend are clear errors on both sides. MG7 outputs get their own default reweight card. - Common output: both backends add their weights to the run's own event file (events.lhe[.gz], or rwgt_ columns of events.npy, carried by npy_to_lhe), after the systematics weights; bin/reweight --output still writes elsewhere. Fixes found on the way: - f2py: the running Python's own f2py is used when none is configured (f2py3 on the PATH may build for another Python). - lhapdf: libLHAPDF is loaded before importing its python module (HEPTools' has no rpath; LD_LIBRARY_PATH set in-process is too late). Tests: test_reweight_backend (new), test_madtrex_standalone (new), test_mg7_launch, test_misc, acceptance test_reweight_backends (both backends agree event by event, same output file); test_cmd_reweight's MG7 test asks for the classic module; hj_nlo.lhe.gz regenerated in the current NLO event format (test_nlo_output2). Docs: madtrex/README.md ("Choosing a backend"), UpdateNotes.txt. Co-Authored-By: Claude Opus 5.5 (1M context) --- UpdateNotes.txt | 27 + madgraph/interface/common_run_interface.py | 14 + madgraph/interface/madevent_interface.py | 54 +- madgraph/interface/madgraph_interface.py | 20 +- madgraph/interface/reweight_interface.py | 43 +- madgraph/iolibs/export_v4.py | 10 +- .../iolibs/template_files/mg7/bootstrap.py | 4 + .../mg7/generic_reweight_card.py | 207 + .../iolibs/template_files/mg7/gridpack.py | 7 + madgraph/iolibs/template_files/mg7/launch.py | 355 +- madgraph/iolibs/template_files/mg7/madtrex.py | 138 + .../template_files/mg7/madtrex_driver.py | 1211 +++ .../iolibs/template_files/mg7/npy_to_lhe.py | 27 +- .../template_files/mg7/param_reweight_card.py | 602 ++ .../template_files/mg7/pdf_reweight_card.py | 478 ++ .../mg7/reweight_card_default.dat | 86 + .../iolibs/template_files/mg7/run_card.toml | 2 + .../template_files/mg7/run_interface.py | 91 + madgraph/various/banner.py | 5 + madgraph/various/misc.py | 37 +- madgraph/various/reweight_backend.py | 477 ++ madmatrix/output.py | 5 +- madspace/CMakeLists.txt | 5 +- madspace/include/madspace/driver/io.hpp | 17 + madspace/src/driver/backend.cpp | 9 +- madspace/src/driver/io.cpp | 150 +- madtrex/.gitignore | 5 + madtrex/CMakeLists.txt | 132 + madtrex/README.md | 256 + madtrex/Rex/.gitignore | 16 + madtrex/Rex/CMakeLists.txt | 71 + madtrex/Rex/FORKED_FROM.md | 16 + madtrex/Rex/README.md | 50 + madtrex/Rex/examples/showcase.py | 804 ++ madtrex/Rex/include/Rex.h | 3797 ++++++++ madtrex/Rex/include/teaRex.h | 451 + madtrex/Rex/lgpl-3.0.txt | 165 + madtrex/Rex/makefile | 44 + madtrex/Rex/pyproject.toml | 21 + madtrex/Rex/pyrex/__init__.py | 14 + madtrex/Rex/src/Rex.cc | 7600 +++++++++++++++++ madtrex/Rex/src/python/rex.cpp | 985 +++ madtrex/Rex/src/python/tearex.cpp | 461 + madtrex/Rex/src/python/tearex.hpp | 7 + madtrex/Rex/src/teaRex.cc | 1501 ++++ madtrex/include/madtrex/binary_io.hpp | 135 + madtrex/include/madtrex/madtrex.hpp | 523 ++ madtrex/include/madtrex/param_handler.hpp | 85 + madtrex/include/madtrex/pdfware.hpp | 353 + madtrex/include/madtrex/tupperware.hpp | 504 ++ madtrex/install.py | 257 + madtrex/madtrex/__init__.py | 13 + madtrex/pyproject.toml | 34 + madtrex/src/binary_io.cpp | 468 + madtrex/src/madtrex.cpp | 900 ++ madtrex/src/param_handler.cpp | 95 + madtrex/src/pdfware.cpp | 674 ++ madtrex/src/python/madtrex.cpp | 784 ++ madtrex/src/tupperware.cpp | 1463 ++++ madtrex/tools/check_helicity_map.py | 136 + tests/acceptance_tests/test_cmd_reweight.py | 10 +- .../test_reweight_backends.py | 180 + tests/input_files/hj_nlo.lhe.gz | Bin 65949 -> 82787 bytes tests/unit_tests/interface/test_mg7_launch.py | 353 +- .../various/test_madtrex_standalone.py | 597 ++ tests/unit_tests/various/test_misc.py | 26 + .../various/test_reweight_backend.py | 464 + 67 files changed, 28416 insertions(+), 115 deletions(-) create mode 100644 madgraph/iolibs/template_files/mg7/generic_reweight_card.py create mode 100644 madgraph/iolibs/template_files/mg7/madtrex.py create mode 100644 madgraph/iolibs/template_files/mg7/madtrex_driver.py create mode 100644 madgraph/iolibs/template_files/mg7/param_reweight_card.py create mode 100644 madgraph/iolibs/template_files/mg7/pdf_reweight_card.py create mode 100644 madgraph/iolibs/template_files/mg7/reweight_card_default.dat create mode 100644 madgraph/various/reweight_backend.py create mode 100644 madtrex/.gitignore create mode 100644 madtrex/CMakeLists.txt create mode 100644 madtrex/README.md create mode 100644 madtrex/Rex/.gitignore create mode 100644 madtrex/Rex/CMakeLists.txt create mode 100644 madtrex/Rex/FORKED_FROM.md create mode 100644 madtrex/Rex/README.md create mode 100644 madtrex/Rex/examples/showcase.py create mode 100644 madtrex/Rex/include/Rex.h create mode 100644 madtrex/Rex/include/teaRex.h create mode 100644 madtrex/Rex/lgpl-3.0.txt create mode 100644 madtrex/Rex/makefile create mode 100644 madtrex/Rex/pyproject.toml create mode 100644 madtrex/Rex/pyrex/__init__.py create mode 100644 madtrex/Rex/src/Rex.cc create mode 100644 madtrex/Rex/src/python/rex.cpp create mode 100644 madtrex/Rex/src/python/tearex.cpp create mode 100644 madtrex/Rex/src/python/tearex.hpp create mode 100644 madtrex/Rex/src/teaRex.cc create mode 100644 madtrex/include/madtrex/binary_io.hpp create mode 100644 madtrex/include/madtrex/madtrex.hpp create mode 100644 madtrex/include/madtrex/param_handler.hpp create mode 100644 madtrex/include/madtrex/pdfware.hpp create mode 100644 madtrex/include/madtrex/tupperware.hpp create mode 100644 madtrex/install.py create mode 100644 madtrex/madtrex/__init__.py create mode 100644 madtrex/pyproject.toml create mode 100644 madtrex/src/binary_io.cpp create mode 100644 madtrex/src/madtrex.cpp create mode 100644 madtrex/src/param_handler.cpp create mode 100644 madtrex/src/pdfware.cpp create mode 100644 madtrex/src/python/madtrex.cpp create mode 100644 madtrex/src/tupperware.cpp create mode 100755 madtrex/tools/check_helicity_map.py create mode 100644 tests/acceptance_tests/test_reweight_backends.py create mode 100644 tests/unit_tests/various/test_madtrex_standalone.py create mode 100644 tests/unit_tests/various/test_reweight_backend.py diff --git a/UpdateNotes.txt b/UpdateNotes.txt index 6f4d59cfd0..7747cccd33 100644 --- a/UpdateNotes.txt +++ b/UpdateNotes.txt @@ -5,6 +5,33 @@ ANNOUNCEMENT: A new LTS (based on the current 3.5.X) is starting now and will act as the stable release for the coming years. 0.3.0 (XX/XX/XX): + ZW: MG7 outputs have two reweighting backends: MadtRex ('install madtrex'; + matrix-element and PDF/scale reweighting on lhe or lhe_npy events) and the + classic reweighting module (f2py). Both read Cards/reweight_card.dat with the + classic vocabulary ('change' for options, 'set' for parameters). The backend + comes from 'generate_events --reweight-backend=classic|madtrex|auto', the + reweight switch of the launch question (now OFF/ON/classic/madtrex/density, + ON being auto), or the card's new 'change backend' line; auto prefers + MadtRex when it can run the card. The choice is logged and recorded in + Events//reweighting.json, and a choice that cannot run stops the launch + before the generation. Reweighting runs once, after the systematics; + '--reweight=CARD' now just switches it on with CARD as the reweight card. + MadtRex cards: 'change helicity', 'change fptype', 'change mode LO|pdf', + 'change verbose/max_batch/...' replace the former 'set' spellings (still + accepted, with a warning), 'set MT 175' (a parameter by its param_card + name) works, and directives only the classic module knows are an error + instead of being ignored. MG7 outputs get their own default reweight card + documenting both backends. madevent and aMC@NLO outputs are unchanged. + Both backends add their weights to the run's own event file + (Events//events.lhe.gz, or events.npy for an lhe_npy run reweighted by + MadtRex, as rwgt_ columns that npy_to_lhe carries over), after the + systematics weights; MadtRex no longer writes a separate *_reweighted file + (bin/reweight --output still can). MadtRex reads lhe_npy events with + systematics weights (they were refused as "Invalid header for event file"). + ZW: Without f2py_compiler set, the f2py installed with the running Python (e.g. + in a virtual environment) is used before f2py3/f2py on the PATH, which may + build modules for another Python version (reweighting then failed with "file + too short" or a missing all_matrix2py module). OM: new command 'install cuttools_iregi' recompiling CutTools and IREGI (vendor/) with the current fortran compiler. The error raised when CutTools/IREGI were compiled with another compiler (e.g. after a gfortran update) now points to it. diff --git a/madgraph/interface/common_run_interface.py b/madgraph/interface/common_run_interface.py index 521fc8f0b0..613662b9e0 100755 --- a/madgraph/interface/common_run_interface.py +++ b/madgraph/interface/common_run_interface.py @@ -2169,6 +2169,20 @@ def check_multicore(self): raise self.ConfigurationError('''Can\'t load Reweight module. The variable mg5_path might not be correctly configured.''') + # the reweighting backend (madgraph/various/reweight_backend.py): this + # is the classic module; MadtRex runs from the MG7 output's launcher + backend = [a.split('=', 1)[1] for a in line.split() + if a.startswith(('--backend=', '--reweight-backend='))] + if backend: + line = ' '.join(a for a in line.split() + if not a.startswith(('--backend=', '--reweight-backend='))) + if backend[-1].lower() == 'madtrex': + raise self.InvalidCmd('--backend=madtrex: MadtRex needs an MG7 output; ' + 'this reweighting uses the classic module') + if backend[-1].lower() not in ('classic', 'auto'): + raise self.InvalidCmd('--backend=%s: expected classic, madtrex or auto' + % backend[-1]) + # plugin option plugin = False reweight_mode = None diff --git a/madgraph/interface/madevent_interface.py b/madgraph/interface/madevent_interface.py index 9f8a039c05..a0abe60981 100755 --- a/madgraph/interface/madevent_interface.py +++ b/madgraph/interface/madevent_interface.py @@ -523,8 +523,8 @@ class AskRun(cmd.ControlSwitch): def __init__(self, question, line_args=[], mode=None, force=False, *args, **opt): - self.check_available_module(opt['mother_interface'].options) self.me_dir = opt['mother_interface'].me_dir + self.check_available_module(opt['mother_interface'].options) super(AskRun,self).__init__(self.to_control, opt['mother_interface'], *args, **opt) @@ -557,6 +557,18 @@ def check_available_module(self, options): if misc.has_f2py() or options['f2py_compiler']: self.available_module.add('reweight') + # An MG7 output reweights with the classic module or with MadtRex + # (see madgraph/various/reweight_backend.py): reweighting is + # available when either one can run. + self.reweight_env = None + if not MADEVENT and os.path.exists(pjoin(self.me_dir, 'Cards', 'run_card.toml')): + from madgraph.various import reweight_backend + self.reweight_env = reweight_backend.probe_environment( + self.me_dir, dict(options, mg5_path=options.get('mg5_path') or MG5DIR), + has_f2py='reweight' in self.available_module) + if reweight_backend.available_backends(self.reweight_env): + self.available_module.add('reweight') + # old mode to activate the shower def ans_parton(self, value=None): """None: means that the user type 'pythia' @@ -907,7 +919,16 @@ def get_allowed_reweight(self): if 'reweight' not in self.available_module: self.allowed_reweight = [] return self.allowed_reweight - self.allowed_reweight = ['OFF', 'ON', 'density'] + if self.reweight_env is not None: + # MG7 output: ON is auto; then the backends that can run here + from madgraph.various import reweight_backend + backends = reweight_backend.available_backends(self.reweight_env) + self.allowed_reweight = ['OFF', 'ON'] + backends + if 'classic' not in backends: + return self.allowed_reweight + self.allowed_reweight.append('density') + else: + self.allowed_reweight = ['OFF', 'ON', 'density'] # check for plugin mode plugin_path = self.mother_interface.plugin_path @@ -920,13 +941,7 @@ def set_default_reweight(self): if 'reweight' in self.available_module: if os.path.exists(pjoin(self.me_dir,'Cards','reweight_card.dat')): - reweightcard = open(pjoin(self.me_dir,'Cards','reweight_card.dat'), 'r') - content = reweightcard.read() - if 'change particle_in_density_matrix' in content: - self.switch['reweight'] = 'density' - else: - self.switch['reweight'] = 'ON' - reweightcard.close() + self.switch['reweight'] = self.switch_value_from_card_reweight() else: self.switch['reweight'] = 'OFF' else: @@ -934,6 +949,9 @@ def set_default_reweight(self): def get_cardcmd_for_reweight(self, value): """set some command to run before allowing the user to modify the cards.""" + if value in ['classic', 'madtrex']: + # the two backends of an MG7 output read the same card as ON + value = 'ON' if value in ['density']: content_rwgt_card = open(pjoin(self.me_dir, "Cards", "reweight_card.dat"), "r") if 'change particle_in_density_matrix' in content_rwgt_card.read(): @@ -961,14 +979,26 @@ def get_cardcmd_for_reweight(self, value): def switch_value_from_card_reweight(self): """re-derive the reweight switch (ON vs density) from the content of - reweight_card.dat (called after the user has edited the cards).""" + reweight_card.dat (called after the user has edited the cards). For + an MG7 output, a "change backend classic|madtrex" line of the card + gives that backend (when it can run here).""" path = pjoin(self.me_dir, 'Cards', 'reweight_card.dat') if not os.path.exists(path): return None with open(path) as fsock: - if 'change particle_in_density_matrix' in fsock.read(): - return 'density' + content = fsock.read() + if 'change particle_in_density_matrix' in content: + return 'density' + if getattr(self, 'reweight_env', None) is not None: + # a backend picked in the question outranks the card's line + # (the run warns when the two disagree) + current = getattr(self, 'switch', {}).get('reweight') + if current in ('classic', 'madtrex'): + return current + match = re.search(r'^\s*change\s+backend\s+(\w+)', content, re.M | re.I) + if match and match.group(1).lower() in self.get_allowed_reweight(): + return match.group(1).lower() return 'ON' diff --git a/madgraph/interface/madgraph_interface.py b/madgraph/interface/madgraph_interface.py index dd6806e7a6..5fac3f64f8 100755 --- a/madgraph/interface/madgraph_interface.py +++ b/madgraph/interface/madgraph_interface.py @@ -615,6 +615,14 @@ def help_install(self): logger.info(" defaults to the 'nb_core' option).") logger.info(" --cuda Enable CUDA GPU backend (source build).") logger.info(" --hip Enable HIP/ROCm GPU backend (source build).") + logger.info(" ") + logger.info(" \"install madtrex [options]\"",'$MG:BOLD') + logger.info(" Install MadtRex, the event-reweighting library used by MG7, together") + logger.info(" with its bundled Rex fork. Requires madspace to be installed first.") + logger.info(" Without options an interactive installer is launched. Available options:") + logger.info(" -y/--yes Install non-interactively.") + logger.info(" -j N/--jobs=N Parallel compilation jobs (defaults to the 'nb_core' option).") + logger.info(" --clean Rebuild from scratch.") logger.info(" --openblas Build OpenBLAS from source (default on Linux/Windows, source build).") logger.info(" --no-openblas Use system BLAS library (default on Apple, source build).") logger.info(" --simd Enable SIMD backend (source build, experimental).") @@ -3747,6 +3755,12 @@ def complete_install(self, text, line, begidx, endidx): if any(a.startswith(opt) for a in args[2:]): options.remove(opt) return self.list_completion(text, options) + elif len(args) >= 2 and args[1] == 'madtrex': + options = ['-y', '--yes', '--clean', '--jobs='] + for opt in options[:]: + if any(a.startswith(opt) for a in args[2:]): + options.remove(opt) + return self.list_completion(text, options) elif len(args)>=2 and args[1] in self._advanced_install_opts: options = ['--keep_source','--logging='] if args[1]=='pythia8': @@ -3802,7 +3816,7 @@ def _tutorial_opts(self): _install_opts = ['Delphes', 'ExRootAnalysis', 'update', 'Golem95', 'QCDLoop', 'maddm', 'maddump', 'looptools', 'cuttools_iregi', 'MadSTR', 'RunningCoupling', - 'madspace'] + 'madspace', 'madtrex'] # The targets below are installed using the HEPToolsInstaller.py script _advanced_install_opts = ['pythia8','zlib','boost','lhapdf6','lhapdf5','collier', @@ -8586,8 +8600,8 @@ def do_install(self, line, paths=None, additional_options=[]): elif args[0] == 'cuttools_iregi': self.install_cuttools_iregi() return - elif args[0] == 'madspace': - install_script = pjoin(MG5DIR, 'madspace', 'install.py') + elif args[0] in ('madspace', 'madtrex'): + install_script = pjoin(MG5DIR, args[0], 'install.py') install_args = args[1:] # The source build compiles in parallel only if it is told how many # jobs it may use (see set_build_parallelism in install.py), so hand diff --git a/madgraph/interface/reweight_interface.py b/madgraph/interface/reweight_interface.py index 3229840c9d..e913913a21 100755 --- a/madgraph/interface/reweight_interface.py +++ b/madgraph/interface/reweight_interface.py @@ -62,6 +62,27 @@ #lhapdf = None +# The "change " in text: + text = text.replace("", group + "", 1) + else: + text = text.replace("", f"\n{group}\n", 1) + with open(header + ".tmp", "w") as f: + f.write(text) + os.replace(header + ".tmp", header) + + +def write_weights_in_place(driver, result, source_path: str, events_path: str) -> str: + """Writes the weights driver (a Driver or PdfDriver, whose reweighted + sample is result) computed into the event file they were computed for, + and returns its path: source_path, the run's event file, as the other + tools (MadSpin, the shower, npy_to_lhe) and the classic reweighting + module use it. An LHE file is rewritten through a temporary file next + to it (gzipped again when it was) and replaced atomically, the + decompressed copy events_path read from removed; an npy file gets the + new weights as rwgt_ columns (see _merge_npy_weights()). Either + way the existing weights (systematics, earlier reweightings) are kept, + and the new ones follow them.""" + run_dir = os.path.dirname(os.path.abspath(source_path)) + if source_path.endswith(".npy"): + prefix = os.path.join(run_dir, ".madtrex_weights") + existing = set(_npy_existing_weight_ids(source_path)) + driver.write_weights(prefix) + ids = list(result.weight_ids) + contexts = list(getattr(result, "weight_context", [])) or [""] * len(ids) + new = [(i, c) for i, c in zip(ids, contexts + [""] * len(ids)) if i not in existing] + for weight_id in ids: + if weight_id in existing and os.path.isfile(f"{prefix}_{weight_id}.npy"): + os.remove(f"{prefix}_{weight_id}.npy") + _merge_npy_weights(source_path, prefix, [i for i, _ in new], [c for _, c in new]) + return source_path + tmp = os.path.join(run_dir, ".madtrex_reweighted.lhe") + driver.write_weights(tmp) + _replace_atomically(tmp, source_path) + if os.path.abspath(events_path) != os.path.abspath(source_path) and os.path.isfile(events_path): + os.remove(events_path) # the decompressed copy, stale now + return source_path + + +def _npy_existing_weight_ids(events_path: str) -> list[str]: + """The weight ids events_path already has columns for (rwgt_).""" + import numpy as np + events = np.load(events_path, mmap_mode="r", max_header_size=1 << 30) + return [name[len("rwgt_"):] for name in events.dtype.names if name.startswith("rwgt_")] + + +def _tag_output_path(output_path: str, tag: str) -> str: + root, ext = os.path.splitext(output_path) + return f"{root}_{tag}{ext}" + + +def run_parameter_reweighting( + rwgt_path: str, + events_path: str | None = None, + output_path: str | None = None, + process_directory: str = ".", + param_card: str = "", + regenerate: Regenerator | None = None, + helicity_reweighting: bool | None = None, +) -> dict[tuple[str | None, str], mtx.Driver]: + """Runs MadtRex reweighting end to end against an existing MadGraph7 run. + + Parses rwgt_path (see reweight_card.py) into groups of launches by which + UFO model *and* which floating-point precision (fptype) they target (a + bare card with no "change model"/"set FPTYPE" lines is one group + targeting the original process_directory as-is, at its baseline + precision -- see reweight_card.group_by_fptype()). Each group with at + least one launch runs as its own independent reweighting pass, since + each needs its own MatrixElementApi loaded (a different model needs + different physics code entirely; a different fptype needs a + differently-compiled library for the *same* code -- see + Driver.set_fptype_priority()): any group with a "change model" is + regenerated/compiled into its own subdirectory of process_directory + first by regenerate (mg7/madtrex.py's regenerate_process_in_model(); + without one, such a card is an error) -- named "rw_me" if the card + only ever requests that one model, else "rw_me_" per distinct + model (shared across that model's fptype groups, since regeneration + itself doesn't depend on fptype) -- and every group, regenerated + directory or not, has its own fptype compiled on demand (see + _compile_subprocesses()) before it's loaded. By default (output_path + None) every pass adds its weights to events_path itself, after the + weights already there (see write_weights_in_place()), so the groups' + weights accumulate in that one file. With output_path, each pass writes + its own output file next to output_path instead: untagged if it is the + card's only group, else suffixed "__" (the original-model + group, if not alone, uses "original" for ). events_path is + auto-discovered from the latest run under process_directory/Events if + not given. + + The card's preamble may also set any of reweight_card.DRIVER_OPTION_NAMES + (helicity reweighting, warning verbosity, event-batch splitting, deferred- + iteration flushing, streaming -- see reweight_card.DriverOptions), applied + to every Driver this builds, regardless of group. helicity_reweighting, + unless None, overrides the card's "set helicity_reweighting": True + reweights each event at its own helicity (Driver's default), False uses + helicity-summed matrix elements. + + Returns a dict of the Driver built for each group, keyed by its + (model, fptype) (model is None for the original-model group), so + callers can inspect driver.warehouse().get() afterwards.""" + if events_path is None: + events_path = find_run_events( + find_latest_run_dir(os.path.join(process_directory, "Events")) + ) + source_path = events_path + events_path = prepare_events(source_path) + in_place = output_path is None + init_meta = read_init_node(events_path) + + with open(rwgt_path) as f: + parsed = param_rwgt_card.expand_scans(param_rwgt_card.parse_reweight_card(f.read())) + # Card-wide, not per-group -- see DriverOptions -- so resolved once here + # from the still-ungrouped ParsedCard and applied to every Driver below. + driver_options = parsed.driver_options + # The sample's own, actual generation-time fptype -- see + # _read_run_fptype()'s own doc comment. Used both as group_by_fptype()'s + # default *target* fptype (a card that sets no FPTYPE at all should stay + # at the sample's own precision, not drift to a hardcoded default) and, + # unconditionally below, as every group.model_spec-is-None group's + # *normaliser* fptype -- a launch must always be normalised against this + # original configuration, never against whichever fptype it targets. + original_fptype = _read_run_fptype(process_directory) or param_rwgt_card.DEFAULT_FPTYPE + groups = [ + group for group in param_rwgt_card.group_by_fptype(parsed, default_fptype=original_fptype) + if group.launches + ] + if not groups: + raise ValueError(f"reweight card {rwgt_path!r} has no 'launch' blocks") + multi = len(groups) > 1 + + devices = _read_devices(process_directory) + drivers: dict[tuple[str | None, str], mtx.Driver] = {} + for group in groups: + model_tag = _sanitize_dirname(group.model_spec) if group.model_spec is not None else "original" + tag = f"{model_tag}_{group.fptype}" if multi else None + + if group.model_spec is None: + load_directory, group_param_card = process_directory, param_card + else: + if regenerate is None: + raise RuntimeError( + f"reweight card {rwgt_path!r} asks for 'change model {group.model_spec}', " + f"which needs a full MadGraph installation to regenerate the process in " + f"that model: run it through MadGraph (mg7/madtrex.py) rather than this " + f"standalone driver, or reweight within the process's own model" + ) + rw_subdir = "rw_me" if not multi else f"rw_me_{model_tag}" + load_directory = regenerate( + process_directory, group.model_spec, group.process_definition, rw_subdir, + fptype=group.fptype, + ) + group_param_card = "" + + # Guarantees group.fptype is actually compiled for load_directory, + # regardless of whether regenerate() just did so + # (only true on the first group to regenerate a given rw_subdir -- + # see its own doc comment) or load_directory is process_directory + # itself (never compiled for anything but launch.py's own + # hardcoded default fptype at generation time). + _compile_subprocesses(load_directory, devices, group.fptype) + # The normaliser needs original_fptype's own compiled library too, + # whenever it differs from the target fptype just compiled above + # (see the driver.set_normaliser_fptype_priority() call below) -- + # only for group.model_spec is None, since that's the only case + # normaliser_fptype_priority is actually set to something other than + # group.fptype; a "change model" group's normaliser still shares its + # own (regenerated) target-fptype library, unchanged. + if group.model_spec is None and original_fptype != group.fptype: + _compile_subprocesses(load_directory, devices, original_fptype) + + if in_place: + # each group adds its weights to the file the previous one wrote + events_path = prepare_events(source_path) + else: + group_output_path = output_path if tag is None else _tag_output_path(output_path, tag) + + launches = group.launches + if param_rwgt_card.has_parameter_names(launches): + # "set MT 175": named by the param_card this group runs with + card_path = group_param_card or os.path.join(load_directory, "Cards", "param_card.dat") + with open(card_path) as f: + launches = param_rwgt_card.resolve_parameter_names(launches, f.read()) + + with tempfile.NamedTemporaryFile("w", suffix=".rwgt", delete=False) as fh: + fh.write(param_rwgt_card.render_launches(launches)) + resolved_rwgt_path = fh.name + try: + logger.info( + "MadtRex: reweighting %s against model '%s' at fptype '%s' with %s", + events_path, group.model_spec or "", group.fptype, rwgt_path, + ) + driver = mtx.Driver() + driver_options.apply(driver) + if helicity_reweighting is not None: + driver.set_helicity_reweighting(helicity_reweighting) + driver.set_device_priority(devices) + driver.set_fptype_priority([group.fptype]) + if group.model_spec is None: + # Reweighting the original UFO model, possibly to a + # different fptype: the normaliser must always reflect the + # sample's own actual generation-time fptype + # (original_fptype), never group.fptype -- see + # Driver.set_normaliser_fptype_priority()'s own doc comment. + # A no-op (normaliser_priority == fptype_priority, one api + # shared for both roles) whenever the card doesn't actually + # change fptype, since group.fptype then also resolves to + # original_fptype (see group_by_fptype()'s default_fptype). + driver.set_normaliser_fptype_priority([original_fptype]) + driver.load_process_directory(load_directory, group_param_card) + driver.load_param_reweighting(resolved_rwgt_path) + if init_meta is not None: + driver.load_events(events_path, init_meta) + else: + driver.load_events(events_path) + driver.warehouse().get().run() + if in_place: + group_output_path = write_weights_in_place( + driver, driver.warehouse().get(), source_path, events_path) + else: + driver.write_weights(group_output_path) + finally: + os.remove(resolved_rwgt_path) + logger.info("MadtRex: wrote %s", group_output_path) + drivers[(group.model_spec, group.fptype)] = driver + + return drivers + + +def run_pdf_reweighting( + rwgt_path: str, + events_path: str | None = None, + output_path: str | None = None, + process_directory: str = ".", +) -> mtx.PdfDriver: + """Runs MadtRex PDF-only reweighting end to end against an existing + MadGraph7 run, via PdfDriver -- entirely independent of run_parameter_reweighting() + (matrix-element/parameter reweighting): see pdf_reweight_card.py's module + docstring for why the two are split into separate cards/flows (PDFs and + matrix elements factorise), and main() for running both together against + the same sample. + + Unlike run_parameter_reweighting(), no model regeneration or subprocess compilation + is needed: a PDF reweighting hypothesis is just a different (PDF set, + scale) pair fed to the same PdfSet/PdfDriver machinery, not a different + physics library, so this is a single pass regardless of how many "launch" + blocks or distinct PDF sets rwgt_path uses. + + Parses rwgt_path (see pdf_reweight_card.py), first expanding any + scan:[...] MUF/MUR/MUF_FACTOR/MUR_FACTOR values into explicit per-point + launches (pdf_reweight_card.expand_scans(), the same mechanism + run_parameter_reweighting() uses for physics parameter scans), then resolving the + result into a ResolvedPdfCard: a required baseline PDF set -- unlike + run_parameter_reweighting()'s FPTYPE, which falls back to a hard-set default when + unspecified, MadtRex does not + currently record which PDF set an existing sample was generated with + anywhere *on the sample itself*. If the card's preamble never sets + PDF_SET/BEAM1_PDF_SET/BEAM2_PDF_SET at all, this looks it up instead, + from process_directory's own run_card.toml [beam] pdf (see + _read_default_pdf_set()) -- a real fact about the run, not a guessed + default, available whenever process_directory still has (or ever had) + the Cards/ that generated events_path, which covers both "reweighting + immediately after generation" and any later run against that same + process directory. Only once that lookup also comes up empty (no + run_card.toml, or one predating [beam] pdf) does pdf_rwgt_card.resolve() + raise, requiring PDF_SET explicitly -- eg for a bare/relocated sample, + or to deliberately reweight against a baseline other than the one the + run actually used. A further hook could still infer the baseline PDF + set straight from the LHE file's own metadata instead of the run_card + -- not implemented, since MadGraph7's LHE output carries no such field + today. Every distinct PDF set name the card references (baseline and + launches alike) is load_pdf_set()'d once, resolved to its LHAPDF + grid/.info files via resolve_pdf_set_files(). + + Beam energies are taken from events_path's sibling header.lhe/init.lhe + (see read_init_node()) if present, else from its LHE block directly + (PdfDriver.load_events() sniffs this itself). events_path is auto- + discovered from the latest run under process_directory/Events if not + given. The weights are added to events_path itself unless output_path + is given (see write_weights_in_place()). + + Returns the built PdfDriver, so callers can inspect driver.get() + afterwards.""" + if events_path is None: + events_path = find_run_events( + find_latest_run_dir(os.path.join(process_directory, "Events")) + ) + source_path = events_path + events_path = prepare_events(source_path) + init_meta = read_init_node(events_path) + + with open(rwgt_path) as f: + parsed = pdf_rwgt_card.expand_scans(pdf_rwgt_card.parse_pdf_reweight_card(f.read())) + baseline = parsed.baseline + if baseline.pdf_set_name is None and baseline.beam1_pdf_set_name is None and baseline.beam2_pdf_set_name is None: + default_pdf_set = _read_default_pdf_set(process_directory) + if default_pdf_set is not None: + logger.info( + "MadtRex: PDF reweight card %s sets no baseline PDF_SET; inferring '%s' " + "from %s", + rwgt_path, default_pdf_set, os.path.join(process_directory, "Cards", "run_card.toml"), + ) + baseline.pdf_set_name = default_pdf_set + card = pdf_rwgt_card.resolve(parsed) + if not card.launches: + raise ValueError(f"PDF reweight card {rwgt_path!r} has no 'launch' blocks") + + driver = mtx.PdfDriver() + if card.verbose is not None: + driver.set_verbose(card.verbose) + if card.alpha_s_power is not None: + driver.set_alpha_s_power(card.alpha_s_power) + if card.scale_variation_output is not None: + driver.set_scale_variation_output( + getattr(mtx.ScaleVariationOutput, card.scale_variation_output.upper()) + ) + + def as_scale_choice(scale: pdf_rwgt_card.PdfScale) -> mtx.ScaleChoice: + return mtx.ScaleChoice(scale.value, scale.absolute) + + loaded_pdf_sets: set[str] = set() + + def ensure_loaded(name: str) -> None: + if name in loaded_pdf_sets: + return + grid_file, alpha_s_grid_file = resolve_pdf_set_files(name) + driver.load_pdf_set(name, grid_file, alpha_s_grid_file) + loaded_pdf_sets.add(name) + + for name in card.baseline_pdf_set_names: + ensure_loaded(name) + for launch in card.launches: + for name in launch.pdf_set_names: + ensure_loaded(name) + + driver.set_baseline( + *card.baseline_pdf_set_names, + as_scale_choice(card.baseline_muF_scale), + as_scale_choice(card.baseline_muR_scale), + ) + for i, launch in enumerate(card.launches): + driver.add_hypothesis( + launch.name or f"pdf_rwgt_{i + 1}", + *launch.pdf_set_names, + as_scale_choice(launch.muF_scale), + as_scale_choice(launch.muR_scale), + ) + + logger.info("MadtRex: PDF-reweighting %s with %s", events_path, rwgt_path) + if init_meta is not None: + # PdfDriver.load_events() only actually consults meta's beam + # energies for binary input -- for XML LHE it reads the file's own + # block instead (older MadGraph7 writers left a zeroed-out + # stub there, not real beam energies). set_beam_energies() is the + # documented way to override that, so use it explicitly whenever + # init_meta (from header.lhe/init.lhe) is available, regardless of + # events_path's format. + driver.set_beam_energies(init_meta.beam1_energy, init_meta.beam2_energy) + driver.load_events(events_path, init_meta) + else: + driver.load_events(events_path) + driver.get().run() + if output_path is None: + output_path = write_weights_in_place(driver, driver.get(), source_path, events_path) + else: + driver.write_weights(output_path) + logger.info("MadtRex: wrote %s", output_path) + return driver + + +def _merge_reweighting_results(target, source) -> None: + """Merges source's weight columns onto target, in place: appends every + one of source's weight_ids after target's own, and correspondingly + extends every target event's own wgts with the matching source event's. + target and source must come from reweighting the exact same events, in + the same order (true for run_generic_reweighting()'s own dispatch, + since both passes load the same events_path) -- raises if their event + counts differ, as a cheap sanity check of that assumption. + + Uses add_weight_id() (in-place push_back through the *existing* + weight_ids shared_ptr) rather than reassigning target.weight_ids + directly -- the bound weight_ids property's setter replaces the whole + shared_ptr, which would desync every event's own (separately + shared_ptr-aliased) weight_ids from it and corrupt the write. See + madtrex::PdfDriver::keep_weights()'s own doc comment (pdfware.hpp) for + the same gotcha encountered the other way round (dropping columns + rather than adding them).""" + target_events = target.events + source_events = source.events + if len(target_events) != len(source_events): + raise ValueError( + "cannot coalesce reweighting results: event counts differ " + f"({len(target_events)} vs {len(source_events)}) -- were they reweighted " + f"against the same events file?" + ) + for name in source.weight_ids: + target.add_weight_id(name) + for t_ev, s_ev in zip(target_events, source_events): + t_ev.wgts = t_ev.wgts + s_ev.wgts + + +def run_generic_reweighting( + rwgt_path: str, + events_path: str | None = None, + output_path: str | None = None, + process_directory: str = ".", + param_card: str = "", + regenerate: Regenerator | None = None, + helicity_reweighting: bool | None = None, +) -> dict[str, object]: + """Runs MadtRex's generic reweight card end to end: splits rwgt_path + (see generic_reweight_card.py) into its "parameter"/"pdf" sections -- + both, or whichever one is present -- runs each through its own real + routine (run_parameter_reweighting()/run_pdf_reweighting()) exactly as + if it had been handed a standalone card of that kind, then, if *both* + modes ran, coalesces the results back into a single sample per + parameter-reweighting group: every parameter-reweighting output + additionally gets the PDF-reweighting weights merged onto it (in place, + then rewritten to that same group's own output path), so a caller only + looking at that one "primary" output file sees every requested weight, + from both modes, on one set of events -- not two separate files to + reconcile themselves. If only one mode is present, nothing needs + coalescing -- that mode's own output, written by its own routine, is + already the whole answer. + + events_path/output_path/process_directory/param_card are all forwarded + to whichever of run_parameter_reweighting()/run_pdf_reweighting() + actually runs; param_card is unused if the card has no "parameter" + section. Without output_path, each pass adds its weights to events_path + in turn and nothing needs coalescing. With output_path, it anchors the + *parameter* pass's own naming (its usual untagged-unless-multiple-groups + scheme, same as run_parameter_reweighting() alone); the PDF pass's own + intermediate output is separately tagged "_pdf" whenever a parameter + section is also present, mirroring main()'s own two-flag convention, so + the two never collide, and the PDF weights are merged onto the + parameter output(s). + + Returns {"parameter": dict[(model, fptype), Driver] | None, "pdf": + PdfDriver | None} -- one real entry per mode that actually ran + (mirroring each routine's own native return value), None for a mode + the card didn't request. Callers after the *merged* result should read + the output file(s) written, not these driver objects -- which of them + ends up with its in-memory weight_ids/events mutated by the merge (the + parameter side's, when both modes ran) is an implementation detail, not + part of the return contract.""" + with open(rwgt_path) as f: + sections = generic_rwgt_card.split_by_mode(f.read()) + if not sections: + raise ValueError(f"generic reweight card {rwgt_path!r} has no 'set mode' sections") + + if events_path is None: + events_path = find_run_events( + find_latest_run_dir(os.path.join(process_directory, "Events")) + ) + # output_path None: each pass adds its weights to events_path in turn + in_place = output_path is None + + result: dict[str, object] = {"parameter": None, "pdf": None} + with tempfile.TemporaryDirectory() as tmp_dir: + if "parameter" in sections: + param_path = os.path.join(tmp_dir, "parameter.rwgt") + with open(param_path, "w") as f: + f.write(sections["parameter"]) + result["parameter"] = run_parameter_reweighting( + param_path, + events_path=events_path, + output_path=output_path, + process_directory=process_directory, + param_card=param_card, + regenerate=regenerate, + helicity_reweighting=helicity_reweighting, + ) + if "pdf" in sections: + pdf_path = os.path.join(tmp_dir, "pdf.rwgt") + with open(pdf_path, "w") as f: + f.write(sections["pdf"]) + pdf_output_path = output_path + if not in_place and "parameter" in sections: + pdf_output_path = _tag_output_path(output_path, "pdf") + result["pdf"] = run_pdf_reweighting( + pdf_path, + events_path=events_path, + output_path=pdf_output_path, + process_directory=process_directory, + ) + + param_drivers, pdf_driver = result["parameter"], result["pdf"] + if not in_place and param_drivers is not None and pdf_driver is not None: + multi = len(param_drivers) > 1 + for (model_spec, fptype), driver in param_drivers.items(): + model_tag = _sanitize_dirname(model_spec) if model_spec is not None else "original" + tag = f"{model_tag}_{fptype}" if multi else None + group_output_path = output_path if tag is None else _tag_output_path(output_path, tag) + _merge_reweighting_results(driver.warehouse().get(), pdf_driver.get()) + driver.write_weights(group_output_path) + logger.info( + "MadtRex: coalesced PDF reweighting into parameter reweighting result for " + "model '%s' at fptype '%s' -> %s", + model_spec or "", fptype, group_output_path, + ) + + return result + + +def run_reweighting( + rwgt_path: str, + events_path: str | None = None, + card_kind: str | None = None, + output_path: str | None = None, + process_directory: str = ".", + param_card: str = "", + regenerate: Regenerator | None = None, + helicity_reweighting: bool | None = None, +): + """Runs the reweight card rwgt_path with the runner its card_kind needs: + "param" -> run_parameter_reweighting(), "pdf" -> run_pdf_reweighting(), + "generic" -> run_generic_reweighting(). card_kind defaults to + generic_reweight_card.card_kind() of the card. Returns that runner's + result.""" + if card_kind is None: + with open(rwgt_path) as f: + card_kind = generic_rwgt_card.card_kind(f.read()) + if card_kind not in generic_rwgt_card.CARD_KINDS: + raise ValueError( + f"unknown reweight card kind {card_kind!r}; expected one of " + f"{list(generic_rwgt_card.CARD_KINDS)}" + ) + common = dict(events_path=events_path, output_path=output_path, + process_directory=process_directory) + if card_kind == "pdf": + return run_pdf_reweighting(rwgt_path, **common) + runner = run_parameter_reweighting if card_kind == "param" else run_generic_reweighting + return runner(rwgt_path, param_card=param_card, regenerate=regenerate, + helicity_reweighting=helicity_reweighting, **common) + + +def check_card_for_standalone(rwgt_path: str) -> None: + """Raises when the reweight card at rwgt_path needs the classic + reweighting module ("change backend classic", or a directive only the + classic module understands): that module is part of MadGraph, which + this standalone driver (a gridpack's bin/reweight) runs without. + "change backend madtrex|auto" is fine.""" + problems = [] + with open(rwgt_path) as f: + for number, raw_line in enumerate(f, 1): + line = raw_line.split("#", 1)[0] + words = line.split() + if len(words) >= 2 and words[0].lower() == "change" and words[1].lower() == "backend": + value = words[2].lower() if len(words) > 2 else "" + if value == "classic": + problems.append(f"line {number}: {line.strip()!r}") + elif value not in generic_rwgt_card.BACKENDS: + raise ValueError( + f"{rwgt_path} line {number}: unknown backend {value!r}; expected " + f"one of {sorted(generic_rwgt_card.BACKENDS)}" + ) + continue + reason = generic_rwgt_card.classic_only_reason(line) + if reason is not None: + problems.append(f"line {number}: {line.strip()!r} ({reason})") + if problems: + raise SystemExit( + f"{rwgt_path} needs the classic reweighting module, and classic reweighting " + f"needs MadGraph (this standalone MadtRex driver runs without it): " + + "; ".join(problems) + ) + + +def _default_process_directory() -> str: + """The process directory this file belongs to when it is shipped inside + one (a gridpack's bin/reweight: /SubProcesses/subprocesses.json + next to /bin/), else the current directory.""" + root = _HERE.parent + if (root / "SubProcesses" / "subprocesses.json").is_file() and (root / "Cards").is_dir(): + return str(root) + return "." + + +def main(argv: list[str] | None = None, regenerate: Regenerator | None = None) -> None: + """Command line entry point. regenerate is passed on to + run_parameter_reweighting(); without it a "change model" card is an + error (see mg7/madtrex.py for the MadGraph-side entry point).""" + parser = argparse.ArgumentParser( + description="Run MadtRex event reweighting against an existing MadGraph7 run. " + "Parameter (matrix-element) and PDF reweighting are independent " + "passes -- see run_parameter_reweighting()/run_pdf_reweighting(). Pass " + "--rwgt-card alone for a generic card that dispatches ('change mode " + "LO'/'change mode pdf') to one or both automatically and coalesces " + "the result -- see run_generic_reweighting() -- or --param-rwgt-card/" + "--pdf-rwgt-card directly (either, or both, for two separate outputs)." + ) + parser.add_argument( + "--rwgt-card", dest="generic_rwgt_path", default=None, + help="generic reweight card dispatching to parameter and/or PDF reweighting (see " + "generic_reweight_card.py), eg Cards/reweight_card.dat; mutually exclusive " + "with --param-rwgt-card/--pdf-rwgt-card", + ) + parser.add_argument( + "--param-rwgt-card", dest="param_rwgt_path", default=None, + help="parameter (SLHA/matrix-element) reweight card, eg " + "Cards/param_reweight_card.dat", + ) + parser.add_argument( + "--pdf-rwgt-card", dest="pdf_rwgt_path", default=None, + help="PDF reweight card, eg Cards/pdf_reweight_card.dat", + ) + parser.add_argument( + "--events", dest="events_path", default=None, + help="events file to reweight (events.lhe or events.npy); " + "defaults to the latest run under Events/", + ) + parser.add_argument( + "--output", dest="output_path", default=None, + help="write the reweighted events to this path (or binary weight-file prefix for " + "events.npy input) instead of adding the weights to --events itself, the " + "default (--pdf-rwgt-card's own output, when both flags are given, is further " + "tagged '_pdf' so the two don't collide)", + ) + parser.add_argument( + "--process-dir", dest="process_directory", default=None, + help="process directory (or gridpack) containing SubProcesses/ and Cards/ " + "(default: the gridpack this script is in, else .)", + ) + parser.add_argument( + "--helicity-reweighting", dest="helicity_reweighting", + action=argparse.BooleanOptionalAction, default=None, + help="reweight each event at its own helicity (--helicity-reweighting, the " + "default) or with helicity-summed matrix elements " + "(--no-helicity-reweighting); overrides the reweight card's 'change " + "helicity'", + ) + parser.add_argument( + "--param-card", dest="param_card", default="", + help="baseline param_card.dat (--param-rwgt-card/--rwgt-card only); defaults to " + "/Cards/param_card.dat", + ) + args = parser.parse_args(argv) + if args.process_directory is None: + args.process_directory = _default_process_directory() + if not logger.handlers and not logging.getLogger().handlers: + # standalone run: nothing has set up MadGraph's logging + logging.basicConfig(level=logging.INFO, format="%(message)s") + if regenerate is None: + # without MadGraph (see mg7/madtrex.py for the MadGraph side) + for path in (args.generic_rwgt_path, args.param_rwgt_path, args.pdf_rwgt_path): + if path is not None: + check_card_for_standalone(path) + if args.generic_rwgt_path is not None: + if args.param_rwgt_path is not None or args.pdf_rwgt_path is not None: + parser.error("--rwgt-card is mutually exclusive with --param-rwgt-card/--pdf-rwgt-card") + run_generic_reweighting( + args.generic_rwgt_path, + events_path=args.events_path, + output_path=args.output_path, + process_directory=args.process_directory, + param_card=args.param_card, + regenerate=regenerate, + helicity_reweighting=args.helicity_reweighting, + ) + return + if args.param_rwgt_path is None and args.pdf_rwgt_path is None: + parser.error("at least one of --rwgt-card/--param-rwgt-card/--pdf-rwgt-card is required") + if args.param_rwgt_path is not None: + run_parameter_reweighting( + args.param_rwgt_path, + events_path=args.events_path, + output_path=args.output_path, + process_directory=args.process_directory, + param_card=args.param_card, + regenerate=regenerate, + helicity_reweighting=args.helicity_reweighting, + ) + if args.pdf_rwgt_path is not None: + # An explicit --output is untagged by default (run_pdf_reweighting() + # only tags "_pdf" onto its own auto-derived default); tag it + # ourselves here so an explicit --output can't collide with + # --param-rwgt-card's own write when both flags are given. + pdf_output_path = args.output_path + if pdf_output_path is not None and args.param_rwgt_path is not None: + pdf_output_path = _tag_output_path(pdf_output_path, "pdf") + run_pdf_reweighting( + args.pdf_rwgt_path, + events_path=args.events_path, + output_path=pdf_output_path, + process_directory=args.process_directory, + ) + + +if __name__ == "__main__": + main() diff --git a/madgraph/iolibs/template_files/mg7/npy_to_lhe.py b/madgraph/iolibs/template_files/mg7/npy_to_lhe.py index df9c62c2f5..7978ae0b07 100644 --- a/madgraph/iolibs/template_files/mg7/npy_to_lhe.py +++ b/madgraph/iolibs/template_files/mg7/npy_to_lhe.py @@ -19,7 +19,8 @@ In both cases the LHE header and ```` block (with the systematics ````) are taken verbatim from the ``header.lhe`` file the run writes next to its npy events; the ``rwgt_`` columns become the per-event -```` block. The ```` reweighting-input block (``[systematics] +```` block: the systematics' numbered ones, then the named ones MadtRex +reweighting adds (``rwgt_``, listed in header.lhe's ````). The ```` reweighting-input block (``[systematics] write_inputs``) is not reconstructed. Usage:: @@ -147,6 +148,19 @@ def _lhe_npy_events(ms, events, columns): yield event, [float(w[n]) for w in rwgt] +def _add_named_weights(text, names, values): + """The formatted LHE event text with the named weights (ids madspace's + integer-id ``rwgt`` cannot carry) appended to its ```` block, which + is created when the event has none.""" + if not names: + return text + extra = "".join(" %+.7e \n" % (name, value) + for name, value in zip(names, values)) + if "" in text: + return text.replace("", extra + "", 1) + return text.replace("", "\n" + extra + "\n", 1) + + def convert(path, output=None, seed=None, compress=True, me_dir=None) -> str: """Write the LHE file for the npy events at ``path`` (npy file, run directory or run name) and return its path. By default it is written as @@ -170,10 +184,13 @@ def convert(path, output=None, seed=None, compress=True, me_dir=None) -> str: "and cannot be converted." % os.path.join(run_path, HEADER_FILE)) header = _read_header(run_path) - events = np.load(npy_path, mmap_mode="r") + events = np.load(npy_path, mmap_mode="r", max_header_size=1 << 30) names = events.dtype.names columns = [n for n in names if re.fullmatch(r"rwgt_\d+", n)] ids = [int(c.split("_", 1)[1]) for c in columns] + # MadtRex reweighting's named weights (rwgt_) + named = [n for n in names if n.startswith("rwgt_") and n not in columns] + named_ids = [n[len("rwgt_"):] for n in named] if "diagram_index" in names: completer_path = os.path.join(run_path, COMPLETER_FILE) if not os.path.isfile(completer_path): @@ -196,11 +213,13 @@ def convert(path, output=None, seed=None, compress=True, me_dir=None) -> str: for start in range(0, len(events), _CHUNK): chunk = np.asarray(events[start:start + _CHUNK]) text = [] - for event, weights in stream(chunk): + named_values = [chunk[n] for n in named] + for i, (event, weights) in enumerate(stream(chunk)): if ids: event.rwgt_ids = ids event.rwgt = weights - text.append(event.format()) + text.append(_add_named_weights(event.format(), named_ids, + [float(v[i]) for v in named_values])) out.write("".join(text)) out.write("\n") if compress: diff --git a/madgraph/iolibs/template_files/mg7/param_reweight_card.py b/madgraph/iolibs/template_files/mg7/param_reweight_card.py new file mode 100644 index 0000000000..4bc19cf29d --- /dev/null +++ b/madgraph/iolibs/template_files/mg7/param_reweight_card.py @@ -0,0 +1,602 @@ +"""Parsing/preprocessing for MadtRex parameter (matrix-element) reweight +cards -- see pdf_reweight_card.py for the separate, independent PDF +reweighting card, and generic_reweight_card.py for the dispatcher that lets +one card mix both. + +madtrex.ParamHandler.read_rwgt_card() (madtrex/include/madtrex/ +param_handler.hpp) only understands the literal "# launch / # set BLOCK_NAME +PARAM_ID PARAM_VALUE" grammar parsed by REX::tea::rwgt_slha::parse_rwgt_card +(Rex/src/teaRex.cc) -- unrecognized lines (eg "change model ...") are silently +ignored there, and scan:[...] value expressions fail to parse as a plain +double. This module extracts the extra directives MadtRex needs to act on +before handing a card to that low-level parser: + +- any number of "change model " lines, each paired with a "change + process " line, requesting the launches that follow be run + against a different UFO model, regenerated and compiled on demand (see + madtrex.py's regenerate_process_in_model()) -- MG7 does not persist the + original "generate " command anywhere on disk, so it must be given + again explicitly whenever the model changes. Launches are grouped by their + (model, process) target regardless of how they're interleaved in the card, + so "change model" may appear more than once (switching back and forth, or + requesting several distinct alternate models) without duplicating work; +- "change fptype " lines (formerly "set FPTYPE ", still + accepted with a deprecation warning), requesting the launch they belong to be + evaluated against a differently-precision-compiled matrix element library + (see MADMATRIX_LIB in madmatrix.mk / Driver.set_fptype_priority() in + madtrex.hpp) -- settable per launch, exactly like a physics "set BLOCK ID + VALUE" line (nothing carries over between launches). This is always the + *target* fptype to reweight TO -- normalisation is a separate, always-on + concern: every launch must be normalised against the sample's own actual + generation-time fptype, never against whichever fptype it's being + reweighted to (this is a fundamental assumption of how reweighting works + here -- see Driver.set_normaliser_fptype_priority() in madtrex.hpp/.cpp, + and madtrex.py's run_parameter_reweighting()). A "set FPTYPE ..." + appearing before the card's very first "launch" is special: it is taken as + the *default target* fptype, used by every launch that doesn't specify its + own "set FPTYPE" (see ParsedCard.baseline_fptype/group_by_fptype()). When + a card sets no FPTYPE at all (neither a baseline nor any per-launch + override), the default target -- like the always-on normaliser -- resolves + to the process directory's own run_card.toml [run] fptype (madtrex.py's + run_parameter_reweighting(), via group_by_fptype()'s default_fptype + parameter), falling back further to DEFAULT_FPTYPE only if that can't be + read either; this means a plain, non-FPTYPE-changing reweight (eg a bare + "set MT 200") correctly stays at whatever precision the sample was already + generated at, rather than silently drifting to a hardcoded default; +- "change