22Evaluation for Challenge Suite Problem 11.
33
44The evaluator dynamically imports a solution module, consumes only the NumPy
5- result dictionary returned by run_solution, and prints compact validation output.
5+ result dictionary returned by run_solution, and computes an exact sparse
6+ ground-state reference for the same spin-1 chain outside the timed solution run.
67"""
78
89import argparse
910import importlib
1011import time
12+ from functools import lru_cache
1113
1214import numpy as np
15+ from scipy .sparse import csr_matrix , eye , kron
16+ from scipy .sparse .linalg import eigsh
17+
18+ DIM = 3
19+ SQRT2 = np .sqrt (2.0 )
20+
21+ SX = (
22+ np .array ([[0.0 , 1.0 , 0.0 ], [1.0 , 0.0 , 1.0 ], [0.0 , 1.0 , 0.0 ]], dtype = np .complex128 )
23+ / SQRT2
24+ )
25+ SY = (
26+ np .array (
27+ [[0.0 , - 1.0j , 0.0 ], [1.0j , 0.0 , - 1.0j ], [0.0 , 1.0j , 0.0 ]],
28+ dtype = np .complex128 ,
29+ )
30+ / SQRT2
31+ )
32+ SZ = np .diag ([1.0 , 0.0 , - 1.0 ]).astype (np .complex128 )
33+ SZ2 = np .diag ([1.0 , 0.0 , 1.0 ]).astype (np .complex128 )
34+ DOT_BOND = np .kron (SX , SX ) + np .kron (SY , SY ) + np .kron (SZ , SZ )
35+ DOT_BOND_SQUARED = DOT_BOND @ DOT_BOND
36+ MS = np .array ([1.0 , 0.0 , - 1.0 ], dtype = np .float64 )
37+ MIDDLE_VALUES = np .array ([- 1.0 , 1.0 , - 1.0 ], dtype = np .float64 )
1338
1439DEFAULT_CONFIG = {
15- "n_qubits" : 20 ,
16- "n_field_params" : 2 ,
17- "n_layers" : 6 ,
18- "max_steps" : 300 ,
19- "learning_rate" : 0.01 ,
20- "initial_parameter_scale" : 0.02 ,
21- "readout_penalty_weight" : 0.05 ,
22- "seed" : 2037 ,
23- "final_response_mse_tolerance" : 1e-9 ,
40+ "n_sites" : 12 ,
41+ "n_layers" : 5 ,
42+ "beta" : 0.20 ,
43+ "single_ion_anisotropy" : 0.15 ,
44+ "max_steps" : 500 ,
45+ "learning_rate" : 0.03 ,
46+ "initial_parameter_scale" : 0.05 ,
47+ "seed" : 2041 ,
48+ "minimum_energy_improvement" : 5e-3 ,
49+ "maximum_energy_density_gap" : 0.12 ,
50+ "maximum_string_order_mae" : 0.12 ,
2451}
2552
2653
27- def sensor_positions (config ):
28- return np .linspace (- 1.0 , 1.0 , config ["n_qubits" ], dtype = np .float64 )
54+ def string_pairs (config ):
55+ n_sites = config ["n_sites" ]
56+ return tuple ((i , n_sites - 1 - i ) for i in range (3 ))
57+
58+
59+ @lru_cache (maxsize = None )
60+ def identity_shell (n_sites ):
61+ return eye (DIM ** n_sites , dtype = np .complex128 , format = "csr" )
62+
63+
64+ def embed_one_site_operator (op , site , n_sites ):
65+ return kron (
66+ kron (identity_shell (site ), csr_matrix (op ), format = "csr" ),
67+ identity_shell (n_sites - site - 1 ),
68+ format = "csr" ,
69+ )
2970
3071
31- def target_response_matrix (config ):
32- x_sites = sensor_positions (config )
33- return np .stack ([np .ones (config ["n_qubits" ]), x_sites ], axis = 1 )
72+ def embed_two_site_operator (op , left , n_sites ):
73+ return kron (
74+ kron (identity_shell (left ), csr_matrix (op ), format = "csr" ),
75+ identity_shell (n_sites - left - 2 ),
76+ format = "csr" ,
77+ )
3478
3579
36- def parameter_count (config ):
37- n_qubits = config ["n_qubits" ]
38- count = 0
39- for layer in range (config ["n_layers" ]):
40- count += 2 * n_qubits
41- count += len (range (layer % 2 , n_qubits - 1 , 2 ))
42- return count
80+ def build_hamiltonian (config ):
81+ n_sites = config ["n_sites" ]
82+ dim = DIM ** n_sites
83+ hamiltonian = csr_matrix ((dim , dim ), dtype = np .complex128 )
84+ bond_term = DOT_BOND + config ["beta" ] * DOT_BOND_SQUARED
85+ for left in range (n_sites - 1 ):
86+ hamiltonian += embed_two_site_operator (bond_term , left , n_sites )
87+ onsite_prefactor = config ["single_ion_anisotropy" ]
88+ for site in range (n_sites ):
89+ hamiltonian += onsite_prefactor * embed_one_site_operator (SZ2 , site , n_sites )
90+ return hamiltonian
91+
92+
93+ @lru_cache (maxsize = None )
94+ def basis_digits (n_sites ):
95+ dim = DIM ** n_sites
96+ digits = np .empty ((dim , n_sites ), dtype = np .int8 )
97+ values = np .arange (dim , dtype = np .int64 )
98+ for site in range (n_sites - 1 , - 1 , - 1 ):
99+ digits [:, site ] = values % DIM
100+ values //= DIM
101+ return digits
102+
103+
104+ def string_order_from_state (state , config , pair ):
105+ i , j = pair
106+ digits = basis_digits (config ["n_sites" ])
107+ weights = MS [digits [:, i ]] * MS [digits [:, j ]]
108+ for site in range (i + 1 , j ):
109+ weights *= MIDDLE_VALUES [digits [:, site ]]
110+ probabilities = np .abs (state ) ** 2
111+ return float (np .sum (probabilities * weights ))
112+
113+
114+ def exact_reference (config ):
115+ hamiltonian = build_hamiltonian (config )
116+ value , vector = eigsh (hamiltonian , k = 1 , which = "SA" , tol = 1e-8 , maxiter = 400 )
117+ ground = vector [:, 0 ]
118+ energy_density = float (np .real (value [0 ])) / config ["n_sites" ]
119+ strings = [
120+ string_order_from_state (ground , config , pair ) for pair in string_pairs (config )
121+ ]
122+ return energy_density , np .asarray (strings , dtype = np .float64 )
43123
44124
45125def evaluate (solution_module , config ):
46126 module = importlib .import_module (solution_module )
47127
128+ start = time .perf_counter ()
129+ exact_energy_density , exact_strings = exact_reference (config )
130+ exact_elapsed = time .perf_counter () - start
131+
48132 start = time .perf_counter ()
49133 results = module .run_solution (config )
50134 elapsed = time .perf_counter () - start
51135
52- loss_history = np .asarray (results ["loss_history" ], dtype = float )
53- response_mse_history = np .asarray (results ["response_mse_history" ], dtype = float )
54- readout_penalty_history = np .asarray (
55- results ["readout_penalty_history" ], dtype = float
56- )
57- response = np .asarray (results ["final_response_matrix" ], dtype = float )
58- exact_target = target_response_matrix (config )
59- zero_readouts = np .asarray (results ["final_zero_field_readouts" ], dtype = float )
60-
61- final_response_mse = float (np .mean ((response - exact_target ) ** 2 ))
62- final_readout_penalty = float (np .mean (zero_readouts ** 2 ))
63- final_loss = (
64- final_response_mse + config ["readout_penalty_weight" ] * final_readout_penalty
136+ pairs = string_pairs (config )
137+ energy_history = np .asarray (results ["energy_density_history" ], dtype = float )
138+ final_energy_density = float (
139+ np .asarray (results ["final_energy_density" ], dtype = float )
65140 )
141+ final_strings = np .asarray (results ["final_string_orders" ], dtype = float )
142+ string_mae = float (np .mean (np .abs (final_strings - exact_strings )))
143+ improvement = float (energy_history [0 ] - final_energy_density )
144+ gap = float (final_energy_density - exact_energy_density )
66145
67- finite_arrays = [
68- loss_history ,
69- response_mse_history ,
70- readout_penalty_history ,
71- response ,
72- zero_readouts ,
73- ]
74146 criteria = {
75- "loss history shape" : loss_history .shape == (config ["max_steps" ],),
76- "response mse history shape" : response_mse_history .shape
77- == (config ["max_steps" ],),
78- "readout penalty history shape" : readout_penalty_history .shape
79- == (config ["max_steps" ],),
80- "response shape" : response .shape
81- == (config ["n_qubits" ], config ["n_field_params" ]),
82- "zero readout shape" : zero_readouts .shape == (config ["n_qubits" ],),
83- "response mse improves" : final_response_mse < float (response_mse_history [0 ]),
84- "final response mse <= tolerance" : final_response_mse
85- <= config ["final_response_mse_tolerance" ],
86- "loss improves" : final_loss < float (loss_history [0 ]),
87- "returned arrays finite" : all (np .all (np .isfinite (a )) for a in finite_arrays ),
147+ "energy history shape" : energy_history .shape == (config ["max_steps" ],),
148+ "final string shape" : final_strings .shape == (len (pairs ),),
149+ "energy improves" : improvement >= config ["minimum_energy_improvement" ],
150+ "returned arrays finite" : np .all (np .isfinite (energy_history ))
151+ and np .isfinite (final_energy_density )
152+ and np .all (np .isfinite (final_strings )),
153+ "energy gap threshold" : gap <= config ["maximum_energy_density_gap" ],
154+ "string-order mae threshold" : string_mae <= config ["maximum_string_order_mae" ],
88155 }
89156
90157 print ("Challenge 11 evaluation" )
91158 print (f"Solution module: { solution_module } " )
92159 print (f"End-to-end solution time: { elapsed :.2f} s" )
93- print (f"Qubits: { config ['n_qubits' ]} " )
160+ print (f"Exact reference time: { exact_elapsed :.2f} s" )
161+ print (f"Sites: { config ['n_sites' ]} " )
94162 print (f"Layers: { config ['n_layers' ]} " )
95- print (f"Physical field parameters: { config ['n_field_params' ]} " )
96- print (f"Readout observables: { config ['n_qubits' ]} " )
97- print (f"Trainable circuit parameters: { parameter_count (config )} " )
98- print (f"Initial response MSE: { float (response_mse_history [0 ]):.8e} " )
99- print (f"Final response MSE: { final_response_mse :.8e} " )
100- print (f"Final zero-field readout penalty: { final_readout_penalty :.8e} " )
101- print (f"Initial total loss: { float (loss_history [0 ]):.8e} " )
102- print (f"Final total loss: { final_loss :.8e} " )
103- print (f"Loss history shape: { loss_history .shape } " )
104- print (f"Final response matrix shape: { response .shape } " )
163+ print (f"Steps: { config ['max_steps' ]} " )
164+ print (f"Initial energy density: { float (energy_history [0 ]):.10f} " )
165+ print (f"Final history energy density: { float (energy_history [- 1 ]):.10f} " )
166+ print (f"Final returned energy density: { final_energy_density :.10f} " )
167+ print (f"Exact ground-state density: { exact_energy_density :.10f} " )
168+ print (f"Energy-density gap: { gap :.10f} " )
169+ print (f"Energy improvement: { improvement :.10f} " )
170+ print ("String correlators (final / exact):" )
171+ for pair , value , ref in zip (pairs , final_strings , exact_strings ):
172+ print (f" O_string^z{ pair } : { value :.10f} / { ref :.10f} " )
173+ print (f"String-order MAE: { string_mae :.10f} " )
174+ print (f"Energy history shape: { energy_history .shape } " )
175+ print (f"Final string-order shape: { final_strings .shape } " )
105176 print (f"Returned NumPy keys: { sorted (results )} " )
106177 print ("Passing criteria:" )
107178 for name , passed in criteria .items ():
@@ -113,10 +184,12 @@ def main():
113184 parser = argparse .ArgumentParser ()
114185 parser .add_argument ("--solution" , default = "solution_11" )
115186 parser .add_argument ("--max-steps" , type = int , default = DEFAULT_CONFIG ["max_steps" ])
187+ parser .add_argument ("--n-layers" , type = int , default = DEFAULT_CONFIG ["n_layers" ])
116188 args = parser .parse_args ()
117189
118190 config = dict (DEFAULT_CONFIG )
119191 config ["max_steps" ] = args .max_steps
192+ config ["n_layers" ] = args .n_layers
120193 evaluate (args .solution , config )
121194
122195
0 commit comments