11# Quantum chemistry example
22# Developed with help from (OpenAI custom GPT) Elara
3+ # Adapted to atomic checks of Z/X/Y basis by (Anthropic) Claude
34# (Requires OpenFermion)
45
56from openfermion import MolecularData , FermionOperator , jordan_wigner , get_fermion_operator
@@ -230,68 +231,93 @@ def geometry_to_atom_str(geometry):
230231 for symbol , (x , y , z ) in geometry
231232 )
232233
233- def compute_energy (theta_bits , phi_bits , zx_hamiltonian ):
234+
235+ # phi encodes Pauli basis per qubit: 0=Z, 1=X, 2=Y
236+ # theta encodes |0> vs |1> in that basis (False=|0>, True=|1>)
237+ #
238+ # For a single-qubit Clifford state |b>_P:
239+ # <b|_P sigma_P |b>_P = (-1)^b (eigenvalue of the matching Pauli)
240+ # <b|_P sigma_Q |b>_P = 0 (orthogonal Pauli, no contribution)
241+ #
242+ # So a term contributes only when ALL its qubits are measured in the
243+ # matching basis, and its sign is the product of (-1)^theta[qubit].
244+
245+ def compute_energy (theta , phi , zxy_hamiltonian ):
234246 energy = 0.0
235- for qubits , bases , coeff in zx_hamiltonian :
236- is_sat = True
237- for i in range (len (qubits )):
238- qubit = qubits [i ]
239- if phi_bits [qubit ] != bases [i ]:
240- is_sat = False
241- break
247+ for qubits , bases , coeff in zxy_hamiltonian :
248+ # bases[i] is 0/1/2 for Z/X/Y; phi[qubit] must match for term to contribute
249+ is_sat = all (phi [qubits [i ]] == bases [i ] for i in range (len (qubits )))
242250 if not is_sat :
243251 continue
244-
245252 for qubit in qubits :
246- if theta_bits [qubit ]:
253+ if theta [qubit ]:
247254 coeff *= - 1
248255 energy += coeff
249-
250256 return energy
251257
252258
253- def bootstrap_worker (theta , phi , zx_hamiltonian , d_theta , d_phi , energy ):
259+ # A candidate update is a pair (new_theta_slice, new_phi_slice) for a subset of qubits,
260+ # tested atomically. We enumerate all 6 single-qubit Clifford states per qubit in the
261+ # subset, and all combinations across qubits for k>1.
262+
263+ def bootstrap_worker (theta , phi , zxy_hamiltonian , qubit_indices , new_states ):
264+ """new_states: list of (theta_val, phi_val) per qubit in qubit_indices."""
254265 local_theta = theta .copy ()
255- for i in d_theta :
256- local_theta [i ] = not local_theta [i ]
257266 local_phi = phi .copy ()
258- for i in d_phi :
259- local_phi [i ] = not local_phi [i ]
260- energy = compute_energy (local_theta , local_phi , zx_hamiltonian )
261-
262- return energy
267+ for idx , (t , p ) in zip (qubit_indices , new_states ):
268+ local_theta [idx ] = t
269+ local_phi [idx ] = p
270+ return compute_energy (local_theta , local_phi , zxy_hamiltonian )
271+
272+
273+ def bootstrap (theta , phi , zxy_hamiltonian , k , qubit_combos ):
274+ """
275+ For each combination of k qubits, test all 6^k assignments of
276+ (theta, phi) per qubit atomically. Returns list of (energy, qubit_indices, new_states).
277+ """
278+ # 6 single-qubit Clifford states: (theta_val, phi_val)
279+ clifford_states = [
280+ (False , 0 ), (True , 0 ), # |0>_Z, |1>_Z
281+ (False , 1 ), (True , 1 ), # |0>_X, |1>_X
282+ (False , 2 ), (True , 2 ), # |0>_Y, |1>_Y
283+ ]
284+
285+ args = []
286+ meta = []
287+ for combo in qubit_combos :
288+ for new_states in itertools .product (clifford_states , repeat = k ):
289+ # Skip if this assignment matches the current state (no change)
290+ if all (
291+ theta [combo [i ]] == new_states [i ][0 ] and phi [combo [i ]] == new_states [i ][1 ]
292+ for i in range (k )
293+ ):
294+ continue
295+ args .append ((theta , phi , zxy_hamiltonian , list (combo ), list (new_states )))
296+ meta .append ((list (combo ), list (new_states )))
263297
298+ if not args :
299+ return []
264300
265- def bootstrap (theta , phi , zx_hamiltonian , k , indices_array , energy ):
266- n = len (indices_array ) // k
267301 with multiprocessing .Pool (processes = os .cpu_count ()) as pool :
268- args = []
269- for i in range (n ):
270- j = i * k
271- args .append ((theta , phi , zx_hamiltonian , indices_array [j : j + k ], [], energy ))
272- for i in range (n ):
273- j = i * k
274- args .append ((theta , phi , zx_hamiltonian , [], indices_array [j : j + k ], energy ))
275- for i in range (n ):
276- j = i * k
277- args .append ((theta , phi , zx_hamiltonian , indices_array [j : j + k ], indices_array [j : j + k ], energy ))
278302 energies = pool .starmap (bootstrap_worker , args )
279303
280- return energies
304+ return list ( zip ( energies , meta ))
281305
282306
283- def multiprocessing_bootstrap (zx_hamiltonian , n_qubits , reheat_tries = 0 ):
284- best_theta = np .random .randint (2 , size = n_qubits )
285- best_phi = np .random .randint (2 , size = n_qubits )
286- min_energy = compute_energy (best_theta , best_phi , zx_hamiltonian )
307+ def multiprocessing_bootstrap (zxy_hamiltonian , n_qubits , reheat_tries = 0 ):
308+ # Initialise randomly across all 6 Clifford states
309+ best_theta = np .array ([bool (random .randint (0 , 1 )) for _ in range (n_qubits )])
310+ best_phi = np .array ([random .randint (0 , 2 ) for _ in range (n_qubits )])
311+ min_energy = compute_energy (best_theta , best_phi , zxy_hamiltonian )
287312
288313 combos_list = []
289314 reheat_theta = best_theta .copy ()
290- reheat_phi = best_phi .copy ()
315+ reheat_phi = best_phi .copy ()
291316 reheat_min_energy = min_energy
317+
292318 for reheat_round in range (reheat_tries + 1 ):
293319 improved = True
294- quality = 1
320+ quality = 1
295321 while improved :
296322 improved = False
297323 k = 1
@@ -300,135 +326,131 @@ def multiprocessing_bootstrap(zx_hamiltonian, n_qubits, reheat_tries=0):
300326 break
301327
302328 if len (combos_list ) < k :
303- combos = np .array (list (
304- item for sublist in itertools .combinations (range (n_qubits ), k ) for item in sublist
305- ))
329+ combos = list (itertools .combinations (range (n_qubits ), k ))
306330 combos_list .append (combos )
307331 else :
308332 combos = combos_list [k - 1 ]
309333
310- energies = bootstrap (reheat_theta , reheat_phi , zx_hamiltonian , k , combos , reheat_min_energy )
334+ results = bootstrap (reheat_theta , reheat_phi , zxy_hamiltonian , k , combos )
311335
312- energy = min (energies )
336+ if not results :
337+ k += 1
338+ continue
339+
340+ best_result = min (results , key = lambda x : x [0 ])
341+ energy , (qubit_indices , new_states ) = best_result
313342
314343 if energy < reheat_min_energy :
315344 reheat_min_energy = energy
316- index_match = energies .index (energy )
317-
318- n_combos = len (combos )
319- is_theta = (index_match < n_combos ) or (index_match >= (n_combos << 1 ))
320- is_phi = index_match >= n_combos
321- index_match %= n_combos
322- indices = combos [index_match * k : (index_match + 1 ) * k ]
323- if is_theta :
324- for i in indices :
325- reheat_theta [i ] = not reheat_theta [i ]
326- if is_phi :
327- for i in indices :
328- reheat_phi [i ] = not reheat_phi [i ]
329-
345+ for idx , (t , p ) in zip (qubit_indices , new_states ):
346+ reheat_theta [idx ] = t
347+ reheat_phi [idx ] = p
330348 improved = True
331349 if quality < (k + 1 ):
332350 quality = k + 1
333351 if reheat_min_energy < min_energy :
334- print (f" Qubits { indices } flip accepted. New energy: { reheat_min_energy } " )
335- print (f" θ: { reheat_theta } " )
352+ basis_names = {0 : 'Z' , 1 : 'X' , 2 : 'Y' }
353+ state_strs = [f"|{ '1' if t else '0' } >_{ basis_names [p ]} "
354+ for t , p in new_states ]
355+ print (f" Qubits { qubit_indices } -> { state_strs } . New energy: { reheat_min_energy } " )
356+ print (f" θ: { reheat_theta .astype (int )} " )
336357 print (f" φ: { reheat_phi } " )
337358 break
338359
339- k = k + 1
340- print (" Qubit flips all rejected." )
360+ k += 1
361+ print (" Qubit updates all rejected." )
341362
342363 if min_energy < reheat_min_energy :
343- reheat_theta = best_theta .copy ()
344- reheat_phi = best_phi .copy ()
364+ reheat_theta = best_theta .copy ()
365+ reheat_phi = best_phi .copy ()
345366 reheat_min_energy = min_energy
346367 else :
347368 best_theta = reheat_theta .copy ()
348- best_phi = reheat_phi .copy ()
369+ best_phi = reheat_phi .copy ()
349370 min_energy = reheat_min_energy
350371
351372 if reheat_round < reheat_tries :
352373 print (" Reheating..." )
353- num_to_flip = int (np .round (np .log2 (len (reheat_theta ))))
354- z_bits_to_flip = random .sample (list (range (n_qubits )), num_to_flip )
355- for bit in z_bits_to_flip :
356- reheat_theta [bit ] = not reheat_theta [bit ]
357- x_bits_to_flip = random .sample (list (range (n_qubits )), num_to_flip )
358- for bit in x_bits_to_flip :
359- reheat_phi [bit ] = not reheat_phi [bit ]
360- reheat_min_energy = compute_energy (reheat_theta , reheat_phi , zx_hamiltonian )
374+ num_to_flip = int (np .round (np .log2 (n_qubits )))
375+ for bit in random .sample (range (n_qubits ), num_to_flip ):
376+ reheat_theta [bit ] = bool (random .randint (0 , 1 ))
377+ reheat_phi [bit ] = random .randint (0 , 2 )
378+ reheat_min_energy = compute_energy (reheat_theta , reheat_phi , zxy_hamiltonian )
361379
362380 return best_theta , best_phi , min_energy
363381
382+
364383is_charge_update = True
365384while is_charge_update :
366385 is_charge_update = False
367386
368- atom_str = geometry_to_atom_str (geometry )
387+ atom_str = geometry_to_atom_str (geometry )
369388 molecule_of = MolecularData (geometry , basis , multiplicity = multiplicity , charge = charge )
370- molecule_of = run_pyscf (molecule_of , run_scf = True , run_mp2 = False , run_cisd = False , run_ccsd = False , run_fci = False )
389+ molecule_of = run_pyscf (molecule_of , run_scf = True , run_mp2 = False , run_cisd = False ,
390+ run_ccsd = False , run_fci = False )
371391 fermion_ham = get_fermion_operator (molecule_of .get_molecular_hamiltonian ())
372392 n_electrons = molecule_of .n_electrons
373- n_qubits = molecule_of .n_qubits
393+ n_qubits = molecule_of .n_qubits
374394 print (f"Hartree-Fock energy: { molecule_of .hf_energy } " )
375395 print (f"{ n_electrons } electrons..." )
376396 print (f"{ n_qubits } qubits..." )
377397
378- # Step 3: Iterate JW terms without materializing full op
379- zx_hamiltonian = []
398+ # Build ZXY Hamiltonian: retain Z, X, and Y terms.
399+ # A Pauli string contributes only when all its qubits are in the matching basis.
400+ # bases[i] = 0 (Z), 1 (X), or 2 (Y).
401+ zxy_hamiltonian = []
380402 for term , coeff in fermion_ham .terms .items ():
381- jw_term = jordan_wigner (FermionOperator (term = term , coefficient = coeff )) # Transform single term
403+ jw_term = jordan_wigner (FermionOperator (term = term , coefficient = coeff ))
382404
383405 for pauli_string , jw_coeff in jw_term .terms .items ():
384- if any (p in ('Y' ) for _ , p in pauli_string ):
385- continue
386-
387406 q = []
388407 b = []
389408 for qubit , op in pauli_string :
390- # Z/I terms: keep only Z
391409 if op == 'I' :
392410 continue
411+ if op == 'Z' :
412+ b .append (0 )
413+ elif op == 'X' :
414+ b .append (1 )
415+ elif op == 'Y' :
416+ b .append (2 )
393417 q .append (qubit )
394- b .append (op != 'Z' )
395-
396- zx_hamiltonian .append ((q , b , jw_coeff .real ))
418+ zxy_hamiltonian .append ((q , b , jw_coeff .real ))
397419
398-
399- # Step 4: Bootstrap!
400- theta , phi , min_energy = multiprocessing_bootstrap (zx_hamiltonian , n_qubits , 1 )
420+ # Bootstrap!
421+ theta , phi , min_energy = multiprocessing_bootstrap (zxy_hamiltonian , n_qubits , 1 )
401422
402423 print (f"\n Final Bootstrap Ground State Energy: { min_energy } Ha" )
403424 print ("Final Bootstrap Parameters:" )
404- print (f" θ: { theta } " )
405- print (f" φ: { phi } " )
425+ basis_names = {0 : 'Z' , 1 : 'X' , 2 : 'Y' }
426+ print (f" θ: { theta .astype (int )} " )
427+ print (f" φ: { [basis_names [p ] for p in phi ]} " )
406428
429+ # Electron count: in Z basis a |1> is a full electron;
430+ # in X or Y basis a |1> contributes 1/2 (superposition of occupied/unoccupied).
407431 r_electrons = 0
408- for i in range (len (theta )):
409- b = theta [i ]
432+ for i in range (n_qubits ):
410433 if theta [i ]:
411- r_electrons += 1 / 2 if phi [i ] else 1
434+ r_electrons += 1 if phi [i ] == 0 else 0.5
412435 if int (r_electrons ) != r_electrons :
413436 print ("Whoops! We don't have an integer number of charges!" )
414437 break
415438 r_electrons = int (r_electrons )
416439
417- d_electrons = r_electrons - n_electrons
418- r_charge = charge - d_electrons
440+ d_electrons = r_electrons - n_electrons
441+ r_charge = charge - d_electrons
419442 r_multiplicity = 1
420443 for i in range (0 , len (theta ), 2 ):
421444 if theta [i ] != theta [i + 1 ]:
422445 r_multiplicity += 1
423446
424447 if n_electrons != r_electrons or multiplicity != r_multiplicity :
425448 print ()
426- print ("Regresssed electron count or multiplicity doesn't match the assumptions!" )
449+ print ("Regressed electron count or multiplicity doesn't match the assumptions!" )
427450 print ("Running again with the natural parameters replacing your assumptions:" )
428451 print (f"charge = { r_charge } " )
429452 print (f"multiplicity = { r_multiplicity } " )
430453 print ()
431-
432- charge = r_charge
454+ charge = r_charge
433455 multiplicity = r_multiplicity
434456 is_charge_update = True
0 commit comments