@@ -308,3 +308,229 @@ def test_seed_loading():
308308 assert len (gammas ) == 5
309309 assert len (betas ) == 5
310310 assert "results" in source
311+
312+
313+ # ============================================================
314+ # FD helper for gradient accuracy tests
315+ # ============================================================
316+
317+ _GRAD_CASES = [(2 , 3 , 1 ), (2 , 3 , 2 ), (3 , 2 , 1 ), (3 , 3 , 1 )]
318+
319+
320+ def _fd_grad (contract_fn , gammas , betas , p , D , k = 2 , h = 1e-5 ):
321+ """Numerical central finite-difference gradient of contract_fn."""
322+ gammas = np .asarray (gammas , dtype = float )
323+ betas = np .asarray (betas , dtype = float )
324+ grad_g = np .empty (p )
325+ grad_b = np .empty (p )
326+ for i in range (p ):
327+ g_p = gammas .copy ()
328+ g_p [i ] += h
329+ g_m = gammas .copy ()
330+ g_m [i ] -= h
331+ grad_g [i ] = (contract_fn (g_p , betas , p , D , k ) - contract_fn (g_m , betas , p , D , k )) / (2 * h )
332+ for i in range (p ):
333+ b_p = betas .copy ()
334+ b_p [i ] += h
335+ b_m = betas .copy ()
336+ b_m [i ] -= h
337+ grad_b [i ] = (contract_fn (gammas , b_p , p , D , k ) - contract_fn (gammas , b_m , p , D , k )) / (2 * h )
338+ return grad_g , grad_b
339+
340+
341+ # ============================================================
342+ # contract.py dispatcher tests
343+ # ============================================================
344+
345+
346+ @pytest .mark .skipif (not (HAS_CPP or HAS_JAX ), reason = "No backend available" )
347+ @pytest .mark .parametrize ("k,D,p" , CASES [:6 ])
348+ def test_dispatcher_contract_symmetric_tree (k , D , p ):
349+ """Dispatcher contract_symmetric_tree auto-selects a backend and returns a valid float."""
350+ from qokit .max_k_xor_sat .contract import contract_symmetric_tree as disp_contract
351+
352+ gammas , betas = _params (p )
353+ val = disp_contract (gammas , betas , p , D , k )
354+ assert isinstance (val , float )
355+ assert - 1.0 <= val <= 1.0
356+
357+
358+ @pytest .mark .skipif (not HAS_CPP , reason = "C++ backend not built" )
359+ def test_dispatcher_contract_symmetric_tree_cpp ():
360+ """Dispatcher backend='cpp' matches cpp_backend directly."""
361+ from qokit .max_k_xor_sat .contract import contract_symmetric_tree as disp_contract
362+
363+ gammas , betas = _params (2 )
364+ val_disp = disp_contract (gammas , betas , 2 , 3 , k = 2 , backend = "cpp" )
365+ val_cpp = cpp_contract (gammas , betas , 2 , 3 , 2 )
366+ assert abs (val_disp - val_cpp ) < 1e-12
367+
368+
369+ @pytest .mark .skipif (not HAS_JAX , reason = "JAX not installed" )
370+ def test_dispatcher_contract_symmetric_tree_jax ():
371+ """Dispatcher backend='jax' matches jax backend directly."""
372+ from qokit .max_k_xor_sat .contract import contract_symmetric_tree as disp_contract
373+
374+ gammas , betas = _params (2 )
375+ val_disp = disp_contract (gammas , betas , 2 , 3 , k = 2 , backend = "jax" )
376+ val_jax = jax_contract (gammas , betas , 2 , 3 , 2 )
377+ assert abs (val_disp - val_jax ) < 1e-12
378+
379+
380+ def test_dispatcher_jax_dd_raises ():
381+ """Dispatcher raises ValueError when backend='jax' + precision='dd' requested."""
382+ from qokit .max_k_xor_sat .contract import contract_symmetric_tree as disp_contract
383+
384+ gammas , betas = _params (2 )
385+ with pytest .raises (ValueError , match = "double-double" ):
386+ disp_contract (gammas , betas , 2 , 3 , k = 2 , precision = "dd" , backend = "jax" )
387+
388+
389+ @pytest .mark .skipif (not (HAS_CPP or HAS_JAX ), reason = "No backend available" )
390+ @pytest .mark .parametrize ("k,D,p" , _GRAD_CASES )
391+ def test_dispatcher_contract_with_grad_shape (k , D , p ):
392+ """Dispatcher contract_with_grad returns (float, array(p,), array(p,))."""
393+ from qokit .max_k_xor_sat .contract import contract_with_grad as disp_grad
394+
395+ gammas , betas = _params (p )
396+ val , dg , db = disp_grad (gammas , betas , p , D , k )
397+ assert isinstance (val , float )
398+ assert np .asarray (dg ).shape == (p ,)
399+ assert np .asarray (db ).shape == (p ,)
400+
401+
402+ @pytest .mark .skipif (not (HAS_CPP or HAS_JAX ), reason = "No backend available" )
403+ @pytest .mark .parametrize ("k,D,p" , _GRAD_CASES )
404+ def test_dispatcher_contract_with_grad_value (k , D , p ):
405+ """Dispatcher contract_with_grad value matches contract_symmetric_tree."""
406+ from qokit .max_k_xor_sat .contract import (
407+ contract_symmetric_tree as disp_contract ,
408+ contract_with_grad as disp_grad ,
409+ )
410+
411+ gammas , betas = _params (p )
412+ val_tree = disp_contract (gammas , betas , p , D , k )
413+ val_grad , _ , _ = disp_grad (gammas , betas , p , D , k )
414+ assert abs (val_tree - val_grad ) < 1e-8 , f"k={ k } D={ D } p={ p } : tree={ val_tree } grad={ val_grad } "
415+
416+
417+ @pytest .mark .skipif (not (HAS_CPP or HAS_JAX ), reason = "No backend available" )
418+ @pytest .mark .parametrize ("k,D,p" , _GRAD_CASES )
419+ def test_dispatcher_contract_with_grad_accuracy (k , D , p ):
420+ """Dispatcher contract_with_grad gradient matches finite differences."""
421+ from qokit .max_k_xor_sat .contract import (
422+ contract_symmetric_tree as disp_contract ,
423+ contract_with_grad as disp_grad ,
424+ )
425+
426+ gammas , betas = _params (p )
427+ _ , dg , db = disp_grad (gammas , betas , p , D , k )
428+ ref_dg , ref_db = _fd_grad (disp_contract , gammas , betas , p , D , k )
429+ np .testing .assert_allclose (dg , ref_dg , atol = 1e-4 , rtol = 1e-4 , err_msg = f"gamma grad k={ k } D={ D } p={ p } " )
430+ np .testing .assert_allclose (db , ref_db , atol = 1e-4 , rtol = 1e-4 , err_msg = f"beta grad k={ k } D={ D } p={ p } " )
431+
432+
433+ # ============================================================
434+ # cpp_backend.py additional tests
435+ # ============================================================
436+
437+
438+ @pytest .mark .skipif (not HAS_CPP , reason = "C++ backend not built" )
439+ @pytest .mark .parametrize ("k,D,p" , [(2 , 3 , 1 ), (2 , 3 , 2 ), (3 , 2 , 1 ), (2 , 2 , 3 )])
440+ def test_cpp_light_cone_size (k , D , p ):
441+ """cpp_backend light_cone_size matches reference formula."""
442+ from qokit .max_k_xor_sat .cpp_backend import light_cone_size as cpp_lcs
443+ from qokit .max_k_xor_sat .contract import light_cone_size as ref_lcs
444+
445+ assert cpp_lcs (p , D , k ) == ref_lcs (p , D , k ), f"k={ k } D={ D } p={ p } "
446+
447+
448+ @pytest .mark .skipif (not HAS_CPP , reason = "C++ backend not built" )
449+ @pytest .mark .parametrize ("k,D,p" , _GRAD_CASES )
450+ def test_cpp_contract_with_grad_shape (k , D , p ):
451+ """cpp_backend contract_with_grad returns (float, array(p,), array(p,))."""
452+ from qokit .max_k_xor_sat .cpp_backend import contract_with_grad as cpp_grad
453+
454+ gammas , betas = _params (p )
455+ val , dg , db = cpp_grad (gammas , betas , p , D , k )
456+ assert isinstance (val , float )
457+ assert np .asarray (dg ).shape == (p ,)
458+ assert np .asarray (db ).shape == (p ,)
459+
460+
461+ @pytest .mark .skipif (not HAS_CPP , reason = "C++ backend not built" )
462+ @pytest .mark .parametrize ("k,D,p" , _GRAD_CASES )
463+ def test_cpp_contract_with_grad_value (k , D , p ):
464+ """cpp_backend contract_with_grad value matches contract_symmetric_tree."""
465+ from qokit .max_k_xor_sat .cpp_backend import contract_with_grad as cpp_grad
466+
467+ gammas , betas = _params (p )
468+ val_tree = cpp_contract (gammas , betas , p , D , k )
469+ val_grad , _ , _ = cpp_grad (gammas , betas , p , D , k )
470+ assert abs (val_tree - val_grad ) < 1e-8 , f"k={ k } D={ D } p={ p } : tree={ val_tree } grad={ val_grad } "
471+
472+
473+ @pytest .mark .skipif (not HAS_CPP , reason = "C++ backend not built" )
474+ @pytest .mark .parametrize ("k,D,p" , _GRAD_CASES )
475+ def test_cpp_grad_accuracy (k , D , p ):
476+ """cpp_backend gradient matches finite differences."""
477+ from qokit .max_k_xor_sat .cpp_backend import contract_with_grad as cpp_grad
478+
479+ gammas , betas = _params (p )
480+ _ , dg , db = cpp_grad (gammas , betas , p , D , k )
481+ ref_dg , ref_db = _fd_grad (cpp_contract , gammas , betas , p , D , k )
482+ np .testing .assert_allclose (dg , ref_dg , atol = 1e-4 , rtol = 1e-4 , err_msg = f"gamma grad k={ k } D={ D } p={ p } " )
483+ np .testing .assert_allclose (db , ref_db , atol = 1e-4 , rtol = 1e-4 , err_msg = f"beta grad k={ k } D={ D } p={ p } " )
484+
485+
486+ # ============================================================
487+ # JAX backend additional tests
488+ # ============================================================
489+
490+
491+ @pytest .mark .skipif (not HAS_JAX , reason = "JAX not installed" )
492+ @pytest .mark .parametrize ("k,D,p" , [(2 , 3 , 1 ), (2 , 3 , 2 ), (3 , 2 , 1 ), (2 , 2 , 3 )])
493+ def test_jax_light_cone_size (k , D , p ):
494+ """JAX backend light_cone_size matches reference formula."""
495+ from qokit .max_k_xor_sat .jax .contract import light_cone_size as jax_lcs
496+ from qokit .max_k_xor_sat .contract import light_cone_size as ref_lcs
497+
498+ assert jax_lcs (p , D , k ) == ref_lcs (p , D , k ), f"k={ k } D={ D } p={ p } "
499+
500+
501+ @pytest .mark .skipif (not HAS_JAX , reason = "JAX not installed" )
502+ @pytest .mark .parametrize ("k,D,p" , _GRAD_CASES )
503+ def test_jax_contract_with_grad_shape (k , D , p ):
504+ """JAX backend contract_with_grad returns (float, array(p,), array(p,))."""
505+ from qokit .max_k_xor_sat .jax import contract_with_grad as jax_grad
506+
507+ gammas , betas = _params (p )
508+ val , dg , db = jax_grad (gammas , betas , p , D , k )
509+ assert isinstance (val , float )
510+ assert np .asarray (dg ).shape == (p ,)
511+ assert np .asarray (db ).shape == (p ,)
512+
513+
514+ @pytest .mark .skipif (not HAS_JAX , reason = "JAX not installed" )
515+ @pytest .mark .parametrize ("k,D,p" , _GRAD_CASES )
516+ def test_jax_contract_with_grad_value (k , D , p ):
517+ """JAX backend contract_with_grad value matches contract_symmetric_tree."""
518+ from qokit .max_k_xor_sat .jax import contract_with_grad as jax_grad
519+
520+ gammas , betas = _params (p )
521+ val_tree = jax_contract (gammas , betas , p , D , k )
522+ val_grad , _ , _ = jax_grad (gammas , betas , p , D , k )
523+ assert abs (val_tree - val_grad ) < 1e-8 , f"k={ k } D={ D } p={ p } : tree={ val_tree } grad={ val_grad } "
524+
525+
526+ @pytest .mark .skipif (not HAS_JAX , reason = "JAX not installed" )
527+ @pytest .mark .parametrize ("k,D,p" , _GRAD_CASES )
528+ def test_jax_grad_accuracy (k , D , p ):
529+ """JAX backend gradient matches finite differences."""
530+ from qokit .max_k_xor_sat .jax import contract_with_grad as jax_grad
531+
532+ gammas , betas = _params (p )
533+ _ , dg , db = jax_grad (gammas , betas , p , D , k )
534+ ref_dg , ref_db = _fd_grad (jax_contract , gammas , betas , p , D , k )
535+ np .testing .assert_allclose (dg , ref_dg , atol = 1e-4 , rtol = 1e-4 , err_msg = f"gamma grad k={ k } D={ D } p={ p } " )
536+ np .testing .assert_allclose (db , ref_db , atol = 1e-4 , rtol = 1e-4 , err_msg = f"beta grad k={ k } D={ D } p={ p } " )
0 commit comments