99from numpy .testing import assert_allclose
1010from validphys .fkparser import load_fktable
1111
12+ import numpy as np
13+
14+ from validphys .api import API as vpAPI
15+ from validphys .convolution import central_predictions
16+
1217from colibri .api import API as colibriAPI
1318from colibri .tests .conftest import (
1419 CLOSURE_TEST_PDFSET ,
1520 TEST_DATASET ,
1621 TEST_DATASET_HAD ,
1722 TEST_DATASETS ,
23+ TEST_DATASETS_DIS_HAD ,
1824 TEST_DATASETS_HAD ,
1925)
2026from colibri .theory_predictions import (
@@ -30,26 +36,12 @@ def __init__(self, xgrid):
3036 self .xgrid = xgrid
3137
3238
33- def test_fktable_xgrid_indices_fill_with_zeros ():
34- # Case where fill_fk_xgrid_with_zeros is True
35- fktable = FKTableDataMock (xgrid = jnp .array ([0.1 , 0.2 , 0.3 ]))
36- FIT_XGRID = jnp .array ([0.05 , 0.1 , 0.15 , 0.2 , 0.25 , 0.3 ])
37-
38- expected_indices = jnp .arange (
39- len (FIT_XGRID )
40- ) # Should return indices for the entire FIT_XGRID
41- result = fktable_xgrid_indices (fktable , FIT_XGRID , fill_fk_xgrid_with_zeros = True )
42-
43- assert jnp .array_equal (result , expected_indices )
44-
45-
46- def test_fktable_xgrid_indices_no_fill ():
47- # Case where fill_fk_xgrid_with_zeros is False
39+ def test_fktable_xgrid_indices ():
4840 fktable = FKTableDataMock (xgrid = jnp .array ([0.1 , 0.2 , 0.3 ]))
4941 FIT_XGRID = jnp .array ([0.05 , 0.1 , 0.15 , 0.2 , 0.25 , 0.3 ])
5042
5143 expected_indices = jnp .array ([1 , 3 , 5 ]) # Indices where fk_xgrid matches FIT_XGRID
52- result = fktable_xgrid_indices (fktable , FIT_XGRID , fill_fk_xgrid_with_zeros = False )
44+ result = fktable_xgrid_indices (fktable , FIT_XGRID )
5345
5446 assert jnp .array_equal (result , expected_indices )
5547
@@ -61,7 +53,7 @@ def test_fktable_xgrid_indices_with_tolerance():
6153
6254 # Due to tolerance, the indices should match as if they were the same
6355 expected_indices = jnp .array ([1 , 3 , 5 ])
64- result = fktable_xgrid_indices (fktable , FIT_XGRID , fill_fk_xgrid_with_zeros = False )
56+ result = fktable_xgrid_indices (fktable , FIT_XGRID )
6557
6658 assert jnp .array_equal (result , expected_indices )
6759
@@ -74,7 +66,7 @@ def test_fktable_xgrid_indices_no_matches():
7466 expected_indices = jnp .array (
7567 []
7668 ) # No matching indices, closest_indices returns empty array
77- result = fktable_xgrid_indices (fktable , FIT_XGRID , fill_fk_xgrid_with_zeros = False )
69+ result = fktable_xgrid_indices (fktable , FIT_XGRID )
7870 assert jnp .array_equal (result , expected_indices )
7971
8072
@@ -122,6 +114,49 @@ def test_fast_kernel_arrays():
122114 assert jnp .any (fk_arrays_filled [0 ][0 ][:, :, non_zero_indices ] != 0 )
123115
124116
117+ def test_fast_kernel_arrays_hadronic_fill_with_zeros ():
118+ """
119+ Test that fast_kernel_arrays correctly fills the x-grid with zeros for hadronic FK tables.
120+ This is a regression test for the bug where the 4D hadronic array was assigned
121+ into a 3D zeros array.
122+ """
123+ from colibri .utils import closest_indices
124+ from validphys .fkparser import load_fktable
125+
126+ dataset = colibriAPI .data (** TEST_DATASETS_HAD )
127+ ds = dataset .datasets [0 ]
128+ FIT_XGRID = colibriAPI .FIT_XGRID (** TEST_DATASETS_HAD )
129+
130+ # This should not raise an error (regression check)
131+ fk_arrays_filled = colibriAPI .fast_kernel_arrays (
132+ ** {** TEST_DATASETS_HAD , "fill_fk_xgrid_with_zeros" : True }
133+ )
134+
135+ fk_arr = fk_arrays_filled [0 ][0 ]
136+
137+ # Hadronic FK array should be 4D: (Ndat, Nfl, Nfit_x, Nfit_x)
138+ assert fk_arr .ndim == 4
139+ assert fk_arr .shape [2 ] == len (FIT_XGRID )
140+ assert fk_arr .shape [3 ] == len (FIT_XGRID )
141+
142+ # Check that non-zero values are placed at the correct x-grid positions
143+ fk_xgrid = load_fktable (ds .fkspecs [0 ]).xgrid
144+ non_zero_indices = closest_indices (FIT_XGRID , fk_xgrid , atol = 1e-8 )
145+ non_zero_indices = np .array (non_zero_indices )
146+
147+ # The non-zero block should contain non-zero values
148+ assert jnp .any (
149+ fk_arr [:, :, non_zero_indices [:, None ], non_zero_indices [None , :]] != 0
150+ )
151+
152+ # Entries outside the non-zero block should be zero
153+ all_indices = np .arange (len (FIT_XGRID ))
154+ zero_indices = np .setdiff1d (all_indices , non_zero_indices )
155+ if len (zero_indices ) > 0 :
156+ assert jnp .all (fk_arr [:, :, zero_indices , :] == 0 )
157+ assert jnp .all (fk_arr [:, :, :, zero_indices ] == 0 )
158+
159+
125160def test_make_dis_prediction ():
126161 """
127162 Test make_dis_prediction function gives the same results
@@ -198,3 +233,54 @@ def test_make_pred_data():
198233
199234 assert callable (eval_preds )
200235 assert pred_data .shape == (fk_arrs [0 ][0 ].shape [0 ],)
236+
237+
238+ def test_predictions_independent_of_fill_fk_xgrid_with_zeros ():
239+ """
240+ Regression test: ``fill_fk_xgrid_with_zeros`` is a memory/layout option and
241+ must not change the theory predictions.
242+
243+ Previously the prediction closures unconditionally indexed the FK array with
244+ ``fktable_xgrid_indices``, which are indices into FIT_XGRID. That is only
245+ valid for zero-padded FK arrays; with ``fill_fk_xgrid_with_zeros=False`` the
246+ FK x-grid axis was silently re-shuffled and out-of-range indices were clamped
247+ by jax, giving wrong predictions.
248+
249+ Uses a mixed DIS + hadronic setup so that both convolution paths are covered
250+ and FIT_XGRID is a strict superset of at least one FK x-grid (otherwise the
251+ index mapping is the identity and the bug is invisible).
252+ """
253+ data = colibriAPI .data (** TEST_DATASETS_DIS_HAD )
254+ FIT_XGRID = colibriAPI .FIT_XGRID (** TEST_DATASETS_DIS_HAD )
255+
256+ # Guard: the setup must actually exercise a non-trivial x-grid mapping.
257+ mappings = [
258+ fktable_xgrid_indices (load_fktable (ds .fkspecs [0 ]).with_cuts (ds .cuts ), FIT_XGRID )
259+ for ds in data .datasets
260+ ]
261+ assert any (
262+ not jnp .array_equal (idx , jnp .arange (len (idx ))) for idx in mappings
263+ ), "test setup is degenerate: every FK x-grid maps onto FIT_XGRID as the identity"
264+
265+ pdf_grid = colibriAPI .closure_test_central_pdf_grid (
266+ ** {** CLOSURE_TEST_PDFSET , ** TEST_DATASETS_DIS_HAD }
267+ )
268+
269+ preds = {}
270+ for fill in (False , True ):
271+ inp = {** TEST_DATASETS_DIS_HAD , "fill_fk_xgrid_with_zeros" : fill }
272+ eval_preds = colibriAPI .make_pred_data (** inp )
273+ preds [fill ] = eval_preds (pdf_grid , colibriAPI .fast_kernel_arrays (** inp ))
274+
275+ assert_allclose (preds [False ], preds [True ], rtol = 1e-6 )
276+
277+ # Cross-check both against validphys, which is the ground truth here.
278+ # NOTE: use central_predictions rather than dataset_inputs_results: the latter
279+ # convolves every replica of the PDF set (and makes LHAPDF load all 101 members)
280+ # only for its central value to be taken, which is ~200x slower here.
281+ pdf = vpAPI .pdf (pdf = CLOSURE_TEST_PDFSET ["closure_test_pdf" ])
282+ vp_preds = np .concatenate (
283+ [np .array (central_predictions (ds , pdf )).ravel () for ds in data .datasets ]
284+ )
285+ assert_allclose (preds [False ], vp_preds , rtol = 1e-6 )
286+ assert_allclose (preds [True ], vp_preds , rtol = 1e-6 )
0 commit comments