diff --git a/.github/workflows/acceptancetest.yml b/.github/workflows/acceptancetest.yml index af0594b40..2bc1785cd 100644 --- a/.github/workflows/acceptancetest.yml +++ b/.github/workflows/acceptancetest.yml @@ -1484,3 +1484,21 @@ jobs: run: | cd $GITHUB_WORKSPACE ./tests/test_manager.py test_madspin_loop_induced -pA -t0 -l INFO + + acceptancetest_107: + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork == true + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/checkout_mg5 + - uses: ./.github/actions/restore-pip-cache + - uses: ./.github/actions/restore_all + + # Loop-induced with a polarised massive particle. Loop-induced goes + # through the LO madevent template, so this checks the LO me_frame boost + # reaches the MadLoop matrix element. + - name: test one of the test test_polarised_loop_induced_me_frame + run: | + cd $GITHUB_WORKSPACE + ./tests/test_manager.py test_polarised_loop_induced_me_frame -pA -t0 -l INFO + ./tests/test_manager.py test_polarised_loop_induced_me_frame_last_leg -pA -t0 -l INFO diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 3c6cbd6be..f83d5482d 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -60,10 +60,10 @@ jobs: - uses: actions/checkout@v5 # Runs a set of commands using the runners shell - - name: test one of the test test_ungrouping_lepton test_all_permutation test_permutation_from_id test_v31_syntax_crash test_the_exit_from_child_cmd test_the_exit_from_child_cmd2 test_get_final_part test_InvalidCmd test_check_generate test_cleaning_history test_generate_polarised test_help_category test_output_default test_shell_and_continuation_line test_simple_generate test_card_type_recognition test_help_category test_autocompletion test_modif_ML_card test_modif_madweight_card test_modif_param_card test_modif_pythia8_card test_modif_run_card test_modif_shower_card test_modif_reweight_card + - name: test one of the test test_ungrouping_lepton test_all_permutation test_permutation_from_id test_v31_syntax_crash test_the_exit_from_child_cmd test_the_exit_from_child_cmd2 test_get_final_part test_InvalidCmd test_check_generate test_polarisation_nlo_regimes test_cleaning_history test_generate_polarised test_help_category test_output_default test_shell_and_continuation_line test_simple_generate test_card_type_recognition test_help_category test_autocompletion test_modif_ML_card test_modif_madweight_card test_modif_param_card test_modif_pythia8_card test_modif_run_card test_modif_shower_card test_modif_reweight_card run: | cd $GITHUB_WORKSPACE - ./tests/test_manager.py test_ungrouping_lepton test_all_permutation test_permutation_from_id test_v31_syntax_crash test_the_exit_from_child_cmd test_the_exit_from_child_cmd2 test_get_final_part test_InvalidCmd test_check_generate test_cleaning_history test_generate_polarised test_help_category test_output_default test_shell_and_continuation_line test_simple_generate test_card_type_recognition test_help_category test_autocompletion test_modif_ML_card test_modif_madweight_card test_modif_param_card test_modif_pythia8_card test_modif_run_card test_modif_shower_card test_modif_reweight_card -t0 + ./tests/test_manager.py test_ungrouping_lepton test_all_permutation test_permutation_from_id test_v31_syntax_crash test_the_exit_from_child_cmd test_the_exit_from_child_cmd2 test_get_final_part test_InvalidCmd test_check_generate test_polarisation_nlo_regimes test_cleaning_history test_generate_polarised test_help_category test_output_default test_shell_and_continuation_line test_simple_generate test_card_type_recognition test_help_category test_autocompletion test_modif_ML_card test_modif_madweight_card test_modif_param_card test_modif_pythia8_card test_modif_run_card test_modif_shower_card test_modif_reweight_card -t0 diff --git a/docs/nlo_polarisation_boost_plan.md b/docs/nlo_polarisation_boost_plan.md index eeec67f7b..3279ff9ac 100644 --- a/docs/nlo_polarisation_boost_plan.md +++ b/docs/nlo_polarisation_boost_plan.md @@ -18,11 +18,14 @@ Status: | M3 `[QCD]` | **done** — `check_poles` cancels 20/20 in every P dir, `calculate_xsect NLO` runs end to end, `[QCD]` enabled | | M4 | **done** — guard open, `help_polarization` rewritten, acceptance test written, run and wired into CI | | M5 NLO+PS | **done** — MC counterterm azimuth boosted, `generate_events` validated, second acceptance test in CI | +| M6 loop-induced | **done** — `[noborn=…]`/`[sqrvirt=…]` opened; the LO boost was already in the generated code, verified at runtime. Uncovered a MadLoop `improve_ps` bug on a last-leg `me_frame`, fixed by PR #91 and now covered by a test | -`[LOonly=QCD]`, `[real=QCD]`, `[QCD]` and `[virt=…]` all accept a polarised -massive particle today. The first three get their frame from `me_frame` in the -run_card; `[virt=…]` is standalone MadLoop, where the user supplies the -phase-space point and therefore picks the frame themselves (see M3). +`[LOonly=QCD]`, `[real=QCD]`, `[QCD]`, `[virt=…]`, `[noborn=…]` and +`[sqrvirt=…]` all accept a polarised massive particle today. The first three +get their frame from `me_frame` in the run_card; `[virt=…]` is standalone +MadLoop, where the user supplies the phase-space point and therefore picks the +frame themselves (see M3); the loop-induced pair is exported through the LO +madevent template and uses `me_frame` exactly as at LO (see M6). **NLO+PS works too**, not only fixed order: `generate_events` on `p p > z{0} j [QCD]` with `me_frame=[3]` gives 2.189e+03 +- 8.2e+00 pb with all @@ -59,6 +62,9 @@ The live NLO polarisation path is massless-colourless loop-induced only, which is exported through the **LO** madevent template and therefore inherits `me_frame` for free. That is why an NLO boost was never needed. +(The claim in that last sentence is now verified at runtime, and the massive +refusal in the loop-induced modes is lifted — see M6.) + ### Generation is alive and correct `FKSLeg` inherits `Leg`, so `polarization` survives; `fks_common.py:747` @@ -1083,11 +1089,10 @@ Still refused, each for a reason rather than by omission: links, `sreal_deg`) go through the same `sborn_frame`/`sborn_sf_frame`, so this is likely to work — but nothing in the QED sector has been run, and the plan does not cover it. `[virt=QED]` is fine, being standalone. -- `[noborn=QCD]` with a *massive* polarised particle. Pre-existing and - unchanged: loop-induced is exported through the LO madevent template and so - already has `me_frame`, but `check()` has always rejected massive there. - Worth revisiting; not touched here because nothing in this work exercises - it. See section 1. + +`[noborn=…]` / `[sqrvirt=…]` (loop-induced) with a massive polarised particle +was on this list. It has since been checked end to end and **opened** — see +M6. `tests/unit_tests/interface/test_cmd.py::test_check_generate` encoded the old restriction (`'u u~ > w+{L} [QCD]'` in the *invalid* list) and is updated: the @@ -1159,7 +1164,7 @@ Remaining: - decide whether `[noborn=QCD]` should accept a massive polarised particle. It is loop-induced, exported through the LO madevent template, so it has `me_frame` already; `check()` has always refused massive there and nothing - in this work exercises it. + in this work exercises it. **Done in M6.** ### M5 — NLO+PS (MC@NLO matching) — **DONE** @@ -1399,6 +1404,176 @@ unrelated processes and is its own piece of work. It is recorded because it explains why `test_MC` passed on the broken code, and why the M5 validation had to be built by hand. +### M6 — Loop-induced: a massive polarised particle — **DONE** + +The verdict is **omission, not limitation**. `[noborn=…]` and `[sqrvirt=…]` +now accept a polarised massive particle, for any perturbation order. + +Section 1 claimed loop-induced already had the frame because it is exported +through the LO madevent template. That was checked end to end rather than +assumed, on `g g > z{0} z{0} [noborn=QCD]` with `me_frame=[3]` (the first Z at +rest -- the one-leg selection, which is the case that needs the M2 fix). + +**The generated code really boosts.** `SubProcesses/P0_gg_z0z0/auto_dsig1.f` +carries the LO wrapper unchanged: + +```fortran + IF(FRAME_ID.NE.6)THEN + CALL BOOST_TO_FRAME(PP, FRAME_ID, P1) + ELSE + P1 = PP + ENDIF + ... + CALL SMATRIX1(P1, IFLAV, RHEL, RCOL, CHANNEL, 1, DSIGUU, ...) +``` + +and `SMATRIX1` hands `P1` straight to `ML5_..._SLOOPMATRIX_THRES`. The +`frame` block appears in the generated LO run_card, and `Source/run_card.inc` +comes out with `FRAME_ID = 8`, i.e. bit 3, the first Z. The boost routine is +`Template/LO/SubProcesses/genps.f`'s `boost_to_frame` — the same one M2 fixed, +so the fix is inherited by construction rather than ported. + +**The quantisation axis survives MadLoop** (on `me_frame=[3]`; `[4]` needed +PR #91, see below). This was the one thing that could +not be settled by reading: `improve_ps` deforms the PS point before the loop is +evaluated (`ImprovePSPoint=2` by default), and moving the deliberately-zeroed +leg off zero is precisely what flips the HELAS `vxxxxx` branch. Instrumented +the `gg > z{0} z{0}` MadLoop dir's `loop_matrix.f` to print the selected leg's +`|p|` and energy immediately before and after `IMPROVE_PS_POINT_PRECISION`, +over a full survey (256 prints, 16 distinct phase-space points): `0.0` exactly +in and `0.0` exactly out, every point. + +**History: it did not, for a frame built on the last external leg.** The +milestone was written on `me_frame=[3]` and the property held; `me_frame=[4]` +broke it, and the two are the same observable because the two Z are identical: + +| `me_frame` | `\|p\|` in | `\|p\|` out | cross-section | +|---|---|---|---| +| `[3]` | `0.0` exactly | `0.0` exactly | 2.543e-02 pb | +| `[4]` | `0.0` exactly | **`1.5888e-14`** | 3.211e-02 pb, +26% | + +An LO control on the same final state gave `[3]` and `[4]` bit-identical, so +the split was a MadLoop artefact, not physics. **Fixed by PR #91** ("MadLoop: +keep an external leg exactly at rest through improve_ps", merged to `main` +2026-09-03): the momentum-conservation residual is absorbed into the +largest-`|p|` final-state leg (`IABSORB`) instead of `NEXTERNAL`. That went on +its own branch because `improve_ps` is shared by every MadLoop output. +`test_polarised_loop_induced_me_frame_last_leg` is the regression and now runs +unskipped. + +The reasoning that first talked this section into "the zero always survives" +was wrong in three places, all worth recording: + +- **PSMC does not merely rescale.** Its *first* step overwrote + `NEWP(1:3,NEXTERNAL)` with the momentum-conservation residual, + unconditionally, before any rescaling, so a frame built on the last external + leg was hit before the argument about common factors applied. This is the + line PR #91 changed. +- **The ORIG half of the argument is vacuous in a boosted frame.** ORIG sets + `ERRCODE=200` as soon as the initial state has transverse momentum and + `GOTO 100`s out (`improve_ps.inc:540`) without touching `P`. Its onshellness + step never runs, so what it would have done to an exact zero is irrelevant. +- **There is a second entry point.** `SET_MP_PS(P_USER)` + (`loop/loop_matrix_standalone.inc:634`, + `loop_optimized/…:954`) re-improves the *raw* momenta in quad on the QP + escalation path, so a point can be improved twice by two different routes. + +**The `NRotations` trap.** `ROTATE_PS` (`improve_ps.inc:382`) is an axis +permutation with sign flips and no arithmetic, so it preserves exact zeros. +That is not enough: a rotation is not a symmetry of a polarised amplitude with +a leg at rest, because HELAS pins that leg's axis to the frame z rather than +to the momentum. `NRotations_DP`/`NRotations_QP` default to 0, so the shipped +path is safe — but they are a documented remedy for suspicious processes, and +a user who turns them on here gets spurious instability flags and a wrongly +"rescued" answer. + +**Note for whoever merges this with the loop-induced pole check** +(`claude/loop-induced-pole-check`). Two costs land on the same runs and +neither is a bug: + +- a non-longitudinal `me_frame` boost gives the initial state transverse + momentum, and `improve_ps`'s ORIG algorithm refuses any such point + (`ERRCODE=200`), so **every** call falls through to the PSMC algorithm. The + log fills with "Attempting to rescue the precision improvement with an + alternative method", capped at 20 lines. It is not a failure and not a + precision downgrade: `IMPROVE_PS_POINT_PRECISION` promotes to `REAL*16` and + calls the MP routine unconditionally, so both algorithms were always quad — + `ERRCODE=200` is an algorithm switch, costing 12.3 microseconds per call + against millisecond-scale loop evaluations, under 1%. The lesson is the + other way round: calling it noise is what left the PSMC branch unread, and + PSMC is where the last-leg bug above lives. +- the pole check re-enables `COLLIERComputeUVpoles`/`COLLIERComputeIRpoles` + for loop-induced, which were previously off for speed. + +A polarised loop-induced run pays both. + +Unrelated but adjacent: `output standalone` on a loop-induced process is being +fixed on `claude/fix-loop-induced-standalone-flavor` (off `main`) -- +`_flavor_enumeration_context` in `helas_objects.py` counted the L-cut +wavefunctions as external legs. Nothing here depends on it; the madevent +output this milestone uses was never affected. + +**Cross-section, and the frame really moves it.** `g g > z{0} z{0} +[noborn=QCD]`, shipped run card except `nevents=100`, `use_syst=F` and +`me_frame`: + +| `me_frame` | cross-section | +|---|---| +| `[3]` (first Z at rest) | **2.534e-02 +- 5.0e-05 pb** | +| `[1,2]` (partonic c.m., boost skipped) | 5.805e-02 +- 4.8e-04 pb | + +A factor 2.3. That gap is what the acceptance test asserts on: a boost that +were silently skipped could not reproduce the first number (it sits 62 sigma +out). + +`me_frame=[4]` must give the same number as `[3]`, the two Z being identical, +and now does. Measured on top of PR #91, same card, one run each: +**2.543e-02 +- 9.9e-05 pb** from both, with `FRAME_ID = 16` and `FRAME_ID = 8` +in their respective `run_card.inc` and **byte-identical `results.dat`**. Before +PR #91 the same pair gave 2.543e-02 against 3.211e-02. + +Re-measured on top of M5 (`montecarlocounter.f`, `boost_to_frame.f`), where +nothing should move because loop-induced goes through the LO madevent template +and never reaches an FKS counterterm: 2.543e-02 +- 9.9e-05 pb, 0.8 sigma from +the row above -- and bit-identical to the same fresh generation on the pole- +check branch, both having started from the same `randinit`. The two numbers in +the table differ only by seed offset (24 vs 21). + +**Cross-checked against the loop-induced pole check** (`MLPoleCheckThres`, on +`claude/loop-induced-pole-check`). That work and this one are orthogonal: it +adds a runtime guard, this one adds a spelling, and neither can stand in for +the other. In particular the pole check cannot see a frame error -- a +loop-induced amplitude is finite helicity by helicity and a boost is a +symmetry of it, so the poles cancel in every frame, including the wrong one. +Nor does polarisation trip it: the same argument makes `ANS(2,0)`/`ANS(3,0)` +shrink with `ANS(1,0)` when the helicity sum is restricted. Measured, with the +two changes merged and the check active at its default `1.0d-2`: +2.543e-02 +- 9.9e-05 pb, zero `##E02`, 0.8 sigma from the number above. + +**What is not touched.** The QCD-only restriction on the run_card NLO modes +stays; it does not apply to loop-induced, which has no subtraction and +therefore no order-dependent counterterms to validate. + +**Colour is not refused here either.** The colour-charged refusal is scoped to +the subtracted regime alone (`madgraph_interface.py`, the `if +subtracted_boost_ok:` branch), a decision taken on the base branch in +`ee63ba492`: a coloured polarised leg is also an FKS emitter, which is what is +untested, and loop-induced has no FKS. `g{L} g > z z [noborn=QCD]` and +`g g > t{L} t~ [noborn=QCD]` are therefore accepted, and the unit test asserts +so. Nothing in the LO path cares: `boost_to_frame` is purely kinematic and +`SMATRIX1` hands the boosted momenta straight to MadLoop. It is a *format* +decision only — no coloured polarised loop-induced process has been run. + +**Write `g g`, not `p p`.** `p p > z z [noborn=QCD]` -- the spelling section 1 +uses -- is not a physical request: `q q~ > z z` has a tree-level Born, so +"no Born" is contradictory for those channels. It generates, and then the +quark-initiated jobs die at runtime (`STOP energy is not conserved +(flag:CT692)`, and the survey then aborts on the missing `results.dat`). +That is correct behaviour, not a bug: it reproduces identically on the +**unpolarised** process, in the same four G dirs, and has nothing to do with +polarisation. The loop-induced process to write is `g g > z{0} z{0} +[noborn=QCD]`. + ## 4. Acceptance test One new test in `tests/acceptance_tests/test_cmd_amcatnlo.py`, modelled on @@ -1418,6 +1593,8 @@ does it. | M3 | `p p > z{0} z{0} j` | `[QCD]` | **run**: `check_poles` 20/20 in all 12 P dirs, 4.779e-01 +- 3.3e-03 pb | | M5 | `p p > z{0} j` | `[QCD]` + `generate_events` | **run**: 2.189e+03 +- 8.2e+00 pb, absolute cross-section ratio 2.00 (2.23 before the fix) | | M5 | `p p > z{0} z{0} j` | `[QCD]` + `generate_events`, `me_frame=[3,4]` | **run**: 3.134e-01 +- 2.2e-03 pb, `test_ME`/`test_MC`/`check_poles` 20/20 in all 12 P dirs | +| M6 | `g g > z{0} z{0}` | `[noborn=QCD]`, `me_frame=[3]` | **run**: 2.534e-02 +- 5.0e-05 pb, against 5.805e-02 +- 4.8e-04 pb in the partonic c.m. | +| M6 | `g g > z{0} z{0}` | `[noborn=QCD]`, `me_frame=[4]` | **run**: 2.543e-02 +- 9.9e-05 pb, byte-identical `results.dat` to `me_frame=[3]` (was 3.211e-02 before PR #91) | **Which process tests what.** The two are complementary, but not in the obvious way: diff --git a/madgraph/interface/madgraph_interface.py b/madgraph/interface/madgraph_interface.py index 92e3227c1..c2832bf99 100755 --- a/madgraph/interface/madgraph_interface.py +++ b/madgraph/interface/madgraph_interface.py @@ -823,8 +823,14 @@ def help_polarization(self): logger.info(" validated, so [QED] and mixed orders are still refused.") logger.info(" - [virt=QCD] and standalone MadLoop take the frame from the momenta the") logger.info(" caller supplies, so any perturbation order is allowed.") + logger.info(" - loop-induced is not restricted at all: [noborn=...] is exported") + logger.info(" through the LO madevent template and uses 'me_frame' exactly as at") + logger.info(" LO, [sqrvirt=...] squares one amplitude standalone.") logger.info(" A polarized massless particle needs no frame and was always allowed.") + logger.info(" > A colour-charged polarized particle is refused in the [QCD]/[real=]/") + logger.info(" [LOonly=] modes only: it is also an FKS emitter, and that is untested.") logger.info(" > Example: generate p p > z{0} z{0} j [QCD] with me_frame = [3,4]",'$MG:color:GREEN') + logger.info(" > Example: generate g g > z{0} z{0} [noborn=QCD] with me_frame = [3]",'$MG:color:GREEN') logger.info(" > Both fixed-order (calculate_xsect) and NLO+PS event generation") logger.info(" (generate_events, i.e. MC@NLO matching) are supported: the MC") logger.info(" counterterms and the weights written to the LHE use the same frame as") diff --git a/tests/acceptance_tests/test_cmd_madevent.py b/tests/acceptance_tests/test_cmd_madevent.py index 7287e4d20..d2381026d 100755 --- a/tests/acceptance_tests/test_cmd_madevent.py +++ b/tests/acceptance_tests/test_cmd_madevent.py @@ -3657,7 +3657,7 @@ def test_loop_induced_ggh(self): '%s/Cards/run_card_default.dat'% self.run_dir) cmd.run_cmd('launch -f') - + self.check_parton_output(cross=15.73, error=0.04) def _get_delphes_path(self): @@ -3785,6 +3785,80 @@ def read(path): self.assertAlmostEqual(w_fused, w_single, delta=1e-6 * abs(w_single) + 1e-30) + def test_polarised_loop_induced_me_frame(self): + """Loop-induced with a polarised massive particle, in a chosen frame. + + g g > z{0} z{0} [noborn=QCD] with me_frame = [3]: the matrix element is + evaluated with the first Z at rest. Loop-induced is exported through + the LO madevent template, so it uses the LO boost in auto_dsig and no + NLO frame machinery is involved; this test is what says so. + + The cross-section is the assertion that the frame actually reached the + matrix element. Measured with this same card: 2.534e-02 +- 5.0e-05 pb + in the Z rest frame against 5.805e-02 +- 4.8e-04 pb in the partonic + c.m. (me_frame = [1,2]), a factor 2.3 apart, so a silently skipped + boost cannot pass. + """ + cmd = MGCmd.MasterCmd() + cmd.no_notification() + cmd.run_cmd('set automatic_html_opening False --no_save') + cmd.run_cmd('import model loop_sm') + cmd.run_cmd('generate g g > z{0} z{0} [noborn=QCD]') + cmd.run_cmd('output madevent %s -f' % self.run_dir) + + card_path = pjoin(self.run_dir, 'Cards', 'run_card.dat') + run_card = banner.RunCardLO(card_path) + run_card.set('me_frame', [3], user=True) + run_card.set('nevents', 100, user=True) + run_card.set('use_syst', False, user=True) + run_card.write(card_path, + pjoin(self.run_dir, 'Cards', 'run_card_default.dat')) + + cmd.run_cmd('launch -f') + + # the frame has to survive into the compiled code: bit 3 of frame_id. + self.assertIn('FRAME_ID = 8', + open(pjoin(self.run_dir, 'Source', 'run_card.inc')).read()) + # ... and the generated wrapper has to boost with it before the ME call. + dsig = misc.glob(pjoin(self.run_dir, 'SubProcesses', 'P*', 'auto_dsig1.f')) + self.assertTrue(dsig, 'no auto_dsig1.f was generated') + text = open(dsig[0]).read() + self.assertIn('CALL BOOST_TO_FRAME(PP, FRAME_ID, P1)', text) + + self.check_parton_output(cross=0.02534, error=5e-4) + + def test_polarised_loop_induced_me_frame_last_leg(self): + """me_frame = [4] must give the same cross-section as me_frame = [3]. + + The two Z are identical, so the rest frame of one and the rest frame of + the other are the same observable. This used to fail: improve_ps + absorbed the momentum-conservation residual into leg NEXTERNAL, so a + frame built on the last leg did not stay exactly at rest and HELAS + picked a different quantisation axis -- 3.211e-02 pb against 2.543e-02 + at [3]. Fixed by PR #91, which absorbs the residual in the largest + final-state leg instead. + """ + cmd = MGCmd.MasterCmd() + cmd.no_notification() + cmd.run_cmd('set automatic_html_opening False --no_save') + cmd.run_cmd('import model loop_sm') + cmd.run_cmd('generate g g > z{0} z{0} [noborn=QCD]') + cmd.run_cmd('output madevent %s -f' % self.run_dir) + + card_path = pjoin(self.run_dir, 'Cards', 'run_card.dat') + run_card = banner.RunCardLO(card_path) + run_card.set('me_frame', [4], user=True) + run_card.set('nevents', 100, user=True) + run_card.set('use_syst', False, user=True) + run_card.write(card_path, + pjoin(self.run_dir, 'Cards', 'run_card_default.dat')) + + cmd.run_cmd('launch -f') + + self.assertIn('FRAME_ID = 16', + open(pjoin(self.run_dir, 'Source', 'run_card.inc')).read()) + self.check_parton_output(cross=0.02534, error=5e-4) + #=============================================================================== # TestCmd diff --git a/tests/unit_tests/interface/test_cmd.py b/tests/unit_tests/interface/test_cmd.py index acba85118..837cc4236 100755 --- a/tests/unit_tests/interface/test_cmd.py +++ b/tests/unit_tests/interface/test_cmd.py @@ -313,7 +313,14 @@ def test_check_generate(self): # theirs and the perturbation orders do not matter cmd.check_process_format('u u~ > z{0} g [virt=QCD]') cmd.check_process_format('u u~ > z{0} g [virt=QED QCD]') - + # loop-induced: 'noborn' goes through the LO madevent template and uses + # me_frame as at LO, 'sqrvirt' squares one amplitude standalone. No NLO + # restriction applies -- any orders, and colour is not refused either. + cmd.check_process_format('g g > z{0} z{0} [noborn=QCD]') + cmd.check_process_format('g g > z{0} z{0} [sqrvirt=QCD]') + cmd.check_process_format('g g > z{0} z{0} [noborn=QED QCD]') + cmd.check_process_format('g{L} g > z z [noborn=QCD]') + # unvalid syntax self.wrong(cmd.check_process_format, ' e+ e-') self.wrong(cmd.check_process_format, ' e+ e- > e+ e-,')