Add single-layer UCJ energy - #684
Conversation
|
@kevinsung I was looking into why the test cases are getting stuck in CI, I believe it can be narrowed down to a deadlock in jax 0.10.2. This error does not occur on my laptop when I run the test cases, but if I restrict to 4 cores like the GitHub Actions runners, then I am able to reproduce the deadlock in the UCJ algorithm implementation. This code also produces the same deadlock on jax import time
import numpy as np
import jax
import jax.numpy as jnp
jax.config.update("jax_enable_x64", True)
n_calls = 16
batch = 1024
n = 5
rng = np.random.default_rng(0)
A = jnp.asarray(rng.normal(size=(batch, n, n)) + 1j * rng.normal(size=(batch, n, n)))
def f(t):
return sum(jnp.real(jnp.sum(jnp.linalg.det(A * jnp.exp(1j * (t + k)))))
for k in range(n_calls))
c = jax.jit(f).lower(0.3).compile()
t = time.time(); o = jax.block_until_ready(c(0.3))
print(f"OK exec {time.time()-t:.2f}s val={float(o):.3f}")on |
|
Sure, you can go ahead and edit |
|
Sounds good, thanks! |
There was a problem hiding this comment.
@hkbelagali Thanks for the contribution! My first request is that you use the newly introduced rotate_one_body_tensor and rotate_two_body_tensor functions from https://github.com/qiskit-community/ffsim/blob/main/python/ffsim/linalg/util.py. I think these can replace the _propagate_through_orbital_rotations and _propagate_spin_sector_tensor functions you introduced here. Note, however, that the orbital rotation convention is transposed from your convention (please check this), so you either need to pass u.T.conj() everywhere, or rework your logic to align with the ffsim convention.
kevinsung
left a comment
There was a problem hiding this comment.
To keep things simple for now, let's get rid of the high-level dispatcher functions like ucj_energy, ucj_energy_and_grad, and optimize_ucj_energy and just force the user to use the appropriate function for their operator and Hamiltonian type.
…raction_pairs arg for energy calculation
kevinsung
left a comment
There was a problem hiding this comment.
I wonder if the variable names with single letters like q, h, and g can be made more descriptive. If you can't think of better names, it's fine though.
There was a problem hiding this comment.
Needs a copyright header.
There was a problem hiding this comment.
Needs a copyright header.
| "Topic :: Software Development :: Libraries :: Python Modules", | ||
| ] | ||
| dependencies = [ | ||
| "jax", |
There was a problem hiding this comment.
jax 0.11.1 was just released, let's try reverting this
| def test_ucj_energy_spin_balanced_n2(): | ||
| """Compare fermionic backpropagation against statevector simulation.""" | ||
| mol = pyscf.gto.Mole() | ||
| mol.build( | ||
| atom=[["N", (0, 0, 0)], ["N", (0, 0, 1.0)]], | ||
| basis="sto-6g", | ||
| symmetry="Dooh", | ||
| ) | ||
| scf = pyscf.scf.RHF(mol).run() | ||
|
|
||
| n_frozen = 2 | ||
| active_space = range(n_frozen, mol.nao_nr()) | ||
| ccsd = pyscf.cc.CCSD( | ||
| scf, frozen=[i for i in range(mol.nao_nr()) if i not in active_space] | ||
| ).run() | ||
|
|
||
| mol_data = ffsim.MolecularData.from_scf(scf, active_space=active_space) | ||
| mol_hamiltonian = mol_data.hamiltonian | ||
| norb = mol_data.norb | ||
| nelec = mol_data.nelec | ||
| assert norb == 8 | ||
| assert nelec == (5, 5) | ||
|
|
||
| pairs_aa = [(p, p) for p in range(norb-1)] | ||
| pairs_ab = [(p, p) for p in range(norb-1)] | ||
|
|
||
| ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(ccsd.t2, t1=ccsd.t1, interaction_pairs=(pairs_aa, pairs_ab), n_reps=1) | ||
|
|
||
| backprop_energy = ffsim.ucj_energy_spin_balanced(ucj_op, mol_hamiltonian, nelec) | ||
| statevector_energy = _statevector_energy(ucj_op, mol_hamiltonian, norb, nelec) | ||
|
|
||
| np.testing.assert_allclose(backprop_energy, statevector_energy) | ||
|
|
||
| optimized_ucj_op, result = ffsim.optimize_ucj_energy_spin_balanced( | ||
| ucj_op, | ||
| mol_hamiltonian, | ||
| nelec, | ||
| options={"maxiter": 1}, | ||
| return_optimize_result=True, | ||
| ) | ||
| optimized_backprop_energy = ffsim.ucj_energy_spin_balanced( | ||
| optimized_ucj_op, mol_hamiltonian, nelec | ||
| ) | ||
| optimized_statevector_energy = _statevector_energy( | ||
| optimized_ucj_op, mol_hamiltonian, norb, nelec | ||
| ) | ||
|
|
||
| np.testing.assert_allclose(optimized_backprop_energy, result.fun) | ||
| np.testing.assert_allclose(optimized_backprop_energy, optimized_statevector_energy) | ||
|
|
||
|
|
||
| def test_ucj_energy_spin_unbalanced_n2(): | ||
| """Compare spin-unbalanced backpropagation against statevector simulation.""" | ||
| mol = pyscf.gto.Mole() | ||
| mol.build( | ||
| atom=[["N", (0, 0, 0)], ["N", (0, 0, 1.0)]], | ||
| basis="sto-6g", | ||
| symmetry="Dooh", | ||
| ) | ||
| scf = pyscf.scf.RHF(mol).run() | ||
|
|
||
| n_frozen = 2 | ||
| active_space = range(n_frozen, mol.nao_nr()) | ||
| mol_data = ffsim.MolecularData.from_scf(scf, active_space=active_space) | ||
| mol_hamiltonian = mol_data.hamiltonian | ||
| norb = mol_data.norb | ||
| nelec = mol_data.nelec | ||
|
|
||
| ucj_op = ffsim.random.random_ucj_op_spin_unbalanced( | ||
| norb, | ||
| n_reps=1, | ||
| with_final_orbital_rotation=True, | ||
| diag_coulomb_scale=0.5, | ||
| seed=RNG, | ||
| ) | ||
|
|
||
| backprop_energy = ffsim.ucj_energy_spin_unbalanced(ucj_op, mol_hamiltonian, nelec) | ||
| statevector_energy = _statevector_energy(ucj_op, mol_hamiltonian, norb, nelec) | ||
|
|
||
| np.testing.assert_allclose(backprop_energy, statevector_energy) | ||
|
|
||
|
|
||
| def test_ucj_energy_spinless_n2(): | ||
| """Compare spinless backpropagation against statevector simulation.""" | ||
| mol = pyscf.gto.Mole() | ||
| mol.build( | ||
| atom=[["N", (0, 0, 0)], ["N", (0, 0, 1.0)]], | ||
| basis="sto-6g", | ||
| symmetry="Dooh", | ||
| ) | ||
| scf = pyscf.scf.RHF(mol).run() | ||
|
|
||
| n_frozen = 2 | ||
| active_space = range(n_frozen, mol.nao_nr()) | ||
| mol_data = ffsim.MolecularData.from_scf(scf, active_space=active_space) | ||
| mol_hamiltonian = ffsim.MolecularHamiltonianSpinless( | ||
| one_body_tensor=mol_data.hamiltonian.one_body_tensor, | ||
| two_body_tensor=mol_data.hamiltonian.two_body_tensor, | ||
| constant=mol_data.hamiltonian.constant, | ||
| ) | ||
| norb = mol_data.norb | ||
| nelec = mol_data.nelec[0] | ||
|
|
||
| ucj_op = ffsim.random.random_ucj_op_spinless( | ||
| norb, | ||
| n_reps=1, | ||
| with_final_orbital_rotation=True, | ||
| diag_coulomb_scale=0.5, | ||
| seed=RNG, | ||
| ) | ||
|
|
||
| backprop_energy = ffsim.ucj_energy_spinless(ucj_op, mol_hamiltonian, nelec) | ||
| statevector_energy = _statevector_energy(ucj_op, mol_hamiltonian, norb, nelec) | ||
|
|
||
| np.testing.assert_allclose(backprop_energy, statevector_energy) | ||
|
|
||
|
|
||
| def test_optimize_ucj_energy_spin_unbalanced(): | ||
| """Compare spin-unbalanced optimization objective against statevector simulation.""" | ||
| norb = 3 | ||
| nelec = (1, 1) | ||
| mol_hamiltonian = ffsim.random.random_molecular_hamiltonian(norb, seed=RNG) | ||
| ucj_op = ffsim.random.random_ucj_op_spin_unbalanced( | ||
| norb, | ||
| n_reps=1, | ||
| with_final_orbital_rotation=True, | ||
| diag_coulomb_scale=0.5, | ||
| seed=RNG, | ||
| ) | ||
|
|
||
| optimized_ucj_op, result = ffsim.optimize_ucj_energy_spin_unbalanced( | ||
| ucj_op, | ||
| mol_hamiltonian, | ||
| nelec, | ||
| options={"maxiter": 1}, | ||
| return_optimize_result=True, | ||
| ) | ||
| optimized_backprop_energy = ffsim.ucj_energy_spin_unbalanced( | ||
| optimized_ucj_op, mol_hamiltonian, nelec | ||
| ) | ||
| optimized_statevector_energy = _statevector_energy( | ||
| optimized_ucj_op, mol_hamiltonian, norb, nelec | ||
| ) | ||
|
|
||
| np.testing.assert_allclose(optimized_backprop_energy, result.fun) | ||
| np.testing.assert_allclose(optimized_backprop_energy, optimized_statevector_energy) | ||
|
|
||
|
|
||
| def test_optimize_ucj_energy_spinless(): | ||
| """Compare spinless optimization objective against statevector simulation.""" | ||
| norb = 4 | ||
| nelec = 2 | ||
| mol_hamiltonian = ffsim.random.random_molecular_hamiltonian_spinless(norb, seed=RNG) | ||
| ucj_op = ffsim.random.random_ucj_op_spinless( | ||
| norb, | ||
| n_reps=1, | ||
| with_final_orbital_rotation=True, | ||
| diag_coulomb_scale=0.5, | ||
| seed=RNG, | ||
| ) | ||
|
|
||
| optimized_ucj_op, result = ffsim.optimize_ucj_energy_spinless( | ||
| ucj_op, | ||
| mol_hamiltonian, | ||
| nelec, | ||
| options={"maxiter": 1}, | ||
| return_optimize_result=True, | ||
| ) | ||
| optimized_backprop_energy = ffsim.ucj_energy_spinless( | ||
| optimized_ucj_op, mol_hamiltonian, nelec | ||
| ) | ||
| optimized_statevector_energy = _statevector_energy( | ||
| optimized_ucj_op, mol_hamiltonian, norb, nelec | ||
| ) | ||
|
|
||
| np.testing.assert_allclose(optimized_backprop_energy, result.fun) | ||
| np.testing.assert_allclose(optimized_backprop_energy, optimized_statevector_energy) |
There was a problem hiding this comment.
Let's simplify these 5 tests into 3 tests that are more consistent. One test for each spin variant. Each test should
- Generate a random UCJ operator (no need to use a molecule)
- Compute the energy and compare it to the state vector reference
- Optimize
- Compute the energy and compare it to the state vector reference
- Assert that the optimized energy is lower than the starting energy
- Be parameterized for a few different values of interaction pairs, such as
[],None,[(1, 2), (3, 4)]
These tests should exercise the default path of not specifying return_optimize_result. Then, add separate (cheaper) tests exercising the return_optimize_result=True path.
| np.testing.assert_allclose(optimized_backprop_energy, optimized_statevector_energy) | ||
|
|
||
|
|
||
| def test_ucj_energy_chunk_size_nondivisor(): |
There was a problem hiding this comment.
Split this into three tests, one for each spin variant.
| np.testing.assert_allclose(chunked, unchunked) | ||
|
|
||
|
|
||
| def test_ucj_energy_and_grad(): |
There was a problem hiding this comment.
Split this into three consistent tests, one for each spin variant. Extract a helper function to compute the finite difference gradient, and compute the whole gradient instead of just index 0. If this is too expensive, perhaps do it just for a few randomly chosen indices.
| return jax.jit(jax.value_and_grad(energy, argnums=0)) | ||
|
|
||
|
|
||
| def _spin_balanced_jastrow_phase(same, diff, norb): |
There was a problem hiding this comment.
Please add type annotations to all function signatures, including the helper functions.
| def _interaction_pairs_key( | ||
| interaction_pairs: Sequence[tuple[int, int]], | ||
| ) -> tuple[tuple[int, int], ...]: | ||
| """Functools requires arguments to be hashable.""" |
There was a problem hiding this comment.
| """Functools requires arguments to be hashable.""" | |
| """functools.cache requires arguments to be hashable.""" |
and similarly elsewhere
| ) | ||
|
|
||
|
|
||
| def ucj_energy_and_grad_spin_balanced( |
There was a problem hiding this comment.
I noticed that this function isn't used in the guide, and it's also not used in optimize_ucj_energy_spin_balanced. Let's replace both ucj_energy_and_grad_spin_balanced and optimize_ucj_energy_spin_balanced with a single function that returns the callable that can be passed to scipy. Something like
def ucj_energy_and_grad_func_spin_balanced(
hamiltonian, nelec, *, norb, interaction_pairs=None,
with_final_orbital_rotation=False, occupied_orbitals=None, chunk_size=None,
) -> Callable[[np.ndarray], tuple[float, np.ndarray]]:Then, the guide should use this function to construct the callable, and pass it scipy.optimize.minimize. This gives the user more control over the optimization and lets us remove some more code from this file.
Same for the other spin variants.
No description provided.