diff --git a/.github/workflows/acceptancetest.yml b/.github/workflows/acceptancetest.yml index 9884aec39..867658d74 100644 --- a/.github/workflows/acceptancetest.yml +++ b/.github/workflows/acceptancetest.yml @@ -491,6 +491,7 @@ jobs: cd $GITHUB_WORKSPACE ./tests/test_manager.py test_ML_check_brs_gd_gd -pA -t0 -l INFO ./tests/test_manager.py test_generate_output_semicolon_preserves_loop_process -pA -t0 -l INFO + ./tests/test_manager.py test_loop_nc_memo_not_poisoned_across_generations -pA -t0 -l INFO diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 3c6cbd6be..0f7c041a8 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -171,7 +171,7 @@ jobs: - name: test one of the test test_color_flow_string_epsilon test_colorize_funny_model test_colorize_uu_gg test_colorize_uux_ggg test_color_matrix_Nc_restrictions test_color_matrix_fixed_indices test_color_matrix_multi_gluons test_color_matrix_multi_quarks test_helper_lcm_functions test_get_s_and_t_ub_tdg test_diagram_list test_diagram_list_nice_string test_representation test_setget_diagram_correct test_setget_diagram_exceptions test_values_for_prop test_make_unique test_generating_dict_to_0 test_generating_dict_to_1 test_interaction_list test_representation test_setget_interaction_correct test_setget_interaction_exceptions test_values_for_prop test_leg_list run: | cd $GITHUB_WORKSPACE - ./tests/test_manager.py test_color_flow_string_epsilon test_colorize_funny_model test_colorize_uu_gg test_colorize_uux_ggg test_color_matrix_Nc_restrictions test_color_matrix_fixed_indices test_color_matrix_multi_gluons test_color_matrix_multi_quarks test_helper_lcm_functions test_get_s_and_t_ub_tdg test_diagram_list test_diagram_list_nice_string test_representation test_setget_diagram_correct test_setget_diagram_exceptions test_values_for_prop test_make_unique test_generating_dict_to_0 test_generating_dict_to_1 test_interaction_list test_representation test_setget_interaction_correct test_setget_interaction_exceptions test_values_for_prop test_leg_list -t0 + ./tests/test_manager.py test_color_flow_string_epsilon test_colorize_funny_model test_colorize_uu_gg test_colorize_uux_ggg test_color_matrix_Nc_restrictions test_color_matrix_fixed_indices test_color_matrix_multi_gluons test_color_matrix_multi_quarks test_helper_lcm_functions test_loop_Nc_power_survives_a_poisoned_memo test_memo_is_shared_across_loop_bases test_get_s_and_t_ub_tdg test_diagram_list test_diagram_list_nice_string test_representation test_setget_diagram_correct test_setget_diagram_exceptions test_values_for_prop test_make_unique test_generating_dict_to_0 test_generating_dict_to_1 test_interaction_list test_representation test_setget_interaction_correct test_setget_interaction_exceptions test_values_for_prop test_leg_list -t0 diff --git a/madgraph/core/color_amp.py b/madgraph/core/color_amp.py index 20850bde9..b64f05317 100755 --- a/madgraph/core/color_amp.py +++ b/madgraph/core/color_amp.py @@ -47,7 +47,10 @@ class ColorBasis(dict): diagram, coeff the corresponding coefficient (a fraction), is_imaginary if this contribution is real or complex, and Nc_power the Nc power.""" - # Dictionary to save simplifications already done in a canonical form + # Dictionary to save simplifications already done in a canonical form. + # Keyed on the canonical color string alone, so nothing stored here may + # depend on anything else (see loop_Nc_power in update_color_basis). + # LoopColorBasis shares this class-level dict process-wide. _canonical_dict = {} # Dictionary store the raw colorize information @@ -268,6 +271,9 @@ def update_color_basis(self, colorize_dict, index): # Remove overall coefficient for cs in canonical_col_fact: cs.coeff = cs.coeff / col_str.coeff + # loop_Nc_power is not a property of the canonical color + # structure, so it must not be stored in the memo. + cs.loop_Nc_power = 0 self._canonical_dict[canonical_rep] = canonical_col_fact else: # If this representation has already been considered, @@ -289,6 +295,11 @@ def update_color_basis(self, colorize_dict, index): # in case we have K6 or K6bar Clebsch Gordan coefficients for colstr in col_fact: colstr.order_summation() + # Restore loop_Nc_power, which the simplification above propagates + # unchanged from the incoming string but the memo does not carry. + for cs in col_fact: + cs.loop_Nc_power = col_str.loop_Nc_power + # loop over color strings in the resulting color factor for col_str in col_fact: immutable_col_str = col_str.to_immutable() diff --git a/madgraph/loop/loop_color_amp.py b/madgraph/loop/loop_color_amp.py index 9a7f49a62..96deb74e5 100755 --- a/madgraph/loop/loop_color_amp.py +++ b/madgraph/loop/loop_color_amp.py @@ -42,8 +42,13 @@ def __init__(self, compute_loop_nc = False): """ Defines the instance attribute compute_loop_nc. The compute_loop_nc sets wheter independent tracking of Nc power coming from the color loop trace is necessary or not (it is time consuming).""" - + self.compute_loop_nc = compute_loop_nc + self._list_color_dict = [] + # NB: ColorBasis.__init__ is deliberately not called, so that + # _canonical_dict stays the class-level (process-wide) memo. Loop color + # simplification is expensive and sharing it across matrix elements is + # what makes multi-subprocess loop generation tractable. def closeColorLoop(self, colorize_dict, lcut_charge, lcut_numbers): """ Add a color delta in the right representation (depending on the diff --git a/tests/acceptance_tests/test_cmd_madloop.py b/tests/acceptance_tests/test_cmd_madloop.py index a57767284..6a7b7b89f 100755 --- a/tests/acceptance_tests/test_cmd_madloop.py +++ b/tests/acceptance_tests/test_cmd_madloop.py @@ -14,6 +14,7 @@ ################################################################################ from __future__ import division from __future__ import absolute_import +import glob import subprocess import unittest import os @@ -200,6 +201,25 @@ def test_generate_output_semicolon_preserves_loop_process(self): self.assertNotIn('No processes generated', str(ctx.exception)) self.assertTrue(self.interface._curr_amps) + def test_loop_nc_memo_not_poisoned_across_generations(self): + """Two loop generations in one process. The first one runs with + compute_loop_nc=False; it must not pin loop_Nc_power in the + process-wide color memo for the loop-induced generation that follows, + which needs the real value to write coloramps.inc.""" + + self.do('import model loop_sm') + self.do('generate g g > h [sqrvirt=QCD]') + self.do('output standalone %s -f' % pjoin(self.tmpdir, 'sqrvirt')) + + self.do('generate g g > h [noborn=QCD]') + self.do('output madevent %s -f' % pjoin(self.tmpdir, 'noborn')) + + coloramps = glob.glob(pjoin(self.tmpdir, 'noborn', 'SubProcesses', + 'P*', 'coloramps.inc')) + self.assertTrue(coloramps) + for f in coloramps: + self.assertIn('ICOLAMP', open(f).read()) + def test_ML_check_full_epem_ttx(self): """ Test that check full e+ e- > t t~ works fine """ diff --git a/tests/unit_tests/core/test_color_amp.py b/tests/unit_tests/core/test_color_amp.py index 0926cae2c..a9123b441 100755 --- a/tests/unit_tests/core/test_color_amp.py +++ b/tests/unit_tests/core/test_color_amp.py @@ -27,6 +27,7 @@ import madgraph.core.color_amp as color_amp import madgraph.core.color_algebra as color +import madgraph.loop.loop_color_amp as loop_color_amp import tests.unit_tests as unittest class ColorAmpTest(unittest.TestCase): @@ -771,3 +772,45 @@ def test_helper_lcm_functions(self): self.assertEqual(color_amp.ColorMatrix.lcm(6, 3), 6) self.assertEqual(color_amp.ColorMatrix.lcmm(6, 3, 5, 2), 30) + +class LoopNcMemoTest(unittest.TestCase): + """The process-wide canonical color memo must not leak loop_Nc_power.""" + + def setUp(self): + self.saved = dict(color_amp.ColorBasis._canonical_dict) + color_amp.ColorBasis._canonical_dict.clear() + + def tearDown(self): + color_amp.ColorBasis._canonical_dict.clear() + color_amp.ColorBasis._canonical_dict.update(self.saved) + + @staticmethod + def _fill(compute_loop_nc, loop_Nc_power): + """Feed one T(-1,1,2)T(-1,3,4) chain through a fresh LoopColorBasis.""" + basis = loop_color_amp.LoopColorBasis(compute_loop_nc=compute_loop_nc) + col_str = color.ColorString([color.T(-1, 1, 2), color.T(-1, 3, 4)]) + col_str.loop_Nc_power = loop_Nc_power + basis.update_color_basis({(0,): col_str}, 0) + return basis + + def test_loop_Nc_power_survives_a_poisoned_memo(self): + """A compute_loop_nc=False basis must not pin loop_Nc_power for the + next basis: the memo is keyed on the canonical color string only.""" + + # A [sqrvirt=]/standalone-style generation: loop_Nc_power is None. + self._fill(False, None) + # A later loop-induced madevent generation asks for the real value. + basis = self._fill(True, 1) + + powers = [entry[5] for entries in basis.values() for entry in entries] + self.assertTrue(powers) + self.assertEqual(powers, [1] * len(powers)) + + def test_memo_is_shared_across_loop_bases(self): + """The memo must stay process-wide: that sharing is what makes + multi-subprocess loop generation tractable.""" + + self._fill(True, 1) + self.assertTrue(color_amp.ColorBasis._canonical_dict) + self.assertTrue(self._fill(True, 1)._canonical_dict is + color_amp.ColorBasis._canonical_dict)