From 3f141ed01b9325748cf478e1e2492f4068dac1d1 Mon Sep 17 00:00:00 2001 From: Valentin Senicourt <41597680+ValentinS4t1qbit@users.noreply.github.com> Date: Wed, 9 Nov 2022 11:50:35 -0800 Subject: [PATCH 01/14] Update develop to catch up with main version 0.3.3 (#249) --- CHANGELOG.md | 28 +++++++++++++++++++ examples/linq/1.the_basics.ipynb | 0 tangelo/_version.py | 2 +- .../algorithms/variational/sa_vqe_solver.py | 1 - tangelo/algorithms/variational/vqe_solver.py | 1 - tangelo/toolboxes/ansatz_generator/ilc.py | 0 tangelo/toolboxes/ansatz_generator/qcc.py | 0 7 files changed, 29 insertions(+), 3 deletions(-) mode change 100755 => 100644 examples/linq/1.the_basics.ipynb mode change 100755 => 100644 tangelo/toolboxes/ansatz_generator/ilc.py mode change 100755 => 100644 tangelo/toolboxes/ansatz_generator/qcc.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c94fb5e3..505ae3129 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,34 @@ This file documents the main changes between versions of the code. +## [0.3.3] - 2022-11-09 + +### Added + +- Circuit translation from any supported source to any supported target format, with a single function +- Translation for qubit / Pauli operators for qiskit format +- All algorithms now run with any built-in or user-defined backend, simulator or QPU. +- TETRIS-ADAPT VQE +- iQCC-ILC +- Quantum signal processing time-evolution +- Higher even-order trotterization for time-evolution +- Histogram class, featuring methods for renormalization, post-selection, aggregation +- Computation of variance of expectation values +- Function to compute RDMs from experimental data / classical shadow +- IBMConnection Class for submission of experiments to IBM Quantum +- qchem_modelling_basics and excited_states notebooks + +### Changed + +- All notebooks now launchable with Google Collab +- Docker image updated + +### Deprecated + +- Simulator class deprecated in favor of get_backend function in linq +- backend-specific translate_xxx functions (e.g translate_qiskit, translate_qulacs...) deprecated in favor of translate_circuit in linq + + ## [0.3.2] - 2022-08-06 ### Added diff --git a/examples/linq/1.the_basics.ipynb b/examples/linq/1.the_basics.ipynb old mode 100755 new mode 100644 diff --git a/tangelo/_version.py b/tangelo/_version.py index 0c0f6aa1e..aba700655 100644 --- a/tangelo/_version.py +++ b/tangelo/_version.py @@ -14,4 +14,4 @@ """ Define version number here. It is read in setup.py, and bumped automatically when using the new release Github action. """ -__version__ = "0.3.2" +__version__ = "0.3.3" diff --git a/tangelo/algorithms/variational/sa_vqe_solver.py b/tangelo/algorithms/variational/sa_vqe_solver.py index fdf143074..15399952e 100644 --- a/tangelo/algorithms/variational/sa_vqe_solver.py +++ b/tangelo/algorithms/variational/sa_vqe_solver.py @@ -221,7 +221,6 @@ def get_resources(self): self.reference_circuits[0] + self.ansatz.circuit) resources["circuit_width"] = circuit.width resources["circuit_depth"] = circuit.depth() - # For now, only CNOTs supported. resources["circuit_2qubit_gates"] = circuit.counts_n_qubit.get(2, 0) resources["circuit_var_gates"] = len(self.ansatz.circuit._variational_gates) resources["vqe_variational_parameters"] = len(self.initial_var_params) diff --git a/tangelo/algorithms/variational/vqe_solver.py b/tangelo/algorithms/variational/vqe_solver.py index 380bfdccc..37c009555 100644 --- a/tangelo/algorithms/variational/vqe_solver.py +++ b/tangelo/algorithms/variational/vqe_solver.py @@ -266,7 +266,6 @@ def get_resources(self): circuit += self.deflation_circuits[0] resources["circuit_width"] = circuit.width resources["circuit_depth"] = circuit.depth() - # For now, only CNOTs supported. resources["circuit_2qubit_gates"] = circuit.counts_n_qubit.get(2, 0) resources["circuit_var_gates"] = len(self.ansatz.circuit._variational_gates) resources["vqe_variational_parameters"] = len(self.initial_var_params) diff --git a/tangelo/toolboxes/ansatz_generator/ilc.py b/tangelo/toolboxes/ansatz_generator/ilc.py old mode 100755 new mode 100644 diff --git a/tangelo/toolboxes/ansatz_generator/qcc.py b/tangelo/toolboxes/ansatz_generator/qcc.py old mode 100755 new mode 100644 From f45cdf553c1ff5f99642f599028f8de2ae723fc2 Mon Sep 17 00:00:00 2001 From: AlexandreF-1qbit <76115575+AlexandreF-1qbit@users.noreply.github.com> Date: Wed, 9 Nov 2022 15:11:33 -0500 Subject: [PATCH 02/14] Fix link of fig in qchem & excited states notebook (#250) --- examples/excited_states.ipynb | 8 ++++---- examples/qchem_modelling_basics.ipynb | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/excited_states.ipynb b/examples/excited_states.ipynb index a325af2b3..daf619f52 100644 --- a/examples/excited_states.ipynb +++ b/examples/excited_states.ipynb @@ -37,7 +37,7 @@ "source": [ "To be more concrete, a colorant must emit light in a narrow region in the visible spectrum to be appropriate for the purpose, that is to say it must exhibit a specific wavelength. Another example is solar panels, where the absorption spectrum of a molecule is tuned via chemical functionalization to fit the solar emission spectrum to optimize the energy output efficiency. Here we show an example of a spectrum for the BODIPY molecule, a molecule widely used for fluorescent dyes. BODIPY absorbs light at a lower wavelength (higher energy) and emits light at a higher wavelength (lower energy). To compute this spectrum, one needs to calculate the ground and excited state energies and calculate their intensities. The absorption spectrum for the simplest BODIPY is shown below. Different absorption and emission wavelengths can be targeted by substituting the hydrogen atoms with different functional groups [J. Chem. Phys. 155, 244102 (2021)](https://aip.scitation.org/doi/10.1063/5.0076787).\n", "\n", - "![BODIPY](https://drive.google.com/uc?id=1OTfF2-9tKZ6DvClbftDP1qWB9nNvRm6d)\n", + "![BODIPY](img/bodipy_absorption.png)\n", "\n", "As there are a very large number of compounds to be considered, predicting absorption/emission UV-visible spectra would be a valuable asset to the scientific community.\n", "\n", @@ -885,10 +885,10 @@ " \n", " \n", " \n", - " \n", + " \n", " \n", " \n", - " \n", + " \n", " \n", " \n", "\n" @@ -1254,7 +1254,7 @@ "The above plot shows promise that the correct energies indeed align with peaks in the success probability, despite our small number of iterations. To save time, below is the result after running the above code for 1000 iterations. The peaks are centered on the exact energies, represented by the vertical red dashed lines.\n", "\n", "\n", - "" + "" ] }, { diff --git a/examples/qchem_modelling_basics.ipynb b/examples/qchem_modelling_basics.ipynb index 245207581..1c88ea2e1 100644 --- a/examples/qchem_modelling_basics.ipynb +++ b/examples/qchem_modelling_basics.ipynb @@ -54,7 +54,7 @@ "\n", "The analysis of natural products is an efficient way of quickly getting inspiration for the design of new materials, while taking into account the million years of evolution that nature had to optimize a biochemical process. For instance, one difficult thing to do in the laboratory is to change the spin state of a compound during a chemical reaction. Nature circumvents this problem by leveraging transition metals to achieve this challenging task: the oxygen fixation process is achieved by the heme biomolecule, where a Fe(II) atom is involved in the spin state change mechanism (see figure below) [[10.1074/jbc.M314007200](https://doi.org/10.1074/jbc.M314007200)].\n", "\n", - "![FeIIPorImO2 system](https://drive.google.com/uc?id=17J4NNTvISeAqrNczZDWvGAKlCu0HpA6O)\n", + "![FeIIPorImO2 system](img/FeIIPorImO2.png)\n", "\n", "Fully understanding the mechanism of oxygen fixation, that is to say knowing the rate constants for all elementary reactions, would lead to valuable insights for the design of chemical catalysts. Achieving this goal would take us one step closer to [*making the world cleaner, healthier, and more sustainable*](https://goodchemistry.com/). This is one of the main applications of quantum chemistry. However, the Schrödinger equation shown below cannot be solved exactly for systems beyond one electron, i.e. all relevant chemical systems for industrial applications.\n", "\n", @@ -628,7 +628,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.9" + "version": "3.10.6" } }, "nbformat": 4, From e58c8b0dca4fafaf03b14c815d292dee9064f067 Mon Sep 17 00:00:00 2001 From: James Brown <84878946+JamesB-1qbit@users.noreply.github.com> Date: Mon, 14 Nov 2022 23:07:58 -0500 Subject: [PATCH 03/14] allow single flip index dis for qcc (#247) * allow single flip index dis for jkmn --- .../algorithms/variational/tests/test_iqcc_ilc_solver.py | 2 +- tangelo/algorithms/variational/tests/test_iqcc_solver.py | 2 +- tangelo/toolboxes/ansatz_generator/_qubit_cc.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tangelo/algorithms/variational/tests/test_iqcc_ilc_solver.py b/tangelo/algorithms/variational/tests/test_iqcc_ilc_solver.py index 271b4cd84..5d06a10bb 100644 --- a/tangelo/algorithms/variational/tests/test_iqcc_ilc_solver.py +++ b/tangelo/algorithms/variational/tests/test_iqcc_ilc_solver.py @@ -62,7 +62,7 @@ def test_iqcc_ilc_h4(self): iqcc_ilc_solver.build() iqcc_ilc_energy = iqcc_ilc_solver.simulate() - self.assertAlmostEqual(iqcc_ilc_energy, -1.976817, places=4) + self.assertAlmostEqual(iqcc_ilc_energy, -1.9786, places=3) def test_iqcc_ilc_h4_cation(self): """Test the energy after 2 iterations for H4+ using the maximum diff --git a/tangelo/algorithms/variational/tests/test_iqcc_solver.py b/tangelo/algorithms/variational/tests/test_iqcc_solver.py index 0a36fc8fd..b2a6cb07c 100644 --- a/tangelo/algorithms/variational/tests/test_iqcc_solver.py +++ b/tangelo/algorithms/variational/tests/test_iqcc_solver.py @@ -85,7 +85,7 @@ def test_iqcc_h4_cation(self): iqcc.build() iqcc_energy = iqcc.simulate() - self.assertAlmostEqual(iqcc_energy, -1.638524, places=4) + self.assertAlmostEqual(iqcc_energy, -1.639, places=3) def test_iqcc_h4_double_cation(self): """Test the energy after 1 iteration for H4+2""" diff --git a/tangelo/toolboxes/ansatz_generator/_qubit_cc.py b/tangelo/toolboxes/ansatz_generator/_qubit_cc.py index 4a0d5d74f..7d165b74a 100644 --- a/tangelo/toolboxes/ansatz_generator/_qubit_cc.py +++ b/tangelo/toolboxes/ansatz_generator/_qubit_cc.py @@ -130,8 +130,8 @@ def get_idxs_deriv(qham_term, *qham_qmf_data): gen = (idx, "Y") if idxs == "" else (idx, "X") idxs = idxs + f" {idx}" if idxs != "" else f"{idx}" gen_tup += (gen, ) - # Generators must have at least two flip indices - if len(gen_tup) > 1: + # Generators must have at least one flip index + if len(gen_tup) > 0: qham_gen_comm = QubitOperator(qham_term, -1j * coef) qham_gen_comm *= QubitOperator(gen_tup, 1.) deriv = get_op_expval(qham_gen_comm, pure_params).real @@ -152,7 +152,7 @@ def get_gens_from_idxs(group_idxs): """ dis_group_gens = [] - for n_y in range(1, len(group_idxs), 2): + for n_y in range(1, len(group_idxs)+1, 2): # Create combinations of odd numbers of flip indices for the Pauli Y operators for xy_idx in combinations(group_idxs, n_y): # If a flip index idx matches xy_idx, add a Y operator From 196d2d78e854f92174e5cfc0dca7bcab90c18f97 Mon Sep 17 00:00:00 2001 From: KrzysztofB-1qbit <86750444+KrzysztofB-1qbit@users.noreply.github.com> Date: Mon, 21 Nov 2022 11:22:20 -0800 Subject: [PATCH 04/14] Richardson extrapolation: bug fix + error estimation (#252) --- tangelo/toolboxes/post_processing/__init__.py | 2 +- .../post_processing/extrapolation.py | 80 +++++++++++++------ .../tests/test_extrapolation.py | 45 +++++++---- 3 files changed, 88 insertions(+), 39 deletions(-) diff --git a/tangelo/toolboxes/post_processing/__init__.py b/tangelo/toolboxes/post_processing/__init__.py index bbcb504e1..30df66ca7 100644 --- a/tangelo/toolboxes/post_processing/__init__.py +++ b/tangelo/toolboxes/post_processing/__init__.py @@ -14,5 +14,5 @@ from .histogram import Histogram, aggregate_histograms, filter_hist from .mc_weeny_rdm_purification import mcweeny_purify_2rdm -from .extrapolation import diis, richardson +from .extrapolation import diis, richardson, extrapolation from .post_selection import ancilla_symmetry_circuit, post_select, strip_post_selection diff --git a/tangelo/toolboxes/post_processing/extrapolation.py b/tangelo/toolboxes/post_processing/extrapolation.py index f9bf8f048..2ebe76d52 100644 --- a/tangelo/toolboxes/post_processing/extrapolation.py +++ b/tangelo/toolboxes/post_processing/extrapolation.py @@ -16,53 +16,62 @@ import scipy.optimize as sp -def diis(energies, coeffs): +def diis(coeffs, energies, stderr=None): """ - DIIS extrapolation, originally developped by Pulay in + DIIS extrapolation, originally developed by Pulay in Chemical Physics Letters 73, 393-398 (1980) Args: - energies (array-like): Energy expectation values for amplified noise rates coeffs (array-like): Noise rate amplification factors + energies (array-like): Energy expectation values for amplified noise rates + stderr (array-like, optional): Energy standard error estimates Returns: float: Extrapolated energy + float: Error estimation for extrapolated energy """ - return extrapolation(energies, coeffs, 1) + return extrapolation(coeffs, energies, stderr, 1) -def richardson(energies, coeffs, estimate_exp=False): +def richardson(coeffs, energies, stderr=None, estimate_exp=False): """ General, DIIS-like extrapolation procedure as found in Nature 567, 491-495 (2019) [arXiv:1805.04492] Args: - energies (array-like): Energy expectation values for amplified noise rates coeffs (array-like): Noise rate amplification factors + energies (array-like): Energy expectation values for amplified noise rates + stderr (array-like, optional): Energy standard error estimates + estimate_exp (bool, optional): Choose to estimate exponent in the Richardson method. Default is False. Returns: float: Extrapolated energy + float: Error estimation for extrapolated energy """ - if estimate_exp is False: + if not estimate_exp: # If no exponent estimation, run the direct Richardson solution - return richardson_analytical(energies, coeffs) + return richardson_analytical(coeffs, energies, stderr) else: # For exponent estimation run the Richardson recursive algorithm - return richardson_with_exp_estimation(energies, coeffs) + return richardson_with_exp_estimation(coeffs, energies, stderr) -def extrapolation(energies, coeffs, taylor_order=None): +def extrapolation(coeffs, energies, stderr=None, taylor_order=None): """ General, DIIS-like extrapolation procedure as found in Nature 567, 491-495 (2019) [arXiv:1805.04492] Args: - energies (array-like): Energy expectation values for amplified noise rates coeffs (array-like): Noise rate amplification factors - taylor_order (int): Taylor expansion order; None for Richardson extrapolation (order determined from number of datapoints), 1 for DIIS extrapolation + energies (array-like): Energy expectation values for amplified noise rates + stderr (array-like, optional): Energy standard error estimates + taylor_order (int, optional): Taylor expansion order; + None for Richardson extrapolation (order determined from number of datapoints), + 1 for DIIS extrapolation Returns: float: Extrapolated energy + float: Error estimation for extrapolated energy """ n = len(coeffs) if taylor_order is None: @@ -71,55 +80,70 @@ def extrapolation(energies, coeffs, taylor_order=None): Eh = np.array(energies) coeffs = np.array(coeffs) - # Setup the linear system matrix + # Set up the linear system matrix ck = np.array([coeffs**k for k in range(1, taylor_order+1)]) B = np.ones((n+1, n+1)) B[n, n] = 0 B[:n, :n] = ck.T @ ck - # Setup the free coefficients + # Set up the free coefficients b = np.zeros(n+1) b[n] = 1 # For the Lagrange multiplier # Solve the DIIS equations by least squares - x = np.linalg.lstsq(B, b, rcond=None)[0] - return np.dot(Eh, x[:-1]) + x = np.linalg.lstsq(B, b, rcond=None)[0][:-1] + + if stderr is None: + return np.dot(Eh, x) + else: + stderr = np.array(stderr) + return np.dot(Eh, x), np.sqrt(np.dot(stderr**2, x**2)) -def richardson_analytical(energies, coeffs): +def richardson_analytical(coeffs, energies, stderr=None): """ Richardson extrapolation explicit result as found in - Phys. Rev. Lett. 119, 180509 [arXiv:1612.02058] + Phys. Rev. Lett. 119, 180509 [arXiv:1612.02058] (up to sign difference) Args: - energies (array-like): Energy expectation values for amplified noise rates coeffs (array-like): Noise rate amplification factors + energies (array-like): Energy expectation values for amplified noise rates + stderr (array-like, optional): Energy standard error estimates Returns: float: Extrapolated energy + float: Error estimation for extrapolated energy """ Eh = np.array(energies) ck = np.array(coeffs) - x = np.array([np.prod(ai/(a - ai)) for i, a in enumerate(ck) - for ai in [np.delete(ck, i)]]) - return np.dot(Eh, x) + x = np.array([np.prod(ai / (ai - a)) for i, a in enumerate(ck) for ai in [np.delete(ck, i)]]) + + if stderr is None: + return np.dot(Eh, x) + else: + stderr = np.array(stderr) + return np.dot(Eh, x), np.sqrt(np.dot(stderr**2, x**2)) -def richardson_with_exp_estimation(energies, coeffs): +def richardson_with_exp_estimation(coeffs, energies, stderr=None): """ Richardson extrapolation by recurrence, with exponent estimation Args: energies (array-like): Energy expectation values for amplified noise rates coeffs (array-like): Noise rate amplification factors + stderr (array-like, optional): Energy standard error estimates Returns: float: Extrapolated energy + float: Error estimation for extrapolated energy """ n = len(coeffs) Eh = np.array(energies) c = np.array(coeffs) ck = np.array(coeffs) + if stderr is not None: + stderr = np.array(stderr) p, p_old = 1, 0 # Define a helper function for exponent optimization @@ -153,6 +177,14 @@ def energy_diff(k, ti, si): ti = (ck[j]/ck[j+1]) else: ck[j] = ck[j]*(c[j] - c[j+1])/(ti - 1) + Eh[j] = (ti*Eh[j+1] - Eh[j])/(ti - 1) + if stderr is not None: + stderr[j] = np.sqrt(((ti*stderr[j+1])**2 + stderr[j]**2)/(ti - 1)**2) + p_old = p - return(Eh[0]) + + if stderr is None: + return Eh[0] + else: + return Eh[0], stderr[0] diff --git a/tangelo/toolboxes/post_processing/tests/test_extrapolation.py b/tangelo/toolboxes/post_processing/tests/test_extrapolation.py index 6f8ffd88f..fc430c7cf 100644 --- a/tangelo/toolboxes/post_processing/tests/test_extrapolation.py +++ b/tangelo/toolboxes/post_processing/tests/test_extrapolation.py @@ -13,29 +13,46 @@ # limitations under the License. import unittest -import numpy as np -from tangelo.toolboxes.post_processing import diis, richardson +from tangelo.toolboxes.post_processing import diis, richardson, extrapolation -energies = [-1.04775574, -1.04302289, -1.03364568, -1.03005245] -coeffs = [1., 1.1, 1.2, 1.3] +energies = [-1.1070830819357105, -1.0778342538877541, -1.0494855002828576, -1.0220085207923948, + -0.995365932747342, -0.9695424717692709, -0.9445011607426314] +errors = [0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07] +coeffs = [1, 2, 3, 4, 5, 6, 7] class ExtrapolationTest(unittest.TestCase): def test_diis(self): - """Test DIIS extrapolation on small sample data - """ - calculated = diis(energies, coeffs) - self.assertAlmostEqual(-1.11047933, calculated, delta=1e-6) + """Test DIIS extrapolation on small sample data""" + diis_ref = [-1.1357318603549604, -1.13499594848339, -1.1341334673708405, -1.1331451262769456, -1.1320385453585242] + err_ref = [0.024944382578492966, 0.02449489742783177, 0.02481934729198171, 0.025438378704451894, 0.026186146828319073] + for n, ref, eref in zip(range(3, 8), diis_ref, err_ref): + calc, err = diis(coeffs[:n], energies[:n], errors[:n]) + self.assertAlmostEqual(ref, calc, delta=1e-6) + self.assertAlmostEqual(eref, err, delta=1e-4) def test_richardson(self): - """Test Richardson extrapolation on small sample data - """ - calculated = richardson(energies, coeffs) - self.assertAlmostEqual(-1.45459036, calculated, delta=1e-6) - calculated = richardson(energies, coeffs, estimate_exp=True) - self.assertAlmostEqual(-1.05601603, calculated, delta=1e-6) + """Test Richardson extrapolation on small sample data""" + rich_ref = [-1.1372319844267267, -1.1372602847553528, -1.137251202414955, -1.137220001783962, -1.1371449701252567] + err_ref = [0.07348469228349534, 0.17888543819998318, 0.4183300132670378, 0.9524704719832527, 2.127815781499893] + for n, ref, eref in zip(range(3, 8), rich_ref, err_ref): + calc, err = richardson(coeffs[:n], energies[:n], errors[:n]) + self.assertAlmostEqual(ref, calc, delta=1e-6) + self.assertAlmostEqual(eref, err, delta=1e-4) + extr, erre = extrapolation(coeffs[:n], energies[:n], errors[:n]) + self.assertAlmostEqual(ref, extr, delta=1e-6) + self.assertAlmostEqual(eref, erre, delta=1e-4) + + def test_richardson_exp(self): + """Test Richardson extrapolation with exponent estimation on small sample data""" + rich_ref = [-1.1168326912850293, -1.1216325249654286, -1.1222201155004157, -1.1297496614161582, -1.1689623909539615] + err_ref = [0.014907119849998597, 0.023110652702992267, 0.04009450743442095, 0.1353768232960827, 0.35052067817606436] + for n, ref, erref in zip(range(3, 8), rich_ref, err_ref): + calc, err = richardson(coeffs[:n], energies[:n], errors, estimate_exp=True) + self.assertAlmostEqual(ref, calc, delta=1e-6) + self.assertAlmostEqual(erref, err, delta=1e-6) if __name__ == "__main__": From 6a5e7635d34af4f9d10b234e06f2f91177c619ad Mon Sep 17 00:00:00 2001 From: Alexandre Fleury <76115575+AlexandreF-1qbit@users.noreply.github.com> Date: Wed, 23 Nov 2022 10:53:43 -0500 Subject: [PATCH 05/14] Bugfix: DMET with QCC (#253) --- .../dmet/dmet_problem_decomposition.py | 3 +++ tangelo/toolboxes/ansatz_generator/qcc.py | 27 ++++++++++--------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/tangelo/problem_decomposition/dmet/dmet_problem_decomposition.py b/tangelo/problem_decomposition/dmet/dmet_problem_decomposition.py index 0d2d2f33a..e9b211cd1 100644 --- a/tangelo/problem_decomposition/dmet/dmet_problem_decomposition.py +++ b/tangelo/problem_decomposition/dmet/dmet_problem_decomposition.py @@ -176,6 +176,9 @@ def __init__(self, opt_dict): self.orb_list2 = None self.onerdm_low = None + # If save_results in _oneshot_loop is True, the dict is populated. + self.solver_fragment_dict = dict() + @property def quantum_fragments_data(self): """This aims to return a dictionary with all necessary components to diff --git a/tangelo/toolboxes/ansatz_generator/qcc.py b/tangelo/toolboxes/ansatz_generator/qcc.py index 179f85443..4e5605a65 100644 --- a/tangelo/toolboxes/ansatz_generator/qcc.py +++ b/tangelo/toolboxes/ansatz_generator/qcc.py @@ -40,7 +40,6 @@ from tangelo.toolboxes.qubit_mappings.mapping_transform import get_qubit_number,\ fermion_to_qubit_mapping from tangelo.linq import Circuit -from tangelo import SecondQuantizedMolecule from tangelo.toolboxes.ansatz_generator.ansatz import Ansatz from tangelo.toolboxes.ansatz_generator.ansatz_utils import exp_pauliword_to_gates from tangelo.toolboxes.ansatz_generator._qubit_mf import init_qmf_from_hf, get_qmf_circuit, purify_qmf_state @@ -55,9 +54,11 @@ class QCC(Ansatz): state is obtained using a RHF or ROHF Hamiltonian, respectively. Args: - molecule (SecondQuantizedMolecule or dict): The molecular system, which can - be passed as a SecondQuantizedMolecule or a dictionary with keys that - specify n_spinoribtals, n_electrons, and spin. Default, None. + molecule (SecondQuantizedMolecule, SecondQuantizedDMETFragment or dict): + The molecular system, which can be passed as a + SecondQuantizedMolecule/SecondQuantizedDMETFragment or a dictionary + with keys that specify n_spinoribtals, n_electrons, and spin. + Default, None. mapping (str): One of the supported qubit mapping identifiers. Default, "jw". up_then_down (bool): Change basis ordering putting all spin-up orbitals first, followed by all spin-down. Default, False. @@ -86,18 +87,20 @@ def __init__(self, molecule, mapping="jw", up_then_down=False, dis=None, qmf_circuit=None, qmf_var_params=None, qubit_ham=None, qcc_tau_guess=1e-2, deqcc_dtau_thresh=1e-3, max_qcc_gens=None, reference_state="HF"): - if not molecule and not (isinstance(molecule, SecondQuantizedMolecule) and isinstance(molecule, dict)): - raise ValueError("An instance of SecondQuantizedMolecule or a dict is required for " - "initializing the self.__class__.__name__ ansatz class.") + if isinstance(molecule, dict) and not qubit_ham: + raise ValueError(f"An instance of SecondQuantizedMolecule or a dict" + " + qubit operator is required for initializing the " + f"{self.__class__.__name__} ansatz class.") + self.molecule = molecule - if isinstance(self.molecule, SecondQuantizedMolecule): - self.n_spinorbitals = self.molecule.n_active_sos - self.n_electrons = self.molecule.n_active_electrons - self.spin = self.molecule.spin - elif isinstance(self.molecule, dict): + if isinstance(self.molecule, dict): self.n_spinorbitals = self.molecule["n_spinorbitals"] self.n_electrons = self.molecule["n_electrons"] self.spin = self.molecule["spin"] + else: + self.n_spinorbitals = self.molecule.n_active_sos + self.n_electrons = self.molecule.n_active_electrons + self.spin = self.molecule.spin self.mapping = mapping self.up_then_down = up_then_down From 9af6682ab1fed065256fba699d4ed51ce6106c00 Mon Sep 17 00:00:00 2001 From: James Brown <84878946+JamesB-1qbit@users.noreply.github.com> Date: Wed, 23 Nov 2022 11:26:53 -0500 Subject: [PATCH 06/14] iQCC using only Clifford circuits notebook (#254) --- examples/iqcc_using_clifford.ipynb | 598 +++++++++++++++++++++++++++++ 1 file changed, 598 insertions(+) create mode 100644 examples/iqcc_using_clifford.ipynb diff --git a/examples/iqcc_using_clifford.ipynb b/examples/iqcc_using_clifford.ipynb new file mode 100644 index 000000000..eae74da66 --- /dev/null +++ b/examples/iqcc_using_clifford.ipynb @@ -0,0 +1,598 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Iterative Qubit Coupled Cluster using only Clifford circuits\n", + "\n", + "This notebook shows how to implement iQCC using only Clifford circuits with Tangelo, and accompanies a note released at [arXiv:2211.10501](https://arxiv.org/abs/2211.10501). We provide here the abstract of the article in this notebook for convenience and highlight the main steps of our implementation. Please refer to the article for full details.\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/goodchemistryco/Tangelo/blob/develop/examples/iqcc_using_clifford.ipynb)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Abstract\n", + "We draw attention to a variant of the iterative qubit coupled cluster (iQCC) method that only\n", + "uses Clifford circuits. The iQCC method relies on a small parameterized wave function ansatz, which\n", + "takes form as a product of exponentiated Pauli word operators, to approximate the ground state\n", + "electronic energy of a mean field reference state through iterative qubit Hamiltonian transformations.\n", + "In this variant of the iQCC method, the wave function ansatz at each iteration is restricted to a single\n", + "exponentiated Pauli word operator and parameter. The Rotosolve algorithm utilizes Hamiltonian\n", + "expectation values computed with Clifford circuits to optimize the single-parameter Pauli word\n", + "ansatz. Although the exponential growth of Hamiltonian terms is preserved with this variation\n", + "of iQCC, we suggest several methods to mitigate this effect. This method is useful for near-term\n", + "variational quantum algorithm applications as it generates good initial parameters by using Clifford\n", + "circuits which can be efficiently simulated on a classical computers according to the Gottesman–Knill\n", + "theorem. It may also be useful beyond the NISQ era to create short-depth Clifford pre-optimized\n", + "circuits that improve the success probability for fault-tolerant algorithms such as phase estimation." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Installation of tangelo if not already installed.\n", + "try:\n", + " import tangelo\n", + "except ModuleNotFoundError:\n", + " !pip install git+https://github.com/goodchemistryco/Tangelo.git@develop --quiet" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define Clifford simulator\n", + "\n", + "We define a Clifford simulator using cirq. This is a child class of `CirqSimulator` where we overwrite `simulate_circuit` and use `cirq.CliffordSimulator`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from tangelo.linq import Circuit\n", + "from tangelo.linq.target.target_cirq import CirqSimulator\n", + "from tangelo.linq.target.backend import Backend\n", + "from tangelo.linq.translator import translate_circuit as translate\n", + "\n", + "class CirqCliffordSimulator(CirqSimulator):\n", + "\n", + " def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, initial_statevector=None):\n", + " \"\"\"Perform state preparation corresponding to the input circuit using cirq.CliffordSimulator\n", + "\n", + " Args:\n", + " source_circuit: a circuit in the Tangelo format to be translated\n", + " return_statevector (bool): option to return the statevector.\n", + " initial_statevector(list/array) : Not currently supported\n", + "\n", + " Returns:\n", + " dict: A dictionary mapping multi-qubit states to their corresponding frequency.\n", + " numpy.array: The statevector, if available for the target backend\n", + " and requested by the user (if not, set to None).\n", + " \"\"\"\n", + "\n", + " cirq_circuit = translate(source_circuit, \"cirq\")\n", + " self.circuit = cirq_circuit\n", + "\n", + " cirq_simulator = self.cirq.CliffordSimulator()\n", + "\n", + " self.result = cirq_simulator.simulate(cirq_circuit)\n", + " self._current_state = self.result.final_state.to_numpy()\n", + " frequencies = self._statevector_to_frequencies(self._current_state)\n", + "\n", + " # If requested, set initial state\n", + " if initial_statevector is not None:\n", + " raise ValueError(f\"Initial statevector is not currently supported in {self.__class__}\")\n", + "\n", + " return (frequencies, self._current_state) if return_statevector else (frequencies, None)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Class that implements iQCC using only Clifford circuits\n", + "The `iQCConlyClifford` class is defined as a child class of `ADAPTSolver` and replaces the `simulate` and `rank_pool` functions. A non-clifford backend is used only to check that the full circuit obtains the same energy using the original qubit hamiltonian." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Type\n", + "from copy import deepcopy\n", + "\n", + "from tangelo.algorithms import ADAPTSolver\n", + "from tangelo.linq import Circuit\n", + "from tangelo.toolboxes.operators import QubitOperator\n", + "from tangelo.toolboxes.ansatz_generator.ansatz_utils import get_exponentiated_qubit_operator_circuit\n", + "from tangelo.toolboxes.molecular_computation.molecule import SecondQuantizedMolecule\n", + "from tangelo.toolboxes.qubit_mappings.mapping_transform import fermion_to_qubit_mapping\n", + "from tangelo.toolboxes.ansatz_generator._qubit_cc import get_dis_groups, get_gens_from_idxs\n", + "from tangelo.toolboxes.ansatz_generator._qubit_mf import init_qmf_from_hf\n", + "from tangelo.toolboxes.ansatz_generator._qubit_cc import qcc_op_dress\n", + "\n", + "\n", + "class iQCConlyClifford(ADAPTSolver):\n", + " \"\"\"iQCC using only Clifford Class. This is an iterative algorithm that uses Clifford circuits\n", + " along with Rotosolve to rank single Pauli word operators and obtain its optimal energy and rotation.\n", + "\n", + " Attributes:\n", + " molecule (SecondQuantizedMolecule): The molecular system.\n", + " tol (float): Maximum gradient allowed for a particular operator before\n", + " convergence.\n", + " max_cycles (int): Maximum number of iterations for ADAPT.\n", + " pool (func): Function that returns a list of FermionOperator. Each\n", + " element represents excitation/operator that has an effect of the\n", + " total energy.\n", + " pool_args (dict) : The arguments for the pool function. Will be unpacked in\n", + " function call as pool(**pool_args)\n", + " qubit_mapping (str): One of the supported qubit mapping identifiers.\n", + " qubit_hamiltonian (QubitOperator-like): Self-explanatory.\n", + " up_then_down (bool): Spin orbitals ordering.\n", + " n_spinorbitals (int): Self-explanatory.\n", + " n_electrons (int): Self-explanatory.\n", + " optimizer (func): Optimization function for VQE minimization.\n", + " backend_options (dict): Backend options for the underlying VQE object.\n", + " verbose (bool): Flag for verbosity of VQE.\n", + " deflation_circuits (list[Circuit]): Deflation circuits to add an\n", + " orthogonalization penalty with.\n", + " deflation_coeff (float): The coefficient of the deflation.\n", + " ref_state (array or Circuit): The reference configuration to use. Replaces HF state\n", + " clifford_simulator(Type[Backend]): The clifford simulator used for the operator selection\n", + " and minimization.\n", + " \"\"\"\n", + "\n", + " def __init__(self, opt_dict: dict, clifford_simulator: Type[Backend]):\n", + " super().__init__(opt_dict=opt_dict)\n", + " self.clifford_simulator = clifford_simulator\n", + " self.qu_op_length = []\n", + "\n", + " def rank_pool(self, circuit: Circuit, backend: Type[Backend]):\n", + " \"\"\"Rank pool of operators with a specific circuit.\n", + "\n", + " Args:\n", + " reference_circuit (tangelo.linq.Circuit): Reference circuit that only uses Clifford gates\n", + " backend (tangelo.linq.backend): Clifford backend to compute expectation values as child class of Backend\n", + "\n", + " Returns:\n", + " int: Index of the operators with the highest gradient. If it is not\n", + " bigger than tolerance, returns -1.\n", + " \"\"\"\n", + "\n", + " if len(self.vqe_solver.ansatz.operators) > 0:\n", + " dressed_qu_op = qcc_op_dress(deepcopy(self.qubit_hamiltonian), self.vqe_solver.ansatz.operators[::-1],\n", + " self.vqe_solver.optimal_var_params[::-1])\n", + " self.energies[-1] = backend.get_expectation_value(dressed_qu_op, self.vqe_solver.ansatz.prepare_reference_state())\n", + " print(\"Dressed Hamiltonian, same initial state energy\", self.energies[-1])\n", + " else:\n", + " dressed_qu_op = self.qubit_hamiltonian\n", + "\n", + " self.qu_op_length.append(len(dressed_qu_op.terms))\n", + " self.pool_operators = self.pool(self.molecule, self.qubit_mapping, self.up_then_down, dressed_qu_op)\n", + "\n", + " len_pool = len(self.pool_operators)\n", + " thetas = np.zeros(len_pool)\n", + " eners = np.zeros(len_pool)\n", + " for i, pool_op in enumerate(self.pool_operators):\n", + " # get_exponentiated_qubit_operator_circuit multiplies time by 2\n", + " cpl = get_exponentiated_qubit_operator_circuit(pool_op, time=np.pi/4)\n", + " cmi = get_exponentiated_qubit_operator_circuit(pool_op, time=-np.pi/4)\n", + "\n", + " epl = backend.get_expectation_value(dressed_qu_op, circuit+cpl)\n", + " emi = backend.get_expectation_value(dressed_qu_op, circuit+cmi)\n", + "\n", + " ener = self.energies[-1]\n", + " theta_min = -0.5 * np.pi - np.arctan2(2. * ener - epl - emi, epl - emi)\n", + " a = 0.5*np.sqrt((2*ener-epl-emi)**2+(epl-emi)**2)\n", + " b = np.arctan2(2. * ener - epl - emi, epl - emi)\n", + " c = 1/2*(epl+emi)\n", + " eners[i] = a*np.sin(theta_min+b)+c\n", + " thetas[i] = theta_min\n", + "\n", + " index = np.argmin(eners)\n", + " self.new_param = thetas[index]\n", + " if self.verbose:\n", + " print(f'Chosen parameter is {self.new_param} with energy {eners[index]}')\n", + "\n", + " return index if self.energies[-1]-eners[index] > self.tol else -1\n", + "\n", + " def simulate(self):\n", + " \"\"\"Performs the iQCC cycles. No VQE minimization is performed but there is a check of the full circuit\n", + " with the initial Hamiltonian\n", + " \"\"\"\n", + "\n", + " params = self.vqe_solver.ansatz.var_params\n", + " self.new_param = 0.\n", + "\n", + " self.energies.append(self.vqe_solver.energy_estimation([]))\n", + "\n", + " # Construction of the ansatz. self.max_cycles terms are added, unless\n", + " # all operator gradients are less than self.tol.\n", + " while self.iteration < self.max_cycles:\n", + " self.iteration += 1\n", + " if self.verbose:\n", + " print(f\"\\n Iteration {self.iteration} of iQCC using only Clifford circuits.\")\n", + "\n", + " ref_circuit = (self.vqe_solver.ansatz.prepare_reference_state() if self.ref_state is None else\n", + " self.vqe_solver.reference_circuit + self.vqe_solver.ansatz.prepare_reference_state())\n", + "\n", + " pool_select = self.rank_pool(ref_circuit,\n", + " backend=self.clifford_simulator)\n", + "\n", + " # If pool selection returns an operator that changes the energy by\n", + " # more than self.tol. Else, the loop is complete and the energy is\n", + " # considered as converged.\n", + " if pool_select > -1:\n", + "\n", + " # Adding a new operator to beginning of operator list\n", + " # Previous parameters are kept as they were.\n", + " self.vqe_solver.ansatz.operators = [self.pool_operators[pool_select]] + self.vqe_solver.ansatz.operators\n", + " params = [self.new_param] if self.vqe_solver.optimal_var_params is None else [self.new_param] + list(self.vqe_solver.optimal_var_params)\n", + " \n", + "\n", + " self.vqe_solver.ansatz._n_terms_operators = [1] + self.vqe_solver.ansatz._n_terms_operators\n", + " self.vqe_solver.ansatz._var_params_prefactor = [1] + self.vqe_solver.ansatz._var_params_prefactor\n", + " self.vqe_solver.initial_var_params = params\n", + " self.vqe_solver.ansatz.build_circuit()\n", + "\n", + " # Non-clifford simulator used to verify that the energy is the same as determined using the Clifford simulator.\n", + " self.vqe_solver.optimal_energy = self.vqe_solver.energy_estimation(params)\n", + " if self.verbose:\n", + " print(f\"Full circuit with original Hamiltonian energy = {self.vqe_solver.optimal_energy}\")\n", + " self.vqe_solver.optimal_var_params = params\n", + "\n", + " opt_energy = self.vqe_solver.optimal_energy\n", + " params = list(self.vqe_solver.optimal_var_params)\n", + " self.energies.append(opt_energy)\n", + " else:\n", + " self.converged = True\n", + " break\n", + "\n", + " return self.energies[-1]\n", + "\n", + "def full_qcc_pool(mol: SecondQuantizedMolecule, mapping: str, up_then_down: bool, qubit_hamiltonian: QubitOperator):\n", + " \"\"\"Generate all possible generators in the DIS for qubit_hamiltonian\n", + " \n", + " Args:\n", + " mol (SecondQuantizedMolecule): The molecule to determine the DIS pool for.\n", + " mapping (str): One of the support qubit mappings\n", + " up_then_down (bool): Spin orbitals ordering. True is all alpha then beta orbitals. False is alternating.\n", + " qubit_hamiltonian (QubitOperator): The current dressed qubit hamiltonian.\n", + "\n", + " Returns:\n", + " list: DIS pool operators\n", + " \"\"\"\n", + " qmf_var_params = init_qmf_from_hf(mol.n_active_sos, mol.n_active_electrons, \n", + " mapping, up_then_down, mol.spin)\n", + "\n", + " dis, dis_groups = [], get_dis_groups(qubit_hamiltonian, qmf_var_params, 3.e-5)\n", + " for dis_group in dis_groups:\n", + " dis_group_idxs = [int(idxs) for idxs in dis_group[0].split(\" \")]\n", + " dis_group_gens = get_gens_from_idxs(dis_group_idxs)\n", + " dis.append(dis_group_gens)\n", + " dis_flat = [item for sublist in dis for item in sublist]\n", + " return dis_flat\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Running the algorithm for H3" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + " Iteration 1 of iQCC using only Clifford circuits.\n", + "Chosen parameter is 0.19497620390733506 with energy -1.5146298806979668\n", + "Full circuit with original Hamiltonian energy = -1.5146298806979654\n", + "\n", + " Iteration 2 of iQCC using only Clifford circuits.\n", + "Dressed Hamiltonian, same initial state energy -1.5146298806979663\n", + "Chosen parameter is -0.10349849269458078 with energy -1.5218965900518353\n", + "Full circuit with original Hamiltonian energy = -1.5218965900518329\n", + "\n", + " Iteration 3 of iQCC using only Clifford circuits.\n", + "Dressed Hamiltonian, same initial state energy -1.521896590051836\n", + "Chosen parameter is 0.08657354901273173 with energy -1.5244271035716803\n", + "Full circuit with original Hamiltonian energy = -1.5244271035716768\n", + "\n", + " Iteration 4 of iQCC using only Clifford circuits.\n", + "Dressed Hamiltonian, same initial state energy -1.5244271035716799\n", + "Chosen parameter is -0.08225117291478923 with energy -1.5267582996827964\n", + "Full circuit with original Hamiltonian energy = -1.526758299682792\n", + "\n", + "The pool operators used are\n", + "[1.0 [Y1 X4], 1.0 [Y5], 1.0 [Y1 X4 X5], 1.0 [Y0 X2 X3 X4]]\n", + "with corresponding parameters\n", + "[-0.08225117291478923, 0.08657354901273173, -0.10349849269458078, 0.19497620390733506]\n", + "\n", + " The final energy is -1.526758299682792 with error 0.0001887054536875432\n", + "\n", + " The number of H terms at each iteration is\n", + "[62, 96, 125, 138]\n" + ] + } + ], + "source": [ + "from tangelo.toolboxes.molecular_computation.molecule import SecondQuantizedMolecule\n", + "from tangelo.algorithms.classical import FCISolver\n", + "\n", + "# Define molecular system\n", + "xyz_H3 = [\n", + " (\"H\", (0., 0., 0.)),\n", + " (\"H\", (0., 0., 0.7414)),\n", + " (\"H\", (0., 0., 2*0.7414))\n", + "]\n", + "mol = SecondQuantizedMolecule(xyz_H3, q=0, spin=1, basis=\"sto-3g\")\n", + "max_cycles = 4\n", + "\n", + "# Calculate reference FCI energy\n", + "cc = FCISolver(mol)\n", + "exact = cc.simulate()\n", + "\n", + "mapping = \"JKMN\"\n", + "\n", + "qu_op = fermion_to_qubit_mapping(mol.fermionic_hamiltonian, mapping, mol.n_active_sos, mol.n_active_electrons, up_then_down=False, spin=mol.active_spin)\n", + "pool_args = {\"mol\": mol, \"mapping\": mapping, \"up_then_down\": False, \"qubit_hamiltonian\": qu_op}\n", + "\n", + "backend_options = {} # {\"target\": QulacsSimulator}\n", + "opt_dict = {\"molecule\": mol, \"tol\": 1.e-9, \"max_cycles\": max_cycles, \"verbose\": True,\n", + " \"qubit_mapping\": mapping, \"n_spinorbitals\": mol.n_active_sos, \"n_electrons\": mol.n_active_electrons, \n", + " \"pool\": full_qcc_pool, \"pool_args\": pool_args, \"up_then_down\": False, \"backend_options\": backend_options}\n", + "\n", + "iQCC_C_solver = iQCConlyClifford(opt_dict, CirqCliffordSimulator())\n", + "iQCC_C_solver.build()\n", + "iQCC_C_solver.simulate()\n", + "print(\"\\nThe pool operators used are\")\n", + "print(iQCC_C_solver.ansatz.operators)\n", + "print(\"with corresponding parameters\")\n", + "print(iQCC_C_solver.vqe_solver.optimal_var_params)\n", + "print(f\"\\n The final energy is {iQCC_C_solver.energies[-1]} with error {iQCC_C_solver.energies[-1]-exact}\")\n", + "print(\"\\n The number of H terms at each iteration is\")\n", + "print(iQCC_C_solver.qu_op_length)\n", + "#adapt_circ = adapt_solver.vqe_solver.optimal_circuit\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plots of energy convergence and growth of Hamiltonian terms" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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EZFTdmPPBJL3HuH7lTu8jiISEo5PQ1QnNoabezX0Oo6ISImngldERE5Zv1Y1qVupDKM0RZFIofhkQQrLtx7gvvObUyZK0xCJiJyukC54zWygmY1MTU31OoqISEiKCA/jhh7xzEnay7Iteq8VOR2HjmXyz29W0z6uEhedUcfrOCIiISGkC17n3ETn3PDY2Fivo4iIhKyrutSnXFS4zvKKnKbXf1rProPHeHiApiESESksIV3wiohI0asYE8kVneozaclWdhxI8zqOSImUsu8II3/ewMXt6tAhrrLXcUREQoYKXhEROW1De8aTme1475dNXkcRKZGenbKKMIP7+msaIhGRwqSCV0RETluDquXo26ImH8zeRFpGltdxREqUeRv3MmnJNoaf2Yg6lcp4HUdEJKSo4BURkUIxLDGBfUcy+HTBFq+jiJQY2dmOJyatoGbFaG7prWmIREQKmwpeEREpFF0SqtC6bkVGz0zCOed1HJES4fNFW1ickspf+zWnbFSE13FEREKOCl4RESkUZsawxATW7TzEtDW7vI4jEvSOpGfyj69X0bZeLJe2r+t1HBGRkKSCV0RECs2FbepQo0K0pigSCcAb0zaw48AxHhnQkrAwTUMkIlIUVPCKiEihiYoIY0j3Bvy8djdrdhz0Oo5I0Nq6/yhvTF/PgLa16RRfxes4IiIhSwWviIgUqmu6NiA6IozROssrkq/nvl5FtoP7z9c0RCIiRUkFr4iIFKoq5aK4rEM9Pl24hT2HjnkdRyToLEzex+eLtnJzrwTqVS7rdRwRkZCmgldERArdsMR40jOz+WB2stdRRIKKc47HJ62geoVobj2rsddxRERCngpeEREpdI1rVKB30+q89+smjmVmeR1HJGh8uXgrC5P385fzmlE+WtMQiYgUtZAueM1soJmNTE1N9TqKiEipMywxgV0HjzFp8Tavo4gEhaPpWfxjyipa1anI5R3reR1HRKRUCOmC1zk30Tk3PDY21usoIiKlTq8m1WhaszyjZiThnPM6jojn3vp5A1tT03h4QEvCNQ2RiEixCOmCV0REvGNm3NgzgRXbDvDrhr1exxHx1I4Dabz203rOb12Lbg2reh1HRKTUUMErIiJF5pL2dalSLopRmqJISrnnvl5NVrbjgfNbeB1FRKRUUcErIiJFJiYynGu7xjF11Q427j7sdRwRTyxJ2c8n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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, (ax1, ax2) = plt.subplots(1, 2)\n", + "fig.set_size_inches(16, 8)\n", + "\n", + "ax1.set_xticks(list(range(len(iQCC_C_solver.energies))))\n", + "ax1.set_title(\"Energy convergence\", fontdict={\"size\": 20})\n", + "ax1.set_xlabel(\"Iteration Number\", fontdict={\"size\": 14})\n", + "ax1.set_ylabel(\"Energy Error (Hartree)\", fontdict={\"size\": 14})\n", + "ax1.semilogy(range(len(iQCC_C_solver.energies)), np.abs(iQCC_C_solver.energies-exact), \"-x\" )\n", + "\n", + "ax2.set_title(\"Hamiltonian Growth\", fontdict={\"size\": 20})\n", + "ax2.set_ylabel(\"# H terms\", fontdict={\"size\": 14})\n", + "ax2.set_xticks(list(range(len(iQCC_C_solver.qu_op_length))))\n", + "ax2.set_xlabel(\"Iteration #\", fontdict={\"size\": 14})\n", + "ax2.semilogy(list(range(len(iQCC_C_solver.qu_op_length))), iQCC_C_solver.qu_op_length)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Optimizing interior generators\n", + "\n", + "Below is the method highlighted in the manuscript to optimize interior generators" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from tangelo.linq import get_backend, Gate, Circuit\n", + "dressed_qu_op = qcc_op_dress(deepcopy(iQCC_C_solver.qubit_hamiltonian),\n", + " iQCC_C_solver.vqe_solver.ansatz.operators[::-1],\n", + " iQCC_C_solver.vqe_solver.optimal_var_params[::-1])\n", + "ref_circ = iQCC_C_solver.vqe_solver.ansatz.prepare_reference_state()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def optimize_interior_generator(op_index, ref_circ):\n", + " dressed_qu_op = qcc_op_dress(deepcopy(iQCC_C_solver.qubit_hamiltonian),\n", + " list(reversed(iQCC_C_solver.vqe_solver.ansatz.operators[op_index+1:4])),\n", + " list(reversed(iQCC_C_solver.vqe_solver.optimal_var_params[op_index+1:4])))\n", + " dressed_ref_qu_op = QubitOperator((), 1)\n", + " for i in range(op_index-1, -1, -1):\n", + " p = iQCC_C_solver.vqe_solver.optimal_var_params[i]\n", + " q = iQCC_C_solver.vqe_solver.ansatz.operators[i]\n", + " dressed_ref_qu_op *= (np.cos(p/2)*QubitOperator((), 1) - 1j*np.sin(p/2)*q)\n", + "\n", + " # CirqCliffordSimulator is very slow but does work\n", + " # sim = CirqCliffordSimulator()\n", + " # Use default simulator instead, ideally qulacs is installed\n", + " sim = get_backend()\n", + " \n", + "\n", + " expect0 = 0\n", + " expectpl = 0\n", + " expectmi = 0\n", + " circpl = get_exponentiated_qubit_operator_circuit(iQCC_C_solver.ansatz.operators[op_index], time=np.pi/4)\n", + " circmi = get_exponentiated_qubit_operator_circuit(iQCC_C_solver.ansatz.operators[op_index], time=-np.pi/4)\n", + " circ0 = get_exponentiated_qubit_operator_circuit(iQCC_C_solver.ansatz.operators[op_index], time=0)\n", + "\n", + " zero_one_qu_op = QubitOperator(\"X6\") + QubitOperator(\"Y6\", 1j)\n", + "\n", + " for term1, coeff1 in dressed_ref_qu_op.terms.items():\n", + " ref_circ1 = Circuit([Gate(\"C\"+op, q, 6) for q, op in term1], n_qubits=7)\n", + "\n", + " for term2, coeff2 in dressed_ref_qu_op.terms.items():\n", + " ref_circ2 = Circuit([Gate(\"C\"+op, q, 6) for q, op in term2], n_qubits=7)\n", + "\n", + " prep_12_circuit = ref_circ + Circuit([Gate(\"H\", 6)]) + ref_circ1 + Circuit([Gate(\"X\", 6)]) + ref_circ2\n", + "\n", + " for hterm, hcoeff in dressed_qu_op.terms.items():\n", + " qu_circ = Circuit([Gate(\"C\"+op, q, 6) for q, op in hterm], n_qubits=7)\n", + " \n", + " plus_circ = prep_12_circuit + circpl + qu_circ + Circuit([Gate(\"X\", 6)])\n", + " expectpl += sim.get_expectation_value(zero_one_qu_op, plus_circ) * coeff1 * np.conj(coeff2) * hcoeff\n", + " \n", + " minus_circ = prep_12_circuit + circmi + qu_circ + Circuit([Gate(\"X\", 6)])\n", + " expectmi += sim.get_expectation_value(zero_one_qu_op, minus_circ) * coeff1 * np.conj(coeff2) * hcoeff\n", + "\n", + " zero_circ = prep_12_circuit + circ0 + qu_circ + Circuit([Gate(\"X\", 6)])\n", + " expect0 += sim.get_expectation_value(zero_one_qu_op, zero_circ) * coeff1 * np.conj(coeff2) * hcoeff\n", + " ener= expect0.real\n", + " epl = expectpl.real\n", + " emi = expectmi.real\n", + " theta_min = -0.5 * np.pi - np.arctan2(2. * ener - epl - emi, epl - emi)\n", + " a = 0.5*np.sqrt((2*ener-epl-emi)**2+(epl-emi)**2)\n", + " b = np.arctan2(2. * ener - epl - emi, epl - emi)\n", + " c = 1/2*(epl+emi)\n", + " new_energy = a*np.sin(theta_min+b)+c\n", + "\n", + " return theta_min, new_energy\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Perform one rotosolve sweep" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iQCC using only Clifford circuits results in energy -1.526758299682792 with error 0.0001887054536875432\n", + "\n", + " Optimizing operator 3 results in new energy of -1.5268018129766077\n", + "The calculated new energy is -1.5268018129766032 with error 0.00014519215987629508\n", + "\n", + " Optimizing operator 2 results in new energy of -1.5268018780136394\n", + "The calculated new energy is -1.526801878013637 with error 0.0001451271228425366\n", + "\n", + " Optimizing operator 1 results in new energy of -1.5268303820972458\n", + "The calculated new energy is -1.5268303820972426 with error 0.0001166230392368739\n", + "\n", + " Optimizing operator 0 results in new energy of -1.5268319664163135\n", + "The calculated new energy is -1.5268319664163106 with error 0.00011503872016893624\n" + ] + } + ], + "source": [ + "starting_energy = iQCC_C_solver.vqe_solver.energy_estimation(iQCC_C_solver.vqe_solver.optimal_var_params)\n", + "print(f\"iQCC using only Clifford circuits results in energy {starting_energy} with error {starting_energy-exact}\")\n", + "\n", + "# Start at max_cycles-1 as operator 0 was the last optimized.\n", + "for op_index in range(max_cycles-1, -1, -1):\n", + " theta_min, new_energy = optimize_interior_generator(op_index, ref_circ)\n", + " print(f\"\\n Optimizing operator {op_index} results in new energy of {new_energy}\")\n", + " iQCC_C_solver.vqe_solver.optimal_var_params[op_index] = theta_min\n", + " calculated_new_energy = iQCC_C_solver.vqe_solver.energy_estimation(iQCC_C_solver.vqe_solver.optimal_var_params)\n", + " print(f\"The calculated new energy is {calculated_new_energy} with error {calculated_new_energy-exact}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Closing words\n", + "\n", + "This notebook shows that a variant of iQCC can be implemented while only using Clifford circuits as described in our [manuscript](https://arxiv.org/abs/2211.10501). This method can be used to initialize the QCC ansatz for VQE or to create short depth circuits that improve the success probability of fault-tolerant algorithms such as phase estimation. " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.10" + }, + "vscode": { + "interpreter": { + "hash": "95050af2697fca56ed7491a4fb0b04c1282c0de0a7e0a7cacd318a8297b0b1d8" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 1383c35e406216aeb00e1d30a07c0ed7cb74cd1c Mon Sep 17 00:00:00 2001 From: Alexandre Fleury <76115575+AlexandreF-1qbit@users.noreply.github.com> Date: Thu, 24 Nov 2022 22:19:46 -0500 Subject: [PATCH 07/14] pUCCD ansatz (#251) --- tangelo/algorithms/variational/vqe_solver.py | 6 + .../toolboxes/ansatz_generator/__init__.py | 1 + .../ansatz_generator/ansatz_utils.py | 4 +- tangelo/toolboxes/ansatz_generator/puccd.py | 205 ++++++++++++++++++ .../ansatz_generator/tests/test_puccd.py | 73 +++++++ .../molecular_computation/coefficients.py | 51 +++++ .../molecular_computation/molecule.py | 34 --- .../toolboxes/molecular_computation/rdms.py | 2 +- .../tests/test_coefficients.py | 38 ++++ .../tests/test_molecule.py | 13 +- tangelo/toolboxes/operators/__init__.py | 2 +- tangelo/toolboxes/operators/operators.py | 22 +- tangelo/toolboxes/qubit_mappings/hcb.py | 93 ++++++++ .../qubit_mappings/mapping_transform.py | 12 +- .../tests/test_mapping_transform.py | 17 ++ .../qubit_mappings/tests/test_qubitizer.py | 21 +- 16 files changed, 519 insertions(+), 75 deletions(-) create mode 100644 tangelo/toolboxes/ansatz_generator/puccd.py create mode 100644 tangelo/toolboxes/ansatz_generator/tests/test_puccd.py create mode 100644 tangelo/toolboxes/molecular_computation/coefficients.py create mode 100644 tangelo/toolboxes/molecular_computation/tests/test_coefficients.py create mode 100644 tangelo/toolboxes/qubit_mappings/hcb.py diff --git a/tangelo/algorithms/variational/vqe_solver.py b/tangelo/algorithms/variational/vqe_solver.py index 37c009555..ece4c8b12 100644 --- a/tangelo/algorithms/variational/vqe_solver.py +++ b/tangelo/algorithms/variational/vqe_solver.py @@ -46,6 +46,7 @@ class BuiltInAnsatze(Enum): VSQS = agen.VSQS UCCGD = agen.UCCGD ILC = agen.ILC + pUCCD = agen.pUCCD class VQESolver: @@ -125,6 +126,9 @@ def __init__(self, opt_dict): warnings.warn("Spin-orbital ordering shifted to all spin-up first then down to ensure efficient generator screening " "for the Jordan-Wigner mapping with QCC-based ansatze.", RuntimeWarning) self.up_then_down = True + if self.ansatz == BuiltInAnsatze.pUCCD and self.qubit_mapping.lower() != "hcb": + warnings.warn("Forcing the hard-core boson mapping for the pUCCD ansatz.", RuntimeWarning) + self.mapping = "HCB" # QCC and QMF and ILC require a reference state that can be represented by a single layer of RZ-RX gates on each qubit. # This decomposition can not be determined from a general Circuit reference state. if isinstance(self.ref_state, Circuit): @@ -210,6 +214,8 @@ def build(self): if isinstance(self.ansatz, BuiltInAnsatze): if self.ansatz in {BuiltInAnsatze.UCC1, BuiltInAnsatze.UCC3}: self.ansatz = self.ansatz.value + elif self.ansatz == BuiltInAnsatze.pUCCD: + self.ansatz = self.ansatz.value(self.molecule, **self.ansatz_options) elif self.ansatz in self.builtin_ansatze: self.ansatz = self.ansatz.value(self.molecule, self.qubit_mapping, self.up_then_down, **self.ansatz_options) else: diff --git a/tangelo/toolboxes/ansatz_generator/__init__.py b/tangelo/toolboxes/ansatz_generator/__init__.py index 11616c116..0a4f31c01 100644 --- a/tangelo/toolboxes/ansatz_generator/__init__.py +++ b/tangelo/toolboxes/ansatz_generator/__init__.py @@ -24,3 +24,4 @@ from .hea import HEA from .variational_circuit import VariationalCircuitAnsatz from .uccgd import UCCGD +from .puccd import pUCCD diff --git a/tangelo/toolboxes/ansatz_generator/ansatz_utils.py b/tangelo/toolboxes/ansatz_generator/ansatz_utils.py index 543fe725b..b99082107 100644 --- a/tangelo/toolboxes/ansatz_generator/ansatz_utils.py +++ b/tangelo/toolboxes/ansatz_generator/ansatz_utils.py @@ -425,10 +425,10 @@ def givens_gate(target, theta, is_variational=False): """Generates the list of gates corresponding to a givens rotation exp(-theta*(XX+YY)) Explicitly the two-qubit matrix is - [[0, 0, 0, 0], + [[1, 0, 0, 0], [0, cos(theta), -sin(theta), 0], [0, sin(theta), cos(theta), 0], - [0, 0, 0, 0]] + [0, 0, 0, 1]] Args: target (list): list of two integers that indicate which qubits are involved in the givens rotation diff --git a/tangelo/toolboxes/ansatz_generator/puccd.py b/tangelo/toolboxes/ansatz_generator/puccd.py new file mode 100644 index 000000000..01839c568 --- /dev/null +++ b/tangelo/toolboxes/ansatz_generator/puccd.py @@ -0,0 +1,205 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module defines the pUCCD ansatz class. The molecular FermionOperator is +expected to be converted to a BosonOperator (electrons in pairs). Single bosonic +excitations (corresponding to double fermion excitations) form the ansatz. Those +excitations are transformed into a quantum circuit via Givens rotations. + +Ref: + - V.E. Elfving, M. Millaruelo, J.A. Gámez, and C. Gogolin. + Phys. Rev. A 103, 032605 (2021). +""" + +import itertools +import numpy as np + +from tangelo.linq import Circuit +from tangelo.toolboxes.ansatz_generator import Ansatz +from tangelo.toolboxes.ansatz_generator.ansatz_utils import givens_gate +from tangelo.toolboxes.qubit_mappings.statevector_mapping import vector_to_circuit + + +class pUCCD(Ansatz): + """This class implements the pUCCD ansatz, as described in Phys. Rev. A 103, + 032605 (2021). Electrons are described as hard-core boson and only double + excitations are considered. + + Args: + molecule (SecondQuantizedMolecule): Self-explanatory. + reference_state (string): String refering to an initial state. + Default: "HF". + """ + + def __init__(self, molecule, reference_state="HF"): + + if molecule.spin != 0: + raise NotImplementedError("pUCCD is implemented only for closed-shell system.") + + self.molecule = molecule + self.n_spatial_orbitals = molecule.n_active_mos + self.n_electrons = molecule.n_active_electrons + + # Set total number of parameters. + self.n_occupied = int(self.n_electrons / 2) + self.n_virtual = self.n_spatial_orbitals - self.n_occupied + self.n_var_params = self.n_occupied * self.n_virtual + + # Supported reference state initialization. + self.supported_reference_state = {"HF", "zero"} + # Supported var param initialization + self.supported_initial_var_params = {"zeros", "ones", "random"} + + # Default initial parameters for initialization. + self.var_params_default = "ones" + self.reference_state = reference_state + + self.var_params = None + self.circuit = None + + def set_var_params(self, var_params=None): + """Set values for variational parameters, such as ones, zeros or random + numbers providing some keywords for users, and also supporting direct + user input (list or numpy array). Return the parameters so that + workflows such as VQE can retrieve these values. + """ + if var_params is None: + var_params = self.var_params_default + + if isinstance(var_params, str): + var_params = var_params.lower() + if (var_params not in self.supported_initial_var_params): + raise ValueError(f"Supported keywords for initializing variational parameters: {self.supported_initial_var_params}") + if var_params == "ones": + initial_var_params = np.ones((self.n_var_params,), dtype=float) + elif var_params == "random": + initial_var_params = 2.e-1 * (np.random.random((self.n_var_params,)) - 0.5) + else: + initial_var_params = np.array(var_params) + if initial_var_params.size != self.n_var_params: + raise ValueError(f"Expected {self.n_var_params} variational parameters but " + f"received {initial_var_params.size}.") + self.var_params = initial_var_params + return initial_var_params + + def prepare_reference_state(self): + """Returns circuit preparing the reference state of the ansatz (e.g + prepare reference wavefunction with HF, multi-reference state, etc). + These preparations must be consistent with the transform used to obtain + the qubit operator. + """ + + if self.reference_state not in self.supported_reference_state: + raise ValueError(f"Only supported reference state methods are:{self.supported_reference_state}") + + if self.reference_state == "HF": + vector = [1 if i < self.n_electrons // 2 else 0 for i in range(self.n_spatial_orbitals)] + return vector_to_circuit(vector) + elif self.reference_state == "zero": + return Circuit() + + def build_circuit(self, var_params=None): + """Build and return the quantum circuit implementing the state + preparation ansatz (with currently specified initial_state and + var_params). + """ + + if var_params is not None: + self.set_var_params(var_params) + elif self.var_params is None: + self.set_var_params() + + excitations = self._get_double_excitations() + + # Prepend reference state circuit + reference_state_circuit = self.prepare_reference_state() + + # Obtain quantum circuit through trivial trotterization of the qubit operator + # Keep track of the order in which pauli words have been visited for fast subsequent parameter updates + self.exc_to_param_mapping = dict() + rotation_gates = [] + + # Parallel ordering (rotations on different qubits can happen at the + # same time. + excitations_per_layer = [[]] + free_qubits_per_layer = [set(range(self.n_spatial_orbitals))] + + # Classify excitations into circuit layers (single pass on all + # excitations). + for p, q in excitations: + excitations_added = False + for qubit_indices, gates in zip(free_qubits_per_layer, excitations_per_layer): + if p in qubit_indices and q in qubit_indices: + qubit_indices -= {p, q} + gates += [(p, q)] + excitations_added = True + break + + # If the excitation cannot be added to at least one previous layer, + # create a new layer. + if not excitations_added: + excitations_per_layer.append([(p, q)]) + qubit_indices = set(range(self.n_spatial_orbitals)) + qubit_indices -= {p, q} + free_qubits_per_layer.append(qubit_indices) + + excitations = list(itertools.chain.from_iterable(excitations_per_layer)) + self.exc_to_param_mapping = {v: k for k, v in enumerate(excitations)} + + rotation_gates = [givens_gate((p, q), 0., is_variational=True) for (p, q) in excitations] + rotation_gates = list(itertools.chain.from_iterable(rotation_gates)) + + puccd_circuit = Circuit(rotation_gates) + + # Skip over the reference state circuit if it is empty. + if reference_state_circuit.size != 0: + self.circuit = reference_state_circuit + puccd_circuit + else: + self.circuit = puccd_circuit + + self.update_var_params(self.var_params) + return self.circuit + + def update_var_params(self, var_params): + """Shortcut: set value of variational parameters in the already-built + ansatz circuit member. Preferable to rebuilt your circuit from scratch, + which can be an involved process. + """ + + self.set_var_params(var_params) + var_params = self.var_params + + excitations = self._get_double_excitations() + for i, (p, q) in enumerate(excitations): + gate_index = self.exc_to_param_mapping[(p, q)] + self.circuit._variational_gates[gate_index].parameter = var_params[i] + + def _get_double_excitations(self): + """Construct the UCC double excitations for the given amount of occupied + and virtual orbitals. + + Returns: + list of int tuples: List of (p, q) excitations corresponding to the + occupied orbital p to virtual orbital q. + """ + + # Generate double indices in seniority 0 space. + excitations = list() + for p, q in itertools.product( + range(self.n_occupied), + range(self.n_occupied, self.n_occupied+self.n_virtual) + ): + excitations += [(p, q)] + + return excitations diff --git a/tangelo/toolboxes/ansatz_generator/tests/test_puccd.py b/tangelo/toolboxes/ansatz_generator/tests/test_puccd.py new file mode 100644 index 000000000..b4061e885 --- /dev/null +++ b/tangelo/toolboxes/ansatz_generator/tests/test_puccd.py @@ -0,0 +1,73 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +import numpy as np + +from tangelo.molecule_library import mol_H2_sto3g +from tangelo.toolboxes.qubit_mappings.hcb import hard_core_boson_operator, boson_to_qubit_mapping +from tangelo.toolboxes.ansatz_generator.puccd import pUCCD +from tangelo.linq import get_backend + + +class pUCCDTest(unittest.TestCase): + + def test_puccd_set_var_params(self): + """Verify behavior of set_var_params for different inputs (keyword, + list, numpy array). + """ + + puccd_ansatz = pUCCD(mol_H2_sto3g) + + one_ones = np.ones((1,)) + + puccd_ansatz.set_var_params("ones") + np.testing.assert_array_almost_equal(puccd_ansatz.var_params, one_ones, decimal=6) + + puccd_ansatz.set_var_params([1.]) + np.testing.assert_array_almost_equal(puccd_ansatz.var_params, one_ones, decimal=6) + + puccd_ansatz.set_var_params(np.array([1.])) + np.testing.assert_array_almost_equal(puccd_ansatz.var_params, one_ones, decimal=6) + + def test_puccd_incorrect_number_var_params(self): + """Return an error if user provide incorrect number of variational + parameters. + """ + + puccd_ansatz = pUCCD(mol_H2_sto3g) + + self.assertRaises(ValueError, puccd_ansatz.set_var_params, np.array([1., 1., 1., 1.])) + + def test_puccd_H2(self): + """Verify closed-shell pUCCD functionalities for H2.""" + + # Build circuit. + puccd_ansatz = pUCCD(mol_H2_sto3g) + puccd_ansatz.build_circuit() + + # Build qubit hamiltonian for energy evaluation. + qubit_hamiltonian = boson_to_qubit_mapping( + hard_core_boson_operator(mol_H2_sto3g.fermionic_hamiltonian) + ) + + # Assert energy returned is as expected for given parameters. + sim = get_backend() + puccd_ansatz.update_var_params([-0.22617753]) + energy = sim.get_expectation_value(qubit_hamiltonian, puccd_ansatz.circuit) + self.assertAlmostEqual(energy, -1.13727, delta=1e-4) + + +if __name__ == "__main__": + unittest.main() diff --git a/tangelo/toolboxes/molecular_computation/coefficients.py b/tangelo/toolboxes/molecular_computation/coefficients.py new file mode 100644 index 000000000..cae4d236d --- /dev/null +++ b/tangelo/toolboxes/molecular_computation/coefficients.py @@ -0,0 +1,51 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module containing functions to manipulate molecular coefficient arrays.""" + +import numpy as np + + +def spatial_from_spinorb(one_body_coefficients, two_body_coefficients): + """Function to reverse openfermion.chem.molecular_data.spinorb_from_spatial. + + Args: + one_body_coefficients: One-body coefficients (array of 2N*2N, where N + is the number of molecular orbitals). + two_body_coefficients: Two-body coefficients (array of 2N*2N*2N*2N, + where N is the number of molecular orbitals). + + Returns: + (array of floats, array of floats): One- and two-body integrals (arrays + of N*N and N*N*N*N elements, where N is the number of molecular + orbitals. + """ + # Get the number of MOs = number of SOs / 2. + n_mos = one_body_coefficients.shape[0] // 2 + + # Initialize Hamiltonian integrals. + one_body_integrals = np.zeros((n_mos, n_mos), dtype=complex) + two_body_integrals = np.zeros((n_mos, n_mos, n_mos, n_mos), dtype=complex) + + # Loop through coefficients. + for p in range(n_mos): + for q in range(n_mos): + # Populate 1-body integrals. + one_body_integrals[p, q] = one_body_coefficients[2*p, 2*q] + # Continue looping to prepare 2-body integrals. + for r in range(n_mos): + for s in range(n_mos): + two_body_integrals[p, q, r, s] = two_body_coefficients[2*p, 2*q+1, 2*r+1, 2*s] + + return one_body_integrals, two_body_integrals diff --git a/tangelo/toolboxes/molecular_computation/molecule.py b/tangelo/toolboxes/molecular_computation/molecule.py index 715038ade..56a2941d8 100644 --- a/tangelo/toolboxes/molecular_computation/molecule.py +++ b/tangelo/toolboxes/molecular_computation/molecule.py @@ -65,40 +65,6 @@ def molecule_to_secondquantizedmolecule(mol, basis_set="sto-3g", frozen_orbitals return converted_mol -def spatial_from_spinorb(one_body_coefficients, two_body_coefficients): - """Function to reverse openfermion.chem.molecular_data.spinorb_from_spatial. - - Args: - one_body_coefficients: One-body coefficients (array of 2N*2N, where N - is the number of molecular orbitals). - two_body_coefficients: Two-body coefficients (array of 2N*2N*2N*2N, - where N is the number of molecular orbitals). - - Returns: - (array of floats, array of floats): One- and two-body integrals (arrays - of N*N and N*N*N*N elements, where N is the number of molecular - orbitals. - """ - # Get the number of MOs = number of SOs / 2. - n_mos = one_body_coefficients.shape[0] // 2 - - # Initialize Hamiltonian integrals. - one_body_integrals = np.zeros((n_mos, n_mos), dtype=complex) - two_body_integrals = np.zeros((n_mos, n_mos, n_mos, n_mos), dtype=complex) - - # Loop through coefficients. - for p in range(n_mos): - for q in range(n_mos): - # Populate 1-body integrals. - one_body_integrals[p, q] = one_body_coefficients[2*p, 2*q] - # Continue looping to prepare 2-body integrals. - for r in range(n_mos): - for s in range(n_mos): - two_body_integrals[p, q, r, s] = two_body_coefficients[2*p, 2*q+1, 2*r+1, 2*s] - - return one_body_integrals, two_body_integrals - - @dataclass class Molecule: """Custom datastructure to store information about a Molecule. This contains diff --git a/tangelo/toolboxes/molecular_computation/rdms.py b/tangelo/toolboxes/molecular_computation/rdms.py index 5c462dbe6..dbe53faeb 100644 --- a/tangelo/toolboxes/molecular_computation/rdms.py +++ b/tangelo/toolboxes/molecular_computation/rdms.py @@ -18,12 +18,12 @@ import numpy as np +from tangelo.toolboxes.molecular_computation.coefficients import spatial_from_spinorb from tangelo.linq.helpers import pauli_string_to_of, get_compatible_bases from tangelo.toolboxes.operators import FermionOperator from tangelo.toolboxes.measurements import ClassicalShadow from tangelo.toolboxes.post_processing import Histogram, aggregate_histograms from tangelo.toolboxes.qubit_mappings.mapping_transform import fermion_to_qubit_mapping, get_qubit_number -from tangelo.toolboxes.molecular_computation.molecule import spatial_from_spinorb from tangelo.linq.helpers.circuits import pauli_of_to_string diff --git a/tangelo/toolboxes/molecular_computation/tests/test_coefficients.py b/tangelo/toolboxes/molecular_computation/tests/test_coefficients.py new file mode 100644 index 000000000..e32b1f09c --- /dev/null +++ b/tangelo/toolboxes/molecular_computation/tests/test_coefficients.py @@ -0,0 +1,38 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import numpy as np +from openfermion.chem.molecular_data import spinorb_from_spatial + +from tangelo.molecule_library import mol_H2_sto3g +from tangelo.toolboxes.molecular_computation.coefficients import spatial_from_spinorb + + +class CoefficientsTest(unittest.TestCase): + + def test_spatial_from_spinorb(self): + """Test the conversion from spinorbitals to MO coefficients.""" + _, one_body_mos, two_body_mos = mol_H2_sto3g.get_integrals() + + one_body_sos, two_body_sos = spinorb_from_spatial(one_body_mos, two_body_mos) + one_body_mos_recomputed, two_body_mos_recomputed = spatial_from_spinorb(one_body_sos, two_body_sos) + + np.testing.assert_array_almost_equal(one_body_mos, one_body_mos_recomputed) + np.testing.assert_array_almost_equal(two_body_mos, two_body_mos_recomputed) + + +if __name__ == "__main__": + unittest.main() diff --git a/tangelo/toolboxes/molecular_computation/tests/test_molecule.py b/tangelo/toolboxes/molecular_computation/tests/test_molecule.py index 96a3e51b2..a3e1370b0 100644 --- a/tangelo/toolboxes/molecular_computation/tests/test_molecule.py +++ b/tangelo/toolboxes/molecular_computation/tests/test_molecule.py @@ -16,12 +16,11 @@ import os import numpy as np -from openfermion.chem.molecular_data import spinorb_from_spatial from openfermion.utils import load_operator from tangelo import SecondQuantizedMolecule from tangelo.molecule_library import mol_H2_sto3g, xyz_H2O -from tangelo.toolboxes.molecular_computation.molecule import atom_string_to_list, spatial_from_spinorb +from tangelo.toolboxes.molecular_computation.molecule import atom_string_to_list # For openfermion.load_operator function. pwd_this_test = os.path.dirname(os.path.abspath(__file__)) @@ -54,16 +53,6 @@ def test_atoms_string_to_list(self): """Verify conversion from string to list format for atom coordinates.""" assert(atom_string_to_list(H2_string) == H2_list) - def test_spatial_from_spinorb(self): - """Test the conversion from spinorbitals to MO coefficients.""" - _, one_body_mos, two_body_mos = mol_H2_sto3g.get_integrals() - - one_body_sos, two_body_sos = spinorb_from_spatial(one_body_mos, two_body_mos) - one_body_mos_recomputed, two_body_mos_recomputed = spatial_from_spinorb(one_body_sos, two_body_sos) - - np.testing.assert_array_almost_equal(one_body_mos, one_body_mos_recomputed) - np.testing.assert_array_almost_equal(two_body_mos, two_body_mos_recomputed) - class SecondQuantizedMoleculeTest(unittest.TestCase): diff --git a/tangelo/toolboxes/operators/__init__.py b/tangelo/toolboxes/operators/__init__.py index 61525ecb2..1c51be328 100644 --- a/tangelo/toolboxes/operators/__init__.py +++ b/tangelo/toolboxes/operators/__init__.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .operators import FermionOperator, QubitOperator, QubitHamiltonian +from .operators import FermionOperator, QubitOperator, QubitHamiltonian, BosonOperator from .operators import count_qubits, normal_ordered, squared_normal_ordered, list_to_fermionoperator, qubitop_to_qubitham from .multiformoperator import MultiformOperator diff --git a/tangelo/toolboxes/operators/operators.py b/tangelo/toolboxes/operators/operators.py index a9a396620..9d86aa11f 100644 --- a/tangelo/toolboxes/operators/operators.py +++ b/tangelo/toolboxes/operators/operators.py @@ -23,6 +23,8 @@ from scipy.special import comb import openfermion as of +from tangelo.toolboxes.molecular_computation.coefficients import spatial_from_spinorb + class FermionOperator(of.FermionOperator): """Custom FermionOperator class. Based on openfermion's, with additional functionalities. @@ -101,17 +103,18 @@ def __eq__(self, other): else: return super(FermionOperator, self).__eq__(other) - def get_coeffs(self, coeff_threshold=1e-8): + def get_coeffs(self, coeff_threshold=1e-8, spatial=False): """Method to get the coefficient tensors from a fermion operator. Args: coeff_threshold (float): Ignore coefficient below the threshold. Default value is 1e-8. + spatial (bool): Spatial orbital or spin orbital. Returns: (float, array float, array of float): Core constant, one- (N*N) and two-body coefficient matrices (N*N*N*N), where N is the number - of spinorbitals. + of spinorbitals or spatial orbitals. """ n_sos = of.count_qubits(self) @@ -141,16 +144,25 @@ def get_coeffs(self, coeff_threshold=1e-8): p, q, r, s = [operator[0] for operator in term] two_body[p, q, r, s] = coefficient + if spatial: + one_body, two_body = spatial_from_spinorb(one_body, two_body) + return constant, one_body, two_body def to_openfermion(self): """Converts Tangelo FermionOperator to openfermion""" ferm_op = of.FermionOperator() ferm_op.terms = self.terms.copy() - return ferm_op +class BosonOperator(of.BosonOperator): + """Currently, this class is coming from openfermion. Can be later on be + replaced by our own implementation. + """ + pass + + class QubitOperator(of.QubitOperator): """Currently, this class is coming from openfermion. Can be later on be replaced by our own implementation. @@ -160,7 +172,7 @@ def frobenius_norm_compression(self, epsilon, n_qubits): """Reduces the number of operator terms based on its Frobenius norm and a user-defined threshold, epsilon. The eigenspectrum of the compressed operator will not deviate more than epsilon. For more - details, see J. Chem. Theory Comput. 2020, 16, 2, 1055–1063. + details, see J. Chem. Theory Comput. 2020, 16, 2, 1055-1063. Args: epsilon (float): Parameter controlling the degree of compression @@ -190,7 +202,7 @@ def get_max_number_hamiltonian_terms(self, n_qubits): """Compute the possible number of terms for a qubit Hamiltonian. In the absence of an external magnetic field, each Hamiltonian term must have an even number of Pauli Y operators to preserve time-reversal symmetry. - See J. Chem. Theory Comput. 2020, 16, 2, 1055–1063 for more details. + See J. Chem. Theory Comput. 2020, 16, 2, 1055-1063 for more details. Args: n_qubits (int): Number of qubits in the register. diff --git a/tangelo/toolboxes/qubit_mappings/hcb.py b/tangelo/toolboxes/qubit_mappings/hcb.py new file mode 100644 index 000000000..fae75030c --- /dev/null +++ b/tangelo/toolboxes/qubit_mappings/hcb.py @@ -0,0 +1,93 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Module that defines function to convert a fermionic operator to a boson +operator. For electronic system, it means to couple all the electrons by pair. +""" + +import itertools + +from tangelo.toolboxes.operators import BosonOperator, QubitOperator + + +def hard_core_boson_operator(ferm_op): + """Function to extract the coefficient of the Hard-Core Bosonic (HCB) + Hamiltonian. + + Refs: + - V.E. Elfving, M. Millaruelo, J.A. Gámez, and C. Gogolin. Phys. Rev. A + 103, 032605 (2021). + - N.T. Thang and P.T.T. Nga, Communications in Physics 21, 301 (2011). + + Args: + ferm_op (FermionOperator): Self-explanatory. + + Returns: + BosonOperator: Self-explanatory. + """ + + # Getting the molecular integrals. + cte, e_sei, e_tei = ferm_op.get_coeffs(spatial=True) + e_tei *= 2 + + boson_op = BosonOperator((), cte) + n_mos = e_sei.shape[0] + for i, j in itertools.product(range(n_mos), repeat=2): + if i == j: + coeff = 2*e_sei[i, i] + e_tei[i, i, i, i] + boson_op += BosonOperator(f"{i}^ {i}", coeff) + else: + r1_coeff = e_tei[i, i, j, j] + boson_op += BosonOperator(f"{i}^ {j}", r1_coeff) + + r2_coeff = 2*e_tei[i, j, j, i] - e_tei[i, j, i, j] + boson_op += BosonOperator(f"{i}^ {i} {j}^ {j}", r2_coeff) + + return boson_op + + +def boson_to_qubit_mapping(bos_op): + """Function to convert a Bosonic operator to a qubit operator. As qubits are + bosons, the mapping is similar to the Jordan-Wigner mapping, but without the + trailing Pauli-Z to account for anticommutation of the creation and + annihilation operators. + + In short, every creation operator b^{\dagger} (resp. annihilation b) are + mapped to X+iY strings (resp. X-iY), where X and Y are referring to the + Pauli matrices. + + Args: + bos_op (BosonOperator): Self-explanatory. + + Returns: + QubitOperator: Self-explanatory. + """ + + def b(p, dagger=False): + prefactor = -1 if dagger else 1 + return QubitOperator(f"X{p}", 0.5) + QubitOperator(f"Y{p}", prefactor*0.5j) + + qu_op = QubitOperator((), bos_op.constant) + for term, coeff in bos_op.terms.items(): + if not term: + continue + + qubit_term = QubitOperator((), coeff) + for mo, dagger in term: + qubit_term *= b(mo, dagger) + + qu_op += qubit_term + + return qu_op diff --git a/tangelo/toolboxes/qubit_mappings/mapping_transform.py b/tangelo/toolboxes/qubit_mappings/mapping_transform.py index 4ffa704e1..f552f429e 100644 --- a/tangelo/toolboxes/qubit_mappings/mapping_transform.py +++ b/tangelo/toolboxes/qubit_mappings/mapping_transform.py @@ -19,16 +19,17 @@ - symmetry-conserving Bravyi-Kitaev (2-qubit reduction via Z2 taper) """ - -import warnings +from math import ceil import numpy as np from collections.abc import Iterable + from openfermion import FermionOperator as ofFermionOperator from tangelo.toolboxes.operators import FermionOperator, QubitOperator from tangelo.toolboxes.qubit_mappings import jordan_wigner, bravyi_kitaev, symmetry_conserving_bravyi_kitaev, jkmn +from tangelo.toolboxes.qubit_mappings.hcb import hard_core_boson_operator, boson_to_qubit_mapping -available_mappings = {"JW", "BK", "SCBK", "JKMN"} +available_mappings = {"JW", "BK", "SCBK", "JKMN", "HCB"} def get_qubit_number(mapping, n_spinorbitals): @@ -45,6 +46,8 @@ def get_qubit_number(mapping, n_spinorbitals): """ if mapping.upper() == "SCBK": return n_spinorbitals - 2 + elif mapping.upper() == "HCB": + return ceil(n_spinorbitals / 2) else: return n_spinorbitals @@ -126,6 +129,9 @@ def fermion_to_qubit_mapping(fermion_operator, mapping, n_spinorbitals=None, n_e spin=spin) elif mapping.upper() == "JKMN": qubit_operator = jkmn(fermion_operator, n_qubits=n_spinorbitals) + elif mapping.upper() == "HCB": + boson_operator = hard_core_boson_operator(fermion_operator) + qubit_operator = boson_to_qubit_mapping(boson_operator) converted_qubit_op = QubitOperator() converted_qubit_op.terms = qubit_operator.terms.copy() diff --git a/tangelo/toolboxes/qubit_mappings/tests/test_mapping_transform.py b/tangelo/toolboxes/qubit_mappings/tests/test_mapping_transform.py index 4b050f703..725b0cbcd 100644 --- a/tangelo/toolboxes/qubit_mappings/tests/test_mapping_transform.py +++ b/tangelo/toolboxes/qubit_mappings/tests/test_mapping_transform.py @@ -24,6 +24,7 @@ from tangelo.toolboxes.operators import QubitOperator, FermionOperator from tangelo.toolboxes.qubit_mappings.mapping_transform import fermion_to_qubit_mapping, make_up_then_down +from tangelo.molecule_library import mol_H2_sto3g class MappingTest(unittest.TestCase): @@ -175,6 +176,22 @@ def test_scbk_reorder(self): up_then_down=False) self.assertEqual(scBK_reordered, scBK_notreordered) + def test_hcb(self): + """The HCB mapping forces the fermionic operator to be expressed as a + bosonic operator. + """ + + hcb_operator = QubitOperator((), 0.244107) + hcb_operator += QubitOperator(((0, "X"), (1, "X")), 0.090644) + hcb_operator += QubitOperator(((0, "Y"), (1, "Y")), 0.090644) + hcb_operator += QubitOperator(((0, "Z")), 0.342395) + hcb_operator += QubitOperator(((0, "Z"), (1, "Z")), 0.572824) + hcb_operator += QubitOperator(((1, "Z")), -0.445572) + + hcb = fermion_to_qubit_mapping(fermion_operator=mol_H2_sto3g.fermionic_hamiltonian, mapping="hcb") + + self.assertTrue(hcb_operator.isclose(hcb, tol=1e-5)) + if __name__ == "__main__": unittest.main() diff --git a/tangelo/toolboxes/qubit_mappings/tests/test_qubitizer.py b/tangelo/toolboxes/qubit_mappings/tests/test_qubitizer.py index 88569a8e6..15e2e6818 100644 --- a/tangelo/toolboxes/qubit_mappings/tests/test_qubitizer.py +++ b/tangelo/toolboxes/qubit_mappings/tests/test_qubitizer.py @@ -16,22 +16,7 @@ from tangelo.molecule_library import mol_H2_sto3g from tangelo.toolboxes.qubit_mappings import jordan_wigner - - -def assert_term_dict_almost_equal(d1, d2, delta=1e-10): - """Utility function to check whether two qubit operators are almost equal, - looking at their term dictionary, for an arbitrary absolute tolerance. - """ - d1k, d2k = set(d1.keys()), set(d2.keys()) - if d1k != d2k: - d1_minus_d2 = d1k.difference(d2k) - d2_minus_d1 = d2k.difference(d1k) - raise AssertionError("Term dictionary keys differ. Qubit operators are not almost equal.\n" - f"d1-d2 keys: {d1_minus_d2} \nd2-d1 keys: {d2_minus_d1}") - else: - for k in d1k: - if abs(d1[k] - d2[k]) > delta: - raise AssertionError(f"Term {k}, difference={abs(d1[k]-d2[k])} > delta={delta}:\n {d1[k]} != {d2[k]}") +from tangelo.toolboxes.operators import QubitOperator class QubitizerTest(unittest.TestCase): @@ -52,8 +37,10 @@ def test_qubit_hamiltonian_JW_h2(self): ((0, "X"), (1, "Y"), (2, "Y"), (3, "X")): 0.045322202052874, ((0, "Y"), (1, "X"), (2, "X"), (3, "Y")): 0.045322202052874, ((0, "Y"), (1, "Y"), (2, "X"), (3, "X")): -0.045322202052874} + reference_op = QubitOperator() + reference_op.terms = reference_terms - assert_term_dict_almost_equal(qubit_hamiltonian.terms, reference_terms, delta=1e-8) + self.assertTrue(qubit_hamiltonian.isclose(reference_op, tol=1e-5)) if __name__ == "__main__": From d207e3545b6f4c25fe33ec0fd8b1e753b09f8e16 Mon Sep 17 00:00:00 2001 From: James Brown <84878946+JamesB-1qbit@users.noreply.github.com> Date: Mon, 28 Nov 2022 14:11:38 -0500 Subject: [PATCH 08/14] UHF reference (#240) * uhf implementation with VQESolver functionality * support for all types of orbital freezing * add active_spin and uhf attributes to SecondQuantizedDMETFragment * add spin to adapt_ansatz arguments --- tangelo/algorithms/classical/ccsd_solver.py | 12 +- tangelo/algorithms/classical/fci_solver.py | 3 + .../classical/tests/test_ccsd_solver.py | 14 +- .../variational/adapt_vqe_solver.py | 4 +- .../variational/tests/test_iqcc_solver.py | 10 +- .../tests/test_tetris_adapt_vqe_solver.py | 16 +- .../variational/tests/test_vqe_solver.py | 12 +- tangelo/algorithms/variational/vqe_solver.py | 6 +- tangelo/molecule_library.py | 8 +- .../problem_decomposition/dmet/fragment.py | 2 + .../ansatz_generator/_unitary_cc_openshell.py | 45 +- .../ansatz_generator/adapt_ansatz.py | 7 +- tangelo/toolboxes/ansatz_generator/hea.py | 16 +- tangelo/toolboxes/ansatz_generator/ilc.py | 4 +- tangelo/toolboxes/ansatz_generator/qcc.py | 2 +- tangelo/toolboxes/ansatz_generator/qmf.py | 2 +- .../tests/test_adapt_ansatz.py | 6 +- .../ansatz_generator/tests/test_qcc.py | 15 +- .../ansatz_generator/tests/test_qmf.py | 16 +- tangelo/toolboxes/ansatz_generator/uccgd.py | 2 +- tangelo/toolboxes/ansatz_generator/uccsd.py | 42 +- tangelo/toolboxes/ansatz_generator/upccgsd.py | 2 +- tangelo/toolboxes/ansatz_generator/vsqs.py | 2 + .../molecular_computation/molecule.py | 393 ++++++++++++++++-- .../tests/test_molecule.py | 26 ++ 25 files changed, 546 insertions(+), 121 deletions(-) diff --git a/tangelo/algorithms/classical/ccsd_solver.py b/tangelo/algorithms/classical/ccsd_solver.py index 90d621cc9..335024c03 100644 --- a/tangelo/algorithms/classical/ccsd_solver.py +++ b/tangelo/algorithms/classical/ccsd_solver.py @@ -17,6 +17,8 @@ from pyscf import cc, lib from pyscf.cc.ccsd_rdm import _make_rdm1, _make_rdm2, _gamma1_intermediates, _gamma2_outcore +from pyscf.cc.uccsd_rdm import (_make_rdm1 as _umake_rdm1, _make_rdm2 as _umake_rdm2, + _gamma1_intermediates as _ugamma1_intermediates, _gamma2_outcore as _ugamma2_outcore) from tangelo.algorithms.electronic_structure_solver import ElectronicStructureSolver @@ -39,6 +41,7 @@ def __init__(self, molecule): self.mean_field = molecule.mean_field self.frozen = molecule.frozen_mos + self.uhf = molecule.uhf def simulate(self): """Perform the simulation (energy calculation) for the molecule. @@ -80,11 +83,12 @@ def get_rdm(self): l1 = self.cc_fragment.l1 l2 = self.cc_fragment.l2 - d1 = _gamma1_intermediates(self.cc_fragment, t1, t2, l1, l2) + d1 = _gamma1_intermediates(self.cc_fragment, t1, t2, l1, l2) if not self.uhf else _ugamma1_intermediates(self.cc_fragment, t1, t2, l1, l2) f = lib.H5TmpFile() - d2 = _gamma2_outcore(self.cc_fragment, t1, t2, l1, l2, f, False) + d2 = _gamma2_outcore(self.cc_fragment, t1, t2, l1, l2, f, False) if not self.uhf else _ugamma2_outcore(self.cc_fragment, t1, t2, l1, l2, f, False) - one_rdm = _make_rdm1(self.cc_fragment, d1, with_frozen=False) - two_rdm = _make_rdm2(self.cc_fragment, d1, d2, with_dm1=True, with_frozen=False) + one_rdm = _make_rdm1(self.cc_fragment, d1, with_frozen=False) if not self.uhf else _umake_rdm1(self.cc_fragment, d1, with_frozen=False) + two_rdm = (_make_rdm2(self.cc_fragment, d1, d2, with_dm1=True, with_frozen=False) if not self.uhf + else _umake_rdm2(self.cc_fragment, d1, d2, with_dm1=True, with_frozen=False)) return one_rdm, two_rdm diff --git a/tangelo/algorithms/classical/fci_solver.py b/tangelo/algorithms/classical/fci_solver.py index 84580144e..0b38d5a50 100644 --- a/tangelo/algorithms/classical/fci_solver.py +++ b/tangelo/algorithms/classical/fci_solver.py @@ -38,6 +38,9 @@ class FCISolver(ElectronicStructureSolver): def __init__(self, molecule): + if molecule.uhf: + raise NotImplementedError(f"SecondQuantizedMolecule that use UHF are not currently supported in {self.__class__.__name__}. Use CCSDSolver") + self.ci = None self.norb = molecule.n_active_mos self.nelec = molecule.n_active_electrons diff --git a/tangelo/algorithms/classical/tests/test_ccsd_solver.py b/tangelo/algorithms/classical/tests/test_ccsd_solver.py index 52df2b494..f5539ba0b 100644 --- a/tangelo/algorithms/classical/tests/test_ccsd_solver.py +++ b/tangelo/algorithms/classical/tests/test_ccsd_solver.py @@ -15,7 +15,7 @@ import unittest from tangelo.algorithms.classical.ccsd_solver import CCSDSolver -from tangelo.molecule_library import mol_H2_321g, mol_Be_321g +from tangelo.molecule_library import mol_H2_321g, mol_Be_321g, mol_H4_sto3g_uhf_a1_frozen # TODO: Can we test the get_rdm method on H2 ? How do we get our reference? Whole matrix or its properties? @@ -29,6 +29,18 @@ def test_ccsd_h2(self): self.assertAlmostEqual(energy, -1.1478300596229851, places=6) + def test_ccsd_h4_uhf_a1_frozen(self): + """Test CCSDSolver against result from reference implementation.""" + + solver = CCSDSolver(mol_H4_sto3g_uhf_a1_frozen) + energy = solver.simulate() + + self.assertAlmostEqual(energy, -1.95831052, places=6) + + one_rdms, two_rdms = solver.get_rdm() + + self.assertAlmostEqual(mol_H4_sto3g_uhf_a1_frozen.energy_from_rdms(one_rdms, two_rdms), -1.95831052, places=6) + def test_ccsd_be(self): """Test CCSDSolver against result from reference implementation.""" diff --git a/tangelo/algorithms/variational/adapt_vqe_solver.py b/tangelo/algorithms/variational/adapt_vqe_solver.py index e31de3272..cf5ce7558 100644 --- a/tangelo/algorithms/variational/adapt_vqe_solver.py +++ b/tangelo/algorithms/variational/adapt_vqe_solver.py @@ -140,7 +140,7 @@ def build(self): self.n_spinorbitals = self.molecule.n_active_sos self.n_electrons = self.molecule.n_active_electrons - self.spin = self.molecule.spin + self.spin = self.molecule.active_spin # Compute qubit hamiltonian for the input molecular system self.qubit_hamiltonian = fermion_to_qubit_mapping(fermion_operator=self.molecule.fermionic_hamiltonian, @@ -153,7 +153,7 @@ def build(self): # Build / set ansatz circuit. ansatz_options = {"mapping": self.qubit_mapping, "up_then_down": self.up_then_down, "reference_state": "HF" if self.ref_state is None else "zero"} - self.ansatz = ADAPTAnsatz(self.n_spinorbitals, self.n_electrons, ansatz_options) + self.ansatz = ADAPTAnsatz(self.n_spinorbitals, self.n_electrons, self.spin, ansatz_options) # Build underlying VQE solver. Options remain consistent throughout the ADAPT cycles. self.vqe_options = {"qubit_hamiltonian": self.qubit_hamiltonian, diff --git a/tangelo/algorithms/variational/tests/test_iqcc_solver.py b/tangelo/algorithms/variational/tests/test_iqcc_solver.py index b2a6cb07c..a6ecde73b 100644 --- a/tangelo/algorithms/variational/tests/test_iqcc_solver.py +++ b/tangelo/algorithms/variational/tests/test_iqcc_solver.py @@ -17,7 +17,7 @@ import unittest from tangelo.algorithms.variational import iQCC_solver -from tangelo.molecule_library import mol_H2_sto3g, mol_H4_sto3g, mol_H4_cation_sto3g,\ +from tangelo.molecule_library import mol_H2_sto3g, mol_H4_sto3g, mol_H4_sto3g_uhf_a1_frozen,\ mol_H4_doublecation_minao @@ -69,12 +69,12 @@ def test_iqcc_h4(self): self.assertAlmostEqual(iqcc_energy, -1.96259, places=4) - def test_iqcc_h4_cation(self): - """Test the energy after 3 iterations for H4+ with generators limited to 8""" + def test_iqcc_h4_uhf(self): + """Test the energy after 3 iterations for H4 uhf with 1 alpha orbital frozen and generators limited to 8""" ansatz_options = {"max_qcc_gens": 8} - iqcc_options = {"molecule": mol_H4_cation_sto3g, + iqcc_options = {"molecule": mol_H4_sto3g_uhf_a1_frozen, "qubit_mapping": "scbk", "up_then_down": True, "ansatz_options": ansatz_options, @@ -85,7 +85,7 @@ def test_iqcc_h4_cation(self): iqcc.build() iqcc_energy = iqcc.simulate() - self.assertAlmostEqual(iqcc_energy, -1.639, places=3) + self.assertAlmostEqual(iqcc_energy, -1.95831, places=3) def test_iqcc_h4_double_cation(self): """Test the energy after 1 iteration for H4+2""" diff --git a/tangelo/algorithms/variational/tests/test_tetris_adapt_vqe_solver.py b/tangelo/algorithms/variational/tests/test_tetris_adapt_vqe_solver.py index 29d052789..98cf84f06 100644 --- a/tangelo/algorithms/variational/tests/test_tetris_adapt_vqe_solver.py +++ b/tangelo/algorithms/variational/tests/test_tetris_adapt_vqe_solver.py @@ -15,7 +15,8 @@ import unittest from tangelo.algorithms.variational import TETRISADAPTSolver -from tangelo.molecule_library import mol_H2_sto3g +from tangelo.molecule_library import mol_H2_sto3g, mol_H4_sto3g_uhf_a1_frozen +from tangelo.toolboxes.ansatz_generator._unitary_majorana_cc import get_majorana_uccgsd_pool class TETRISADAPTSolverTest(unittest.TestCase): @@ -37,6 +38,19 @@ def test_single_cycle_tetris_adapt(self): self.assertAlmostEqual(adapt_solver.optimal_energy, -1.13727, places=4) + def test_multiple_cycle_tetris_adapt_uhf(self): + """Try running TETRISADAPTSolver with JKMN mapping and uhf H4 with majorana uccgsd pool for 7 iterations""" + + opt_dict = {"molecule": mol_H4_sto3g_uhf_a1_frozen, "max_cycles": 7, "verbose": False, + "pool": get_majorana_uccgsd_pool, "pool_args": {"molecule": mol_H4_sto3g_uhf_a1_frozen}, + "qubit_mapping": "JKMN"} + adapt_solver = TETRISADAPTSolver(opt_dict) + adapt_solver.build() + adapt_solver.simulate() + + self.assertAlmostEqual(adapt_solver.optimal_energy, -1.95831, places=3) + self.assertTrue(adapt_solver.ansatz.n_var_params > 7) + if __name__ == "__main__": unittest.main() diff --git a/tangelo/algorithms/variational/tests/test_vqe_solver.py b/tangelo/algorithms/variational/tests/test_vqe_solver.py index cfaeb9146..aaf25b4ab 100644 --- a/tangelo/algorithms/variational/tests/test_vqe_solver.py +++ b/tangelo/algorithms/variational/tests/test_vqe_solver.py @@ -19,7 +19,7 @@ from tangelo.helpers.utils import installed_backends from tangelo.linq.target import QiskitSimulator from tangelo.algorithms import BuiltInAnsatze, VQESolver -from tangelo.molecule_library import mol_H2_sto3g, mol_H4_sto3g, mol_H4_cation_sto3g, mol_NaH_sto3g, mol_H4_sto3g_symm +from tangelo.molecule_library import mol_H2_sto3g, mol_H4_sto3g, mol_H4_cation_sto3g, mol_NaH_sto3g, mol_H4_sto3g_symm, mol_H4_sto3g_uhf_a1_frozen from tangelo.toolboxes.ansatz_generator.uccsd import UCCSD from tangelo.toolboxes.qubit_mappings.mapping_transform import fermion_to_qubit_mapping from tangelo.toolboxes.molecular_computation.rdms import matricize_2rdm @@ -299,6 +299,16 @@ def test_simulate_h4_open(self): energy = vqe_solver.simulate() self.assertAlmostEqual(energy, -1.6394, delta=1e-3) + def test_simulate_h4_open(self): + """Run VQE on H4 molecule, with UCCSD ansatz, scbk qubit mapping, initial parameters, exact simulator """ + vqe_options = {"molecule": mol_H4_sto3g_uhf_a1_frozen, "ansatz": BuiltInAnsatze.UCCSD, "qubit_mapping": "scbk", + "initial_var_params": [0.001]*15, "verbose": False, "up_then_down": True} + vqe_solver = VQESolver(vqe_options) + vqe_solver.build() + + energy = vqe_solver.simulate() + self.assertAlmostEqual(energy, -1.95831, delta=1e-3) + def test_simulate_qmf_h4_open(self): """Run VQE on H4 + molecule, with QMF ansatz, JW qubit mapping, initial parameters, exact simulator. diff --git a/tangelo/algorithms/variational/vqe_solver.py b/tangelo/algorithms/variational/vqe_solver.py index ece4c8b12..27fe6e5e5 100644 --- a/tangelo/algorithms/variational/vqe_solver.py +++ b/tangelo/algorithms/variational/vqe_solver.py @@ -184,7 +184,7 @@ def build(self): n_spinorbitals=self.molecule.n_active_sos, n_electrons=self.molecule.n_active_electrons, up_then_down=self.up_then_down, - spin=self.molecule.spin) + spin=self.molecule.active_spin) if self.penalty_terms: pen_ferm = agen.penalty_terms.combined_penalty(self.molecule.n_active_mos, self.penalty_terms) @@ -193,7 +193,7 @@ def build(self): n_spinorbitals=self.molecule.n_active_sos, n_electrons=self.molecule.n_active_electrons, up_then_down=self.up_then_down, - spin=self.molecule.spin) + spin=self.molecule.active_spin) self.qubit_hamiltonian += pen_qubit if self.ansatz == BuiltInAnsatze.QCC: self.ansatz_options["qubit_ham"] = self.qubit_hamiltonian.to_qubitoperator() @@ -368,7 +368,7 @@ def operator_expectation(self, operator, var_params=None, n_active_mos=None, n_a if self.molecule: n_active_electrons = self.molecule.n_active_electrons n_active_sos = self.molecule.n_active_sos - spin = self.molecule.spin + spin = self.molecule.active_spin else: raise KeyError("Must supply n_active_electrons, n_active_sos, and spin with a FermionOperator and scbk mapping.") diff --git a/tangelo/molecule_library.py b/tangelo/molecule_library.py index d9ce74ab6..615714eec 100644 --- a/tangelo/molecule_library.py +++ b/tangelo/molecule_library.py @@ -26,6 +26,12 @@ mol_H2_sto3g = SecondQuantizedMolecule(xyz_H2, q=0, spin=0, basis="sto-3g") mol_H2_321g = SecondQuantizedMolecule(xyz_H2, q=0, spin=0, basis="3-21g") +# Dihydrogen stretched. UHF different from HF. +xyz_H2_stretch = [ + ("H", (0., 0., 0.)), + ("H", (0., 0., 1.6)) +] +mol_H2_sto3g_uhf = SecondQuantizedMolecule(xyz_H2_stretch, q=0, spin=0, basis="sto-3g", uhf=True) # Tetrahydrogen. xyz_H4 = [ @@ -40,7 +46,7 @@ mol_H4_cation_sto3g = SecondQuantizedMolecule(xyz_H4, q=1, spin=1, basis="sto-3g") mol_H4_doublecation_minao = SecondQuantizedMolecule(xyz_H4, q=2, spin=0, basis="minao") mol_H4_doublecation_321g = SecondQuantizedMolecule(xyz_H4, q=2, spin=0, basis="3-21g") - +mol_H4_sto3g_uhf_a1_frozen = SecondQuantizedMolecule(xyz_H4, q=0, spin=0, basis="sto-3g", uhf=True, frozen_orbitals=[[1], []]) # Decahydrogen. xyz_H10 = [ diff --git a/tangelo/problem_decomposition/dmet/fragment.py b/tangelo/problem_decomposition/dmet/fragment.py index 5d50535b6..34df6a36d 100644 --- a/tangelo/problem_decomposition/dmet/fragment.py +++ b/tangelo/problem_decomposition/dmet/fragment.py @@ -55,6 +55,7 @@ def __post_init__(self): self.n_active_electrons = self.molecule.nelectron self.q = self.molecule.charge self.spin = self.molecule.spin + self.active_spin = self.spin self.basis = self.molecule.basis self.n_active_mos = len(self.mean_field.mo_energy) @@ -62,6 +63,7 @@ def __post_init__(self): self.fermionic_hamiltonian = self._get_fermionic_hamiltonian() self.frozen_mos = None + self.uhf = False def _get_fermionic_hamiltonian(self): """This method returns the fermionic hamiltonian. It written to take diff --git a/tangelo/toolboxes/ansatz_generator/_unitary_cc_openshell.py b/tangelo/toolboxes/ansatz_generator/_unitary_cc_openshell.py index 11c009459..38897b190 100644 --- a/tangelo/toolboxes/ansatz_generator/_unitary_cc_openshell.py +++ b/tangelo/toolboxes/ansatz_generator/_unitary_cc_openshell.py @@ -36,26 +36,24 @@ from tangelo.toolboxes.operators import FermionOperator -def uccsd_openshell_paramsize(n_spinorbitals, n_alpha_electrons, n_beta_electrons): +def uccsd_openshell_paramsize(n_alpha_electrons, n_beta_electrons, n_orb_a, n_orb_b): """Determine number of independent amplitudes for open-shell UCCSD Args: - n_spinorbitals(int): Number of spin-orbitals in the system n_alpha_electrons(int): Number of alpha electrons in the reference state n_beta_electrons(int): Number of beta electrons in the reference state + n_orb_a(int): Number of active alpha orbitals + n_orb_b(int): Number of active beta orbitals Returns: The number of unique single amplitudes, double amplitudes and the number of single alpha and beta amplitudes, as well as the number of double alpha-alpha, beta-beta and alpha-beta amplitudes """ - if n_spinorbitals % 2 != 0: - raise ValueError("The total number of spin-orbitals should be even.") # Compute the number of occupied and virtual alpha and beta orbitals - n_orb_a_b = n_spinorbitals // 2 n_occ_a = n_alpha_electrons n_occ_b = n_beta_electrons - n_virt_a = n_orb_a_b - n_alpha_electrons - n_virt_b = n_orb_a_b - n_beta_electrons + n_virt_a = n_orb_a - n_alpha_electrons + n_virt_b = n_orb_b - n_beta_electrons # Calculate the number of alpha single amplitudes n_single_a = n_occ_a * n_virt_a @@ -82,8 +80,8 @@ def uccsd_openshell_paramsize(n_spinorbitals, n_alpha_electrons, n_beta_electron n_double_aa, n_double_bb, n_double_ab -def uccsd_openshell_generator(packed_amplitudes, n_spinorbitals, n_alpha_electrons, - n_beta_electrons, anti_hermitian=True): +def uccsd_openshell_generator(packed_amplitudes, n_alpha_electrons, + n_beta_electrons, n_orb_a, n_orb_b, anti_hermitian=True): r"""Create an open-shell UCCSD generator for a system with n_alpha_electrons and n_beta_electrons This function generates a FermionOperator for a UCCSD generator designed @@ -95,28 +93,26 @@ def uccsd_openshell_generator(packed_amplitudes, n_spinorbitals, n_alpha_electro and double excitation amplitudes for an open-shell UCCSD operator. The ordering lists unique single excitations before double excitations. - n_spinorbitals(int): Number of spin-orbitals used to represent the system n_alpha_electrons(int): Number of alpha electrons in the physical system. n_beta_electrons(int): Number of beta electrons in the physical system. + n_orb_a(int): Number of active alpha orbitals + n_orb_b(int): Number of active beta orbitals anti_hermitian(Bool): Flag to generate only normal CCSD operator rather than unitary variant, primarily for testing Returns: generator(FermionOperator): Generator of the UCCSD operator that builds the open-shell UCCSD wavefunction. """ - if n_spinorbitals % 2 != 0: - raise ValueError("The total number of spin-orbitals should be even.") # Compute the number of occupied and virtual alpha and beta orbitals - n_orb_a_b = n_spinorbitals // 2 n_occ_a = n_alpha_electrons n_occ_b = n_beta_electrons - n_virt_a = n_orb_a_b - n_alpha_electrons - n_virt_b = n_orb_a_b - n_beta_electrons + n_virt_a = n_orb_a - n_alpha_electrons + n_virt_b = n_orb_b - n_beta_electrons # Unpack the single and double amplitudes _, _, n_single_a, n_single_b, \ - n_double_aa, n_double_bb, _ = uccsd_openshell_paramsize(n_spinorbitals, n_alpha_electrons, n_beta_electrons) + n_double_aa, n_double_bb, _ = uccsd_openshell_paramsize(n_alpha_electrons, n_beta_electrons, n_orb_a, n_orb_b) # Define the various increments for the sizes of the orbital spaces n_s_1 = n_single_a @@ -292,8 +288,8 @@ def uccsd_openshell_generator(packed_amplitudes, n_spinorbitals, n_alpha_electro def uccsd_openshell_get_packed_amplitudes(alpha_double_amplitudes, beta_double_amplitudes, - alpha_beta_double_amplitudes, n_spinorbitals, n_alpha_electrons, - n_beta_electrons, alpha_single_amplitudes=None, + alpha_beta_double_amplitudes, n_alpha_electrons, + n_beta_electrons, n_orb_a, n_orb_b, alpha_single_amplitudes=None, beta_single_amplitudes=None): r"""Convert amplitudes for use with the open-shell UCCSD (e.g. from a UHF MP2 guess) The output list contains only the non-redundant amplitudes that are @@ -321,9 +317,10 @@ def uccsd_openshell_get_packed_amplitudes(alpha_double_amplitudes, beta_double_a double excitation amplitudes corresponding to t[i_alpha,j_beta,a_alpha,b_beta] * (a_a_alpha^\dagger a_i_alpha a_b_beta^\dagger a_j_beta - H.C.) - n_spinorbitals(int): Number of spin-orbitals used to represent the system n_alpha_electrons(int): Number of alpha electrons in the physical system. n_beta_electrons(int): Number of beta electrons in the physical system + n_orb_a(int): Number of active alpha orbitals + n_orb_b(int): Number of active beta orbitals alpha_single_amplitudes(ndarray optional): optional [N_occupied_alpha x N_virtual_alpha] array string the alpha single excitation amplitudes corresponding to t[i_alpha,a_alpha] @@ -339,18 +336,14 @@ def uccsd_openshell_get_packed_amplitudes(alpha_double_amplitudes, beta_double_a excitations. """ - if n_spinorbitals % 2 != 0: - raise ValueError("The total number of spin-orbitals should be even.") - # Compute the number of occupied and virtual alpha and beta orbitals - n_orb_a_b = n_spinorbitals // 2 n_occ_a = n_alpha_electrons n_occ_b = n_beta_electrons - n_virt_a = n_orb_a_b - n_alpha_electrons - n_virt_b = n_orb_a_b - n_beta_electrons + n_virt_a = n_orb_a - n_alpha_electrons + n_virt_b = n_orb_b - n_beta_electrons # Calculate the number of non-redundant single and double amplitudes - _, _, n_single_a, n_single_b, _, _, _ = uccsd_openshell_paramsize(n_spinorbitals, n_alpha_electrons, n_beta_electrons) + _, _, n_single_a, n_single_b, _, _, _ = uccsd_openshell_paramsize(n_alpha_electrons, n_beta_electrons, n_orb_a, n_orb_b) # packed amplitudes list packed_amplitudes = [] diff --git a/tangelo/toolboxes/ansatz_generator/adapt_ansatz.py b/tangelo/toolboxes/ansatz_generator/adapt_ansatz.py index 39377a601..b18460366 100644 --- a/tangelo/toolboxes/ansatz_generator/adapt_ansatz.py +++ b/tangelo/toolboxes/ansatz_generator/adapt_ansatz.py @@ -32,6 +32,7 @@ class ADAPTAnsatz(Ansatz): Attributes: n_spinorbitals (int): Number of spin orbitals in a given basis. n_electrons (int): Number of electrons. + spin (int): Spin of system. operators (list of QubitOperator): List of operators to consider at the construction step. Can be useful for restarting computation. ferm_operators (list of FermionOperator): Same as operators, but in @@ -43,7 +44,7 @@ class ADAPTAnsatz(Ansatz): circuit (Circuit): Quantum circuit defined by a list of Gates. """ - def __init__(self, n_spinorbitals, n_electrons, ansatz_options=None): + def __init__(self, n_spinorbitals, n_electrons, spin, ansatz_options=None): default_options = {"operators": list(), "ferm_operators": list(), "mapping": "jw", "up_then_down": False, "reference_state": "HF"} @@ -60,6 +61,7 @@ def __init__(self, n_spinorbitals, n_electrons, ansatz_options=None): self.n_spinorbitals = n_spinorbitals self.n_electrons = n_electrons + self.spin = spin self.var_params = None self.circuit = None @@ -105,7 +107,8 @@ def update_var_params(self, var_params): def prepare_reference_state(self): """Prepare a circuit generating the HF reference state.""" if self.reference_state.upper() == "HF": - return get_reference_circuit(n_spinorbitals=self.n_spinorbitals, n_electrons=self.n_electrons, mapping=self.mapping, up_then_down=self.up_then_down) + return get_reference_circuit(n_spinorbitals=self.n_spinorbitals, n_electrons=self.n_electrons, + mapping=self.mapping, up_then_down=self.up_then_down, spin=self.spin) else: return Circuit(n_qubits=get_qubit_number(self.mapping, self.n_spinorbitals)) diff --git a/tangelo/toolboxes/ansatz_generator/hea.py b/tangelo/toolboxes/ansatz_generator/hea.py index 83f94dbe6..82000481a 100644 --- a/tangelo/toolboxes/ansatz_generator/hea.py +++ b/tangelo/toolboxes/ansatz_generator/hea.py @@ -49,20 +49,22 @@ class HEA(Ansatz): """ def __init__(self, molecule=None, mapping="jw", up_then_down=False, - n_layers=2, rot_type="euler", n_qubits=None, n_electrons=None, - reference_state="HF"): + n_layers=2, rot_type="euler", n_qubits=None, n_electrons=None, + spin=None, reference_state="HF"): if not (bool(molecule) ^ (bool(n_qubits) and (bool(n_electrons) | (reference_state == "zero")))): raise ValueError(f"A molecule OR qubit + electrons number must be " - "provided when instantiating the HEA with the HF reference state. " - "For reference_state='zero', only the number of qubits is needed.") + "provided when instantiating the HEA with the HF reference state. " + "For reference_state='zero', only the number of qubits is needed.") if n_qubits: self.n_qubits = n_qubits self.n_electrons = n_electrons + self.spin = spin else: self.n_qubits = get_qubit_number(mapping, molecule.n_active_sos) self.n_electrons = molecule.n_active_electrons + self.spin = molecule.active_spin self.qubit_mapping = mapping self.up_then_down = up_then_down @@ -129,12 +131,14 @@ def prepare_reference_state(self): return get_reference_circuit(n_spinorbitals=self.n_qubits, n_electrons=self.n_electrons, mapping=self.qubit_mapping, - up_then_down=self.up_then_down) + up_then_down=self.up_then_down, + spin=self.spin) elif self.reference_state == "zero": return get_reference_circuit(n_spinorbitals=self.n_qubits, n_electrons=0, mapping=self.qubit_mapping, - up_then_down=self.up_then_down) + up_then_down=self.up_then_down, + spin=self.spin) def build_circuit(self, var_params=None): """Construct the variational circuit to be used as our ansatz.""" diff --git a/tangelo/toolboxes/ansatz_generator/ilc.py b/tangelo/toolboxes/ansatz_generator/ilc.py index 11afe5749..8e5c8c3f1 100644 --- a/tangelo/toolboxes/ansatz_generator/ilc.py +++ b/tangelo/toolboxes/ansatz_generator/ilc.py @@ -88,8 +88,8 @@ def __init__(self, molecule, mapping="jw", up_then_down=False, acs=None, self.molecule = molecule if isinstance(self.molecule, SecondQuantizedMolecule): self.n_spinorbitals = self.molecule.n_active_sos - self.n_electrons = self.molecule.n_electrons - self.spin = self.molecule.spin + self.n_electrons = self.molecule.n_active_electrons + self.spin = self.molecule.active_spin elif isinstance(self.molecule, dict): self.n_spinorbitals = self.molecule["n_spinorbitals"] self.n_electrons = self.molecule["n_electrons"] diff --git a/tangelo/toolboxes/ansatz_generator/qcc.py b/tangelo/toolboxes/ansatz_generator/qcc.py index 4e5605a65..1dd349b75 100644 --- a/tangelo/toolboxes/ansatz_generator/qcc.py +++ b/tangelo/toolboxes/ansatz_generator/qcc.py @@ -100,7 +100,7 @@ def __init__(self, molecule, mapping="jw", up_then_down=False, dis=None, else: self.n_spinorbitals = self.molecule.n_active_sos self.n_electrons = self.molecule.n_active_electrons - self.spin = self.molecule.spin + self.spin = self.molecule.active_spin self.mapping = mapping self.up_then_down = up_then_down diff --git a/tangelo/toolboxes/ansatz_generator/qmf.py b/tangelo/toolboxes/ansatz_generator/qmf.py index 23799b7e9..54cc519ad 100755 --- a/tangelo/toolboxes/ansatz_generator/qmf.py +++ b/tangelo/toolboxes/ansatz_generator/qmf.py @@ -86,7 +86,7 @@ def __init__(self, molecule, mapping="jw", up_then_down=False, init_qmf=None, re raise ValueError("The total number of spin-orbitals should be even.") self.n_orbitals = self.n_spinorbitals // 2 - self.spin = molecule.spin + self.spin = molecule.active_spin self.fermi_ham = self.molecule.fermionic_hamiltonian self.n_electrons = self.molecule.n_active_electrons diff --git a/tangelo/toolboxes/ansatz_generator/tests/test_adapt_ansatz.py b/tangelo/toolboxes/ansatz_generator/tests/test_adapt_ansatz.py index 9ee77831e..6cdf25b05 100644 --- a/tangelo/toolboxes/ansatz_generator/tests/test_adapt_ansatz.py +++ b/tangelo/toolboxes/ansatz_generator/tests/test_adapt_ansatz.py @@ -29,13 +29,13 @@ class ADAPTAnsatzTest(unittest.TestCase): def test_adaptansatz_init(self): """Verify behavior of ADAPTAnsatz class.""" - ansatz = ADAPTAnsatz(n_spinorbitals=4, n_electrons=2) + ansatz = ADAPTAnsatz(n_spinorbitals=4, n_electrons=2, spin=0) ansatz.build_circuit() def test_adaptansatz_adding(self): """Verify operator addition behavior of ADAPTAnsatz class.""" - ansatz = ADAPTAnsatz(n_spinorbitals=4, n_electrons=2) + ansatz = ADAPTAnsatz(n_spinorbitals=4, n_electrons=2, spin=0) ansatz.build_circuit() ansatz.add_operator(qu_op) @@ -46,7 +46,7 @@ def test_adaptansatz_adding(self): def test_adaptansatz_set_var_params(self): """Verify variational parameter tuning behavior of ADAPTAnsatz class.""" - ansatz = ADAPTAnsatz(n_spinorbitals=4, n_electrons=2) + ansatz = ADAPTAnsatz(n_spinorbitals=4, n_electrons=2, spin=0) ansatz.build_circuit() ansatz.add_operator(qu_op) diff --git a/tangelo/toolboxes/ansatz_generator/tests/test_qcc.py b/tangelo/toolboxes/ansatz_generator/tests/test_qcc.py index 86c90f879..c5b9ab0ae 100644 --- a/tangelo/toolboxes/ansatz_generator/tests/test_qcc.py +++ b/tangelo/toolboxes/ansatz_generator/tests/test_qcc.py @@ -23,7 +23,8 @@ from tangelo.linq import get_backend from tangelo.toolboxes.ansatz_generator.qcc import QCC from tangelo.toolboxes.operators.operators import QubitOperator -from tangelo.molecule_library import mol_H2_sto3g, mol_H4_cation_sto3g, mol_H4_doublecation_minao +from tangelo.molecule_library import mol_H2_sto3g, mol_H4_cation_sto3g, mol_H4_doublecation_minao, mol_H4_sto3g_uhf_a1_frozen +from tangelo.toolboxes.qubit_mappings.mapping_transform import fermion_to_qubit_mapping sim = get_backend() @@ -141,6 +142,18 @@ def test_qmf_qcc_h4_double_cation(self): energy = sim.get_expectation_value(qubit_hamiltonian, qcc_ansatz.circuit) self.assertAlmostEqual(energy, -0.85465810, delta=1e-6) + def test_qmf_qcc_h4_uhf_ref(self): + """ Verify unrestricted open-shell functionality when using the QCC ansatz for H4 a1 frozen """ + + qcc_ansatz = QCC(mol_H4_sto3g_uhf_a1_frozen, "scbk", True) + + mol = mol_H4_sto3g_uhf_a1_frozen + qu_op = fermion_to_qubit_mapping(mol.fermionic_hamiltonian, "scbk", mol.n_active_sos, mol.n_active_electrons, True, mol.active_spin) + + # Assert energy returned is the same as mean_field for reference state + energy = sim.get_expectation_value(qu_op, qcc_ansatz.prepare_reference_state()) + self.assertAlmostEqual(energy, mol.mean_field.e_tot, delta=1e-6) + if __name__ == "__main__": unittest.main() diff --git a/tangelo/toolboxes/ansatz_generator/tests/test_qmf.py b/tangelo/toolboxes/ansatz_generator/tests/test_qmf.py index be1b03d2c..68d8feb22 100644 --- a/tangelo/toolboxes/ansatz_generator/tests/test_qmf.py +++ b/tangelo/toolboxes/ansatz_generator/tests/test_qmf.py @@ -20,7 +20,7 @@ from tangelo.linq import get_backend from tangelo.toolboxes.ansatz_generator.qmf import QMF -from tangelo.molecule_library import mol_H2_sto3g, mol_H4_sto3g, mol_H4_cation_sto3g +from tangelo.molecule_library import mol_H2_sto3g, mol_H4_sto3g, mol_H4_cation_sto3g, mol_H4_sto3g_uhf_a1_frozen sim = get_backend() @@ -137,6 +137,20 @@ def test_qmf_open_h4_cation(self): qmf_ansatz.update_var_params(qmf_var_params) self.assertAlmostEqual(energy, -1.5859184313544759, delta=1e-6) + def test_qmf_uhf_h4_cation(self): + """ Verify open-shell QMF functionalities for H4 + """ + + # Build ansatz and circuit + qmf_ansatz = QMF(mol_H4_sto3g_uhf_a1_frozen, "scbk", True) + qmf_ansatz.build_circuit() + + # Build qubit hamiltonian for energy evaluation + qubit_hamiltonian = qmf_ansatz.qubit_ham + + # Assert energy returned is as expected for given parameters + energy = sim.get_expectation_value(qubit_hamiltonian, qmf_ansatz.circuit) + self.assertAlmostEqual(energy, mol_H4_sto3g_uhf_a1_frozen.mean_field.e_tot, delta=1e-6) + if __name__ == "__main__": unittest.main() diff --git a/tangelo/toolboxes/ansatz_generator/uccgd.py b/tangelo/toolboxes/ansatz_generator/uccgd.py index fc757b85a..d40d3cd41 100644 --- a/tangelo/toolboxes/ansatz_generator/uccgd.py +++ b/tangelo/toolboxes/ansatz_generator/uccgd.py @@ -54,7 +54,7 @@ def __init__(self, molecule, mapping="JW", up_then_down=False, reference_state=" self.n_spinorbitals = molecule.n_active_sos self.n_electrons = molecule.n_active_electrons - self.spin = molecule.spin + self.spin = molecule.active_spin self.qubit_mapping = mapping self.up_then_down = up_then_down diff --git a/tangelo/toolboxes/ansatz_generator/uccsd.py b/tangelo/toolboxes/ansatz_generator/uccsd.py index 22d852f20..3c6bc6b62 100644 --- a/tangelo/toolboxes/ansatz_generator/uccsd.py +++ b/tangelo/toolboxes/ansatz_generator/uccsd.py @@ -37,7 +37,7 @@ from .ansatz import Ansatz from .ansatz_utils import exp_pauliword_to_gates -from ._unitary_cc_openshell import uccsd_openshell_paramsize, uccsd_openshell_generator +from ._unitary_cc_openshell import uccsd_openshell_paramsize, uccsd_openshell_generator, uccsd_openshell_get_packed_amplitudes from tangelo.toolboxes.qubit_mappings.mapping_transform import fermion_to_qubit_mapping from tangelo.toolboxes.qubit_mappings.statevector_mapping import get_reference_circuit @@ -64,19 +64,24 @@ def __init__(self, molecule, mapping="JW", up_then_down=False, spin=None, refere self.molecule = molecule self.n_spinorbitals = molecule.n_active_sos self.n_electrons = molecule.n_active_electrons - self.spin = molecule.spin if spin is None else spin + self.spin = molecule.active_spin if spin is None else spin self.mapping = mapping self.up_then_down = up_then_down # Later: refactor to handle various flavors of UCCSD - if self.n_spinorbitals % 2 != 0: - raise ValueError("The total number of spin-orbitals should be even.") + if not self.molecule.uhf and self.n_spinorbitals % 2 != 0: + raise ValueError("The total number of spin-orbitals should be even when reference is RHF or ROHF.") # choose open-shell uccsd if spin not zero, else choose singlet ccsd - if self.spin != 0: - self.n_alpha = self.n_electrons//2 + self.spin//2 + 1 * (self.n_electrons % 2) - self.n_beta = self.n_electrons//2 - self.spin//2 - self.n_singles, self.n_doubles, _, _, _, _, _ = uccsd_openshell_paramsize(self.n_spinorbitals, self.n_alpha, self.n_beta) + if self.spin != 0 or self.molecule.uhf: + self.n_alpha, self.n_beta = self.molecule.n_active_ab_electrons + if self.molecule.uhf: + self.n_orb_a = self.molecule.n_active_mos[0] + self.n_orb_b = self.molecule.n_active_mos[1] + else: + self.n_orb_a = self.n_spinorbitals//2 + self.n_orb_b = self.n_spinorbitals//2 + self.n_singles, self.n_doubles, _, _, _, _, _ = uccsd_openshell_paramsize(self.n_alpha, self.n_beta, self.n_orb_a, self.n_orb_b) else: self.n_spatial_orbitals = self.n_spinorbitals // 2 self.n_occupied = int(np.ceil(self.n_electrons / 2)) @@ -91,11 +96,11 @@ def __init__(self, molecule, mapping="JW", up_then_down=False, spin=None, refere # TODO: support for others self.supported_reference_state = {"HF", "zero"} # Supported var param initialization - self.supported_initial_var_params = {"ones", "random", "mp2"} if self.spin == 0 else {"ones", "random"} + self.supported_initial_var_params = {"ones", "random", "mp2"} if (self.spin == 0 and not self.molecule.uhf) else {"ones", "random"} # Default initial parameters for initialization # TODO: support for openshell MP2 initialization - self.var_params_default = "mp2" if self.spin == 0 else "ones" + self.var_params_default = "mp2" if (self.spin == 0 and not self.molecule.uhf) else "ones" self.reference_state = reference_state self.var_params = None @@ -160,7 +165,7 @@ def build_circuit(self, var_params=None): self.set_var_params() # Build qubit operator required to build UCCSD - qubit_op = self._get_singlet_qubit_operator() if self.spin == 0 else self._get_openshell_qubit_operator() + qubit_op = self._get_singlet_qubit_operator() if (self.spin == 0 and not self.molecule.uhf) else self._get_openshell_qubit_operator() # Prepend reference state circuit reference_state_circuit = self.prepare_reference_state() @@ -190,7 +195,7 @@ def update_var_params(self, var_params): self.set_var_params(var_params) # Build qubit operator required to build UCCSD - qubit_op = self._get_singlet_qubit_operator() if self.spin == 0 else self._get_openshell_qubit_operator() + qubit_op = self._get_singlet_qubit_operator() if (self.spin == 0 and not self.molecule.uhf) else self._get_openshell_qubit_operator() # If qubit operator terms have changed, rebuild circuit. Else, simply update variational gates directly if set(self.pauli_to_angles_mapping.keys()) != set(qubit_op.terms.keys()): @@ -212,7 +217,8 @@ def _get_singlet_qubit_operator(self): mapping=self.mapping, n_spinorbitals=self.n_spinorbitals, n_electrons=self.n_electrons, - up_then_down=self.up_then_down) + up_then_down=self.up_then_down, + spin=self.spin) # Cast all coefs to floats (rotations angles are real) for key in qubit_op.terms: @@ -228,14 +234,16 @@ def _get_openshell_qubit_operator(self): QubitOperator: qubit-encoded elements of the UCCSD ansatz. """ fermion_op = uccsd_openshell_generator(self.var_params, - self.n_spinorbitals, self.n_alpha, - self.n_beta) + self.n_beta, + self.n_orb_a, + self.n_orb_b) qubit_op = fermion_to_qubit_mapping(fermion_operator=fermion_op, mapping=self.mapping, n_spinorbitals=self.n_spinorbitals, n_electrons=self.n_electrons, - up_then_down=self.up_then_down) + up_then_down=self.up_then_down, + spin=self.spin) # Cast all coefs to floats (rotations angles are real) for key in qubit_op.terms: @@ -254,6 +262,8 @@ def _compute_mp2_params(self): Returns: list of float: The initial variational parameters. """ + if self.molecule.uhf: + raise NotImplementedError(f"MP2 initialization is not currently implemented for UHF reference in {self.__class__}") mp2_fragment = mp.MP2(self.molecule.mean_field, frozen=self.molecule.frozen_mos) mp2_fragment.verbose = 0 diff --git a/tangelo/toolboxes/ansatz_generator/upccgsd.py b/tangelo/toolboxes/ansatz_generator/upccgsd.py index e8c289a2f..c6cfda5a4 100644 --- a/tangelo/toolboxes/ansatz_generator/upccgsd.py +++ b/tangelo/toolboxes/ansatz_generator/upccgsd.py @@ -57,7 +57,7 @@ def __init__(self, molecule, mapping="JW", up_then_down=False, k=2, reference_st self.n_spinorbitals = molecule.n_active_sos self.n_electrons = molecule.n_active_electrons - self.spin = molecule.spin + self.spin = molecule.active_spin self.k = k self.qubit_mapping = mapping diff --git a/tangelo/toolboxes/ansatz_generator/vsqs.py b/tangelo/toolboxes/ansatz_generator/vsqs.py index 708ff8f67..2bd8a3805 100644 --- a/tangelo/toolboxes/ansatz_generator/vsqs.py +++ b/tangelo/toolboxes/ansatz_generator/vsqs.py @@ -75,6 +75,8 @@ def __init__(self, molecule=None, mapping="jw", up_then_down=False, intervals=2, raise ValueError("Reference state Circuit must be provided when simulating a qubit hamiltonian directly") self.reference_state = reference_state else: + if molecule.uhf: + raise NotImplementedError("VSQS does not currently support UHF reference states.") self.n_electrons = molecule.n_active_electrons self.n_spinorbitals = int(molecule.n_sos) self.n_qubits = get_qubit_number(mapping, self.n_spinorbitals) diff --git a/tangelo/toolboxes/molecular_computation/molecule.py b/tangelo/toolboxes/molecular_computation/molecule.py index 56a2941d8..8a9cd6971 100644 --- a/tangelo/toolboxes/molecular_computation/molecule.py +++ b/tangelo/toolboxes/molecular_computation/molecule.py @@ -18,11 +18,13 @@ import copy from dataclasses import dataclass, field +from itertools import product import numpy as np from pyscf import gto, scf, ao2mo, symm, lib import openfermion import openfermion.ops.representations as reps +from openfermion.utils import down_index, up_index from openfermion.chem.molecular_data import spinorb_from_spatial from openfermion.ops.representations.interaction_operator import get_active_space_integrals as of_get_active_space_integrals @@ -167,6 +169,7 @@ class SecondQuantizedMolecule(Molecule): symmetry (bool or str): Whether to use symmetry in RHF or ROHF calculation. Can also specify point group using pyscf allowed string. e.g. "Dooh", "D2h", "C2v", ... + uhf (bool): If True, Use UHF instead of RHF or ROHF reference. Default False mf_energy (float): Mean-field energy (RHF or ROHF energy depending on the spin). mo_energies (list of float): Molecular orbital energies. @@ -200,6 +203,7 @@ class SecondQuantizedMolecule(Molecule): basis: str = "sto-3g" ecp: dict = field(default_factory=dict) symmetry: bool = False + uhf: bool = False frozen_orbitals: list or int = field(default="frozen_core", repr=False) # Defined in __post_init__. @@ -224,15 +228,25 @@ def __post_init__(self): @property def n_active_electrons(self): - return int(sum([self.mo_occ[i] for i in self.active_occupied])) + return sum(self.n_active_ab_electrons) + + @property + def n_active_ab_electrons(self): + if self.uhf: + return (int(sum([self.mo_occ[0][i] for i in self.active_occupied[0]])), int(sum([self.mo_occ[1][i] for i in self.active_occupied[1]]))) + else: + n_active_electrons = int(sum([self.mo_occ[i] for i in self.active_occupied])) + n_alpha = n_active_electrons//2 + self.spin//2 + (n_active_electrons % 2) + n_beta = n_active_electrons//2 - self.spin//2 + return (n_alpha, n_beta) @property def n_active_sos(self): - return 2*len(self.active_mos) + return 2*len(self.active_mos) if not self.uhf else max(len(self.active_mos[0])*2, len(self.active_mos[1])*2) @property def n_active_mos(self): - return len(self.active_mos) + return len(self.active_mos) if not self.uhf else [len(self.active_mos[0]), len(self.active_mos[1])] @property def fermionic_hamiltonian(self): @@ -250,7 +264,8 @@ def frozen_mos(self): list: MOs indexes frozen (occupied + virtual). """ if self.frozen_occupied and self.frozen_virtual: - return self.frozen_occupied + self.frozen_virtual + return (self.frozen_occupied + self.frozen_virtual if not self.uhf else + [self.frozen_occupied[0] + self.frozen_virtual[0], self.frozen_occupied[1] + self.frozen_virtual[1]]) elif self.frozen_occupied: return self.frozen_occupied elif self.frozen_virtual: @@ -265,7 +280,7 @@ def active_mos(self): Returns: list: MOs indexes that are active (occupied + virtual). """ - return self.active_occupied + self.active_virtual + return self.active_occupied + self.active_virtual if not self.uhf else [self.active_occupied[i]+self.active_virtual[i] for i in range(2)] @property def active_spin(self): @@ -274,7 +289,8 @@ def active_spin(self): Returns: int: n_alpha - n_beta electrons of the active occupied orbital space. """ - return sum([self.mo_occ[i] == 1 for i in self.active_occupied]) + n_alpha, n_beta = self.n_active_ab_electrons + return n_alpha - n_beta @property def mo_coeff(self): @@ -288,10 +304,17 @@ def mo_coeff(self): @mo_coeff.setter def mo_coeff(self, new_mo_coeff): # Asserting the new molecular coefficient matrix have the same dimensions. - assert self.mean_field.mo_coeff.shape == new_mo_coeff.shape, \ - f"The new molecular coefficients matrix has a {new_mo_coeff.shape}"\ - f" shape: expected shape is {self.mean_field.mo_coeff.shape}." - self.mean_field.mo_coeff = new_mo_coeff + if self.uhf: + assert len(new_mo_coeff) == 2, "Must provide [alpha mo_coeff, beta_mo_coeff]" + assert ((self.mean_field.mo_coeff[0].shape == new_mo_coeff[0].shape) and + (self.mean_field.mo_coeff[1].shape == new_mo_coeff[1].shape)), \ + f"The new molecular coefficients has shape {[new_mo_coeff[0].shape, new_mo_coeff[1].shape]}"\ + f" shape: expected shape is {[self.mean_field.mo_coeff[0].shape, self.mean_field.mo_coeff[1].shape]}." + else: + assert self.mean_field.mo_coeff.shape == new_mo_coeff.shape, \ + f"The new molecular coefficients matrix has a {new_mo_coeff.shape}"\ + f" shape: expected shape is {self.mean_field.mo_coeff.shape}." + self.mean_field.mo_coeff = np.array(new_mo_coeff) def _compute_mean_field(self): """Computes the mean-field for the molecule. Depending on the molecule @@ -304,9 +327,21 @@ def _compute_mean_field(self): molecule = self.to_pyscf(self.basis, self.symmetry, self.ecp) - self.mean_field = scf.RHF(molecule) + self.mean_field = scf.RHF(molecule) if not self.uhf else scf.UHF(molecule) self.mean_field.verbose = 0 - self.mean_field.kernel() + # Force broken symmetry for uhf calculation when spin is 0 as shown in + # https://github.com/sunqm/pyscf/blob/master/examples/scf/32-break_spin_symm.py + if self.uhf and self.spin == 0: + dm_alpha, dm_beta = self.mean_field.get_init_guess() + dm_beta[:1, :] = 0 + dm = (dm_alpha, dm_beta) + self.mean_field.kernel(dm) + else: + self.mean_field.kernel() + + self.mean_field.analyze() + if not self.mean_field.converged: + raise ValueError("Hartree-Fock calculation did not converge") if self.symmetry: self.mo_symm_ids = list(symm.label_orb_symm(self.mean_field.mol, self.mean_field.mol.irrep_id, @@ -336,6 +371,8 @@ def _get_fermionic_hamiltonian(self, mo_coeff=None): FermionOperator: Self-explanatory. """ + if self.uhf: + return get_fermion_operator(self._get_molecular_hamiltonian_uhf()) core_constant, one_body_integrals, two_body_integrals = self.get_active_space_integrals(mo_coeff) one_body_coefficients, two_body_coefficients = spinorb_from_spatial(one_body_integrals, two_body_integrals) @@ -370,34 +407,69 @@ def _convert_frozen_orbitals(self, frozen_orbitals): # First case: frozen_orbitals is an int. # The first n MOs are frozen. - if isinstance(frozen_orbitals, int): + if isinstance(frozen_orbitals, (int, np.integer)): frozen_orbitals = list(range(frozen_orbitals)) + if self.uhf: + frozen_orbitals = [frozen_orbitals, frozen_orbitals] # Second case: frozen_orbitals is a list of int. # All MOs with indexes in this list are frozen (first MO is 0, second is 1, ...). # Everything else raise an exception. - elif not (isinstance(frozen_orbitals, list) and all(isinstance(_, int) for _ in frozen_orbitals)): - raise TypeError("frozen_orbitals argument must be an (or a list of) integer(s).") - - occupied = [i for i in range(self.n_mos) if self.mo_occ[i] > 0.] - virtual = [i for i in range(self.n_mos) if self.mo_occ[i] == 0.] + elif isinstance(frozen_orbitals, list): + if self.uhf and not (len(frozen_orbitals) == 2 and + all(isinstance(_, (int, np.integer)) for _ in frozen_orbitals[0]) and + all(isinstance(_, (int, np.integer)) for _ in frozen_orbitals[1])): + raise TypeError("frozen_orbitals argument must be a list of int for both alpha and beta electrons") + elif not self.uhf and not all(isinstance(_, int) for _ in frozen_orbitals): + raise TypeError("frozen_orbitals argument must be an (or a list of) integer(s).") + else: + raise TypeError("frozen_orbitals argument must be an (or a list of) integer(s)") + + if self.uhf: + occupied, virtual = list(), list() + frozen_occupied, frozen_virtual = list(), list() + active_occupied, active_virtual = list(), list() + n_active_electrons = list() + n_active_mos = list() + for e in range(2): + occupied.append([i for i in range(self.n_mos) if self.mo_occ[e][i] > 0.]) + virtual.append([i for i in range(self.n_mos) if self.mo_occ[e][i] == 0.]) + + frozen_occupied.append([i for i in frozen_orbitals[e] if i in occupied[e]]) + frozen_virtual.append([i for i in frozen_orbitals[e] if i in virtual[e]]) + + # Redefined active orbitals based on frozen ones. + active_occupied.append([i for i in occupied[e] if i not in frozen_occupied[e]]) + active_virtual.append([i for i in virtual[e] if i not in frozen_virtual[e]]) + + # Calculate number of active electrons and active_mos + n_active_electrons.append(round(sum([self.mo_occ[e][i] for i in active_occupied[e]]))) + n_active_mos.append(len(active_occupied[e] + active_virtual[e])) + + if n_active_electrons[0] + n_active_electrons[1] == 0: + raise ValueError("There are no active electrons.") + if (n_active_electrons[0] == 2*n_active_mos[0]) and (n_active_electrons[1] == 2*n_active_mos[1]): + raise ValueError("All active orbitals are fully occupied.") + else: + occupied = [i for i in range(self.n_mos) if self.mo_occ[i] > 0.] + virtual = [i for i in range(self.n_mos) if self.mo_occ[i] == 0.] - frozen_occupied = [i for i in frozen_orbitals if i in occupied] - frozen_virtual = [i for i in frozen_orbitals if i in virtual] + frozen_occupied = [i for i in frozen_orbitals if i in occupied] + frozen_virtual = [i for i in frozen_orbitals if i in virtual] - # Redefined active orbitals based on frozen ones. - active_occupied = [i for i in occupied if i not in frozen_occupied] - active_virtual = [i for i in virtual if i not in frozen_virtual] + # Redefined active orbitals based on frozen ones. + active_occupied = [i for i in occupied if i not in frozen_occupied] + active_virtual = [i for i in virtual if i not in frozen_virtual] - # Calculate number of active electrons and active_mos - n_active_electrons = round(sum([self.mo_occ[i] for i in active_occupied])) - n_active_mos = len(active_occupied + active_virtual) + # Calculate number of active electrons and active_mos + n_active_electrons = round(sum([self.mo_occ[i] for i in active_occupied])) + n_active_mos = len(active_occupied + active_virtual) - # Exception raised here if there is no active electron. - # An exception is raised also if all active orbitals are fully occupied. - if n_active_electrons == 0: - raise ValueError("There are no active electrons.") - if n_active_electrons == 2*n_active_mos: - raise ValueError("All active orbitals are fully occupied.") + # Exception raised here if there is no active electron. + # An exception is raised also if all active orbitals are fully occupied. + if n_active_electrons == 0: + raise ValueError("There are no active electrons.") + if n_active_electrons == 2*n_active_mos: + raise ValueError("All active orbitals are fully occupied.") return active_occupied, frozen_occupied, active_virtual, frozen_virtual @@ -406,8 +478,9 @@ def freeze_mos(self, frozen_orbitals, inplace=True): list_of_active_frozen = self._convert_frozen_orbitals(frozen_orbitals) - if any([self.mo_occ[i] == 1 for i in list_of_active_frozen[1]]): - raise NotImplementedError("Freezing half-filled orbitals is not implemented yet.") + if not self.uhf: + if any([self.mo_occ[i] == 1 for i in list_of_active_frozen[1]]): + raise NotImplementedError("Freezing half-filled orbitals is not implemented yet for RHF/ROHF.") if inplace: self.frozen_orbitals = frozen_orbitals @@ -439,8 +512,10 @@ def energy_from_rdms(self, one_rdm, two_rdm): are supported with this method. Args: - one_rdm (numpy.array): One-particle density matrix in MO basis. - two_rdm (numpy.array): Two-particle density matrix in MO basis. + one_rdm (array or List[array]): One-particle density matrix in MO basis. + If UHF [alpha one_rdm, beta one_rdm] + two_rdm (array or List[array]): Two-particle density matrix in MO basis. + If UHF [alpha-alpha two_rdm, alpha-beta two_rdm, beta-beta two_rdm] Returns: float: Molecular energy. @@ -453,47 +528,63 @@ def energy_from_rdms(self, one_rdm, two_rdm): # h[p,q,r,s]=\int \phi_p(x)* \phi_q(y)* V_{elec-elec} \phi_r(y) \phi_s(x) dxdy # The convention is not the same with PySCF integrals. So, a change is # reverse back after performing the truncation for frozen orbitals - two_electron_integrals = two_electron_integrals.transpose(0, 3, 1, 2) + if self.uhf: + two_electron_integrals = [two_electron_integrals[i].transpose(0, 3, 1, 2) for i in range(3)] + factor = [1/2, 1, 1/2] + e = (core_constant + + np.sum([np.sum(one_electron_integrals[i] * one_rdm[i]) for i in range(2)]) + + np.sum([np.sum(two_electron_integrals[i] * two_rdm[i]) * factor[i] for i in range(3)])) + else: + two_electron_integrals = two_electron_integrals.transpose(0, 3, 1, 2) - # Computing the total energy from integrals and provided RDMs. - e = core_constant + np.sum(one_electron_integrals * one_rdm) + 0.5*np.sum(two_electron_integrals * two_rdm) + # Computing the total energy from integrals and provided RDMs. + e = core_constant + np.sum(one_electron_integrals * one_rdm) + 0.5*np.sum(two_electron_integrals * two_rdm) return e.real def get_active_space_integrals(self, mo_coeff=None): """Computes core constant, one_body, and two-body coefficients with frozen orbitals folded into one-body coefficients and core constant + For UHF + one_body coefficients are [alpha one_body, beta one_body] + two_body coefficients are [alpha-alpha two_body, alpha-beta two_body, beta-beta two_body] Args: mo_coeff (array): The molecular orbital coefficients to use to generate the integrals Returns: - (float, array, array): (core_constant, one_body coefficients, two_body coefficients) + (float, array or List[array], array or List[array]): (core_constant, one_body coefficients, two_body coefficients) """ return self.get_integrals(mo_coeff, True) def get_full_space_integrals(self, mo_coeff=None): """Computes core constant, one_body, and two-body integrals for all orbitals + For UHF + one_body coefficients are [alpha one_body, beta one_body] + two_body coefficients are [alpha-alpha two_body, alpha-beta two_body, beta-beta two_body] Args: mo_coeff (array): The molecular orbital coefficients to use to generate the integrals. Returns: - (float, array, array): (core_constant, one_body coefficients, two_body coefficients) + (float, array or List[array], array or List[array]): (core_constant, one_body coefficients, two_body coefficients) """ return self.get_integrals(mo_coeff, False) def get_integrals(self, mo_coeff=None, consider_frozen=True): """Computes core constant, one_body, and two-body coefficients for a given active space and mo_coeff + For UHF + one_body coefficients are [alpha one_body, beta one_body] + two_body coefficients are [alpha-alpha two_body, alpha-beta two_body, beta-beta two_body] Args: mo_coeff (array): The molecular orbital coefficients to use to generate the integrals. consider_frozen (bool): If True, the frozen orbitals are folded into the one_body and core constant terms. Returns: - (float, array, array): (core_constant, one_body coefficients, two_body coefficients) + (float, array or List[array], array or List[array]): (core_constant, one_body coefficients, two_body coefficients) """ # Pyscf molecule to get integrals. @@ -501,6 +592,13 @@ def get_integrals(self, mo_coeff=None, consider_frozen=True): if mo_coeff is None: mo_coeff = self.mean_field.mo_coeff + if self.uhf: + if consider_frozen: + return self._get_active_space_integrals_uhf(mo_coeff=mo_coeff) + else: + one_body, two_body = self._compute_uhf_integrals(mo_coeff) + return float(pyscf_mol.energy_nuc()), one_body, two_body + # Corresponding to nuclear repulsion energy and static coulomb energy. core_constant = float(pyscf_mol.energy_nuc()) @@ -527,3 +625,214 @@ def get_integrals(self, mo_coeff=None, consider_frozen=True): core_constant += core_offset return core_constant, one_electron_integrals, two_electron_integrals + + def _compute_uhf_integrals(self, mo_coeff): + """Compute 1-electron and 2-electron integrals + The return is formatted as + [numpy.ndarray]*2 numpy array h_{pq} for alpha and beta blocks + [numpy.ndarray]*3 numpy array storing h_{pqrs} for alpha-alpha, alpha-beta, beta-beta blocks + + Args: + List[array]: The molecular orbital coefficients for both spins [alpha, beta] + + Returns: + List[array], List[array]: One and two body integrals + """ + # step 1 : find nao, nmo (atomic orbitals & molecular orbitals) + + # molecular orbitals (alpha and beta will be the same) + # Lets take alpha blocks to find the shape and things + + # molecular orbitals + nmo = self.nmo = mo_coeff[0].shape[1] + # atomic orbitals + nao = self.nao = mo_coeff[0].shape[0] + + # step 2 : obtain Hcore Hamiltonian in atomic orbitals basis + hcore = self.mean_field.get_hcore() + + # step 3 : obatin two-electron integral in atomic basis + eri = ao2mo.restore(8, self.mean_field._eri, nao) + + # step 4 : create the placeholder for the matrices + # one-electron matrix (alpha, beta) + hpq = [] + + # step 5 : do the mo transformation + # step the mo coeff alpha and beta + mo_a = mo_coeff[0] + mo_b = mo_coeff[1] + + # mo transform the hcore + hpq.append(mo_a.T.dot(hcore).dot(mo_a)) + hpq.append(mo_b.T.dot(hcore).dot(mo_b)) + + # mo transform the two-electron integrals + eri_a = ao2mo.incore.full(eri, mo_a) + eri_b = ao2mo.incore.full(eri, mo_b) + eri_ba = ao2mo.incore.general(eri, (mo_a, mo_a, mo_b, mo_b), compact=False) + + # Change the format of integrals (full) + eri_a = ao2mo.restore(1, eri_a, nmo) + eri_b = ao2mo.restore(1, eri_b, nmo) + eri_ba = eri_ba.reshape(nmo, nmo, nmo, nmo) + + # # convert this into the order OpenFemion like to receive + two_body_integrals_a = np.asarray(eri_a.transpose(0, 2, 3, 1), order='C') + two_body_integrals_b = np.asarray(eri_b.transpose(0, 2, 3, 1), order='C') + two_body_integrals_ab = np.asarray(eri_ba.transpose(0, 2, 3, 1), order='C') + + # Gpqrs has alpha, alphaBeta, Beta blocks + Gpqrs = (two_body_integrals_a, two_body_integrals_ab, two_body_integrals_b) + + return hpq, Gpqrs + + def _get_active_space_integrals_uhf(self, occupied_indices=None, active_indices=None, mo_coeff=None): + """Get active space integrals with uhf reference + The return is + (core_constant, + [alpha one_body, beta one_body], + [alpha-alpha two_body, alpha-beta two_body, beta-beta two_body]) + + Args: + occupied_indices (array-like): The frozen occupied orbital indices + active_indices (array-like): The active orbital indices + mo_coeff (List[array]): The molecular orbital coefficients to use to generate the integrals. + + Returns: + (float, List[array], List[array]): Core constant, one body integrals, two body integrals + """ + + if mo_coeff is None: + mo_coeff = self.mean_field.mo_coeff + + # Get integrals. + one_body_integrals, two_body_integrals = self._compute_uhf_integrals(mo_coeff) + + occupied_indices = self.frozen_occupied if occupied_indices is None else occupied_indices + active_indices = self.active_mos if active_indices is None else active_indices + if (len(active_indices) < 1): + raise ValueError('Some active indices required for reduction.') + + # Determine core constant + core_constant = self.mean_field.mol.energy_nuc() + # alpha part + for i in occupied_indices[0]: + core_constant += one_body_integrals[0][i, i] + # alpha part of j + for j in occupied_indices[0]: + core_constant += 0.5*(two_body_integrals[0][i, j, j, i]-two_body_integrals[0][i, j, i, j]) + # beta part of j + for j in occupied_indices[1]: + core_constant += 0.5*(two_body_integrals[1][i, j, j, i]) + + # beta part + for i in occupied_indices[1]: + core_constant += one_body_integrals[1][i, i] + # alpha part of j + for j in occupied_indices[0]: + core_constant += 0.5*(two_body_integrals[1][j, i, i, j]) # i, j are swaped to make BetaAlpha same as AlphaBeta + # beta part of j + for j in occupied_indices[1]: + core_constant += 0.5*(two_body_integrals[2][i, j, j, i]-two_body_integrals[2][i, j, i, j]) + + # Modified one electon integrals + one_body_integrals_new_aa = np.copy(one_body_integrals[0]) + one_body_integrals_new_bb = np.copy(one_body_integrals[1]) + + # alpha alpha block + for u, v in product(active_indices[0], repeat=2): # u is u_a, v i v_a + for i in occupied_indices[0]: # i belongs to alpha block + one_body_integrals_new_aa[u, v] += (two_body_integrals[0][i, u, v, i] - two_body_integrals[0][i, u, i, v]) + for i in occupied_indices[1]: # i belongs to beta block + one_body_integrals_new_aa[u, v] += two_body_integrals[1][u, i, i, v] # I am swaping u,v with I; to make AlphaBeta + + # beta beta block + for u, v in product(active_indices[1], repeat=2): # u is u_beta, v i v_beta + for i in occupied_indices[1]: # i belongs to beta block + one_body_integrals_new_bb[u, v] += (two_body_integrals[2][i, u, v, i] - two_body_integrals[2][i, u, i, v]) + for i in occupied_indices[0]: # i belongs to alpha block + one_body_integrals_new_bb[u, v] += two_body_integrals[1][i, u, v, i] # this is AlphaBeta + + one_body_integrals_new = [one_body_integrals_new_aa[np.ix_(active_indices[0], active_indices[0])], + one_body_integrals_new_bb[np.ix_(active_indices[1], active_indices[1])]] + + TwInt_aa = two_body_integrals[0][np.ix_(active_indices[0], active_indices[0], + active_indices[0], active_indices[0])] + + TwInt_bb = two_body_integrals[2][np.ix_(active_indices[1], active_indices[1], + active_indices[1], active_indices[1])] + + # (alpha|BetaBeta|alpha) is the format of openfermion InteractionOperator + + TwInt_ab = two_body_integrals[1][np.ix_(active_indices[0], active_indices[1], + active_indices[1], active_indices[0])] + + two_body_integrals_new = [TwInt_aa, TwInt_ab, TwInt_bb] + + return core_constant, one_body_integrals_new, two_body_integrals_new + + def _get_molecular_hamiltonian_uhf(self, occupied_indices=None, + active_indices=None): + """Output arrays of the second quantized Hamiltonian coefficients. + Note: + The indexing convention used is that even indices correspond to + spin-up (alpha) modes and odd indices correspond to spin-down + (beta) modes. + + Args: + occupied_indices(list): A list of spatial orbital indices + indicating which orbitals should be considered doubly occupied. + active_indices(list): A list of spatial orbital indices indicating + which orbitals should be considered active. + + Returns: + InteractionOperator: The molecular hamiltonian + """ + + constant, one_body_integrals, two_body_integrals = self._get_active_space_integrals_uhf(occupied_indices, active_indices) + + # Lets find the dimensions + n_orb_a = one_body_integrals[0].shape[0] + n_orb_b = one_body_integrals[1].shape[0] + + # TODO: Implement more compact ordering. May be possible by defining own up_index and down_index functions + # Instead of + # n_qubits = n_orb_a + n_orb_b + # We use + n_qubits = 2*max(n_orb_a, n_orb_b) + + # Initialize Hamiltonian coefficients. + one_body_coefficients = np.zeros((n_qubits, n_qubits)) + two_body_coefficients = np.zeros((n_qubits, n_qubits, n_qubits, n_qubits)) + + # aa + for p, q in product(range(n_orb_a), repeat=2): + pi = up_index(p) + qi = up_index(q) + # Populate 1-body coefficients. Require p and q have same spin. + one_body_coefficients[pi, qi] = one_body_integrals[0][p, q] + for r, s in product(range(n_orb_a), repeat=2): + two_body_coefficients[pi, qi, up_index(r), up_index(s)] = (two_body_integrals[0][p, q, r, s] / 2.) + + # bb + for p, q in product(range(n_orb_b), repeat=2): + pi = down_index(p) + qi = down_index(q) + # Populate 1-body coefficients. Require p and q have same spin. + one_body_coefficients[pi, qi] = one_body_integrals[1][p, q] + for r, s in product(range(n_orb_b), repeat=2): + two_body_coefficients[pi, qi, down_index(r), down_index(s)] = (two_body_integrals[2][p, q, r, s] / 2.) + + # abba + for p, q, r, s in product(range(n_orb_a), range(n_orb_b), range(n_orb_b), range(n_orb_a)): + two_body_coefficients[up_index(p), down_index(q), down_index(r), up_index(s)] = (two_body_integrals[1][p, q, r, s] / 2.) + + # baab + for p, q, r, s in product(range(n_orb_b), range(n_orb_a), range(n_orb_a), range(n_orb_b)): + two_body_coefficients[down_index(p), up_index(q), up_index(r), down_index(s)] = (two_body_integrals[1][q, p, s, r] / 2.) + + # Cast to InteractionOperator class and return. + molecular_hamiltonian = openfermion.InteractionOperator(constant, one_body_coefficients, two_body_coefficients) + + return molecular_hamiltonian diff --git a/tangelo/toolboxes/molecular_computation/tests/test_molecule.py b/tangelo/toolboxes/molecular_computation/tests/test_molecule.py index a3e1370b0..28bdd1c72 100644 --- a/tangelo/toolboxes/molecular_computation/tests/test_molecule.py +++ b/tangelo/toolboxes/molecular_computation/tests/test_molecule.py @@ -98,6 +98,12 @@ def test_freezing_orbitals(self): assert(freeze_with_list.frozen_occupied == [0, 1, 2]) assert(freeze_with_list.frozen_virtual == [6]) + freeze_with_list_uhf = SecondQuantizedMolecule(H2O_list, frozen_orbitals=[[0, 1, 2, 6], [0, 1, 2]], uhf=True) + assert(freeze_with_list_uhf.active_occupied == [[3, 4], [3, 4]]) + assert(freeze_with_list_uhf.active_virtual == [[5], [5, 6]]) + assert(freeze_with_list_uhf.frozen_occupied == [[0, 1, 2], [0, 1, 2]]) + assert(freeze_with_list_uhf.frozen_virtual == [[6], []]) + def test_freezing_empty(self): """Verify freezing orbitals empty input.""" @@ -115,6 +121,13 @@ def test_freezing_empty(self): assert(empty_as_frozen.frozen_occupied == []) assert(empty_as_frozen.frozen_virtual == []) + # An empty list should result in the same as nothing. + empty_as_frozen = SecondQuantizedMolecule(H2O_list, frozen_orbitals=None, uhf=True) + assert(empty_as_frozen.active_occupied == [[0, 1, 2, 3, 4]]*2) + assert(empty_as_frozen.active_virtual == [[5, 6]]*2) + assert(empty_as_frozen.frozen_occupied == [[]]*2) + assert(empty_as_frozen.frozen_virtual == [[]]*2) + def test_freezing_type_exception(self): """Verify freezing orbitals exceptions.""" @@ -125,6 +138,8 @@ def test_freezing_type_exception(self): SecondQuantizedMolecule(H2O_list, frozen_orbitals=3.141592) with self.assertRaises(TypeError): SecondQuantizedMolecule(H2O_list, frozen_orbitals=[0, 1, 2.2222, 3, 4, 5]) + with self.assertRaises(TypeError): + SecondQuantizedMolecule(H2O_list, frozen_orbitals=[[0, 1, 2.2222, 3, 4, 5]]*2, uhf=True) def test_no_active_electron(self): """Verify if freezing all active orbitals fails.""" @@ -217,6 +232,17 @@ def test_mo_coeff_setter(self): with self.assertRaises(AssertionError): molecule.mo_coeff = bad_dummy_mo_coeff + molecule = SecondQuantizedMolecule(H2_list, 0, 0, "sto-3g", uhf=True) + + # Should work. + dummy_mo_coeff = [np.ones((2, 2))]*2 + molecule.mo_coeff = dummy_mo_coeff + + # Should raise an AssertionError. + bad_dummy_mo_coeff = [np.ones((3, 3))]*2 + with self.assertRaises(AssertionError): + molecule.mo_coeff = bad_dummy_mo_coeff + if __name__ == "__main__": unittest.main() From f4d46ad0a5bb6c10b456e1d24dc8d8c8546837e7 Mon Sep 17 00:00:00 2001 From: James Brown <84878946+JamesB-1qbit@users.noreply.github.com> Date: Mon, 28 Nov 2022 16:10:34 -0500 Subject: [PATCH 09/14] added multi-product, grid_circuits and discrete_clock (#257) * added multi-product, grid_circuits and discrete_clock --- tangelo/toolboxes/circuits/discrete_clock.py | 88 ++++++++++++ tangelo/toolboxes/circuits/grid_circuits.py | 97 +++++++++++++ tangelo/toolboxes/circuits/multiproduct.py | 133 ++++++++++++++++++ .../circuits/tests/test_discrete_clock.py | 122 ++++++++++++++++ tangelo/toolboxes/circuits/tests/test_grid.py | 81 +++++++++++ tangelo/toolboxes/circuits/tests/test_mp.py | 110 +++++++++++++++ 6 files changed, 631 insertions(+) create mode 100644 tangelo/toolboxes/circuits/discrete_clock.py create mode 100644 tangelo/toolboxes/circuits/grid_circuits.py create mode 100644 tangelo/toolboxes/circuits/multiproduct.py create mode 100644 tangelo/toolboxes/circuits/tests/test_discrete_clock.py create mode 100644 tangelo/toolboxes/circuits/tests/test_grid.py create mode 100644 tangelo/toolboxes/circuits/tests/test_mp.py diff --git a/tangelo/toolboxes/circuits/discrete_clock.py b/tangelo/toolboxes/circuits/discrete_clock.py new file mode 100644 index 000000000..50cb43f21 --- /dev/null +++ b/tangelo/toolboxes/circuits/discrete_clock.py @@ -0,0 +1,88 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module to generate the circuits necessary to implement discrete clock time +Refs: + [1] Jacob Watkins, Nathan Wiebe, Alessandro Roggero, Dean Lee, "Time-dependent Hamiltonian + Simulation using Discrete Clock Constructions" arXiv: 2203.11353 +""" +import math +from typing import List, Callable + +import numpy as np + +from tangelo.linq import Circuit, Gate +from tangelo.toolboxes.circuits.multiproduct import get_multi_product_circuit +from tangelo.toolboxes.ansatz_generator.ansatz_utils import get_qft_circuit + + +def get_adder_circuit(qubit_list: List[int], t: int) -> Circuit: + """Return circuit that takes all bitstrings and add binary(t) to it. + + Args: + qubit_list (List[int]): The qubits to apply the addition of t. + t (int): The integer to add + + Returns: + Circuit: The circuit that applies the addition of t""" + + flip_gate = Circuit([Gate("X", qubit_list[-1])]) + fft = flip_gate + get_qft_circuit(qubit_list, swap=True) + flip_gate + ifft = flip_gate + get_qft_circuit(qubit_list, inverse=True, swap=True) + flip_gate + + gate_list = [] + for i, q in enumerate(qubit_list): + gate_list.append(Gate('PHASE', target=q, parameter=2*np.pi*t*2**i/2**len(qubit_list))) + + return fft+Circuit(gate_list)+ifft + + +def get_discrete_clock_circuit(trotter_func: Callable[..., Circuit], trotter_kwargs: dict, n_state_qus: int, + time: float, n_time_steps: int, mp_order: int) -> Circuit: + """Return discrete clock circuit as described in arXiv: 2203.11353 + + Args: + trotter_func (Callable[..., Circuit]): The function that implements the controlled 2nd order trotter time-evolution + starting at "t0" for "time" using "n_trotter_steps" using "control" + trotter_kwargs (dict): Other keyword arguments for trotter_func. + n_state_qus (int): The number of qubits used to represent the state to time-evolve. + time (float): The total time to evolve. + n_time_steps (int): The number of time steps in the discrete clock. + mp_order (int): The multi-product order to use for the time-evolution. + + Returns: + Circuit: The time-evolution circuit using the discrete clock construction. + """ + + circuit = Circuit() + n_mp_qus = math.ceil(np.log2(mp_order+2)) + n_fft_qus = math.ceil(np.log2(n_time_steps)) + fft_start = n_state_qus+n_mp_qus + fft_qus = list(reversed(range(fft_start, fft_start+n_fft_qus))) + + dt = time/n_time_steps + + for i in range(n_time_steps): + birep = np.binary_repr(i, width=n_fft_qus) + x_ladder = Circuit([Gate("X", c+fft_start) for c, j in enumerate(birep) if j == "0"]) + circuit += x_ladder + trotter_kwargs['t0'] = i*dt + trotter_kwargs['time'] = dt + circuit += get_multi_product_circuit(dt, mp_order, n_state_qus, control=fft_qus, + second_order_trotter=trotter_func, trotter_kwargs=trotter_kwargs) + circuit += x_ladder + get_adder_circuit(fft_qus, 1) + + circuit += get_adder_circuit(fft_qus, -n_time_steps) + + return circuit diff --git a/tangelo/toolboxes/circuits/grid_circuits.py b/tangelo/toolboxes/circuits/grid_circuits.py new file mode 100644 index 000000000..33eb04c8a --- /dev/null +++ b/tangelo/toolboxes/circuits/grid_circuits.py @@ -0,0 +1,97 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module to generate the circuits for grid based computation""" +from typing import Union, List + +import numpy as np + +from tangelo.toolboxes.ansatz_generator.ansatz_utils import get_qft_circuit +from tangelo.linq import Gate, Circuit + + +def get_xsquared_circuit(dt: float, dx: float, fac: float, x0: float, delta: float, + qubit_list: List[int], control: Union[None, int, List[int]] = None) -> Circuit: + """Return circuit for exp(-1j*dt*[fac*(x - x0)**2 + delta]) as defined in arXiv:2006.09405 + + Args: + dt (float): Time to evolve. + dx (float): Grid spacing. + fac (float): Factor in front of x^2 term. + x0 (float): Shift for (x-x0)^2 term + delta (float): Constant shift + qubit_list (List[int]): Qubits to apply circuit to. The order is important depending on lsq_first or msq_first + control (Union[int, List[int]]): The control qubits + + Returns: + Circuit: The circuit that applies exp(-1j*dt*[fac*(x-x0)**2 +delta]) + """ + + if control is not None: + gate_name = 'CPHASE' + clist = [control] if isinstance(control, (int, np.integer)) else control + clist2 = clist + else: + gate_name = 'PHASE' + clist = None + clist2 = [] + + gate_list = [] + + # Constant terms + prefac = -dt*(fac*x0**2 + delta) + gate_list.append(Gate(gate_name, target=qubit_list[0], parameter=prefac, control=clist)) + gate_list.append(Gate('X', target=qubit_list[0])) + gate_list.append(Gate(gate_name, target=qubit_list[0], parameter=prefac, control=clist)) + gate_list.append(Gate('X', target=qubit_list[0])) + + # Linear terms + prefac = 2*dt*fac*x0*dx + for i, q in enumerate(qubit_list): + gate_list.append(Gate(gate_name, target=q, parameter=prefac*2**i, control=clist)) + + # Quadratic terms + prefac = -dt*fac*dx**2 + for i, q1 in enumerate(qubit_list): + gate_list.append(Gate(gate_name, target=q1, parameter=prefac*2**(2*i), control=clist)) + for j, q2 in enumerate(qubit_list): + if (i != j): + gate_list.append(Gate('CPHASE', control=[q1]+clist2, target=q2, parameter=prefac*2**(i+j))) + + return Circuit(gate_list) + + +def get_psquared_circuit(dt: float, dx: float, mass: float, qubit_list: List[int], + control: Union[int, List[int]] = None) -> Circuit: + """Return circuit for p^2/2/m as defined in arXiv:2006.09405 using qft + + Args: + dt (float): Time to evolve. + dx (float): Grid spacing. + mass (float): The mass used for the time-evolution. + qubit_list (List[int]): Qubits to apply circuit to. The order is important depending on lsq_first or msq_first + control (Union[int, List[int]]): The control qubits + + Returns: + Circuit: The circuit that applies exp(-1j*dt*p^2/2/m) + """ + n_b = 2**len(qubit_list) + dp = 2*np.pi/n_b/dx + p0 = n_b//2*dp + flip_gate = Circuit([Gate('X', target=qubit_list[-1])]) + circuit = Circuit() + circuit += flip_gate + get_qft_circuit(qubit_list, swap=True) + flip_gate + circuit += get_xsquared_circuit(dt, dp, 1/2/mass, p0, 0, qubit_list, control=control) + circuit += flip_gate + get_qft_circuit(qubit_list, inverse=True, swap=True) + flip_gate + return circuit diff --git a/tangelo/toolboxes/circuits/multiproduct.py b/tangelo/toolboxes/circuits/multiproduct.py new file mode 100644 index 000000000..de6d61205 --- /dev/null +++ b/tangelo/toolboxes/circuits/multiproduct.py @@ -0,0 +1,133 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module to generate the circuits necessary to implement multi-product time-evolution +Refs: + [1] Guang Hao Low, Vadym Kliuchnikov and Nathan Wiebe, "Well-conditioned multi-product + Hamiltonian simulation" arXiv: 1907.11679 +""" + +import math +from typing import Union, Tuple, List, Callable + +import numpy as np + +from tangelo.linq import Circuit, Gate +from tangelo.linq.helpers.circuits.statevector import StateVector +from tangelo.toolboxes.ansatz_generator.ansatz_utils import trotterize +from tangelo.toolboxes.circuits.lcu import sign_flip +from tangelo.toolboxes.operators import QubitOperator, FermionOperator + + +def get_ajs_kjs(order: int) -> Tuple[List[float], List[int], int]: + """Return aj coefficients and number of steps kj for multi-product order + The first two indices of aj coefficients are the portion need to make the one-norm sum to two. + + Args: + order (int): The desired order of expansion + + Returns: + List[float], List[int], int: aj coefficients, kj steps, number of ancilla qubits needed + """ + + if not isinstance(order, (int, np.integer)): + raise TypeError("order must be of integer type") + if order < 1 or order > 6: + raise ValueError("Tangelo currently only supports orders between 1 and 6") + + mp_qus = math.ceil(np.log2(order+2)) + + adict = {1: [1], + 2: [1/3, 4/3], + 3: [1/105, 1/6, 81/70], + 4: [1/2376, 2/45, 729/3640, 31250/27027], + 5: [1/165888, 256/89775, 6561/179200, 390625/2128896, 6975757441/6067353600], + 6: [1/5544000, 8/19665, 81/4480, 65536/669375, 216/875, 7626831723/6537520000]} + kdict = {1: [1], + 2: [1, 2], + 3: [1, 2, 6], + 4: [1, 2, 3, 10], + 5: [1, 2, 3, 5, 17], + 6: [1, 2, 3, 4, 6, 21]} + fac = sum(adict[order]) + vlen = 2**mp_qus + ajs = np.sqrt(np.abs([(2 - fac)/2, (2 - fac)/2] + [0]*(vlen-2-order) + adict[order])) + ajs /= np.linalg.norm(ajs) + kjs = [0]*(vlen-order) + kdict[order] + return list(ajs), kjs, mp_qus + + +def get_multi_product_circuit(time: float, order: int, n_state_qus: int, + operator: Union[None, QubitOperator, FermionOperator] = None, + control: Union[int, list] = None, + second_order_trotter: Union[None, Callable[..., Circuit]] = None, + trotter_kwargs: Union[dict, None] = None) -> Circuit: + """Return multi-product circuit as defined in arXiv: 1907.11679. Only up to 6th order is currently supported + + Args: + time (float): The time to evolve. + order (int): The order of the multi-product expansion + n_state_qus (int): The number of qubits in the state to evolve. + operator (Union[QubitOperator, FermionOperator]): The operator to evolve in time. Default None + control (Union[int, List[int]]): The control qubit(s). Default None + second_order_trotter (Callable[..., Circuit]): The callable function that defines the controlled 2nd order + time-evolution. Must have arguments "control" and "n_trotter_steps". + trotter_kwargs (dict): Other keyword arguments necessary to evaluate second_order_trotter. + + Returns: + Circuit: The circuit representing the time-evolution using the multi-product construction. + """ + + if second_order_trotter is None: + if operator is None: + raise ValueError("Must supply second_order_trotter function or operator.") + second_order_trotter = trotterize + if trotter_kwargs is None: + trotter_kwargs = {"operator": operator, "time": time, "trotter_order": 2} + + if control is not None: + cont_list = control if isinstance(control, list) else [control] + else: + cont_list = [] + + ajs, kjs, n_mp_qus = get_ajs_kjs(order) + prep_qus = list(range(n_state_qus, n_state_qus+n_mp_qus)) + prep_state = StateVector(ajs, order="lsq_first") + prep_circ = prep_state.initializing_circuit() + prep_circ.reindex_qubits(prep_qus) + + ctrott = prep_circ + for ii in range(2**n_mp_qus-1, 2**n_mp_qus-order-1, -1): + birep = np.binary_repr(ii, width=n_mp_qus) + x2_ladder = Circuit([Gate("X", c+n_state_qus) for c, j in enumerate(birep) if j == "0"]) + ctrott += x2_ladder + ctrott += second_order_trotter(control=cont_list+prep_qus, n_trotter_steps=kjs[ii], **trotter_kwargs) + # Add -1 phase for every other term in multi-product expansion + if ii % 2 == 0: + ctrott += Circuit([Gate("CRZ", 0, parameter=2*np.pi, control=cont_list+prep_qus)]) + ctrott += x2_ladder + + # add -I term for oblivious amplitude amplification + birep = np.binary_repr(0, width=n_mp_qus) + x2_ladder = Circuit([Gate("X", c+n_state_qus) for c, j in enumerate(birep) if j == "0"]) + ctrott += x2_ladder + Circuit([Gate("CRZ", 0, parameter=2*np.pi, control=cont_list+prep_qus)]) + x2_ladder + + ctrott += prep_circ.inverse() + + flip = sign_flip(prep_qus, control=cont_list) + oaa_mp_circuit = ctrott + flip + ctrott.inverse() + flip + ctrott + if control is not None: + oaa_mp_circuit += Circuit([Gate("CRZ", 0, control=control, parameter=-2*np.pi)]) + + return oaa_mp_circuit diff --git a/tangelo/toolboxes/circuits/tests/test_discrete_clock.py b/tangelo/toolboxes/circuits/tests/test_discrete_clock.py new file mode 100644 index 000000000..98d0bd968 --- /dev/null +++ b/tangelo/toolboxes/circuits/tests/test_discrete_clock.py @@ -0,0 +1,122 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +import math + +from openfermion import get_sparse_operator +import numpy as np +from scipy.linalg import expm + +from tangelo.linq import get_backend, Circuit +from tangelo.helpers.utils import installed_backends +from tangelo.linq.helpers.circuits.statevector import StateVector +from tangelo.toolboxes.operators.operators import QubitOperator +from tangelo.toolboxes.qubit_mappings.mapping_transform import fermion_to_qubit_mapping +from tangelo.toolboxes.ansatz_generator.ansatz_utils import get_qft_circuit, trotterize +from tangelo.molecule_library import mol_H2_sto3g +from tangelo.toolboxes.circuits.discrete_clock import get_discrete_clock_circuit +from tangelo.toolboxes.circuits.grid_circuits import get_psquared_circuit, get_xsquared_circuit + +# Test for both "cirq" and if available "qulacs". These have different orderings. +# qiskit is not currently supported because does not have multi controlled general gates. +backends = ["cirq", "qulacs"] if "qulacs" in installed_backends else ["cirq"] +# Initiate Simulator using cirq for phase estimation tests as it has the same ordering as openfermion +# and we are using an exact eigenvector for testing. +sim_cirq = get_backend("cirq") + + +class DiscreteClockTest(unittest.TestCase): + + def test_time_independant_hamiltonian(self): + """Test time-evolution of discrete clock for a time-independant Hamiltonian""" + + qu_op = fermion_to_qubit_mapping(mol_H2_sto3g.fermionic_hamiltonian, "scbk", mol_H2_sto3g.n_active_sos, mol_H2_sto3g.n_active_electrons, + True, 0) + + ham = get_sparse_operator(qu_op).toarray() + _, vecs = np.linalg.eigh(ham) + vec = (vecs[:, 0] + vecs[:, 1])/np.sqrt(2) + + time = 10. + exact = expm(-1j*ham*time)@vec + + def trotter_func(t0, time, n_trotter_steps, control): + return trotterize(operator=qu_op, time=time, n_trotter_steps=n_trotter_steps, control=control, trotter_order=2) + + for backend in backends: + sim = get_backend(backend) + statevector_order = sim.backend_info()["statevector_order"] + sv = StateVector(vec, order=statevector_order) + sv_circuit = sv.initializing_circuit() + + for k in [2, 3]: + taylor_circuit = get_discrete_clock_circuit(trotter_func=trotter_func, trotter_kwargs={}, time=time, mp_order=k, n_state_qus=2, + n_time_steps=4) + _, v = sim.simulate(sv_circuit + taylor_circuit, return_statevector=True) + n_ancilla = 2 + math.ceil(np.log2(k+2)) + len_ancilla = 2**n_ancilla + v = v.reshape([4, len_ancilla])[:, 0] if statevector_order == "lsq_first" else v.reshape([len_ancilla, 4])[0, :] + self.assertAlmostEqual(1, np.abs(v.conj().dot(exact)), delta=1.e-1**k) + + def test_time_dependant_hamiltonian(self): + """Test time-evolution of discrete clock for a time-dependant Hamiltonian taken from + arXiv: 1412.1802 H = 1/2/m * p^2 + (4*exp(-2*t) - 1/16) * x^2 - 2*exp(-t) with mass=1/2 + and exact answer (2/pi)^(1/4)*exp(-x^2*exp(-t) - 1/4*t + 1j/8*x^2)""" + + n_qubits = 6 + n_pts = 2**n_qubits + dx = 0.2 + x0 = dx*(n_pts//2 - 1/2) + gridpts = np.linspace(-x0, x0, n_pts) + mass = 1/2 + time = 1. + + def psiexact(xpts, t): + return (2/np.pi)**(1/4)*np.exp(-xpts**2*np.exp(-t)-1/4*t+1j/8*xpts**2)*np.sqrt(dx) + + exact = psiexact(gridpts, time) + + def trotter_func(t0, time, n_trotter_steps, control, dx, qubit_list): + circ = Circuit() + dt = time/n_trotter_steps + p2 = get_psquared_circuit(dt/2, dx, mass, qubit_list, control) + for i in range(n_trotter_steps): + th = t0 + (i + 1/2) * dt + circ += p2 + circ += get_xsquared_circuit(dt, dx, (4*np.exp(-2*th) - 1/16), x0, -2*np.exp(-th), qubit_list, control) + circ += p2 + return circ + + for backend in backends: + sim = get_backend(backend) + statevector_order = sim.backend_info()["statevector_order"] + vec = psiexact(gridpts, 0) + sv = StateVector(vec, order=statevector_order) + sv_circuit, phase = sv.initializing_circuit(return_phase=True) + + for k in [2, 3]: + qubit_list = list(reversed(range(n_qubits))) if statevector_order == "lsq_first" else list((range(n_qubits))) + taylor_circuit = get_discrete_clock_circuit(trotter_func=trotter_func, trotter_kwargs={"dx": dx, "qubit_list": qubit_list}, time=time, + mp_order=k, n_state_qus=6, + n_time_steps=2) + _, v = sim.simulate(sv_circuit + taylor_circuit, return_statevector=True) + n_ancilla = 1 + math.ceil(np.log2(k+2)) + len_ancilla = 2**n_ancilla + v = v.reshape([n_pts, len_ancilla])[:, 0] if statevector_order == "lsq_first" else v.reshape([len_ancilla, n_pts])[0, :] + self.assertAlmostEqual(1, (v.conj().dot(exact)*np.exp(-1j*phase)).real, delta=1.e-1**k) + + +if __name__ == "__main__": + unittest.main() diff --git a/tangelo/toolboxes/circuits/tests/test_grid.py b/tangelo/toolboxes/circuits/tests/test_grid.py new file mode 100644 index 000000000..fb2ad120a --- /dev/null +++ b/tangelo/toolboxes/circuits/tests/test_grid.py @@ -0,0 +1,81 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +import math + +from openfermion import get_sparse_operator +import numpy as np +from scipy.linalg import expm + +from tangelo.linq import get_backend +from tangelo.linq.helpers.circuits.statevector import StateVector +from tangelo.helpers.utils import installed_backends +from tangelo.toolboxes.ansatz_generator.ansatz_utils import get_qft_circuit +from tangelo.toolboxes.post_processing.histogram import Histogram +from tangelo.toolboxes.circuits.grid_circuits import get_psquared_circuit, get_xsquared_circuit + +# Test for both "cirq" and if available "qulacs". These have different orderings. +# qiskit is not currently supported because does not have multi controlled general gates. +backends = ["cirq", "qulacs"] if "qulacs" in installed_backends else ["cirq"] +# Initiate Simulator using cirq for phase estimation tests as it has the same ordering as openfermion +# and we are using an exact eigenvector for testing. +sim_cirq = get_backend("cirq") + + +class GridTest(unittest.TestCase): + + def test_controlled_time_evolution_by_phase_estimation(self): + """ Verify that the controlled time-evolution is correct by calculating the eigenvalue of an eigenstate of the + harmonic oscillator (1/2/mass p^2/2 + 1/4*x^2 - 1/4) with mass=2 and ground state eigenvalue 1/8 + """ + + n_qubits = 6 + n_pts = 2**n_qubits + dx = 0.2 + x0 = dx*(n_pts//2 - 1/2) + gridpts = np.linspace(-x0, x0, n_pts) + mass = 2 + + # Kronecker product 13 qubits in the zero state to eigenvector 9 to account for ancilla qubits + wave_0 = np.exp(-1/2*(gridpts)**2)*np.sqrt(dx)/np.pi**(1/4) + + fft_list = [8, 7, 6] + for backend in backends: + sim = get_backend(backend) + sim_order = sim.backend_info()["statevector_order"] + + qubit_list = list(reversed(range(n_qubits))) if sim_order == "lsq_first" else list((range(n_qubits))) + start_circ = StateVector(wave_0, order=sim_order).initializing_circuit() + + pe_circuit = start_circ + get_qft_circuit(fft_list) + for i, qubit in enumerate(fft_list): + xsquared = get_xsquared_circuit(-2*np.pi/20, dx, 1/4, x0, -1/8., qubit_list=qubit_list, control=qubit) + psquared = get_psquared_circuit(-2*np.pi/10, dx, mass, qubit_list=qubit_list, control=qubit) + pe_circuit += (xsquared + psquared + xsquared) * (10 * 2**i) + pe_circuit += get_qft_circuit(fft_list, inverse=True) + + freqs, _ = sim.simulate(pe_circuit) + + # Trace out all but final 3 indices + hist = Histogram(freqs) + hist.remove_qubit_indices(*qubit_list) + trace_freq = hist.frequencies + + # State 0 has eigenvalue 0.125 so return should be 001 (0*1/2 + 0*1/4 + 1*1/8) + self.assertAlmostEqual(trace_freq["001"], 1.0, delta=1.e-3) + + +if __name__ == "__main__": + unittest.main() diff --git a/tangelo/toolboxes/circuits/tests/test_mp.py b/tangelo/toolboxes/circuits/tests/test_mp.py new file mode 100644 index 000000000..6a1a60230 --- /dev/null +++ b/tangelo/toolboxes/circuits/tests/test_mp.py @@ -0,0 +1,110 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +import math + +from openfermion import get_sparse_operator +import numpy as np +from scipy.linalg import expm + +from tangelo.linq import get_backend +from tangelo.helpers.utils import installed_backends +from tangelo.linq.helpers.circuits.statevector import StateVector +from tangelo.toolboxes.operators.operators import QubitOperator +from tangelo.toolboxes.qubit_mappings.mapping_transform import fermion_to_qubit_mapping +from tangelo.toolboxes.ansatz_generator.ansatz_utils import get_qft_circuit +from tangelo.molecule_library import mol_H2_sto3g +from tangelo.toolboxes.circuits.multiproduct import get_multi_product_circuit, get_ajs_kjs + +# Test for both "cirq" and if available "qulacs". These have different orderings. +# qiskit is not currently supported because does not have multi controlled general gates. +backends = ["cirq", "qulacs"] if "qulacs" in installed_backends else ["cirq"] +# Initiate Simulator using cirq for phase estimation tests as it has the same ordering as openfermion +# and we are using an exact eigenvector for testing. +sim_cirq = get_backend("cirq") + + +class MultiProductTest(unittest.TestCase): + + def test_time_evolution(self): + """Test time-evolution of multi-product circuit for different orders""" + + qu_op = fermion_to_qubit_mapping(mol_H2_sto3g.fermionic_hamiltonian, "scbk", mol_H2_sto3g.n_active_sos, mol_H2_sto3g.n_active_electrons, + True, 0) + + ham = get_sparse_operator(qu_op).toarray() + _, vecs = np.linalg.eigh(ham) + vec = (vecs[:, 0] + vecs[:, 1])/np.sqrt(2) + + time = 1.9 + exact = expm(-1j*ham*time)@vec + + for backend in backends: + sim = get_backend(backend) + statevector_order = sim.backend_info()["statevector_order"] + sv = StateVector(vec, order=statevector_order) + sv_circuit = sv.initializing_circuit() + + # Tested for up to k = 5 but 5 is slow due to needing 23 qubits to simulate. + for k in [1, 2, 3, 4]: + taylor_circuit = get_multi_product_circuit(time, order=k, n_state_qus=2, operator=qu_op) + _, v = sim.simulate(sv_circuit + taylor_circuit, return_statevector=True) + _, _, n_ancilla = get_ajs_kjs(k) + len_ancilla = 2**n_ancilla + v = v.reshape([4, len_ancilla])[:, 0] if statevector_order == "lsq_first" else v.reshape([len_ancilla, 4])[0, :] + self.assertAlmostEqual(1, np.abs(v.conj().dot(exact)), delta=3.e-1**k) + + # Raise ValueError if order is less than 1 or greater than 6 or imaginary coefficients in qubit operator + self.assertRaises(ValueError, get_multi_product_circuit, time, 0, 2, qu_op) + self.assertRaises(ValueError, get_multi_product_circuit, time, 7, 2, qu_op) + # Raise TypeError if order not integer + self.assertRaises(TypeError, get_multi_product_circuit, time, 3., 2, qu_op) + + def test_controlled_time_evolution_by_phase_estimation(self): + """ Verify that the controlled time-evolution is correct by calculating the eigenvalue of an eigenstate through + phase estimation. + """ + + # Generate qubit operator with state 9 having eigenvalue 0.25 + qu_op = (QubitOperator("X0 X1", 0.125) + QubitOperator("Y1 Y2", 0.125) + QubitOperator("Z2 Z3", 0.125) + + QubitOperator("", 0.125)) + + ham_mat = get_sparse_operator(qu_op).toarray() + _, wavefunction = np.linalg.eigh(ham_mat) + + # Kronecker product 13 qubits in the zero state to eigenvector 9 to account for ancilla qubits + wave_9 = wavefunction[:, 9] + for i in range(6): + wave_9 = np.kron(wave_9, np.array([1, 0])) + + qubit_list = [9, 8, 7] + + pe_circuit = get_qft_circuit(qubit_list) + for i, qubit in enumerate(qubit_list): + pe_circuit += get_multi_product_circuit(operator=qu_op, n_state_qus=4, order=5, time=-(2*np.pi)*2**i, control=qubit) + pe_circuit += get_qft_circuit(qubit_list, inverse=True) + + freqs, _ = sim_cirq.simulate(pe_circuit, initial_statevector=wave_9) + # Trace out all but final 3 indices + trace_freq = dict() + for key, value in freqs.items(): + trace_freq[key[-3:]] = trace_freq.get(key[-3:], 0) + value + + # State 9 has eigenvalue 0.25 so return should be 010 (0*1/2 + 1*1/4 + 0*1/8) + self.assertAlmostEqual(trace_freq["010"], 1.0, delta=1.e-4) + + +if __name__ == "__main__": + unittest.main() From b7f62ad9a45a6e81e42e571d4023e8e5d73e73e9 Mon Sep 17 00:00:00 2001 From: James Brown <84878946+JamesB-1qbit@users.noreply.github.com> Date: Mon, 19 Dec 2022 05:22:37 -0500 Subject: [PATCH 10/14] translation to pennylane (#260) * tangelo to pennylane format translation Co-authored-by: Valentin Senicourt <41597680+ValentinS4t1qbit@users.noreply.github.com> --- .github/workflows/continuous_integration.yml | 1 + Dockerfile | 2 +- tangelo/helpers/utils.py | 2 +- tangelo/linq/tests/test_translator_circuit.py | 20 ++++ tangelo/linq/translator/__init__.py | 2 + tangelo/linq/translator/translate_circuit.py | 4 +- .../linq/translator/translate_pennylane.py | 100 ++++++++++++++++++ 7 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 tangelo/linq/translator/translate_pennylane.py diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 3e6964f1b..714a1e32b 100755 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -43,6 +43,7 @@ jobs: pip install amazon-braket-sdk pip install cirq pip install projectq + pip install pennylane if: always() - name: Install Microsoft qsharp/qdk diff --git a/Dockerfile b/Dockerfile index c984d77f9..58103bc8b 100755 --- a/Dockerfile +++ b/Dockerfile @@ -39,4 +39,4 @@ RUN pip3 install tangelo-gc # OPTIONAL: common dependencies (quantum circuit simulator and quantum cloud services) # ==================================================================================== - RUN pip3 install cirq amazon-braket-sdk qiskit qulacs projectq + RUN pip3 install cirq amazon-braket-sdk qiskit qulacs projectq pennylane diff --git a/tangelo/helpers/utils.py b/tangelo/helpers/utils.py index e0a72c64e..fd5fedc30 100644 --- a/tangelo/helpers/utils.py +++ b/tangelo/helpers/utils.py @@ -73,7 +73,7 @@ def new_func(*args, **kwargs): # List all built-in backends supported -all_backends = {"qulacs", "qiskit", "cirq", "braket", "projectq", "qdk"} +all_backends = {"qulacs", "qiskit", "cirq", "braket", "projectq", "qdk", "pennylane"} all_backends_simulator = {"qulacs", "qiskit", "cirq", "qdk"} sv_backends_simulator = {"qulacs", "qiskit", "cirq"} diff --git a/tangelo/linq/tests/test_translator_circuit.py b/tangelo/linq/tests/test_translator_circuit.py index 169887e7d..c71684522 100644 --- a/tangelo/linq/tests/test_translator_circuit.py +++ b/tangelo/linq/tests/test_translator_circuit.py @@ -481,6 +481,26 @@ def test_unsupported_gate(self): circ = Circuit([Gate("Potato", 0)]) self.assertRaises(ValueError, translate_c, circ, "qiskit") + @unittest.skipIf("pennylane" not in installed_backends, "Test Skipped: Backend not available \n") + def test_pennylane(self): + """ Compares state vector of translated pennylane circuit against the expected one.""" + import pennylane as qml + + translated_circuit = translate_c(big_circuit, "pennylane") + + dev = qml.device('default.qubit', wires=list(range(big_circuit.width))) + + @qml.qnode(dev) + def circuit(ops): + for op in ops: + qml.apply(op) + return qml.state() + + v1 = circuit(translated_circuit) + + # Compare statevectors + np.testing.assert_array_almost_equal(v1, reference_big_lsq, decimal=6) + if __name__ == "__main__": unittest.main() diff --git a/tangelo/linq/translator/__init__.py b/tangelo/linq/translator/__init__.py index d23226349..2be79e1bd 100644 --- a/tangelo/linq/translator/__init__.py +++ b/tangelo/linq/translator/__init__.py @@ -24,6 +24,7 @@ from .translate_openqasm import translate_openqasm, _translate_openqasm2abs, get_openqasm_gates from .translate_qubitop import translate_operator from .translate_circuit import translate_circuit +from .translate_pennylane import get_pennylane_gates def get_supported_gates(): @@ -38,5 +39,6 @@ def get_supported_gates(): supported_gates["qiskit"] = sorted(get_qiskit_gates().keys()) supported_gates["cirq"] = sorted(get_cirq_gates().keys()) supported_gates["braket"] = sorted(get_braket_gates().keys()) + supported_gates["pennylane"] = sorted(get_pennylane_gates().keys()) return supported_gates diff --git a/tangelo/linq/translator/translate_circuit.py b/tangelo/linq/translator/translate_circuit.py index 239ae74ce..bf03269b9 100644 --- a/tangelo/linq/translator/translate_circuit.py +++ b/tangelo/linq/translator/translate_circuit.py @@ -22,6 +22,7 @@ from tangelo.linq.translator.translate_qdk import translate_c_to_qsharp from tangelo.linq.translator.translate_qiskit import translate_c_to_qiskit, translate_c_from_qiskit from tangelo.linq.translator.translate_qulacs import translate_c_to_qulacs +from tangelo.linq.translator.translate_pennylane import translate_c_to_pennylane FROM_TANGELO = { @@ -32,7 +33,8 @@ "projectq": translate_c_to_projectq, "qdk": translate_c_to_qsharp, "qiskit": translate_c_to_qiskit, - "qulacs": translate_c_to_qulacs + "qulacs": translate_c_to_qulacs, + "pennylane": translate_c_to_pennylane } TO_TANGELO = { diff --git a/tangelo/linq/translator/translate_pennylane.py b/tangelo/linq/translator/translate_pennylane.py new file mode 100644 index 000000000..c7aec7b0e --- /dev/null +++ b/tangelo/linq/translator/translate_pennylane.py @@ -0,0 +1,100 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions helping with quantum circuit format conversion between abstract +format and pennylane format. + +In order to produce an equivalent circuit for the target backend, it is +necessary to account for: +- how the gate names differ between the source backend to the target backend. +- how the order and conventions for some of the inputs to the gate operations + may also differ. +""" + +from math import pi + + +def get_pennylane_gates(): + """Map gate name of the abstract format to the equivalent methods of the + pennylane class API and supported gates:https://docs.pennylane.ai/en/stable/code/qml.html#classes. + """ + import pennylane as qml + + GATE_PENNYLANE = dict() + GATE_PENNYLANE["H"] = qml.Hadamard + GATE_PENNYLANE["X"] = qml.PauliX + GATE_PENNYLANE["Y"] = qml.PauliY + GATE_PENNYLANE["Z"] = qml.PauliZ + GATE_PENNYLANE["CX"] = qml.MultiControlledX + GATE_PENNYLANE["CY"] = qml.CY + GATE_PENNYLANE["CZ"] = qml.CZ + GATE_PENNYLANE["S"] = qml.S + GATE_PENNYLANE["T"] = qml.T + GATE_PENNYLANE["RX"] = qml.RX + GATE_PENNYLANE["RY"] = qml.RY + GATE_PENNYLANE["RZ"] = qml.RZ + GATE_PENNYLANE["CNOT"] = qml.CNOT + GATE_PENNYLANE["CRZ"] = qml.CRZ + GATE_PENNYLANE["CRX"] = qml.CRX + GATE_PENNYLANE["CRY"] = qml.CRY + GATE_PENNYLANE["PHASE"] = qml.PhaseShift + GATE_PENNYLANE["CPHASE"] = qml.CPhase + GATE_PENNYLANE["XX"] = qml.IsingXX + GATE_PENNYLANE["SWAP"] = qml.SWAP + GATE_PENNYLANE["CSWAP"] = qml.CSWAP + # TODO: Check periodically if better support for measure gates is implemented + # GATE_PENNYLANE["MEASURE"] = qml.measure # Pennylane currently only supports measuring a qubit once + return GATE_PENNYLANE + + +def translate_c_to_pennylane(source_circuit): + """Take in an tangelo Circuit, return an equivalent pennylane List[subclass of pennylane.operator.Operation]. + + Args: + source_circuit (Circuit): quantum circuit in the abstract format. + + Returns: + List[Type[pennylane.operator.Operation]]: corresponding list of objects from child classes of pennylane.operator.Operation. + """ + + GATE_PENNYLANE = get_pennylane_gates() + target_circuit = [] + + # Maps the gate information properly. Different for each backend (order, values) + for gate in source_circuit: + if gate.control is not None and len(gate.control) > 1 and gate.name != "CX": + raise ValueError(f"Can not use {gate.name} with multiple controls. Only CX translates properly to pennylane") + if gate.name in {"H", "X", "Y", "Z", "S", "T"}: + target_circuit.append(GATE_PENNYLANE[gate.name](gate.target[0])) + elif gate.name in {"CY", "CZ", "CNOT"}: + target_circuit.append(GATE_PENNYLANE[gate.name]([gate.control[0], gate.target[0]])) + elif gate.name in {"CX"}: + target_circuit.append(GATE_PENNYLANE[gate.name](wires=[*gate.control, gate.target[0]])) + elif gate.name in {"CH"}: + target_circuit.append(GATE_PENNYLANE["CNOT"]([gate.control[0], gate.target[0]])) + target_circuit.append(GATE_PENNYLANE["CRY"](-pi/2, [gate.control[0], gate.target[0]])) + elif gate.name in {"RX", "RY", "RZ", "PHASE"}: + target_circuit.append(GATE_PENNYLANE[gate.name](gate.parameter, gate.target[0])) + elif gate.name in {"CRZ", "CRX", "CRY", "CPHASE"}: + target_circuit.append(GATE_PENNYLANE[gate.name](gate.parameter, [gate.control[0], gate.target[0]])) + elif gate.name in {"XX"}: + target_circuit.append(GATE_PENNYLANE[gate.name](gate.parameter, [gate.target[0], gate.target[1]])) + elif gate.name in {"SWAP"}: + target_circuit.append(GATE_PENNYLANE[gate.name]([gate.target[0], gate.target[1]])) + elif gate.name in {"CSWAP"}: + target_circuit.append(GATE_PENNYLANE[gate.name]([gate.control[0], gate.target[0], gate.target[1]])) + else: + raise ValueError(f"Gate '{gate.name}' not supported for translation to pennylane") + + return target_circuit From 5d3e9da841550dd30ebefd6c7f7d6abba5785930 Mon Sep 17 00:00:00 2001 From: James Brown <84878946+JamesB-1qbit@users.noreply.github.com> Date: Fri, 23 Dec 2022 04:25:37 -0500 Subject: [PATCH 11/14] bump testing version to 3.8 (#262) Updating python version to 3.8 in automated tests, as 3.7 is no longer maintained by the Python dev team --- .github/workflows/continuous_integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 714a1e32b..e159ccd40 100755 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7] + python-version: [3.8] steps: - uses: actions/checkout@v2 From 3559208427fb2eb55c5be53b1480f10b8f7008ca Mon Sep 17 00:00:00 2001 From: James Brown <84878946+JamesB-1qbit@users.noreply.github.com> Date: Tue, 3 Jan 2023 15:35:41 -0500 Subject: [PATCH 12/14] Auto threshold cutoff for small coefficients in LCU (#261) * check for small value lcu * changed to keep same vector length but apply no operations --- tangelo/toolboxes/circuits/lcu.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tangelo/toolboxes/circuits/lcu.py b/tangelo/toolboxes/circuits/lcu.py index 1c15c7392..a3b6dff38 100644 --- a/tangelo/toolboxes/circuits/lcu.py +++ b/tangelo/toolboxes/circuits/lcu.py @@ -319,8 +319,12 @@ def get_uprep_uselect(qu_op: QubitOperator, control: Union[int, List[int]] = Non max_qu_op = count_qubits(qu_op) for term, coeff in qu_op.terms.items(): acoeff = np.abs(coeff) - vector += [acoeff] - unitaries += [QubitOperator(term, -coeff / acoeff)] + if acoeff > 1.e-8: + vector += [acoeff] + unitaries += [QubitOperator(term, -coeff / acoeff)] + else: + vector += [0] + unitaries += [QubitOperator((), 1)] # create U_{prep} from sqrt of coefficients vector = np.array(vector) From 61bfcae94a67655020b247bf40207c8843da1671 Mon Sep 17 00:00:00 2001 From: Alexandre Fleury <76115575+AlexandreF-1qbit@users.noreply.github.com> Date: Wed, 4 Jan 2023 14:34:32 -0500 Subject: [PATCH 13/14] Openshell DMET (#208) * Open-shell DMET. * Fix for get_rdm CCSD. * Added NAO localization. * Added LiO2 spin=1 DMET test. * Added UHF MF for DMET. New get_rdm for VQESolver. --- tangelo/algorithms/classical/ccsd_solver.py | 34 +++- tangelo/algorithms/variational/vqe_solver.py | 130 ++++++++++++ .../dmet/_helpers/__init__.py | 12 +- .../dmet/_helpers/dmet_fragment.py | 15 +- .../dmet/_helpers/dmet_onerdm.py | 47 ++++- .../dmet/_helpers/dmet_orbitals.py | 122 ++++++++--- .../dmet/_helpers/dmet_scf.py | 82 ++++++-- .../dmet/_helpers/dmet_scf_guess.py | 63 +++++- .../dmet/dmet_problem_decomposition.py | 190 ++++++++++++++---- .../problem_decomposition/dmet/fragment.py | 148 +++++++++++--- .../electron_localization/__init__.py | 1 + .../electron_localization/nao_localization.py | 38 ++++ .../tests/dmet/test_dmet.py | 2 +- .../tests/dmet/test_dmet_oneshot_loop.py | 14 +- .../tests/dmet/test_dmet_orbitals.py | 2 +- .../tests/dmet/test_osdmet.py | 72 +++++++ 16 files changed, 811 insertions(+), 161 deletions(-) create mode 100644 tangelo/problem_decomposition/electron_localization/nao_localization.py create mode 100644 tangelo/problem_decomposition/tests/dmet/test_osdmet.py diff --git a/tangelo/algorithms/classical/ccsd_solver.py b/tangelo/algorithms/classical/ccsd_solver.py index 335024c03..95cc19913 100644 --- a/tangelo/algorithms/classical/ccsd_solver.py +++ b/tangelo/algorithms/classical/ccsd_solver.py @@ -15,6 +15,7 @@ """Class performing electronic structure calculation employing the CCSD method. """ +import numpy as np from pyscf import cc, lib from pyscf.cc.ccsd_rdm import _make_rdm1, _make_rdm2, _gamma1_intermediates, _gamma2_outcore from pyscf.cc.uccsd_rdm import (_make_rdm1 as _umake_rdm1, _make_rdm2 as _umake_rdm2, @@ -39,6 +40,8 @@ class CCSDSolver(ElectronicStructureSolver): def __init__(self, molecule): self.cc_fragment = None + self.spin = molecule.spin + self.mean_field = molecule.mean_field self.frozen = molecule.frozen_mos self.uhf = molecule.uhf @@ -77,18 +80,27 @@ def get_rdm(self): raise RuntimeError("CCSDSolver: Cannot retrieve RDM. Please run the 'simulate' method first") # Solve the lambda equation and obtain the reduced density matrix from CC calculation - self.cc_fragment.solve_lambda() t1 = self.cc_fragment.t1 t2 = self.cc_fragment.t2 - l1 = self.cc_fragment.l1 - l2 = self.cc_fragment.l2 - - d1 = _gamma1_intermediates(self.cc_fragment, t1, t2, l1, l2) if not self.uhf else _ugamma1_intermediates(self.cc_fragment, t1, t2, l1, l2) - f = lib.H5TmpFile() - d2 = _gamma2_outcore(self.cc_fragment, t1, t2, l1, l2, f, False) if not self.uhf else _ugamma2_outcore(self.cc_fragment, t1, t2, l1, l2, f, False) - - one_rdm = _make_rdm1(self.cc_fragment, d1, with_frozen=False) if not self.uhf else _umake_rdm1(self.cc_fragment, d1, with_frozen=False) - two_rdm = (_make_rdm2(self.cc_fragment, d1, d2, with_dm1=True, with_frozen=False) if not self.uhf - else _umake_rdm2(self.cc_fragment, d1, d2, with_dm1=True, with_frozen=False)) + l1, l2 = self.cc_fragment.solve_lambda(t1, t2) + + if self.spin == 0 and not self.uhf: + d1 = _gamma1_intermediates(self.cc_fragment, t1, t2, l1, l2) + f = lib.H5TmpFile() + d2 = _gamma2_outcore(self.cc_fragment, t1, t2, l1, l2, f, False) + + one_rdm = _make_rdm1(self.cc_fragment, d1, with_frozen=False) + two_rdm = _make_rdm2(self.cc_fragment, d1, d2, with_dm1=True, with_frozen=False) + else: + d1 = _ugamma1_intermediates(self.cc_fragment, t1, t2, l1, l2) + f = lib.H5TmpFile() + d2 = _ugamma2_outcore(self.cc_fragment, t1, t2, l1, l2, f, False) + + one_rdm = _umake_rdm1(self.cc_fragment, d1, with_frozen=False) + two_rdm = _umake_rdm2(self.cc_fragment, d1, d2, with_dm1=True, with_frozen=False) + + if not self.uhf: + one_rdm = np.sum(one_rdm, axis=0) + two_rdm = np.sum((two_rdm[0], 2*two_rdm[1], two_rdm[2]), axis=0) return one_rdm, two_rdm diff --git a/tangelo/algorithms/variational/vqe_solver.py b/tangelo/algorithms/variational/vqe_solver.py index 27fe6e5e5..6ebfca966 100644 --- a/tangelo/algorithms/variational/vqe_solver.py +++ b/tangelo/algorithms/variational/vqe_solver.py @@ -506,6 +506,136 @@ def get_rdm(self, var_params, resample=False, sum_spin=True, ref_state=Circuit() return rdm1_spin, rdm2_spin + def get_rdm_uhf(self, var_params, resample=False, ref_state=Circuit()): + """Compute the 1- and 2- RDM matrices using the VQE energy evaluation. + This method allows to combine the DMET problem decomposition technique + with the VQE as an electronic structure solver. The RDMs are computed by + using each fermionic Hamiltonian term, transforming them and computing + the elements one-by-one. Note that the Hamiltonian coefficients will not + be multiplied as in the energy evaluation. The first element of the + Hamiltonian is the nuclear repulsion energy term, not the Hamiltonian + term. + + Args: + var_params (numpy.array or list): variational parameters to use for + rdm calculation + resample (bool): Whether to resample saved frequencies. get_rdm with + savefrequencies=True must be called or a dictionary for each + qubit terms' frequencies must be set to self.rdm_freq_dict + ref_state (Circuit): A reference state preparation circuit. + + Returns: TODO + (numpy.array, numpy.array): One & two-particle spin summed RDMs if + sumspin=True or the full One & two-Particle RDMs if + sumspin=False. + """ + + self.ansatz.update_var_params(var_params) + + # Initialize the RDM arrays + n_mol_orbitals = max(self.molecule.n_active_mos) + rdm1_np_a = np.zeros((n_mol_orbitals,) * 2) + rdm1_np_b = np.zeros((n_mol_orbitals,) * 2) + rdm2_np_a = np.zeros((n_mol_orbitals,) * 4) + rdm2_np_b = np.zeros((n_mol_orbitals,) * 4) + rdm2_np_ba = np.zeros((n_mol_orbitals,) * 4) + + # If resampling is requested, check that a previous savefrequencies run has been called + if resample: + if hasattr(self, "rdm_freq_dict"): + qb_freq_dict = self.rdm_freq_dict + resampled_expect_dict = dict() + else: + raise AttributeError("Need to run RDM calculation with savefrequencies=True") + else: + qb_freq_dict = dict() + qb_expect_dict = dict() + + # Loop over each element of Hamiltonian (non-zero value) + for key in self.molecule.fermionic_hamiltonian.terms: + # Ignore constant / empty term + if not key: + continue + + # Assign indices depending on one- or two-body term + length = len(key) + # One-body terms. + if(length == 2): + pele, qele = int(key[0][0]), int(key[1][0]) + iele, jele = pele // 2, qele // 2 + iele_r, jele_r = pele % 2, qele % 2 + # Two-body terms. + elif(length == 4): + pele, qele, rele, sele = int(key[0][0]), int(key[1][0]), int(key[2][0]), int(key[3][0]) + iele, jele, kele, lele = pele // 2, qele // 2, rele // 2, sele // 2 + iele_r, jele_r, kele_r, lele_r = pele % 2, qele % 2, rele % 2, sele % 2 + + # Create the Hamiltonian with the correct key (Set coefficient to one) + hamiltonian_temp = FermionOperator(key) + + # Obtain qubit Hamiltonian + qubit_hamiltonian2 = fermion_to_qubit_mapping(fermion_operator=hamiltonian_temp, + mapping=self.qubit_mapping, + n_spinorbitals=self.molecule.n_active_sos, + n_electrons=self.molecule.n_active_electrons, + up_then_down=self.up_then_down, + spin=self.molecule.spin) + qubit_hamiltonian2.compress() + + # Run through each qubit term separately, use previously calculated result for the qubit term or + # calculate and save results for that qubit term + opt_energy2 = 0. + for qb_term, qb_coef in qubit_hamiltonian2.terms.items(): + if qb_term: + if qb_term not in qb_freq_dict: + if resample: + warnings.warn(f"Warning: rerunning circuit for missing qubit term {qb_term}") + basis_circuit = Circuit(measurement_basis_gates(qb_term)) + full_circuit = ref_state + self.ansatz.circuit + basis_circuit + qb_freq_dict[qb_term], _ = self.backend.simulate(full_circuit) + if resample: + if qb_term not in resampled_expect_dict: + resampled_freq_dict = get_resampled_frequencies(qb_freq_dict[qb_term], self.backend.n_shots) + resampled_expect_dict[qb_term] = self.backend.get_expectation_value_from_frequencies_oneterm(qb_term, resampled_freq_dict) + expectation = resampled_expect_dict[qb_term] + else: + if qb_term not in qb_expect_dict: + qb_expect_dict[qb_term] = self.backend.get_expectation_value_from_frequencies_oneterm(qb_term, qb_freq_dict[qb_term]) + expectation = qb_expect_dict[qb_term] + opt_energy2 += qb_coef * expectation + else: + opt_energy2 += qb_coef + + # Put the values in np arrays (differentiate 1- and 2-RDM) + if length == 2: + if (iele_r, jele_r) == (0, 0): + rdm1_np_a[iele, jele] += opt_energy2 + elif (iele_r, jele_r) == (1, 1): + rdm1_np_b[iele, jele] += opt_energy2 + elif length == 4: + if ((iele != lele) or (jele != kele)): + if (iele_r, jele_r, kele_r, lele_r) == (0, 0, 0, 0): + rdm2_np_a[lele, iele, kele, jele] += 0.5 * opt_energy2 + rdm2_np_a[iele, lele, jele, kele] += 0.5 * opt_energy2 + elif (iele_r, jele_r, kele_r, lele_r) == (1, 1, 1, 1): + rdm2_np_b[lele, iele, kele, jele] += 0.5 * opt_energy2 + rdm2_np_b[iele, lele, jele, kele] += 0.5 * opt_energy2 + elif (iele_r, jele_r, kele_r, lele_r) == (0, 1, 1, 0): + rdm2_np_ba[iele, lele, jele, kele] += 0.5 * opt_energy2 + rdm2_np_ba[lele, iele, kele, jele] += 0.5 * opt_energy2 + else: + if (iele_r, jele_r, kele_r, lele_r) == (0, 0, 0, 0): + rdm2_np_a[iele, lele, jele, kele] += opt_energy2 + elif (iele_r, jele_r, kele_r, lele_r) == (1, 1, 1, 1): + rdm2_np_b[iele, lele, jele, kele] += opt_energy2 + elif (iele_r, jele_r, kele_r, lele_r) == (0, 1, 1, 0): + rdm2_np_ba[iele, lele, jele, kele] += opt_energy2 + + # save rdm frequency dictionary + self.rdm_freq_dict = qb_freq_dict + + return (rdm1_np_a, rdm1_np_b), (rdm2_np_a, rdm2_np_ba, rdm2_np_b) + def _default_optimizer(self, func, var_params): """Function used as a default optimizer for VQE when user does not provide one. Can be used as an example for users who wish to provide diff --git a/tangelo/problem_decomposition/dmet/_helpers/__init__.py b/tangelo/problem_decomposition/dmet/_helpers/__init__.py index c0e97f589..37d1af48d 100644 --- a/tangelo/problem_decomposition/dmet/_helpers/__init__.py +++ b/tangelo/problem_decomposition/dmet/_helpers/__init__.py @@ -12,10 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Common helper functions for all mean-field. from .dmet_orbitals import dmet_orbitals as _orbitals from .dmet_fragment import dmet_fragment_constructor as _fragment_constructor -from .dmet_onerdm import dmet_low_rdm as _low_rdm from .dmet_onerdm import dmet_fragment_rdm as _fragment_rdm from .dmet_bath import dmet_fragment_bath as _fragment_bath -from .dmet_scf_guess import dmet_fragment_guess as _fragment_guess -from .dmet_scf import dmet_fragment_scf as _fragment_scf + +# Specific helper functions for restricted / unrestricted mean-field. +from .dmet_onerdm import dmet_low_rdm_rhf as _low_rdm_rhf +from .dmet_onerdm import dmet_low_rdm_rohf_uhf as _low_rdm_rohf_uhf +from .dmet_scf_guess import dmet_fragment_guess_rhf as _fragment_guess_rhf +from .dmet_scf_guess import dmet_fragment_guess_rohf_uhf as _fragment_guess_rohf_uhf +from .dmet_scf import dmet_fragment_scf_rhf as _fragment_scf_rhf +from .dmet_scf import dmet_fragment_scf_rohf_uhf as _fragment_scf_rohf_uhf diff --git a/tangelo/problem_decomposition/dmet/_helpers/dmet_fragment.py b/tangelo/problem_decomposition/dmet/_helpers/dmet_fragment.py index 5d2ea55f8..987febf3e 100644 --- a/tangelo/problem_decomposition/dmet/_helpers/dmet_fragment.py +++ b/tangelo/problem_decomposition/dmet/_helpers/dmet_fragment.py @@ -19,7 +19,7 @@ """ -def dmet_fragment_constructor(mol, atom_list, number_fragment): +def dmet_fragment_constructor(mol, atom_list, n_fragment): """Construct orbital list. Make a list of number of orbitals for each fragment while obtaining the list @@ -28,8 +28,7 @@ def dmet_fragment_constructor(mol, atom_list, number_fragment): Args: mol (pyscf.gto.Mole): The molecule to simulate (The full molecule). atom_list (list): The atom list for each fragment (int). - number_fragment (list): Number of element in atom list per fragment - (int). + n_fragment (list): Number of atoms per fragment (int). Returns: list: The number of orbitals for each fragment (int). @@ -39,18 +38,18 @@ def dmet_fragment_constructor(mol, atom_list, number_fragment): """ # Make a new atom list based on how many fragments for DMET calculation - if number_fragment == 0: + if n_fragment == 0: atom_list2 = atom_list else: # Calculate the number of DMET calculations - number_new_fragment = int(len(atom_list)/(number_fragment+1)) # number of DMET calulation per loop + n_new_fragment = int(len(atom_list)/(n_fragment+1)) atom_list2 = [] # Define the number of atoms per DMET calculation - for i in range(number_new_fragment): + for i in range(n_new_fragment): num = 0 - for j in range(number_fragment + 1): - k = (number_fragment+1)*i+j + for j in range(n_fragment + 1): + k = (n_fragment + 1) * i + j num += atom_list[k] atom_list2.append(num) diff --git a/tangelo/problem_decomposition/dmet/_helpers/dmet_onerdm.py b/tangelo/problem_decomposition/dmet/_helpers/dmet_onerdm.py index 65b7ba2ee..0251f1721 100644 --- a/tangelo/problem_decomposition/dmet/_helpers/dmet_onerdm.py +++ b/tangelo/problem_decomposition/dmet/_helpers/dmet_onerdm.py @@ -18,33 +18,60 @@ """ import numpy as np -from functools import reduce -def dmet_low_rdm(active_fock, number_active_electrons): +def dmet_low_rdm_rhf(active_fock, n_active_electrons): """Construct the one-particle RDM from low-level calculation. Args: active_fock (numpy.array): Fock matrix from low-level calculation (float64). - number_active_electrons (int): Number of electrons in the entire system. + n_active_electrons (int): Number of electrons in the entire system. Returns: numpy.array: One-particle RDM of the low-level calculation (float64). """ # Extract the occupied part of the one-particle RDM - num_occ = number_active_electrons / 2 + num_occ = n_active_electrons // 2 e, c = np.linalg.eigh(active_fock) new_index = e.argsort() - e = e[new_index] c = c[:, new_index] - onerdm = np.dot(c[:, : int(num_occ)], c[:, : int(num_occ)].T) * 2 + onerdm = np.dot(c[:, : num_occ], c[:, : num_occ].T) * 2 return onerdm -def dmet_fragment_rdm(t_list, bath_orb, core_occupied, number_active_electrons): +def dmet_low_rdm_rohf_uhf(active_fock_alpha, active_fock_beta, n_active_alpha, n_active_beta): + """Construct the one-particle RDM from low-level calculation. + + Args: + active_fock_alpha (numpy.array): Fock matrix from low-level calculation + (float64). + active_fock_beta (numpy.array): Fock matrix from low-level calculation + (float64). + n_active_alpha (int): Number of alpha electrons. + n_active_beta (int): Number of beta electrons. + + Returns: + onerdm (numpy.array): One-particle RDM of the low-level calculation + (float64). + """ + + e, c = np.linalg.eigh(active_fock_alpha) + new_index = e.argsort() + c = c[:, new_index] + onerdm_alpha = np.dot(c[:, :int(n_active_alpha)], c[:, :int(n_active_alpha)].T) + + e, c = np.linalg.eigh(active_fock_beta) + new_index = e.argsort() + c = c[:, new_index] + onerdm_beta = np.dot(c[:, :int(n_active_beta)], c[:, :int(n_active_beta)].T) + + return onerdm_alpha + onerdm_beta + + +def dmet_fragment_rdm(t_list, bath_orb, core_occupied, n_active_electrons): """Construct the one-particle RDM for the core orbitals. Args: @@ -52,7 +79,7 @@ def dmet_fragment_rdm(t_list, bath_orb, core_occupied, number_active_electrons): bath_orb (numpy.array): The bath orbitals (float64). core_occupied (numpy.array): Core occupied part of the MO coefficients (float64). - number_active_electrons (int): Number of electrons in the entire system. + n_active_electrons (int): Number of electrons in the entire system. Returns: int: Number of orbitals for fragment calculation. @@ -72,9 +99,9 @@ def dmet_fragment_rdm(t_list, bath_orb, core_occupied, number_active_electrons): # Define the number of electrons in the fragment number_ele_temp = np.sum(core_occupied) - number_electrons = int(round(number_active_electrons - number_ele_temp)) + number_electrons = int(round(n_active_electrons - number_ele_temp)) # Obtain the one particle RDM for the fragment (core) - core_occupied_onerdm = reduce(np.dot, (bath_orb, np.diag(core_occupied), bath_orb.T)) + core_occupied_onerdm = bath_orb @ np.diag(core_occupied) @ bath_orb.T return number_orbitals, number_electrons, core_occupied_onerdm diff --git a/tangelo/problem_decomposition/dmet/_helpers/dmet_orbitals.py b/tangelo/problem_decomposition/dmet/_helpers/dmet_orbitals.py index dd73489c7..c0d1cd7bb 100644 --- a/tangelo/problem_decomposition/dmet/_helpers/dmet_orbitals.py +++ b/tangelo/problem_decomposition/dmet/_helpers/dmet_orbitals.py @@ -24,7 +24,6 @@ from pyscf import scf, ao2mo import numpy as np -from functools import reduce class dmet_orbitals: @@ -51,9 +50,10 @@ class dmet_orbitals: active_oneint (numpy.array): One-electron integral in localized MO basis (float64). active_fock (numpy.array): Fock matrix in localized MO basis (float64). + uhf (bool): Flag for an unrestricted mean-field. """ - def __init__(self, mol, mf, active_space, localization_function): + def __init__(self, mol, mf, active_space, localization_function, uhf): """Initialize the class. Localize the orbitals, get energies and integrals for the entire system @@ -66,38 +66,102 @@ def __init__(self, mol, mf, active_space, localization_function): active_space (list): The active space in DMET calculation. All orbitals in the initial SCF calculation (int). localization_function (string): Localization scheme. + uhf (bool): Flag for an unrestricted mean-field. """ - # TODO: Is active space always claculated from the molecule? - - # Obtain the elements from the low-level SCF calculations + # General quantities. self.mol_full = mol self.mf_full = mf self.low_scf_energy = mf.e_tot - low_scf_dm = reduce(np.dot, (mf.mo_coeff, np.diag(mf.mo_occ), mf.mo_coeff.T)) - low_scf_twoint = scf.hf.get_veff(mf.mol, low_scf_dm, 0, 0, 1) - self.low_scf_fock = mf.mol.intor("cint1e_kin_sph") + mf.mol.intor("cint1e_nuc_sph") + low_scf_twoint - # Define the active space if possible + # Define the active space if possible. self.dmet_active_orbitals = np.zeros([mf.mol.nao_nr()], dtype=int) self.dmet_active_orbitals[active_space] = 1 self.number_active_orbitals = np.sum(self.dmet_active_orbitals) - self.number_active_electrons = int(np.rint(mf.mol.nelectron - np.sum(mf.mo_occ[self.dmet_active_orbitals == 0]))) - # Localize the orbitals (IAO) + # Localize the orbitals. self.localized_mo = localization_function(mol, mf) - # Define the core space if possible (Initial calculations treat the entire molecule ...) - core_mo_dm = np.array(mf.mo_occ, copy=True) - core_mo_dm[self.dmet_active_orbitals == 1] = 0 - core_ao_dm = reduce(np.dot, (mf.mo_coeff, np.diag(core_mo_dm), mf.mo_coeff.T)) - core_twoint = scf.hf.get_veff(mf.mol, core_ao_dm, 0, 0, 1) - core_oneint = self.low_scf_fock - low_scf_twoint + core_twoint - - # Define the energies and matrix elements based on the localized orbitals - self.core_constant_energy = mf.mol.energy_nuc() + np.einsum("ij,ij->", core_oneint - 0.5*core_twoint, core_ao_dm) - self.active_oneint = reduce(np.dot, (self.localized_mo.T, core_oneint, self.localized_mo)) - self.active_fock = reduce(np.dot, (self.localized_mo.T, self.low_scf_fock, self.localized_mo)) + if uhf: + self._unrestricted_init() + else: + self._restricted_init() + + def _restricted_init(self): + """Initialize the attributes for a restricted mean-field.""" + + self.number_active_electrons = int(np.rint(self.mf_full.mol.nelectron - np.sum(self.mf_full.mo_occ[self.dmet_active_orbitals == 0]))) + + # RHF + if self.mol_full.spin == 0: + # Obtain the elements from the low-level SCF calculations. + low_scf_dm = self.mf_full.mo_coeff @ np.diag(self.mf_full.mo_occ) @ self.mf_full.mo_coeff.T + low_scf_twoint = scf.hf.get_veff(self.mf_full.mol, low_scf_dm, 0, 0, 1) + self.low_scf_fock = self.mf_full.mol.intor("cint1e_kin_sph") + self.mf_full.mol.intor("cint1e_nuc_sph") + low_scf_twoint + + # Define the core space if possible (Initial calculations treat the entire molecule ...). + core_mo_dm = np.array(self.mf_full.mo_occ, copy=True) + core_mo_dm[self.dmet_active_orbitals == 1] = 0 + core_ao_dm = self.mf_full.mo_coeff @ np.diag(core_mo_dm) @ self.mf_full.mo_coeff.T + core_twoint = scf.hf.get_veff(self.mf_full.mol, core_ao_dm, 0, 0, 1) + core_oneint = self.low_scf_fock - low_scf_twoint + core_twoint + + # Define the energies and matrix elements based on the localized orbitals. + self.core_constant_energy = self.mf_full.mol.energy_nuc() + np.einsum("ij,ij->", core_oneint - 0.5*core_twoint, core_ao_dm) + self.active_oneint = self.localized_mo.T @ core_oneint @ self.localized_mo + self.active_fock = self.localized_mo.T @ self.low_scf_fock @ self.localized_mo + # ROHF + else: + # Obtain the elements from the low-level SCF calculations. + low_scf_rdm = self.mf_full.make_rdm1() + low_scf_twoint = self.mf_full.get_veff(self.mol_full, low_scf_rdm, 0, 0, 1) + + core_oneint = self.mf_full.get_hcore() + low_scf_fock_alpha = core_oneint + low_scf_twoint[0] + low_scf_fock_beta = core_oneint + low_scf_twoint[1] + + elec_paired = self.number_active_electrons - self.mol_full.spin + orbital_paired = elec_paired // 2 + self.number_active_electrons_alpha = orbital_paired + self.mol_full.spin + self.number_active_electrons_beta = orbital_paired + + # Define the energies and matrix elements based on the localized orbitals. + self.core_constant_energy = self.mf_full.mol.energy_nuc() + self.active_oneint = self.localized_mo.T @ core_oneint @ self.localized_mo + + self.active_fock_alpha = self.localized_mo.T @ low_scf_fock_alpha @ self.localized_mo + self.active_fock_beta = self.localized_mo.T @ low_scf_fock_beta @ self.localized_mo + + rdm_a = self.localized_mo.T @ low_scf_rdm[0] @ self.localized_mo + rdm_b = self.localized_mo.T @ low_scf_rdm[1] @ self.localized_mo + rdm_total = np.array((rdm_a, rdm_b)) + + overlap = np.eye(self.number_active_orbitals) + two_int = scf.hf.get_veff(self.mol_full, rdm_total, 0, 0, 1) + new_fock_alpha = self.active_oneint + (self.localized_mo.T @ two_int[0] @ self.localized_mo) + new_fock_beta = self.active_oneint + (self.localized_mo.T @ two_int[1] @ self.localized_mo) + fock_total = np.array((new_fock_alpha, new_fock_beta)) + self.active_fock = scf.rohf.get_roothaan_fock(fock_total, rdm_total, overlap) + + def _unrestricted_init(self): + """Initialize the attributes for an unrestricted mean-field.""" + + low_scf_fock_alpha, low_scf_fock_beta = self.mf_full.get_fock() + core_oneint = self.mf_full.get_hcore() + + self.number_active_electrons = self.mf_full.mol.nelectron + + elec_diff = self.mol_full.spin + elec_paired = self.number_active_electrons-elec_diff + orbital_paired = elec_paired // 2 + self.number_active_electrons_alpha = orbital_paired + elec_diff + self.number_active_electrons_beta = orbital_paired + + self.core_constant_energy = self.mf_full.mol.energy_nuc() + self.active_oneint = self.localized_mo.T @ core_oneint @ self.localized_mo + + self.active_fock_alpha = self.localized_mo.T @ low_scf_fock_alpha @ self.localized_mo + self.active_fock_beta = self.localized_mo.T @ low_scf_fock_beta @ self.localized_mo def dmet_fragment_hamiltonian(self, bath_orb, norb_high, onerdm_core): """Construct the Hamiltonian for a DMET fragment. @@ -116,16 +180,16 @@ def dmet_fragment_hamiltonian(self, bath_orb, norb_high, onerdm_core): (float64). """ - # Calculate one-electron integrals - frag_oneint = reduce(np.dot, (bath_orb[:, : norb_high].T, self.active_oneint, bath_orb[:, : norb_high])) + # Calculate one-electron integrals. + frag_oneint = bath_orb[:, : norb_high].T @ self.active_oneint @ bath_orb[:, : norb_high] - # Calculate the fock matrix - density_matrix = reduce(np.dot, (self.localized_mo, onerdm_core, self.localized_mo.T)) + # Calculate the fock matrix. + density_matrix = self.localized_mo @ onerdm_core @ self.localized_mo.T two_int = scf.hf.get_veff(self.mol_full, density_matrix, 0, 0, 1) - new_fock = self.active_oneint + reduce(np.dot, ((self.localized_mo.T, two_int, self.localized_mo))) - frag_fock = reduce(np.dot, (bath_orb[:, : norb_high].T, new_fock, bath_orb[:, : norb_high])) + new_fock = self.active_oneint + (self.localized_mo.T @ two_int @ self.localized_mo) + frag_fock = bath_orb[:, : norb_high].T @ new_fock @ bath_orb[:, : norb_high] - # Calculate the two-electron integrals + # Calculate the two-electron integrals. coefficients = np.dot(self.localized_mo, bath_orb[:, : norb_high]) frag_twoint = ao2mo.outcore.full_iofree(self.mol_full, coefficients, compact=False).reshape( norb_high, norb_high, norb_high, norb_high) diff --git a/tangelo/problem_decomposition/dmet/_helpers/dmet_scf.py b/tangelo/problem_decomposition/dmet/_helpers/dmet_scf.py index 472cb1afe..c36d15394 100644 --- a/tangelo/problem_decomposition/dmet/_helpers/dmet_scf.py +++ b/tangelo/problem_decomposition/dmet/_helpers/dmet_scf.py @@ -18,11 +18,10 @@ """ from pyscf import gto, scf, ao2mo -from functools import reduce import numpy as np -def dmet_fragment_scf(t_list, two_ele, fock, number_electrons, number_orbitals, guess_orbitals, chemical_potential): +def dmet_fragment_scf_rhf(t_list, two_ele, fock, n_electrons, n_orbitals, guess_orbitals, chemical_potential): """Perform SCF calculation. Args: @@ -30,8 +29,8 @@ def dmet_fragment_scf(t_list, two_ele, fock, number_electrons, number_orbitals, two_ele (numpy.array): Two-electron integrals for fragment calculation (float64). fock (numpy.array): The fock matrix for fragment calculation (float64). - number_electrons (int): Number of electrons for fragment calculation. - number_orbitals (int): Number of orbitals for fragment calculation. + n_electrons (int): Number of electrons for fragment calculation. + n_orbitals (int): Number of orbitals for fragment calculation. guess_orbitals (numpy.array): Guess orbitals for SCF calculation (float64). chemical_potential (float64): The chemical potential. @@ -47,34 +46,81 @@ def dmet_fragment_scf(t_list, two_ele, fock, number_electrons, number_orbitals, fock_frag_copy = fock.copy() # Subtract the chemical potential to make the number of electrons consistent - if (chemical_potential != 0.0): - for orb in range(t_list[0]): - fock_frag_copy[orb, orb] -= chemical_potential + for orb in range(t_list[0]): + fock_frag_copy[orb, orb] -= chemical_potential # Determine the molecular space (set molecule object of pyscf) mol_frag = gto.Mole() mol_frag.build(verbose=0) mol_frag.atom.append(("C", (0, 0, 0))) - mol_frag.nelectron = number_electrons + mol_frag.nelectron = n_electrons mol_frag.incore_anyway = True # Perform SCF calculation (set mean field object of pyscf) mf_frag = scf.RHF(mol_frag) mf_frag.get_hcore = lambda *args: fock_frag_copy - mf_frag.get_ovlp = lambda *args: np.eye(number_orbitals) - mf_frag._eri = ao2mo.restore(8, two_ele, number_orbitals) + mf_frag.get_ovlp = lambda *args: np.eye(n_orbitals) + mf_frag._eri = ao2mo.restore(8, two_ele, n_orbitals) mf_frag.scf(guess_orbitals) - # Calculate the density matrix for the fragment - dm_frag = reduce(np.dot, (mf_frag.mo_coeff, np.diag(mf_frag.mo_occ), mf_frag.mo_coeff.T)) - # Use newton-raphson algorithm if the above SCF calculation is not converged if (mf_frag.converged is False): - mf_frag.get_hcore = lambda *args: fock_frag_copy - mf_frag.get_ovlp = lambda *args: np.eye(number_orbitals) - mf_frag._eri = ao2mo.restore(8, two_ele, number_orbitals) mf_frag = scf.RHF(mol_frag).newton() - energy = mf_frag.kernel(dm0=dm_frag) - dm_frag = reduce(np.dot, (mf_frag.mo_coeff, np.diag(mf_frag.mo_occ), mf_frag.mo_coeff.T)) + + return mf_frag, fock_frag_copy, mol_frag + + +def dmet_fragment_scf_rohf_uhf(nele_ab, two_ele, fock, n_electrons, n_orbitals, guess_orbitals, chemical_potential, uhf): + """Perform SCF calculation. + + Args: + nele_ab (list): List of the alpha and beta electron number (int). + two_ele (numpy.array): Two-electron integrals for fragment calculation + (float64). + fock (numpy.array): The fock matrix for fragment calculation (float64). + n_electrons (int): Number of electrons for fragment calculation. + n_orbitals (int): Number of orbitals for fragment calculation. + guess_orbitals (numpy.array): Guess orbitals for SCF calculation + (float64). + chemical_potential (float64): The chemical potential. + uhf (bool): Flag for UHF mean-field. If not, a ROHF mean-field is + considered. + + Returns: + pyscf.scf.ROHF or pyscf.scf.UHF: The mean field of the molecule + (Fragment calculation). + numpy.array: The fock matrix with chemical potential subtracted + (float64). + pyscf.gto.Mole: The molecule to simulate (Fragment calculation). + """ + + # Deep copy the fock matrix + fock_frag_copy = fock.copy() + + for orb in range(nele_ab[0]): + fock_frag_copy[orb, orb] -= chemical_potential + + # Determine the molecular space (set molecule object of pyscf) + mol_frag = gto.Mole() + mol_frag.build(verbose=0) + mol_frag.atom.append(("C", (0, 0, 0))) + mol_frag.nelectron = n_electrons + mol_frag.incore_anyway = True + mol_frag.spin = nele_ab[0] - nele_ab[1] + + # Perform SCF calculation (set mean field object of pyscf) + mf_frag = scf.UHF(mol_frag) if uhf else scf.ROHF(mol_frag) + mf_frag.get_hcore = lambda *args: fock_frag_copy + mf_frag.get_ovlp = lambda *args: np.eye(n_orbitals) + mf_frag._eri = ao2mo.restore(8, two_ele, n_orbitals) + mf_frag.scf(guess_orbitals) + + orb_temp = mf_frag.mo_coeff + occ_temp = mf_frag.mo_occ + + # Use Newton-Raphson algorithm if the above SCF calculation is not converged + if not mf_frag.converged: + mf_frag = scf.newton(mf_frag) + _ = mf_frag.kernel(orb_temp, occ_temp) return mf_frag, fock_frag_copy, mol_frag diff --git a/tangelo/problem_decomposition/dmet/_helpers/dmet_scf_guess.py b/tangelo/problem_decomposition/dmet/_helpers/dmet_scf_guess.py index a60503006..4532f7ace 100644 --- a/tangelo/problem_decomposition/dmet/_helpers/dmet_scf_guess.py +++ b/tangelo/problem_decomposition/dmet/_helpers/dmet_scf_guess.py @@ -20,18 +20,17 @@ import scipy import numpy as np -from functools import reduce -def dmet_fragment_guess(t_list, bath_orb, chemical_potential, norb_high, number_active_electron, active_fock): +def dmet_fragment_guess_rhf(t_list, bath_orb, chemical_potential, norb_high, n_active_electron, active_fock): """Construct the guess orbitals. Args: - t_list (list): Number of fragment & bath orbitals (int). + t_list (list): Number of [0] fragment & [1] bath orbitals (int). bath_orb (numpy.array): The bath orbitals (float64). chemical_potential (float64): The chemical potential. norb_high (int): The number of orbitals in the fragment calculation. - number_active_electron (int): The number of electrons in the fragment + n_active_electron (int): The number of electrons in the fragment calculation. active_fock (numpy.array): The fock matrix from the low-level calculation (float64). @@ -41,17 +40,61 @@ def dmet_fragment_guess(t_list, bath_orb, chemical_potential, norb_high, number_ """ # Construct the fock matrix of the fragment (subtract the chemical potential for consistency) - fock_fragment = reduce(np.dot, (bath_orb[:, : norb_high].T, active_fock, bath_orb[:, : norb_high])) - norb = t_list[0] - if(chemical_potential != 0): - for i in range(norb): - fock_fragment[i, i] -= chemical_potential + fock_fragment = bath_orb[:, : norb_high].T @ active_fock @ bath_orb[:, : norb_high] + for i in range(t_list[0]): + fock_fragment[i, i] -= chemical_potential # Diagonalize the fock matrix and get the eigenvectors eigenvalues, eigenvectors = scipy.linalg.eigh(fock_fragment) eigenvectors = eigenvectors[:, eigenvalues.argsort()] # Extract the eigenvectors of the occupied orbitals as the guess orbitals - frag_guess = np.dot(eigenvectors[ :, : int(number_active_electron/2)], eigenvectors[ :, : int(number_active_electron/2)].T) * 2 + frag_guess = np.dot(eigenvectors[ :, : n_active_electron // 2], eigenvectors[ :, : n_active_electron // 2].T) * 2 return frag_guess + + +def dmet_fragment_guess_rohf_uhf(t_list, bath_orb, chemical_potential, norb_high, n_active_electron, +active_fock_alpha, active_fock_beta, n_active_alpha, n_active_beta): + """Construct the guess orbitals. + + Args: + t_list (list): Number of [0] fragment & [1] bath orbitals (int). + bath_orb (numpy.array): The bath orbitals (float64). + chemical_potential (float64): The chemical potential. + norb_high (int): The number of orbitals in the fragment calculation. + n_active_electron (int): The number of electrons in the fragment + calculation. + active_fock_alpha (numpy.array): The fock matrix from the low-level + calculation for the alpha electrons (float64). + active_fock_beta (numpy.array): The fock matrix from the low-level + calculation for the beta electrons (float64). + n_active_alpha (int): The number of active alpha electrons. + n_active_beta (int): The number of active beta electrons. + + Returns: + numpy.array: The guess orbitals (float64). + tuple: New electron numbers (alpha then beta electrons) (int). + """ + + n_spin = n_active_alpha - n_active_beta + n_pair = (n_active_electron - n_spin) // 2 + new = {"alpha": n_pair + n_spin, "beta": n_pair} + active_fock = {"alpha": active_fock_alpha, "beta": active_fock_beta} + frag_guess = dict() + + for spin in {"alpha", "beta"}: + # Construct the fock matrix of the fragment (subtract the chemical potential for consistency) + fock_fragment = bath_orb[:, : norb_high].T @ active_fock[spin] @ bath_orb[:, : norb_high] + for i in range(t_list[0]): + fock_fragment[i, i] -= chemical_potential + + # Diagonalize the fock matrix and get the eigenvectors + eigenvalues, eigenvectors = scipy.linalg.eigh(fock_fragment) + eigenvectors = eigenvectors[:, eigenvalues.argsort()] + + # Extract the eigenvectors of the occupied orbitals as the guess orbitals + # Introduce alpha- and beta-electrons + frag_guess[spin] = np.dot(eigenvectors[:, :int(new[spin])], eigenvectors[:, :int(new[spin])].T) + + return np.array((frag_guess["alpha"], frag_guess["beta"])), (new["alpha"], new["beta"]) diff --git a/tangelo/problem_decomposition/dmet/dmet_problem_decomposition.py b/tangelo/problem_decomposition/dmet/dmet_problem_decomposition.py index e9b211cd1..cd57a4ed0 100644 --- a/tangelo/problem_decomposition/dmet/dmet_problem_decomposition.py +++ b/tangelo/problem_decomposition/dmet/dmet_problem_decomposition.py @@ -15,7 +15,6 @@ """Employ DMET as a problem decomposition technique.""" from enum import Enum -from functools import reduce import numpy as np from pyscf import gto, scf import scipy @@ -23,7 +22,7 @@ from tangelo.problem_decomposition.dmet import _helpers as helpers from tangelo.problem_decomposition.problem_decomposition import ProblemDecomposition -from tangelo.problem_decomposition.electron_localization import iao_localization, meta_lowdin_localization +from tangelo.problem_decomposition.electron_localization import iao_localization, meta_lowdin_localization, nao_localization from tangelo.problem_decomposition.dmet.fragment import SecondQuantizedDMETFragment from tangelo.algorithms import FCISolver, CCSDSolver, VQESolver from tangelo.toolboxes.post_processing.mc_weeny_rdm_purification import mcweeny_purify_2rdm @@ -33,6 +32,7 @@ class Localization(Enum): """Enumeration of the electron localization supported by DMET.""" meta_lowdin = 0 iao = 1 + nao = 2 class DMETProblemDecomposition(ProblemDecomposition): @@ -43,6 +43,10 @@ class DMETProblemDecomposition(ProblemDecomposition): used instead of the Meta-Lowdin localization scheme, but it cannot be used for minimal basis set. + The underlying mean-field for the computation is automatically detected + from the SecondQuantizedMolecule. RHF, ROHF and UHF mean-fields are + supported. + Attributes: molecule (SecondQuantizedMolecule): The molecular system. electron_localization (Localization): A type of localization scheme. @@ -95,6 +99,8 @@ def __init__(self, opt_dict): if not self.molecule: raise ValueError(f"A SecondQuantizedMolecule object must be provided when instantiating DMETProblemDecomposition.") + self.uhf = self.molecule.uhf + # Converting our interface to pyscf.mol.gto and pyscf.scf (used by this # code). self.mean_field = self.molecule.mean_field @@ -131,7 +137,7 @@ def __init__(self, opt_dict): # Force recomputing the mean field if the atom ordering has been changed. warnings.warn("The mean field will be recomputed even if one has been provided by the user.", RuntimeWarning) - self.mean_field = scf.RHF(self.molecule) + self.mean_field = scf.UHF(self.molecule) if self.uhf else scf.RHF(self.molecule) self.mean_field.verbose = 0 self.mean_field.scf() @@ -179,6 +185,10 @@ def __init__(self, opt_dict): # If save_results in _oneshot_loop is True, the dict is populated. self.solver_fragment_dict = dict() + # To keep track the number of iteration (was done with an energy list + # before). + self.n_iter = 0 + @property def quantum_fragments_data(self): """This aims to return a dictionary with all necessary components to @@ -209,19 +219,27 @@ def build(self): self.electron_localization = meta_lowdin_localization elif self.electron_localization == Localization.iao: self.electron_localization = iao_localization + elif self.electron_localization == Localization.nao: + self.electron_localization = nao_localization else: raise ValueError(f"Unsupported ansatz. Built-in localization methods:\n\t{self.builtin_localization}") elif not callable(self.electron_localization): raise TypeError(f"Invalid electron localization function. Expecting a function.") # Construct orbital object. - self.orbitals = helpers._orbitals(self.molecule, self.mean_field, range(self.molecule.nao_nr()), self.electron_localization) + self.orbitals = helpers._orbitals(self.molecule, self.mean_field, range(self.molecule.nao_nr()), self.electron_localization, self.uhf) # TODO: remove last argument, combining fragments not supported. self.orb_list, self.orb_list2, _ = helpers._fragment_constructor(self.molecule, self.fragment_atoms, 0) # Calculate the 1-RDM for the entire molecule. - self.onerdm_low = helpers._low_rdm(self.orbitals.active_fock, self.orbitals.number_active_electrons) + if self.molecule.spin == 0 and not self.uhf: + self.onerdm_low = helpers._low_rdm_rhf(self.orbitals.active_fock, self.orbitals.number_active_electrons) + else: + self.onerdm_low = helpers._low_rdm_rohf_uhf(self.orbitals.active_fock_alpha, + self.orbitals.active_fock_beta, + self.orbitals.number_active_electrons_alpha, + self.orbitals.number_active_electrons_beta) def simulate(self): """Perform DMET loop to optimize the chemical potential. It converges @@ -232,10 +250,6 @@ def simulate(self): float: The DMET energy (dmet_energy). """ - # To keep track the number of iteration (was done with an energy list before). - # TODO: A decorator function to do the same thing? - self.n_iter = 0 - # Initialize the energy list and SCF procedure employing newton-raphson algorithm. # TODO : find a better initial guess than 0.0 for chemical potential. DMET fails often currently. if not self.orbitals: @@ -329,12 +343,23 @@ def _build_scf_fragments(self, chemical_potential): one_ele, fock, two_ele = self.orbitals.dmet_fragment_hamiltonian(bath_orb, norb_high, onerdm_high) # Construct guess orbitals for fragment SCF calculations. - guess_orbitals = helpers._fragment_guess(t_list, bath_orb, chemical_potential, norb_high, nelec_high, - self.orbitals.active_fock) - # Carry out SCF calculation for a fragment. - mf_fragment, fock_frag_copy, mol_frag = helpers._fragment_scf(t_list, two_ele, fock, nelec_high, norb_high, - guess_orbitals, chemical_potential) + if self.uhf or self.molecule.spin != 0: + guess_orbitals, nelec_high_ab = helpers._fragment_guess_rohf_uhf( + t_list, bath_orb, chemical_potential, norb_high, nelec_high, + self.orbitals.active_fock_alpha, self.orbitals.active_fock_beta, + self.orbitals.number_active_electrons_alpha, + self.orbitals.number_active_electrons_beta) + + mf_fragment, fock_frag_copy, mol_frag = helpers._fragment_scf_rohf_uhf( + nelec_high_ab, two_ele, fock, nelec_high, norb_high, + guess_orbitals, chemical_potential, self.uhf) + else: + guess_orbitals = helpers._fragment_guess_rhf(t_list, bath_orb, chemical_potential, norb_high, nelec_high, + self.orbitals.active_fock) + mf_fragment, fock_frag_copy, mol_frag = helpers._fragment_scf_rhf( + t_list, two_ele, fock, nelec_high, norb_high, guess_orbitals, + chemical_potential) scf_fragments.append([mf_fragment, fock_frag_copy, mol_frag, t_list, one_ele, two_ele, fock]) @@ -373,7 +398,6 @@ def _oneshot_loop(self, chemical_potential, save_results=False, resample=False, Returns: float: The new chemical potential. """ - self.n_iter += 1 if self.verbose: print(" \tIteration = ", self.n_iter) @@ -411,7 +435,7 @@ def _oneshot_loop(self, chemical_potential, save_results=False, resample=False, # We create a dummy SecondQuantizedMolecule with a DMETFragment class. # It has the same important attributes and methods to be used with # functions of this package. - dummy_mol = SecondQuantizedDMETFragment(mol_frag, mf_fragment, fock, fock_frag_copy, t_list, one_ele, two_ele) + dummy_mol = SecondQuantizedDMETFragment(mol_frag, mf_fragment, fock, fock_frag_copy, t_list, one_ele, two_ele, self.uhf) if self.verbose: print("\t\tFragment Number : # ", i + 1) @@ -449,29 +473,34 @@ def _oneshot_loop(self, chemical_potential, save_results=False, resample=False, solver_fragment.build() solver_fragment.simulate() - if purify and solver_fragment.molecule.n_active_electrons == 2: + if purify and solver_fragment.molecule.n_active_electrons == 2 and not self.uhf: onerdm, twordm = solver_fragment.get_rdm(solver_fragment.optimal_var_params, resample=resample, sum_spin=False) onerdm, twordm = mcweeny_purify_2rdm(twordm) + elif self.uhf: + onerdm, twordm = solver_fragment.get_rdm_uhf(solver_fragment.optimal_var_params, resample=resample) else: onerdm, twordm = solver_fragment.get_rdm(solver_fragment.optimal_var_params, resample=resample) if save_results: self.solver_fragment_dict[i] = solver_fragment self.rdm_measurements[i] = self.solver_fragment_dict[i].rdm_freq_dict - fragment_energy, _, one_rdm = self._compute_energy(mf_fragment, onerdm, twordm, - fock_frag_copy, t_list, one_ele, - two_ele, fock) + # Compute the fragment energy and sum up the number of electrons + if self.uhf: + fragment_energy, _, one_rdm_alpha, one_rdm_beta = self._compute_energy_unrestricted( + mf_fragment, onerdm, twordm, fock_frag_copy, t_list, one_ele, two_ele, fock) + n_electron_frag = np.trace(one_rdm_alpha[ : t_list[0], : t_list[0]]) + np.trace(one_rdm_beta[ : t_list[0], : t_list[0]]) + else: + fragment_energy, _, one_rdm = self._compute_energy_restricted(mf_fragment, onerdm, twordm, + fock_frag_copy, t_list, one_ele, two_ele, fock) + n_electron_frag = np.trace(one_rdm[: t_list[0], : t_list[0]]) + + number_of_electron += n_electron_frag # Sum up the energy. energy_temp += fragment_energy - # Sum up the number of electrons. - number_of_electron += np.trace(one_rdm[: t_list[0], : t_list[0]]) - if self.verbose: - print("\t\tFragment Energy = " + "{:17.10f}".format(fragment_energy)) - print("\t\tNumber of Electrons in Fragment = " + "{:17.10f}".format(np.trace(one_rdm))) - print("") + print(f"\t\tFragment Energy = {fragment_energy}\n\t\tNumber of Electrons in Fragment = {n_electron_frag}") energy_temp += self.orbitals.core_constant_energy self.dmet_energy = energy_temp.real @@ -500,7 +529,7 @@ def get_resources(self): # Unpacking the information for the selected fragment. mf_fragment, fock_frag_copy, mol_frag, t_list, one_ele, two_ele, fock = info_fragment - dummy_mol = SecondQuantizedDMETFragment(mol_frag, mf_fragment, fock, fock_frag_copy, t_list, one_ele, two_ele) + dummy_mol = SecondQuantizedDMETFragment(mol_frag, mf_fragment, fock, fock_frag_copy, t_list, one_ele, two_ele, self.uhf) # Buiding SCF fragments and quantum circuit. Resources are then # estimated. For classical sovlers, this functionality is not @@ -521,7 +550,7 @@ def get_resources(self): return resources_fragments - def _compute_energy(self, mf_frag, onerdm, twordm, fock_frag_copy, t_list, oneint, twoint, fock): + def _compute_energy_restricted(self, mf_frag, onerdm, twordm, fock_frag_copy, t_list, oneint, twoint, fock): """Calculate the fragment energy. Args: @@ -545,7 +574,7 @@ def _compute_energy(self, mf_frag, onerdm, twordm, fock_frag_copy, t_list, onein norb = t_list[0] # Calculate the one- and two- RDM for DMET energy calculation (Transform to AO basis) - one_rdm = reduce(np.dot, (mf_frag.mo_coeff, onerdm, mf_frag.mo_coeff.T)) + one_rdm = mf_frag.mo_coeff @ onerdm @ mf_frag.mo_coeff.T twordm = np.einsum("pi,ijkl->pjkl", mf_frag.mo_coeff, twordm) twordm = np.einsum("qj,pjkl->pqkl", mf_frag.mo_coeff, twordm) @@ -553,22 +582,107 @@ def _compute_energy(self, mf_frag, onerdm, twordm, fock_frag_copy, t_list, onein twordm = np.einsum("sl,pqrl->pqrs", mf_frag.mo_coeff, twordm) # Calculate the total energy based on RDMs - total_energy_rdm = np.einsum("ij,ij->", fock_frag_copy, one_rdm) + 0.5 * np.einsum("ijkl,ijkl->", twoint, - twordm) + total_energy_rdm = np.einsum("ij,ij->", fock_frag_copy, one_rdm) \ + + 0.5 * np.einsum("ijkl,ijkl->", twoint, twordm) # Calculate fragment expectation value - fragment_energy_one_rdm = 0.25 * np.einsum("ij,ij->", one_rdm[: norb, :], fock[: norb, :] + oneint[: norb, :]) \ - + 0.25 * np.einsum("ij,ij->", one_rdm[:, : norb], fock[:, : norb] + oneint[:, : norb]) + fragment_energy_one_rdm = np.einsum("ij,ij->", one_rdm[: norb, :], fock[: norb, :] + oneint[: norb, :]) \ + + np.einsum("ij,ij->", one_rdm[:, : norb], fock[:, : norb] + oneint[:, : norb]) + fragment_energy_one_rdm *= 0.25 - fragment_energy_twordm = 0.125 * np.einsum("ijkl,ijkl->", twordm[: norb, :, :, :], twoint[: norb, :, :, :]) \ - + 0.125 * np.einsum("ijkl,ijkl->", twordm[:, : norb, :, :], twoint[:, : norb, :, :]) \ - + 0.125 * np.einsum("ijkl,ijkl->", twordm[:, :, : norb, :], twoint[:, :, : norb, :]) \ - + 0.125 * np.einsum("ijkl,ijkl->", twordm[:, :, :, : norb], twoint[:, :, :, : norb]) + fragment_energy_two_rdm = np.einsum("ijkl,ijkl->", twordm[: norb, :, :, :], twoint[: norb, :, :, :]) \ + + np.einsum("ijkl,ijkl->", twordm[:, : norb, :, :], twoint[:, : norb, :, :]) \ + + np.einsum("ijkl,ijkl->", twordm[:, :, : norb, :], twoint[:, :, : norb, :]) \ + + np.einsum("ijkl,ijkl->", twordm[:, :, :, : norb], twoint[:, :, :, : norb]) + fragment_energy_two_rdm *= 0.125 - fragment_energy = fragment_energy_one_rdm + fragment_energy_twordm + fragment_energy = fragment_energy_one_rdm + fragment_energy_two_rdm return fragment_energy, total_energy_rdm, one_rdm + def _compute_energy_unrestricted(self, mf_frag, onerdm, twordm, fock_frag_copy, t_list, oneint, twoint, fock): + """Calculate the fragment energy. + + Args: + mf_frag (pyscf.scf.UHF): The mean field of the fragment. + onerdm (numpy.array): one-particle reduced density matrix (float64). + twordm (numpy.array): two-particle reduced density matrix (float64). + fock_frag_copy (numpy.array): Fock matrix with the chemical potential subtracted (float64). + t_list (list): List of number of fragment and bath orbitals (int). + oneint (numpy.array): One-electron integrals of fragment (float64). + twoint (numpy.array): Two-electron integrals of fragment (float64). + fock (numpy.array): Fock matrix of fragment (float64). + + Returns: + float64: Fragment energy (fragment_energy). + float64: Total energy for fragment using RDMs (total_energy_rdm). + numpy.array: One-particle (alpha) RDM for a fragment (onerdm_a, float64). + numpy.array: One-particle (beta) RDM for a fragment (onerdm_b, float64). + """ + + # Execute CCSD calculation + norb = t_list[0] + + # Calculate the one- and two- RDM for DMET energy calculation (Transform to AO basis) + one_rdm_a, one_rdm_b = onerdm + + onerdm_a = mf_frag.mo_coeff[0] @ one_rdm_a @ mf_frag.mo_coeff[0].T + onerdm_b = mf_frag.mo_coeff[1] @ one_rdm_b @ mf_frag.mo_coeff[1].T + + two_rdm_aa, two_rdm_ab, two_rdm_bb = twordm + + twordm_aa = np.einsum("pi,ijkl->pjkl", mf_frag.mo_coeff[0], two_rdm_aa) + twordm_aa = np.einsum("qj,pjkl->pqkl", mf_frag.mo_coeff[0], twordm_aa) + twordm_aa = np.einsum("rk,pqkl->pqrl", mf_frag.mo_coeff[0], twordm_aa) + twordm_aa = np.einsum("sl,pqrl->pqrs", mf_frag.mo_coeff[0], twordm_aa) + + twordm_ab = np.einsum("pi,ijkl->pjkl", mf_frag.mo_coeff[0], two_rdm_ab) + twordm_ab = np.einsum("qj,pjkl->pqkl", mf_frag.mo_coeff[0], twordm_ab) + twordm_ab = np.einsum("rk,pqkl->pqrl", mf_frag.mo_coeff[1], twordm_ab) + twordm_ab = np.einsum("sl,pqrl->pqrs", mf_frag.mo_coeff[1], twordm_ab) + + twordm_bb = np.einsum("pi,ijkl->pjkl", mf_frag.mo_coeff[1], two_rdm_bb) + twordm_bb = np.einsum("qj,pjkl->pqkl", mf_frag.mo_coeff[1], twordm_bb) + twordm_bb = np.einsum("rk,pqkl->pqrl", mf_frag.mo_coeff[1], twordm_bb) + twordm_bb = np.einsum("sl,pqrl->pqrs", mf_frag.mo_coeff[1], twordm_bb) + + # Calculate the total energy based on RDMs + total_energy_rdm = np.einsum("ij,ij->", fock_frag_copy, onerdm_a) \ + + np.einsum("ij,ij->", fock_frag_copy, onerdm_b) \ + + 0.5 * np.einsum("ijkl,ijkl->", twoint, twordm_aa) \ + + 0.5 * np.einsum("ijkl,ijkl->", twoint, twordm_ab) \ + + 0.5 * np.einsum("ijkl,klij->", twoint, twordm_ab) \ + + 0.5 * np.einsum("ijkl,ijkl->", twoint, twordm_bb) + + # Calculate fragment expectation value + fragment_energy_one = np.einsum("ij,ij->", onerdm_a[: norb, :], fock[: norb, :] + oneint[: norb, :]) \ + + np.einsum("ij,ij->", onerdm_a[:, : norb ], fock[:, : norb] + oneint[:, : norb]) \ + + np.einsum("ij,ij->", onerdm_b[: norb, :], fock[: norb, :] + oneint[: norb, :]) \ + + np.einsum("ij,ij->", onerdm_b[:, : norb ], fock[:, : norb] + oneint[:, : norb]) + fragment_energy_one *= 0.25 + + fragment_energy_two = np.einsum("ijkl,ijkl->", twordm_aa[: norb, :, :, :], twoint[: norb, :, :, :]) \ + + np.einsum("ijkl,ijkl->", twordm_aa[:, : norb, :, :], twoint[:, : norb, :, :]) \ + + np.einsum("ijkl,ijkl->", twordm_aa[:, :, : norb, :], twoint[:, :, : norb, :]) \ + + np.einsum("ijkl,ijkl->", twordm_aa[:, :, :, : norb], twoint[:, :, :, : norb]) \ + + np.einsum("ijkl,ijkl->", twordm_ab[: norb, :, :, :], twoint[: norb, :, :, :]) \ + + np.einsum("ijkl,ijkl->", twordm_ab[:, : norb, :, :], twoint[:, : norb, :, :]) \ + + np.einsum("ijkl,ijkl->", twordm_ab[:, :, : norb, :], twoint[:, :, : norb, :]) \ + + np.einsum("ijkl,ijkl->", twordm_ab[:, :, :, : norb], twoint[:, :, :, : norb]) \ + + np.einsum("klij,ijkl->", twordm_ab[:, :, : norb, :], twoint[: norb, :, :, :]) \ + + np.einsum("klij,ijkl->", twordm_ab[:, :, :, : norb], twoint[:, : norb, :, :]) \ + + np.einsum("klij,ijkl->", twordm_ab[: norb, :, :, :], twoint[:, :, : norb, :]) \ + + np.einsum("klij,ijkl->", twordm_ab[:, : norb, :, :], twoint[:, :, :, : norb]) \ + + np.einsum("ijkl,ijkl->", twordm_bb[: norb, :, :, :], twoint[: norb, :, :, :]) \ + + np.einsum("ijkl,ijkl->", twordm_bb[:, : norb, :, :], twoint[:, : norb, :, :]) \ + + np.einsum("ijkl,ijkl->", twordm_bb[:, :, : norb, :], twoint[:, :, : norb, :]) \ + + np.einsum("ijkl,ijkl->", twordm_bb[:, :, :, : norb], twoint[:, :, :, : norb]) + fragment_energy_two *= 0.125 + + fragment_energy = fragment_energy_one + fragment_energy_two + + return fragment_energy, total_energy_rdm, onerdm_a, onerdm_b + def _default_optimizer(self, func, var_params): """Function used as a default optimizer for DMET when user does not provide one. diff --git a/tangelo/problem_decomposition/dmet/fragment.py b/tangelo/problem_decomposition/dmet/fragment.py index 34df6a36d..a9f3e6bd1 100644 --- a/tangelo/problem_decomposition/dmet/fragment.py +++ b/tangelo/problem_decomposition/dmet/fragment.py @@ -15,12 +15,17 @@ """Module for data structure for DMET fragments.""" from dataclasses import dataclass, field +from itertools import product + + import openfermion +from openfermion.chem.molecular_data import spinorb_from_spatial +import openfermion.ops.representations as reps +from openfermion.utils import down_index, up_index import numpy as np import pyscf from pyscf import ao2mo -from tangelo.toolboxes.operators import FermionOperator from tangelo.toolboxes.qubit_mappings.mapping_transform import get_fermion_operator @@ -41,6 +46,8 @@ class SecondQuantizedDMETFragment: one_ele: np.array two_ele: np.array + uhf: bool + n_active_electrons: int = field(init=False) n_active_sos: int = field(init=False) q: int = field(init=False) @@ -48,53 +55,144 @@ class SecondQuantizedDMETFragment: basis: str = field(init=False) n_active_mos: int = field(init=False) - fermionic_hamiltonian: FermionOperator = field(init=False, repr=False) frozen_mos: None = field(init=False) def __post_init__(self): self.n_active_electrons = self.molecule.nelectron + self.n_active_ab_electrons = self.mean_field.nelec if self.uhf else self.n_active_electrons self.q = self.molecule.charge self.spin = self.molecule.spin self.active_spin = self.spin self.basis = self.molecule.basis - self.n_active_mos = len(self.mean_field.mo_energy) - self.n_active_sos = 2*self.n_active_mos + self.n_active_mos = len(self.mean_field.mo_energy) if not self.uhf else (len(self.mean_field.mo_energy[0]), len(self.mean_field.mo_energy[1])) + self.n_active_sos = 2*self.n_active_mos if not self.uhf else max(2*self.n_active_mos[0], 2*self.n_active_mos[1]) - self.fermionic_hamiltonian = self._get_fermionic_hamiltonian() self.frozen_mos = None - self.uhf = False - def _get_fermionic_hamiltonian(self): - """This method returns the fermionic hamiltonian. It written to take - into account calls for this function is without argument, and attributes - are parsed into it. + @property + def fermionic_hamiltonian(self): + if self.uhf: + return self._fermionic_hamiltonian_unrestricted() + return self._fermionic_hamiltonian_restricted() + + def _fermionic_hamiltonian_restricted(self): + """Computes the restricted Fermionic Hamiltonian, using the fragment + attributes. + + Returns: + FermionOperator: Self-explanatory. + """ + mo_coeff = self.mean_field.mo_coeff + + # Corresponding to nuclear repulsion energy and static coulomb energy. + core_constant = float(self.mean_field.mol.energy_nuc()) + + # get_hcore is equivalent to int1e_kin + int1e_nuc. + one_electron_integrals = mo_coeff.T @ self.mean_field.get_hcore() @ mo_coeff + + # Getting 2-body integrals in atomic and converting to molecular basis. + two_electron_integrals = ao2mo.kernel(self.mean_field._eri, mo_coeff) + two_electron_integrals = ao2mo.restore(1, two_electron_integrals, len(mo_coeff)) + + # PQRS convention in openfermion: + # h[p,q]=\int \phi_p(x)* (T + V_{ext}) \phi_q(x) dx + # h[p,q,r,s]=\int \phi_p(x)* \phi_q(y)* V_{elec-elec} \phi_r(y) \phi_s(x) dxdy + # The convention is not the same with PySCF integrals. So, a change is + # made before performing the truncation for frozen orbitals. + two_electron_integrals = two_electron_integrals.transpose(0, 2, 3, 1) + + one_body_coefficients, two_body_coefficients = spinorb_from_spatial(one_electron_integrals, two_electron_integrals) + fragment_hamiltonian = reps.InteractionOperator(core_constant, one_body_coefficients, 0.5 * two_body_coefficients) + + return get_fermion_operator(fragment_hamiltonian) + + def _fermionic_hamiltonian_unrestricted(self): + """Computes the unrestricted Fermionic Hamiltonian, using the fragment + attributes. Returns: FermionOperator: Self-explanatory. """ + mo_coeff = self.mean_field.mo_coeff + + # Molecular and atomic orbitals + nao, nmo = mo_coeff[0].shape + + # Obtain Hcore Hamiltonian in atomic orbitals basis + hcore = self.mean_field.get_hcore() + + # Obtain two-electron integral in atomic basis + eri = ao2mo.restore(8, self.mean_field._eri, nao) + + # Create the placeholder for the matrices one-electron matrix (alpha, + # beta) + hpq = [] + + # Do the MO transformation step the mo coeff alpha and beta + mo_a, mo_b = mo_coeff + + # MO transform the hcore + hpq.append(mo_a.T.dot(hcore).dot(mo_a)) + hpq.append(mo_b.T.dot(hcore).dot(mo_b)) + + # MO transform the two-electron integrals + eri_a = ao2mo.incore.full(eri, mo_a) + eri_b = ao2mo.incore.full(eri, mo_b) + eri_ba = ao2mo.incore.general(eri, (mo_a, mo_a, mo_b, mo_b), compact=False) + + # Change the format of integrals (full) + eri_a = ao2mo.restore(1, eri_a, nmo) + eri_b = ao2mo.restore(1, eri_b, nmo) + eri_ba = eri_ba.reshape(nmo, nmo, nmo, nmo) + + # Convert this into the order OpenFemion like to receive + two_body_integrals_a = np.asarray(eri_a.transpose(0, 2, 3, 1), order='C') + two_body_integrals_b = np.asarray(eri_b.transpose(0, 2, 3, 1), order='C') + two_body_integrals_ab = np.asarray(eri_ba.transpose(0, 2, 3, 1), order='C') + + # Corresponding to nuclear repulsion energy and static coulomb energy. + core_constant = float(self.mean_field.mol.energy_nuc()) + + one_body_integrals = hpq + two_body_integrals = (two_body_integrals_a, two_body_integrals_ab, two_body_integrals_b) + + # Lets find the dimensions + n_orb_a = one_body_integrals[0].shape[0] + n_orb_b = one_body_integrals[1].shape[0] - dummy_of_molecule = openfermion.MolecularData([["C", (0., 0., 0.)]], "sto-3g", self.spin+1, self.q) + n_qubits = 2*max(n_orb_a, n_orb_b) - # Overwrting nuclear repulsion term. - dummy_of_molecule.nuclear_repulsion = self.mean_field.mol.energy_nuc() + # Initialize Hamiltonian coefficients. + one_body_coefficients = np.zeros((n_qubits,) * 2) + two_body_coefficients = np.zeros((n_qubits,) * 4) - canonical_orbitals = self.mean_field.mo_coeff - h_core = self.mean_field.get_hcore() - n_orbitals = len(self.mean_field.mo_energy) + # aa + for p, q in product(range(n_orb_a), repeat=2): + pi, qi = up_index(p), up_index(q) + # Populate 1-body coefficients. Require p and q have same spin. + one_body_coefficients[pi, qi] = one_body_integrals[0][p, q] + for r, s in product(range(n_orb_a), repeat=2): + two_body_coefficients[pi, qi, up_index(r), up_index(s)] = (two_body_integrals[0][p, q, r, s] / 2.) - # Overwriting 1-electron integrals. - dummy_of_molecule._one_body_integrals = canonical_orbitals.T @ h_core @ canonical_orbitals + # bb + for p, q in product(range(n_orb_b), repeat=2): + pi, qi = down_index(p), down_index(q) + # Populate 1-body coefficients. Require p and q have same spin. + one_body_coefficients[pi, qi] = one_body_integrals[1][p, q] + for r, s in product(range(n_orb_b), repeat=2): + two_body_coefficients[pi, qi, down_index(r), down_index(s)] = (two_body_integrals[2][p, q, r, s] / 2.) - twoint = self.mean_field._eri - eri = ao2mo.restore(8, twoint, n_orbitals) - eri = ao2mo.incore.full(eri, canonical_orbitals) - eri = ao2mo.restore(1, eri, n_orbitals) + # abba + for p, q, r, s in product(range(n_orb_a), range(n_orb_b), range(n_orb_b), range(n_orb_a)): + two_body_coefficients[up_index(p), down_index(q), down_index(r), up_index(s)] = (two_body_integrals[1][p, q, r, s] / 2.) - # Overwriting 2-electrons integrals. - dummy_of_molecule._two_body_integrals = np.asarray(eri.transpose(0, 2, 3, 1), order="C") + # baab + for p, q, r, s in product(range(n_orb_b), range(n_orb_a), range(n_orb_a), range(n_orb_b)): + two_body_coefficients[down_index(p), up_index(q), up_index(r), down_index(s)] = (two_body_integrals[1][q, p, s, r] / 2.) - fragment_hamiltonian = dummy_of_molecule.get_molecular_hamiltonian() + # Cast to InteractionOperator class and return. + fragment_hamiltonian = openfermion.InteractionOperator(core_constant, one_body_coefficients, two_body_coefficients) return get_fermion_operator(fragment_hamiltonian) diff --git a/tangelo/problem_decomposition/electron_localization/__init__.py b/tangelo/problem_decomposition/electron_localization/__init__.py index c71c6fd54..0bfb4fc4a 100644 --- a/tangelo/problem_decomposition/electron_localization/__init__.py +++ b/tangelo/problem_decomposition/electron_localization/__init__.py @@ -14,3 +14,4 @@ from .iao_localization import iao_localization from .meta_lowdin_localization import meta_lowdin_localization +from .nao_localization import nao_localization diff --git a/tangelo/problem_decomposition/electron_localization/nao_localization.py b/tangelo/problem_decomposition/electron_localization/nao_localization.py new file mode 100644 index 000000000..e03642d6e --- /dev/null +++ b/tangelo/problem_decomposition/electron_localization/nao_localization.py @@ -0,0 +1,38 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Perform NAO localization. + +The orbital localization of the canonical orbitals using Natural Atomic Orbital +localization is done here. `pyscf.lo` is used. + +For details, refer to: + - Alan E. Reed, Robert B. Weinstock, and Frank Weinhold. + Natural population analysis. J. Chem. Phys., 83(2):735-746, 1985. +""" + +from pyscf.lo import orth + + +def nao_localization(mol, mf): + """Localize the orbitals using NAO localization. + + Args: + mol (pyscf.gto.Mole): The molecule to simulate. + mf (pyscf.scf): The mean field of the molecule. + + Returns: + numpy.array: The localized orbitals (float64). + """ + return orth.orth_ao(mf, "NAO") diff --git a/tangelo/problem_decomposition/tests/dmet/test_dmet.py b/tangelo/problem_decomposition/tests/dmet/test_dmet.py index 7e8cc9ec3..2db1773ca 100644 --- a/tangelo/problem_decomposition/tests/dmet/test_dmet.py +++ b/tangelo/problem_decomposition/tests/dmet/test_dmet.py @@ -201,7 +201,7 @@ def test_retrieving_quantum_data(self): "fragment_atoms": [1]*10, "fragment_solvers": ["vqe"] + ["ccsd"]*9, "electron_localization": Localization.meta_lowdin, - "verbose": True, + "verbose": False, "solvers_options": [{"qubit_mapping": "scBK", "initial_var_params": "ones", "up_then_down": True, diff --git a/tangelo/problem_decomposition/tests/dmet/test_dmet_oneshot_loop.py b/tangelo/problem_decomposition/tests/dmet/test_dmet_oneshot_loop.py index 6eb5f146e..747f8df2c 100644 --- a/tangelo/problem_decomposition/tests/dmet/test_dmet_oneshot_loop.py +++ b/tangelo/problem_decomposition/tests/dmet/test_dmet_oneshot_loop.py @@ -20,10 +20,10 @@ import numpy as np from tangelo.problem_decomposition.dmet._helpers.dmet_orbitals import dmet_orbitals -from tangelo.problem_decomposition.dmet._helpers.dmet_onerdm import dmet_low_rdm, dmet_fragment_rdm +from tangelo.problem_decomposition.dmet._helpers.dmet_onerdm import dmet_low_rdm_rhf, dmet_fragment_rdm from tangelo.problem_decomposition.dmet._helpers.dmet_bath import dmet_fragment_bath -from tangelo.problem_decomposition.dmet._helpers.dmet_scf_guess import dmet_fragment_guess -from tangelo.problem_decomposition.dmet._helpers.dmet_scf import dmet_fragment_scf +from tangelo.problem_decomposition.dmet._helpers.dmet_scf_guess import dmet_fragment_guess_rhf +from tangelo.problem_decomposition.dmet._helpers.dmet_scf import dmet_fragment_scf_rhf from tangelo.problem_decomposition.electron_localization import iao_localization path_file = os.path.dirname(os.path.abspath(__file__)) @@ -74,10 +74,10 @@ def test_dmet_functions(self): mf = scf.RHF(mol) mf.scf() - dmet_orbs = dmet_orbitals(mol, mf, range(mol.nao_nr()), iao_localization) + dmet_orbs = dmet_orbitals(mol, mf, range(mol.nao_nr()), iao_localization, False) # Test the construction of one particle RDM from low-level calculation - onerdm_low = dmet_low_rdm(dmet_orbs.active_fock, dmet_orbs.number_active_electrons) + onerdm_low = dmet_low_rdm_rhf(dmet_orbs.active_fock, dmet_orbs.number_active_electrons) onerdm_low_ref = np.loadtxt("{}/data/test_dmet_oneshot_loop_low_rdm.txt".format(path_file)) for index, value_ref in np.ndenumerate(onerdm_low_ref): self.assertAlmostEqual(value_ref, onerdm_low[index], msg=f"Low-level RDM error at index {str(index)}", @@ -101,10 +101,10 @@ def test_dmet_functions(self): one_ele, fock, two_ele = dmet_orbs.dmet_fragment_hamiltonian(bath_orb, norb_high, onerdm_high) # Test the construction of the guess orbitals for fragment SCF calculation - guess_orbitals = dmet_fragment_guess(t_list, bath_orb, chemical_potential, norb_high, nelec_high, dmet_orbs.active_fock) + guess_orbitals = dmet_fragment_guess_rhf(t_list, bath_orb, chemical_potential, norb_high, nelec_high, dmet_orbs.active_fock) # Test the fock matrix in the SCF calculation for a fragment - mf_fragments, fock_frag_copy, mol = dmet_fragment_scf(t_list, two_ele, fock, nelec_high, norb_high, guess_orbitals, chemical_potential) + mf_fragments, fock_frag_copy, mol = dmet_fragment_scf_rhf(t_list, two_ele, fock, nelec_high, norb_high, guess_orbitals, chemical_potential) # Test the energy calculation and construction of the one-particle RDM from the CC calculation for a fragment # fragment_energy, onerdm_frag, _, _ = dmet_fragment_cc_classical(mf_fragments, fock_frag_copy, t_list, one_ele, two_ele, fock) diff --git a/tangelo/problem_decomposition/tests/dmet/test_dmet_orbitals.py b/tangelo/problem_decomposition/tests/dmet/test_dmet_orbitals.py index e693dc85f..a7656644a 100644 --- a/tangelo/problem_decomposition/tests/dmet/test_dmet_orbitals.py +++ b/tangelo/problem_decomposition/tests/dmet/test_dmet_orbitals.py @@ -69,7 +69,7 @@ def test_orbital_construction(self): mf = scf.RHF(mol) mf.scf() - dmet_orbs = dmet_orbitals(mol, mf, range(mol.nao_nr()), iao_localization) + dmet_orbs = dmet_orbitals(mol, mf, range(mol.nao_nr()), iao_localization, False) dmet_orbitals_ref = np.loadtxt("{}/data/test_dmet_orbitals.txt".format(path_file)) # Test the construction of IAOs diff --git a/tangelo/problem_decomposition/tests/dmet/test_osdmet.py b/tangelo/problem_decomposition/tests/dmet/test_osdmet.py new file mode 100644 index 000000000..36fa1bcd7 --- /dev/null +++ b/tangelo/problem_decomposition/tests/dmet/test_osdmet.py @@ -0,0 +1,72 @@ +# Copyright 2021 Good Chemistry Company. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from tangelo import SecondQuantizedMolecule +from tangelo.problem_decomposition import DMETProblemDecomposition +from tangelo.problem_decomposition.dmet import Localization + +LiO2 = """ + Li 0.000000 0.000000 1.380605 + O 0.000000 0.676045 -0.258863 + O 0.000000 -0.676045 -0.258863 +""" + + +class OSDMETProblemDecompositionTest(unittest.TestCase): + + def test_lio2_sto6g_rohf(self): + """Tests the result from OS-DMET (ROHF) against a value from a reference + implementation with nao localization and CCSD solution to fragments. + """ + + mol_lio2 = SecondQuantizedMolecule(LiO2, q=0, spin=1, basis="STO-6G", frozen_orbitals=None, uhf=False) + + opt_dmet = {"molecule": mol_lio2, + "fragment_atoms": [1, 1, 1], + "fragment_solvers": "ccsd", + "electron_localization": Localization.nao, + "verbose": False + } + + dmet_solver = DMETProblemDecomposition(opt_dmet) + dmet_solver.build() + energy = dmet_solver.simulate() + + self.assertAlmostEqual(energy, -156.6317605935, places=4) + + def test_lio2_sto6g_uhf(self): + """Tests the result from OS-DMET (UHF) against a value from a reference + implementation with nao localization and CCSD solution to fragments. + """ + + mol_lio2 = SecondQuantizedMolecule(LiO2, q=0, spin=1, basis="STO-6G", frozen_orbitals=None, uhf=True) + + opt_dmet = {"molecule": mol_lio2, + "fragment_atoms": [1, 1, 1], + "fragment_solvers": "ccsd", + "electron_localization": Localization.nao, + "verbose": False + } + + dmet_solver = DMETProblemDecomposition(opt_dmet) + dmet_solver.build() + energy = dmet_solver.simulate() + + self.assertAlmostEqual(energy, -156.6243118102, places=4) + + +if __name__ == "__main__": + unittest.main() From 9b9b201f90323cda7776f6370f1013e63f502c3f Mon Sep 17 00:00:00 2001 From: KrzysztofB-1qbit <86750444+KrzysztofB-1qbit@users.noreply.github.com> Date: Wed, 4 Jan 2023 23:03:30 -0800 Subject: [PATCH 14/14] Save mid-circuit measurement (#256) * A flag now allows users to save mid-circuit measurements for each shot run. Co-authored-by: Valentin Senicourt --- examples/adapt.ipynb | 12 +-- tangelo/linq/target/backend.py | 101 +++++++++++------- tangelo/linq/target/target_cirq.py | 79 +++++++++++--- tangelo/linq/target/target_qdk.py | 26 +++-- tangelo/linq/target/target_qiskit.py | 35 ++++-- tangelo/linq/target/target_qulacs.py | 62 ++++++++--- tangelo/linq/tests/test_simulator.py | 16 ++- tangelo/linq/tests/test_simulator_noisy.py | 21 +++- tangelo/linq/translator/translate_cirq.py | 14 ++- tangelo/linq/translator/translate_qdk.py | 17 ++- tangelo/linq/translator/translate_qiskit.py | 16 ++- tangelo/linq/translator/translate_qulacs.py | 17 ++- .../post_processing/post_selection.py | 29 ++++- 13 files changed, 336 insertions(+), 109 deletions(-) diff --git a/examples/adapt.ipynb b/examples/adapt.ipynb index 1f4738f5c..fa5aece32 100755 --- a/examples/adapt.ipynb +++ b/examples/adapt.ipynb @@ -66,14 +66,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "/home/alex/Codes/Tangelo/tangelo/algorithms/variational/vqe_solver.py:260: RuntimeWarning: No variational gate found in the circuit.\n", + "/Users/krzysztofbieniasz/Code/env/lib/python3.9/site-packages/tangelo/algorithms/variational/vqe_solver.py:260: RuntimeWarning: No variational gate found in the circuit.\n", " warnings.warn(\"No variational gate found in the circuit.\", RuntimeWarning)\n" ] }, { "data": { "text/plain": [ - "-2.028211284185079" + "-2.0282112841580124" ] }, "execution_count": 2, @@ -117,7 +117,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -681,9 +681,9 @@ "hash": "fd77f6ebaf3d18999f00320d0aca64091b39e7847b653c69719c9ddc4e72c63f" }, "kernelspec": { - "display_name": "qsdk", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "qsdk" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -695,7 +695,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.9" + "version": "3.9.10" } }, "nbformat": 4, diff --git a/tangelo/linq/target/backend.py b/tangelo/linq/target/backend.py index aae2619a1..e99d7216e 100644 --- a/tangelo/linq/target/backend.py +++ b/tangelo/linq/target/backend.py @@ -48,9 +48,9 @@ def get_expectation_value_from_frequencies_oneterm(term, frequencies): the result of a state-preparation. Args: - term(openfermion-style QubitOperator object): a qubit operator, with + term (openfermion-style QubitOperator object): a qubit operator, with only a single term. - frequencies(dict): histogram of frequencies of measurements (assumed + frequencies (dict): histogram of frequencies of measurements (assumed to be in lsq-first format). Returns: @@ -82,9 +82,9 @@ def get_variance_from_frequencies_oneterm(term, frequencies): """Return the variance of the expectation value of a single-term qubit-operator, given the result of a state-preparation. Args: - term(openfermion-style QubitOperator object): a qubit operator, with + term (openfermion-style QubitOperator object): a qubit operator, with only a single term. - frequencies(dict): histogram of frequencies of measurements (assumed + frequencies (dict): histogram of frequencies of measurements (assumed to be in lsq-first format). Returns: complex: The variance of this operator with regard to the @@ -153,12 +153,19 @@ def simulate_circuit(self): equivalent gates. Args: - source_circuit: a circuit in the abstract format to be translated + source_circuit (Circuit): a circuit in the abstract format to be translated for the target backend. - return_statevector(bool): option to return the statevector as well, + return_statevector (bool): option to return the statevector as well, if available. - initial_statevector(list/array) : A valid statevector in the format + initial_statevector (list/array) : A valid statevector in the format supported by the target backend. + save_mid_circuit_meas (bool): Save mid-circuit measurement results to + self.mid_circuit_meas_freqs. All measurements will be saved to + self.all_frequencies, with keys of length (n_meas + n_qubits). + The leading n_meas values will hold the results of the MEASURE gates, + ordered by their appearance in the source_circuit. + The last n_qubits values will hold the measurements performed on + each of qubits at the end of the circuit. Returns: dict: A dictionary mapping multi-qubit states to their corresponding @@ -168,7 +175,7 @@ def simulate_circuit(self): """ pass - def simulate(self, source_circuit, return_statevector=False, initial_statevector=None): + def simulate(self, source_circuit, return_statevector=False, initial_statevector=None, save_mid_circuit_meas=False): """Perform state preparation corresponding to the input circuit on the target backend, return the frequencies of the different observables, and either the statevector or None depending on the availability of the @@ -178,12 +185,19 @@ def simulate(self, source_circuit, return_statevector=False, initial_statevector equivalent gates. Args: - source_circuit: a circuit in the abstract format to be translated + source_circuit (Circuit): a circuit in the abstract format to be translated for the target backend. - return_statevector(bool): option to return the statevector as well, + return_statevector (bool): option to return the statevector as well, if available. - initial_statevector(list/array) : A valid statevector in the format + initial_statevector (list/array) : A valid statevector in the format supported by the target backend. + save_mid_circuit_meas (bool): Save mid-circuit measurement results to + self.mid_circuit_meas_freqs. All measurements will be saved to + self.all_frequencies, with keys of length (n_meas + n_qubits). + The leading n_meas values will hold the results of the MEASURE gates, + ordered by their appearance in the source_circuit. + The last n_qubits values will hold the measurements performed on + each of qubits at the end of the circuit. Returns: dict: A dictionary mapping multi-qubit states to their corresponding @@ -191,7 +205,6 @@ def simulate(self, source_circuit, return_statevector=False, initial_statevector numpy.array: The statevector, if available for the target backend and requested by the user (if not, set to None). """ - if source_circuit.is_mixed_state and not self.n_shots: raise ValueError("Circuit contains MEASURE instruction, and is assumed to prepare a mixed state." "Please set the n_shots attribute to an appropriate value.") @@ -211,7 +224,22 @@ def simulate(self, source_circuit, return_statevector=False, initial_statevector statevector[0] = 1.0 return (frequencies, statevector) if return_statevector else (frequencies, None) - return self.simulate_circuit(source_circuit, return_statevector=return_statevector, initial_statevector=initial_statevector) + if save_mid_circuit_meas: + # TODO: refactor to break a circular import. May involve by relocating get_xxx_oneterm functions + from tangelo.toolboxes.post_processing.post_selection import split_frequency_dict + + (all_frequencies, statevector) = self.simulate_circuit(source_circuit, + return_statevector=return_statevector, + initial_statevector=initial_statevector, + save_mid_circuit_meas=save_mid_circuit_meas) + n_meas = source_circuit.counts.get("MEASURE", 0) + self.mid_circuit_meas_freqs, frequencies = split_frequency_dict(all_frequencies, list(range(n_meas))) + return (frequencies, statevector) + + return self.simulate_circuit(source_circuit, + return_statevector=return_statevector, + initial_statevector=initial_statevector, + save_mid_circuit_meas=save_mid_circuit_meas) def get_expectation_value(self, qubit_operator, state_prep_circuit, initial_statevector=None): r"""Take as input a qubit operator H and a quantum circuit preparing a @@ -225,9 +253,10 @@ def get_expectation_value(self, qubit_operator, state_prep_circuit, initial_stat actual QPU. Args: - qubit_operator(openfermion-style QubitOperator class): a qubit + qubit_operator (openfermion-style QubitOperator class): a qubit operator. - state_prep_circuit: an abstract circuit used for state preparation. + state_prep_circuit (Circuit): an abstract circuit used for state preparation. + initial_statevector (array): The initial statevector for the simulation Returns: complex: The expectation value of this operator with regards to the @@ -277,10 +306,10 @@ def get_variance(self, qubit_operator, state_prep_circuit, initial_statevector=N actual QPU. Args: - qubit_operator(openfermion-style QubitOperator class): a qubit + qubit_operator (openfermion-style QubitOperator class): a qubit operator. - state_prep_circuit: an abstract circuit used for state preparation. - initial_statevector(list/array) : A valid statevector in the format + state_prep_circuit (Circuit): an abstract circuit used for state preparation. + initial_statevector (list/array) : A valid statevector in the format supported by the target backend. Returns: @@ -328,10 +357,10 @@ def get_standard_error(self, qubit_operator, state_prep_circuit, initial_stateve actual QPU. Args: - qubit_operator(openfermion-style QubitOperator class): a qubit + qubit_operator (openfermion-style QubitOperator class): a qubit operator. - state_prep_circuit: an abstract circuit used for state preparation. - initial_statevector(list/array) : A valid statevector in the format + state_prep_circuit (Circuit): an abstract circuit used for state preparation. + initial_statevector (list/array): A valid statevector in the format supported by the target backend. Returns: @@ -348,10 +377,9 @@ def _get_expectation_value_from_statevector(self, qubit_operator, state_prep_cir this function directly, please call "get_expectation_value" instead. Args: - qubit_operator(openfermion-style QubitOperator class): a qubit - operator. - state_prep_circuit: an abstract circuit used for state preparation - (only pure states). + qubit_operator (openfermion-style QubitOperator class): a qubit operator. + state_prep_circuit (Circuit): an abstract circuit used for state preparation (only pure states). + initial_statevector (array): The initial state of the system Returns: complex: The expectation value of this operator with regards to the @@ -400,9 +428,9 @@ def _get_expectation_value_from_frequencies(self, qubit_operator, state_prep_cir using the frequencies of observable states. Args: - qubit_operator(openfermion-style QubitOperator class): a qubit - operator. - state_prep_circuit: an abstract circuit used for state preparation. + qubit_operator (openfermion-style QubitOperator class): a qubitoperator. + state_prep_circuit (Circuit): an abstract circuit used for state preparation. + initial_statevector (array): The initial state of the system Returns: complex: The expectation value of this operator with regard to the @@ -442,10 +470,9 @@ def _get_variance_from_frequencies(self, qubit_operator, state_prep_circuit, ini using the frequencies of observable states. Args: - qubit_operator(openfermion-style QubitOperator class): a qubit - operator. - state_prep_circuit: an abstract circuit used for state preparation. - initial_statevector(list/array) : A valid statevector in the format + qubit_operator (openfermion-style QubitOperator class): a qubit operator. + state_prep_circuit (Circuit): an abstract circuit used for state preparation. + initial_statevector (list/array) : A valid statevector in the format supported by the target backend. Returns: @@ -487,9 +514,9 @@ def get_expectation_value_from_frequencies_oneterm(term, frequencies): the result of a state-preparation. Args: - term(openfermion-style QubitOperator object): a qubit operator, with + term (openfermion-style QubitOperator object): a qubit operator, with only a single term. - frequencies(dict): histogram of frequencies of measurements (assumed + frequencies (dict): histogram of frequencies of measurements (assumed to be in lsq-first format). Returns: @@ -505,9 +532,9 @@ def get_variance_from_frequencies_oneterm(term, frequencies): the result of a state-preparation. Args: - term(openfermion-style QubitOperator object): a qubit operator, with + term (openfermion-style QubitOperator object): a qubit operator, with only a single term. - frequencies(dict): histogram of frequencies of measurements (assumed + frequencies (dict): histogram of frequencies of measurements (assumed to be in lsq-first format). Returns: @@ -525,7 +552,7 @@ def _statevector_to_frequencies(self, statevector): state |0>. Args: - statevector(list or ndarray(complex)): an iterable 1D data-structure + statevector (list or ndarray(complex)): an iterable 1D data-structure containing the amplitudes. Returns: diff --git a/tangelo/linq/target/target_cirq.py b/tangelo/linq/target/target_cirq.py index 3be677f84..08369795b 100644 --- a/tangelo/linq/target/target_cirq.py +++ b/tangelo/linq/target/target_cirq.py @@ -36,7 +36,7 @@ def __init__(self, n_shots=None, noise_model=None): super().__init__(n_shots=n_shots, noise_model=noise_model) self.cirq = cirq - def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, initial_statevector=None): + def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, initial_statevector=None, save_mid_circuit_meas=False): """Perform state preparation corresponding to the input circuit on the target backend, return the frequencies of the different observables, and either the statevector or None depending on the availability of the @@ -46,12 +46,19 @@ def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, in equivalent gates. Args: - source_circuit: a circuit in the abstract format to be translated + source_circuit (Circuit): a circuit in the abstract format to be translated for the target backend. - return_statevector(bool): option to return the statevector as well, + return_statevector (bool): option to return the statevector as well, if available. - initial_statevector(list/array) : A valid statevector in the format + initial_statevector (list/array) : A valid statevector in the format supported by the target backend. + save_mid_circuit_meas (bool): Save mid-circuit measurement results to + self.mid_circuit_meas_freqs. All measurements will be saved to + self.all_frequencies, with keys of length (n_meas + n_qubits). + The leading n_meas values will hold the results of the MEASURE gates, + ordered by their appearance in the source_circuit. + The last n_qubits values will hold the measurements performed on + each of qubits at the end of the circuit. Returns: dict: A dictionary mapping multi-qubit states to their corresponding @@ -59,12 +66,9 @@ def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, in numpy.array: The statevector, if available for the target backend and requested by the user (if not, set to None). """ - - translated_circuit = translate_c(source_circuit, "cirq", - output_options={"noise_model": self._noise_model}) - - if source_circuit.is_mixed_state or self._noise_model: - # Only DensityMatrixSimulator handles noise well, can use Simulator but it is slower + n_meas = source_circuit.counts.get("MEASURE", 0) + # Only DensityMatrixSimulator handles noise well, can use Simulator, but it is slower + if self._noise_model or (source_circuit.is_mixed_state and not save_mid_circuit_meas): cirq_simulator = self.cirq.DensityMatrixSimulator(dtype=np.complex128) else: cirq_simulator = self.cirq.Simulator(dtype=np.complex128) @@ -73,7 +77,9 @@ def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, in cirq_initial_statevector = initial_statevector if initial_statevector is not None else 0 # Calculate final density matrix and sample from that for noisy simulation or simulating mixed states - if self._noise_model or source_circuit.is_mixed_state: + if (self._noise_model or source_circuit.is_mixed_state) and not save_mid_circuit_meas: + translated_circuit = translate_c(source_circuit, "cirq", + output_options={"noise_model": self._noise_model, "save_measurements": save_mid_circuit_meas}) # cirq.dephase_measurements changes measurement gates to Krauss operators so simulators # can be called once and density matrix sampled repeatedly. translated_circuit = self.cirq.dephase_measurements(translated_circuit) @@ -82,10 +88,45 @@ def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, in indices = list(range(source_circuit.width)) isamples = self.cirq.sample_density_matrix(sim.final_density_matrix, indices, repetitions=self.n_shots) samples = [''.join([str(int(q))for q in isamples[i]]) for i in range(self.n_shots)] - frequencies = {k: v / self.n_shots for k, v in Counter(samples).items()} + # Noiseless simulation using the statevector simulator otherwise + # Run all shots at once and post-process to return measured frequencies on qubits only + elif save_mid_circuit_meas and not return_statevector: + translated_circuit = translate_c(source_circuit, "cirq", + output_options={"noise_model": self._noise_model, "save_measurements": True}) + qubit_list = self.cirq.LineQubit.range(source_circuit.width) + for i, qubit in enumerate(qubit_list): + translated_circuit.append(self.cirq.measure(qubit, key=str(i + n_meas))) + job_sim = cirq_simulator.run(translated_circuit, repetitions=self.n_shots) + samples = dict() + for j in range(self.n_shots): + bitstr = "".join([str(job_sim.measurements[str(i)][j, 0]) for i in range(n_meas + source_circuit.width)]) + samples[bitstr] = samples.get(bitstr, 0) + 1 + self.all_frequencies = {k: v / self.n_shots for k, v in samples.items()} + frequencies = self.all_frequencies + + # Run shot by shot and keep track of desired_meas_result only (generally slower) + elif save_mid_circuit_meas and return_statevector: + translated_circuit = translate_c(source_circuit, "cirq", + output_options={"noise_model": self._noise_model, "save_measurements": True}) + samples = dict() + self._current_state = None + indices = list(range(source_circuit.width)) + for _ in range(self.n_shots): + job_sim = cirq_simulator.simulate(translated_circuit, initial_state=cirq_initial_statevector) + measure = "".join([str(job_sim.measurements[str(i)][0]) for i in range(n_meas)]) + current_state = job_sim.final_density_matrix if self._noise_model else job_sim.final_state_vector + isamples = (self.cirq.sample_density_matrix(current_state, indices, repetitions=1) if self._noise_model + else self.cirq.sample_state_vector(current_state, indices, repetitions=1)) + sample = "".join([str(int(q)) for q in isamples[0]]) + bitstr = measure + sample + samples[bitstr] = samples.get(bitstr, 0) + 1 + self.all_frequencies = {k: v / self.n_shots for k, v in sample.items()} + frequencies = self.all_frequencies + else: + translated_circuit = translate_c(source_circuit, "cirq", output_options={"noise_model": self._noise_model}) job_sim = cirq_simulator.simulate(translated_circuit, initial_state=cirq_initial_statevector) self._current_state = job_sim.final_state_vector frequencies = self._statevector_to_frequencies(self._current_state) @@ -93,7 +134,19 @@ def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, in return (frequencies, np.array(self._current_state)) if return_statevector else (frequencies, None) def expectation_value_from_prepared_state(self, qubit_operator, n_qubits, prepared_state): + """ Compute an expectation value using a representation of the state (density matrix, state vector...) + using Cirq functionalities. + Args: + qubit_operator (QubitOperator): a qubit operator in tangelo format + n_qubits (int): the number of qubits the operator acts on + prepared_state (np.array): a numpy array encoding the state (can be a vector or a matrix) + + Returns: + float64 : the expectation value of the qubit operator w.r.t the input state + """ + + # Construct equivalent Pauli operator in Cirq format GATE_CIRQ = get_cirq_gates() qubit_labels = self.cirq.LineQubit.range(n_qubits) qubit_map = {q: i for i, q in enumerate(qubit_labels)} @@ -101,6 +154,8 @@ def expectation_value_from_prepared_state(self, qubit_operator, n_qubits, prepar for term, coef in qubit_operator.terms.items(): pauli_list = [GATE_CIRQ[pauli](qubit_labels[index]) for index, pauli in term] paulisum += self.cirq.PauliString(pauli_list, coefficient=coef) + + # Compute expectation value using Cirq's features if self._noise_model: exp_value = paulisum.expectation_from_density_matrix(prepared_state, qubit_map) else: diff --git a/tangelo/linq/target/target_qdk.py b/tangelo/linq/target/target_qdk.py index aac6d1b8f..91564e2e6 100644 --- a/tangelo/linq/target/target_qdk.py +++ b/tangelo/linq/target/target_qdk.py @@ -24,7 +24,7 @@ def __init__(self, n_shots=None, noise_model=None): super().__init__(n_shots=n_shots, noise_model=noise_model) self.qsharp = qsharp - def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, initial_statevector=None): + def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, initial_statevector=None, save_mid_circuit_meas=False): """Perform state preparation corresponding to the input circuit on the target backend, return the frequencies of the different observables, and either the statevector or None depending on the availability of the @@ -34,12 +34,19 @@ def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, in equivalent gates. Args: - source_circuit: a circuit in the abstract format to be translated + source_circuit (Circuit): a circuit in the abstract format to be translated for the target backend. - return_statevector(bool): option to return the statevector as well, + return_statevector (bool): option to return the statevector as well, if available. - initial_statevector(list/array) : A valid statevector in the format + initial_statevector (list/array) : A valid statevector in the format supported by the target backend. + save_mid_circuit_meas (bool): Save mid-circuit measurement results to + self.mid_circuit_meas_freqs. All measurements will be saved to + self.all_frequencies, with keys of length (n_meas + n_qubits). + The leading n_meas values will hold the results of the MEASURE gates, + ordered by their appearance in the source_circuit. + The last n_qubits values will hold the measurements performed on + each of qubits at the end of the circuit. Returns: dict: A dictionary mapping multi-qubit states to their corresponding @@ -47,21 +54,26 @@ def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, in numpy.array: The statevector, if available for the target backend and requested by the user (if not, set to None). """ - translated_circuit = translate_c(source_circuit, "qdk") + translated_circuit = translate_c(source_circuit, "qdk", + output_options={"save_measurements": save_mid_circuit_meas}) with open('tmp_circuit.qs', 'w+') as f_out: f_out.write(translated_circuit) + n_meas = source_circuit.counts.get("MEASURE", 0) + key_length = n_meas + source_circuit.width if save_mid_circuit_meas else source_circuit.width # Compile, import and call Q# operation to compute frequencies. Only import qsharp module if qdk is running # TODO: A try block to catch an exception at compile time, for Q#? Probably as an ImportError. self.qsharp.reload() from MyNamespace import EstimateFrequencies - frequencies_list = EstimateFrequencies.simulate(nQubits=source_circuit.width, nShots=self.n_shots) + frequencies_list = EstimateFrequencies.simulate(nQubits=key_length, nShots=self.n_shots) print("Q# frequency estimation with {0} shots: \n {1}".format(self.n_shots, frequencies_list)) # Convert Q# output to frequency dictionary, apply threshold frequencies = {bin(i).split('b')[-1]: freq for i, freq in enumerate(frequencies_list)} - frequencies = {("0"*(source_circuit.width-len(k))+k)[::-1]: v for k, v in frequencies.items() + frequencies = {("0" * (key_length - len(k)) + k)[::-1]: v for k, v in frequencies.items() if v > self.freq_threshold} + self.all_frequencies = frequencies.copy() + return (frequencies, None) @staticmethod diff --git a/tangelo/linq/target/target_qiskit.py b/tangelo/linq/target/target_qiskit.py index 7b09109b2..1dcd82a85 100644 --- a/tangelo/linq/target/target_qiskit.py +++ b/tangelo/linq/target/target_qiskit.py @@ -31,7 +31,7 @@ def __init__(self, n_shots=None, noise_model=None): self.qiskit = qiskit self.AerSimulator = AerSimulator - def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, initial_statevector=None): + def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, initial_statevector=None, save_mid_circuit_meas=False): """Perform state preparation corresponding to the input circuit on the target backend, return the frequencies of the different observables, and either the statevector or None depending on the availability of the @@ -41,12 +41,19 @@ def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, in equivalent gates. Args: - source_circuit: a circuit in the abstract format to be translated + source_circuit (Circuit): a circuit in the abstract format to be translated for the target backend. - return_statevector(bool): option to return the statevector as well, + return_statevector (bool): option to return the statevector as well, if available. - initial_statevector(list/array) : A valid statevector in the format + initial_statevector (list/array) : A valid statevector in the format supported by the target backend. + save_mid_circuit_meas (bool): Save mid-circuit measurement results to + self.mid_circuit_meas_freqs. All measurements will be saved to + self.all_frequencies, with keys of length (n_meas + n_qubits). + The leading n_meas values will hold the results of the MEASURE gates, + ordered by their appearance in the source_circuit. + The last n_qubits values will hold the measurements performed on + each of qubits at the end of the circuit. Returns: dict: A dictionary mapping multi-qubit states to their corresponding @@ -54,8 +61,7 @@ def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, in numpy.array: The statevector, if available for the target backend and requested by the user (if not, set to None). """ - - translated_circuit = translate_c(source_circuit, "qiskit") + translated_circuit = translate_c(source_circuit, "qiskit", output_options={"save_measurements": save_mid_circuit_meas}) # If requested, set initial state if initial_statevector is not None: @@ -63,16 +69,20 @@ def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, in raise ValueError("Cannot load an initial state if using a noise model, with Qiskit") else: n_qubits = int(math.log2(len(initial_statevector))) - initial_state_circuit = self.qiskit.QuantumCircuit(n_qubits, n_qubits) + n_meas = source_circuit.counts.get("MEASURE", 0) + n_registers = n_meas + source_circuit.width if save_mid_circuit_meas else source_circuit.width + initial_state_circuit = self.qiskit.QuantumCircuit(n_qubits, n_registers) initial_state_circuit.initialize(initial_statevector, list(range(n_qubits))) translated_circuit = initial_state_circuit.compose(translated_circuit) # Drawing individual shots with the qasm simulator, for noisy simulation or simulating mixed states - if self._noise_model or source_circuit.is_mixed_state: + if self._noise_model or source_circuit.is_mixed_state and not return_statevector: from tangelo.linq.noisy_simulation.noise_models import get_qiskit_noise_model - meas_range = range(source_circuit.width) - translated_circuit.measure(meas_range, meas_range) + n_meas = source_circuit.counts.get("MEASURE", 0) + meas_start = n_meas if save_mid_circuit_meas else 0 + meas_range = range(meas_start, meas_start + source_circuit.width) + translated_circuit.measure(range(source_circuit.width), meas_range) return_statevector = False backend = self.AerSimulator() @@ -84,6 +94,9 @@ def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, in sim_results = job_sim.result() frequencies = {state[::-1]: count/self.n_shots for state, count in sim_results.get_counts(0).items()} + self.all_frequencies = frequencies.copy() + self._current_state = None + # Noiseless simulation using the statevector simulator otherwise else: backend = self.AerSimulator(method='statevector') @@ -93,7 +106,7 @@ def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, in self._current_state = np.asarray(sim_results.get_statevector(translated_circuit)) frequencies = self._statevector_to_frequencies(self._current_state) - return (frequencies, np.array(sim_results.get_statevector())) if return_statevector else (frequencies, None) + return (frequencies, np.array(self._current_state)) if (return_statevector and self._current_state is not None) else (frequencies, None) @staticmethod def backend_info(): diff --git a/tangelo/linq/target/target_qulacs.py b/tangelo/linq/target/target_qulacs.py index 7010cc6e6..537820f55 100644 --- a/tangelo/linq/target/target_qulacs.py +++ b/tangelo/linq/target/target_qulacs.py @@ -30,7 +30,7 @@ def __init__(self, n_shots=None, noise_model=None): super().__init__(n_shots=n_shots, noise_model=noise_model) self.qulacs = qulacs - def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, initial_statevector=None): + def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, initial_statevector=None, save_mid_circuit_meas=False): """Perform state preparation corresponding to the input circuit on the target backend, return the frequencies of the different observables, and either the statevector or None depending on the availability of the @@ -40,12 +40,19 @@ def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, in equivalent gates. Args: - source_circuit: a circuit in the abstract format to be translated + source_circuit (Circuit): a circuit in the abstract format to be translated for the target backend. - return_statevector(bool): option to return the statevector as well, + return_statevector (bool): option to return the statevector as well, if available. - initial_statevector(list/array) : A valid statevector in the format + initial_statevector (list/array) : A valid statevector in the format supported by the target backend. + save_mid_circuit_meas (bool): Save mid-circuit measurement results to + self.mid_circuit_meas_freqs. All measurements will be saved to + self.all_frequencies, with keys of length (n_meas + n_qubits). + The leading n_meas values will hold the results of the MEASURE gates, + ordered by their appearance in the source_circuit. + The last n_qubits values will hold the measurements performed on + each of qubits at the end of the circuit. Returns: dict: A dictionary mapping multi-qubit states to their corresponding @@ -53,43 +60,66 @@ def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, in numpy.array: The statevector, if available for the target backend and requested by the user (if not, set to None). """ - translated_circuit = translate_c(source_circuit, "qulacs", - output_options={"noise_model": self._noise_model}) + output_options={"noise_model": self._noise_model, "save_measurements": save_mid_circuit_meas}) # Initialize state on GPU if available and desired. Default to CPU otherwise. if ('QuantumStateGpu' in dir(self.qulacs)) and (int(os.getenv("QULACS_USE_GPU", 0)) != 0): state = self.qulacs.QuantumStateGpu(source_circuit.width) else: state = self.qulacs.QuantumState(source_circuit.width) + + python_statevector = None if initial_statevector is not None: state.load(initial_statevector) - if (source_circuit.is_mixed_state or self._noise_model): - samples = list() - for i in range(self.n_shots): + # If you don't want to save the mid-circuit measurements for a mixed state + if (source_circuit.is_mixed_state or self._noise_model) and not save_mid_circuit_meas: + samples = dict() + for _ in range(self.n_shots): + translated_circuit.update_quantum_state(state) + bitstr = state.sampling(1)[0] + samples[bitstr] = samples.get(bitstr, 0) + 1 + if initial_statevector is not None: + state.load(initial_statevector) + else: + state.set_zero_state() + + # To save mid-circuit measurement results + elif save_mid_circuit_meas: + n_meas = source_circuit.counts.get("MEASURE", 0) + samples = dict() + for _ in range(self.n_shots): translated_circuit.update_quantum_state(state) - samples.append(state.sampling(1)[0]) + measurement = "".join([str(state.get_classical_value(i)) for i in range(n_meas)]) + sample = self._int_to_binstr(state.sampling(1)[0], source_circuit.width) + bitstr = measurement + sample + samples[bitstr] = samples.get(bitstr, 0) + 1 if initial_statevector is not None: state.load(initial_statevector) else: state.set_zero_state() - python_statevector = None + self.all_frequencies = {k: v / self.n_shots for k, v in samples.items()} + return (self.all_frequencies, python_statevector) if return_statevector else (self.all_frequencies, None) + + # All other cases for shot-based simulation elif self.n_shots is not None: translated_circuit.update_quantum_state(state) self._current_state = state python_statevector = np.array(state.get_vector()) if return_statevector else None - samples = state.sampling(self.n_shots) + samples = Counter(state.sampling(self.n_shots)) # this sampling still returns a list + + # Statevector simulation else: translated_circuit.update_quantum_state(state) self._current_state = state - python_statevector = state.get_vector() + python_statevector = np.array(state.get_vector()) frequencies = self._statevector_to_frequencies(python_statevector) - return (frequencies, np.array(python_statevector)) if return_statevector else (frequencies, None) + return (frequencies, python_statevector) if return_statevector else (frequencies, None) frequencies = {self._int_to_binstr(k, source_circuit.width): v / self.n_shots - for k, v in Counter(samples).items()} - return (frequencies, python_statevector) + for k, v in samples.items()} + return (frequencies, python_statevector) if return_statevector else (frequencies, None) def expectation_value_from_prepared_state(self, qubit_operator, n_qubits, prepared_state): diff --git a/tangelo/linq/tests/test_simulator.py b/tangelo/linq/tests/test_simulator.py index 70c13311d..5dcaa9e69 100644 --- a/tangelo/linq/tests/test_simulator.py +++ b/tangelo/linq/tests/test_simulator.py @@ -85,6 +85,8 @@ reference_exp_values = np.array([[0., 0., 0.], [0., -1., 0.], [-0.41614684, 0.7651474, -1.6096484], [1., 0., 0.], [-0.20175269, -0.0600213, 1.2972912]]) reference_mixed = {'01': 0.163, '11': 0.066, '10': 0.225, '00': 0.545} # With Qiskit noiseless, 1M shots +reference_all = {'101': 0.163, '011': 0.066, '010': 0.225, '100': 0.545} +reference_mid = {'1': 0.7, '0': 0.3} class TestSimulateAllBackends(unittest.TestCase): @@ -125,6 +127,18 @@ def test_simulate_mixed_state(self): results[b], _ = sim.simulate(circuit_mixed) assert_freq_dict_almost_equal(results[b], reference_mixed, 1e-2) + def test_simulate_mixed_state_save_measures(self): + """ Test mid-circuit measurement (mixed-state simulation) for all installed backends. + Mixed-states do not have a statevector representation, as they are a statistical mixture of several quantum states. + """ + results = dict() + for b in installed_simulator: + sim = get_backend(target=b, n_shots=10 ** 3) + results[b], _ = sim.simulate(circuit_mixed, save_mid_circuit_meas=True) + assert_freq_dict_almost_equal(results[b], reference_mixed, 8e-2) + assert_freq_dict_almost_equal(sim.all_frequencies, reference_all, 8e-2) + assert_freq_dict_almost_equal(sim.mid_circuit_meas_freqs, reference_mid, 8e-2) + def test_get_exp_value_mixed_state(self): """ Test expectation value for mixed-state simulation. Computation done by drawing individual shots. Some simulators are NOT good at this, by design (ProjectQ). """ @@ -447,7 +461,7 @@ def __init__(self, n_shots=None, noise_model=None, return_zeros=True): super().__init__(n_shots=n_shots, noise_model=noise_model) self.return_zeros = return_zeros - def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, initial_statevector=None): + def simulate_circuit(self, source_circuit: Circuit, return_statevector=False, initial_statevector=None, save_mid_circuit_meas=False): """Perform state preparation corresponding self.return_zeros.""" statevector = np.zeros(2**source_circuit.width, dtype=complex) diff --git a/tangelo/linq/tests/test_simulator_noisy.py b/tangelo/linq/tests/test_simulator_noisy.py index 8f85e4937..8b4fb5d75 100644 --- a/tangelo/linq/tests/test_simulator_noisy.py +++ b/tangelo/linq/tests/test_simulator_noisy.py @@ -18,6 +18,7 @@ import unittest +import numpy as np from openfermion.ops import QubitOperator from tangelo.linq import Gate, Circuit, get_backend, backend_info @@ -27,8 +28,9 @@ # Noisy simulation: circuits, noise models, references cn1 = Circuit([Gate('X', target=0)]) cn2 = Circuit([Gate('CNOT', target=1, control=0)]) +circuit_mixed = Circuit([Gate("RX", 0, parameter=2.), Gate("RY", 1, parameter=-1.), Gate("MEASURE", 0), Gate("X", 0)]) -nmp, nmd, nmc = NoiseModel(), NoiseModel(), NoiseModel() +nmp, nmd, nmc, nmm = NoiseModel(), NoiseModel(), NoiseModel(), NoiseModel() # nmp: pauli noise with equal probabilities, on X and CNOT gates nmp.add_quantum_error("X", 'pauli', [1 / 3] * 3) nmp.add_quantum_error("CNOT", 'pauli', [1 / 3] * 3) @@ -38,12 +40,16 @@ # nmc: cumulates 2 Pauli noises (here, is equivalent to no noise, as it applies Y twice when X is ran) nmc.add_quantum_error("X", 'pauli', [0., 1., 0.]) nmc.add_quantum_error("X", 'depol', 4/3) +# nmm: only apply noise to X gate +nmm.add_quantum_error("X", 'pauli', [0.2, 0., 0.]) ref_pauli1 = {'1': 1 / 3, '0': 2 / 3} ref_pauli2 = {'01': 2 / 9, '11': 4 / 9, '10': 2 / 9, '00': 1 / 9} ref_depol1 = {'1': 1 / 2, '0': 1 / 2} ref_depol2 = {'01': 1 / 4, '11': 1 / 4, '10': 1 / 4, '00': 1 / 4} ref_cumul = {'0': 1/3, '1': 2/3} +ref_mixed = {'10': 0.2876, '11': 0.0844, '01': 0.1472, '00': 0.4808} +ref_mixed_0 = {'00': 0.1488, '10': 0.6113, '01': 0.0448, '11': 0.1950} class TestSimulate(unittest.TestCase): @@ -123,6 +129,10 @@ def test_noisy_simulation_qulacs(self): res_cumul, _ = s_nmc.simulate(cn1) assert_freq_dict_almost_equal(res_cumul, ref_cumul, 1e-2) + s_nmm = get_backend(target="qulacs", n_shots=10 ** 4, noise_model=nmm) + res_mixed, _ = s_nmm.simulate(circuit_mixed) + assert_freq_dict_almost_equal(res_mixed, ref_mixed, 7.e-2) + @unittest.skipIf("qiskit" not in installed_backends, "Test Skipped: Backend not available \n") def test_noisy_simulation_qiskit(self): """ @@ -149,6 +159,10 @@ def test_noisy_simulation_qiskit(self): res_cumul, _ = s_nmp.simulate(cn1) assert_freq_dict_almost_equal(res_cumul, ref_cumul, 1e-2) + s_nmm = get_backend(target="qiskit", n_shots=10 ** 4, noise_model=nmm) + res_mixed, _ = s_nmm.simulate(circuit_mixed) + assert_freq_dict_almost_equal(ref_mixed, res_mixed, 7.e-2) + @unittest.skipIf("cirq" not in installed_backends, "Test Skipped: Backend not available \n") def test_noisy_simulation_cirq(self): """ @@ -175,6 +189,11 @@ def test_noisy_simulation_cirq(self): res_cumul, _ = s_nmc.simulate(cn1) assert_freq_dict_almost_equal(res_cumul, ref_cumul, 1e-2) + # Noisy mixed state without returning mid-circuit measurements + s_nmm = get_backend(target="cirq", n_shots=10 ** 4, noise_model=nmm) + res_mixed, _ = s_nmm.simulate(circuit_mixed) + assert_freq_dict_almost_equal(ref_mixed, res_mixed, 7.e-2) + def test_get_expectation_value_noisy(self): """Test of the get_expectation_value function with a noisy simulator""" # Test Hamiltonian. diff --git a/tangelo/linq/translator/translate_cirq.py b/tangelo/linq/translator/translate_cirq.py index 5e39b1345..b313ae202 100644 --- a/tangelo/linq/translator/translate_cirq.py +++ b/tangelo/linq/translator/translate_cirq.py @@ -76,12 +76,16 @@ def translate_cirq(source_circuit): return translate_c_to_cirq(source_circuit) -def translate_c_to_cirq(source_circuit, noise_model=None): +def translate_c_to_cirq(source_circuit, noise_model=None, save_measurements=False): """Take in an abstract circuit, return an equivalent cirq QuantumCircuit object. Args: - source_circuit: quantum circuit in the abstract format. + source_circuit (Circuit): quantum circuit in the abstract format. + noise_model (NoiseModel): The noise model to use + save_measurements (bool): If True, all measurements in the circuit are saved + with the key 'n' for the nth measurement in the Circuit. If False, no + measurements are saved. Returns: cirq.Circuit: a corresponding cirq Circuit. Right now, the structure is @@ -99,6 +103,8 @@ def translate_c_to_cirq(source_circuit, noise_model=None): # cirq will otherwise only initialize qubits that have gates target_circuit.append(cirq.I.on_each(qubit_list)) + measure_count = 0 + # Maps the gate information properly. Different for each backend (order, values) for gate in source_circuit._gates: if (gate.control is not None) and gate.name != 'CNOT': @@ -115,7 +121,9 @@ def translate_c_to_cirq(source_circuit, noise_model=None): elif gate.name in {"CNOT"}: target_circuit.append(GATE_CIRQ[gate.name](qubit_list[gate.control[0]], qubit_list[gate.target[0]])) elif gate.name in {"MEASURE"}: - target_circuit.append(GATE_CIRQ[gate.name](qubit_list[gate.target[0]])) + key = str(measure_count) if save_measurements else None + target_circuit.append(GATE_CIRQ[gate.name](qubit_list[gate.target[0]], key=key)) + measure_count += 1 elif gate.name in {"CRZ", "CRX", "CRY"}: next_gate = GATE_CIRQ[gate.name](gate.parameter).controlled(num_controls) target_circuit.append(next_gate(*control_list, qubit_list[gate.target[0]])) diff --git a/tangelo/linq/translator/translate_qdk.py b/tangelo/linq/translator/translate_qdk.py index 9f3900c05..c24b6910c 100644 --- a/tangelo/linq/translator/translate_qdk.py +++ b/tangelo/linq/translator/translate_qdk.py @@ -64,7 +64,7 @@ def translate_qsharp(source_circuit): return translate_c_to_qsharp(source_circuit) -def translate_c_to_qsharp(source_circuit, operation="MyQsharpOperation"): +def translate_c_to_qsharp(source_circuit, operation="MyQsharpOperation", save_measurements=False): """Take in an abstract circuit, generate the corresponding Q# operation (state prep + measurement) string, in the appropriate Q# template. The Q# output can be written to file and will be compiled at runtime. @@ -72,6 +72,9 @@ def translate_c_to_qsharp(source_circuit, operation="MyQsharpOperation"): Args: source_circuit: quantum circuit in the abstract format. operation (str), optional: name of the Q# operation. + save_measurements (bool), optional: True, return all mid-circuit measurement results. + This returns a frequency vector that is of size 2^(n_meas+n_qubits). False, + all measurements are overwritten. Returns: str: The Q# code (operation + template). This needs to be written into a @@ -80,12 +83,15 @@ def translate_c_to_qsharp(source_circuit, operation="MyQsharpOperation"): GATE_QDK = get_qdk_gates() + n_meas = source_circuit._gate_counts.get("MEASURE", 0) if save_measurements else 0 + n_c = n_meas + source_circuit.width + measurement = 0 # Prepare Q# operation header qsharp_string = "" qsharp_string += "@EntryPoint()\n" qsharp_string += f"operation {operation}() : Result[] {{\n" # qsharp_string += "body (...) {\n\n" - qsharp_string += f"\tmutable c = new Result[{source_circuit.width}];\n" + qsharp_string += f"\tmutable c = new Result[{n_c}];\n" qsharp_string += f"\tusing (qreg = Qubit[{source_circuit.width}]) {{\n" # Generate Q# strings with the right syntax, order and values for the gate inputs @@ -112,10 +118,13 @@ def translate_c_to_qsharp(source_circuit, operation="MyQsharpOperation"): elif gate.name in {"CSWAP"}: body_str += f"\t\tControlled {GATE_QDK[gate.name]}({control_string}, (qreg[{gate.target[0]}], qreg[{gate.target[1]}]));\n" elif gate.name in {"MEASURE"}: - body_str += f"\t\tset c w/= {gate.target[0]} <- {GATE_QDK[gate.name]}(qreg[{gate.target[0]}]);\n" + body_str += f"\t\tset c w/= {measurement} <- {GATE_QDK[gate.name]}(qreg[{gate.target[0]}]);\n" + if save_measurements: + measurement += 1 else: raise ValueError(f"Gate '{gate.name}' not supported on backend qdk") - qsharp_string += body_str + "\n\t\treturn ForEach(MResetZ, qreg);\n" + return_str = f"\n\t\tfor index in 0 .. Length(qreg) - 1 {{\n\t\t\tset c w/= {n_meas} + index <- MResetZ(qreg[index]);\n\t\t}}\n" + qsharp_string += body_str + return_str + "\n\t\treturn c;\n" qsharp_string += "\t}\n" # qsharp_string += "}\n adjoint auto;\n" qsharp_string += "}\n" diff --git a/tangelo/linq/translator/translate_qiskit.py b/tangelo/linq/translator/translate_qiskit.py index e62dd9a68..d115efa7d 100644 --- a/tangelo/linq/translator/translate_qiskit.py +++ b/tangelo/linq/translator/translate_qiskit.py @@ -79,11 +79,13 @@ def translate_qiskit(source_circuit): return translate_c_to_qiskit(source_circuit) -def translate_c_to_qiskit(source_circuit: Circuit): +def translate_c_to_qiskit(source_circuit: Circuit, save_measurements=False): """Take in a Circuit, return an equivalent qiskit.QuantumCircuit Args: - source_circuit (Circuit): quantum circuit in the Tangelo format. + source_circuit (Circuit): quantum circuit in the abstract format. + save_measurements (bool): Return mid-circuit measurements in the order + they appear in the circuit in the classical registers Returns: qiskit.QuantumCircuit: the corresponding qiskit.QuantumCircuit @@ -92,7 +94,11 @@ def translate_c_to_qiskit(source_circuit: Circuit): GATE_QISKIT = get_qiskit_gates() - target_circuit = qiskit.QuantumCircuit(source_circuit.width, source_circuit.width) + n_meas = source_circuit._gate_counts.get("MEASURE", 0) if save_measurements else 0 + n_measures = n_meas + source_circuit.width + target_circuit = qiskit.QuantumCircuit(source_circuit.width, n_measures) + + measurement = 0 # Maps the gate information properly. Different for each backend (order, values) for gate in source_circuit._gates: @@ -114,7 +120,9 @@ def translate_c_to_qiskit(source_circuit: Circuit): elif gate.name in {"XX"}: (GATE_QISKIT[gate.name])(target_circuit, gate.parameter, gate.target[0], gate.target[1]) elif gate.name in {"MEASURE"}: - (GATE_QISKIT[gate.name])(target_circuit, gate.target[0], gate.target[0]) + (GATE_QISKIT[gate.name])(target_circuit, gate.target[0], measurement) + if save_measurements: + measurement += 1 else: raise ValueError(f"Gate '{gate.name}' not supported on backend qiskit") diff --git a/tangelo/linq/translator/translate_qulacs.py b/tangelo/linq/translator/translate_qulacs.py index 2e46d0938..a04f6d68c 100644 --- a/tangelo/linq/translator/translate_qulacs.py +++ b/tangelo/linq/translator/translate_qulacs.py @@ -78,16 +78,19 @@ def translate_qulacs(source_circuit, noise_model=None): return translate_c_to_qulacs(source_circuit, noise_model) -def translate_c_to_qulacs(source_circuit, noise_model=None): +def translate_c_to_qulacs(source_circuit, noise_model=None, save_measurements=False): """Take in an abstract circuit, return an equivalent qulacs QuantumCircuit instance. If provided with a noise model, will add noisy gates at translation. Not very useful to look at, as qulacs does not provide much information about the noisy gates added when printing the "noisy circuit". Args: - source_circuit: quantum circuit in the abstract format. - noise_model: A NoiseModel object from this package, located in the + source_circuit (Circuit): quantum circuit in the abstract format. + noise_model (NoiseModel): A NoiseModel object from this package, located in the noisy_simulation subpackage. + save_measurements (bool): If True, each nth measurement in the circuit is saved in + the nth classical register. Otherwise, each measurement overwrites + the first classical register. Returns: qulacs.QuantumCircuit: the corresponding qulacs quantum circuit. @@ -99,6 +102,8 @@ def translate_c_to_qulacs(source_circuit, noise_model=None): GATE_QULACS = get_qulacs_gates() target_circuit = qulacs.QuantumCircuit(source_circuit.width) + measure_count = 0 + # Maps the gate information properly. Different for each backend (order, values) for gate in source_circuit._gates: if gate.name in {"H", "X", "Y", "Z", "S", "T"}: @@ -141,8 +146,10 @@ def translate_c_to_qulacs(source_circuit, noise_model=None): elif gate.name in {"CNOT"}: (GATE_QULACS[gate.name])(target_circuit, gate.control[0], gate.target[0]) elif gate.name in {"MEASURE"}: - gate = (GATE_QULACS[gate.name])(gate.target[0], gate.target[0]) - target_circuit.add_gate(gate) + m_gate = (GATE_QULACS[gate.name])(gate.target[0], measure_count) + target_circuit.add_gate(m_gate) + if save_measurements: + measure_count += 1 else: raise ValueError(f"Gate '{gate.name}' not supported on backend qulacs") diff --git a/tangelo/toolboxes/post_processing/post_selection.py b/tangelo/toolboxes/post_processing/post_selection.py index 6c4359dcc..d8afb1b5f 100644 --- a/tangelo/toolboxes/post_processing/post_selection.py +++ b/tangelo/toolboxes/post_processing/post_selection.py @@ -47,7 +47,8 @@ def ancilla_symmetry_circuit(circuit, sym_op): if n_qubits < op_len: raise RuntimeError("The size of the symmetry operator is bigger than the number of qubits.") elif n_qubits > op_len: - warnings.warn("The size of the symmetry operator is smaller than the number of qubits. Remaining qubits will be measured in the Z-basis.") + warnings.warn( + "The size of the symmetry operator is smaller than the number of qubits. Remaining qubits will be measured in the Z-basis.") if isinstance(sym_op, str): basis_gates = measurement_basis_gates(pauli_string_to_of(sym_op)) @@ -56,7 +57,8 @@ def ancilla_symmetry_circuit(circuit, sym_op): elif isinstance(sym_op, QubitOperator): basis_gates = measurement_basis_gates(list(sym_op.terms.keys())[0]) else: - raise RuntimeError("The symmetry operator must be an OpenFermion-style operator, a QubitOperator, or a Pauli word.") + raise RuntimeError( + "The symmetry operator must be an OpenFermion-style operator, a QubitOperator, or a Pauli word.") basis_circ = Circuit(basis_gates) parity_gates = [Gate("CNOT", n_qubits, i) for i in range(n_qubits)] @@ -97,3 +99,26 @@ def strip_post_selection(freqs, *qubits): hist = Histogram(freqs, n_shots=0) hist.remove_qubit_indices(*qubits) return hist.frequencies + + +def split_frequency_dict(frequencies, indices): + """Marginalize the frequencies dictionary over the indices. + This splits the frequency dictionary into two frequency dictionaries + and aggregates the corresponding frequencies. + + Args: + frequencies (dict): The input frequency dictionary + indices (list): The list of indices in the frequency dictionary to marginalize over + + Returns: + dict: The marginal frequencies for provided indices + dict: The marginal frequencies for remaining indices""" + key_length = len(next(iter(frequencies))) + other_indices = [i for i in range(key_length) if i not in indices] + + new_hist = Histogram(frequencies) + new_hist.remove_qubit_indices(*other_indices) + other_hist = Histogram(frequencies) + other_hist.remove_qubit_indices(*indices) + + return new_hist.frequencies, other_hist.frequencies