-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Staged controlled time #100
Changes from 37 commits
e16fcd4
3e76972
0e380e8
506258c
b001805
9191d48
71ac2ac
d0be81d
9a58c5d
1ab2ce8
31f90ee
488bfea
6bed749
ae21d1a
b9a36f5
0f48be6
9da6eaf
0e706bc
e5ea2b7
bba3e76
4b7c043
9f46abb
18d2c4c
3f6cca8
5d2dccf
0987b03
49e2309
18f23bf
4e00a7d
694ed18
289f96b
129b08e
4f430b6
2eff3e9
b3a230c
99dde01
d9911f2
8d94167
78e9f3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# 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. | ||
JamesB-1qbit marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,315 @@ | ||
# 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 the Quantum Imaginary Time Algorithm (QITE) | ||
""" | ||
from copy import copy | ||
import math | ||
JamesB-1qbit marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
from openfermion import FermionOperator as ofFermionOperator | ||
import numpy as np | ||
|
||
from tangelo.toolboxes.ansatz_generator.ansatz_utils import trotterize | ||
from tangelo.toolboxes.operators.operators import FermionOperator, QubitOperator | ||
from tangelo.toolboxes.qubit_mappings.mapping_transform import fermion_to_qubit_mapping | ||
from tangelo.toolboxes.ansatz_generator._general_unitary_cc import uccgsd_generator as uccgsd_pool | ||
from tangelo.toolboxes.operators import qubitop_to_qubitham | ||
from tangelo.toolboxes.qubit_mappings.statevector_mapping import get_reference_circuit | ||
from tangelo.linq import Circuit, Simulator | ||
|
||
|
||
class QITESolver: | ||
"""QITE class. This is an iterative algorithm that obtains a unitary operator | ||
that approximates the imaginary time evolution of an initial state. | ||
|
||
Attributes: | ||
molecule (SecondQuantizedMolecule): The molecular system. | ||
dt (float): The imaginary time step size | ||
min_de (float): Maximum energy change allowed before convergence. | ||
max_cycles (int): Maximum number of iterations of QITE. | ||
pool (func): Function that returns a list of FermionOperator. Each | ||
element represents an excitation/operator that has an effect on the | ||
total energy. | ||
pool_args (tuple) : The arguments for the pool function given as a | ||
tuple. | ||
qubit_mapping (str): One of the supported qubit mapping identifiers. | ||
qubit_hamiltonian (QubitOperator-like): Self-explanatory. | ||
up_then_down (bool): Spin orbitals ordering. | ||
n_spinorbitals (int): Self-explanatory. | ||
n_electrons (int): Self-explanatory. | ||
backend_options (dict): Backend options for the underlying QITE propagation | ||
verbose (bool): Flag for verbosity of QITE. | ||
""" | ||
|
||
def __init__(self, opt_dict): | ||
|
||
default_backend_options = {"target": None, "n_shots": None, "noise_model": None} | ||
default_options = {"molecule": None, | ||
"dt": 0.5, "max_cycles": 100, | ||
"min_de": 1.e-7, | ||
"pool": uccgsd_pool, | ||
"pool_args": None, | ||
"frozen_orbitals": "frozen_core", | ||
"qubit_mapping": "jw", | ||
"qubit_hamiltonian": None, | ||
"up_then_down": False, | ||
"n_spinorbitals": None, | ||
"n_electrons": None, | ||
"backend_options": default_backend_options, | ||
"verbose": True} | ||
|
||
# Initialize with default values | ||
self.__dict__ = default_options | ||
# Overwrite default values with user-provided ones, if they correspond to a valid keyword | ||
for k, v in opt_dict.items(): | ||
if k in default_options: | ||
setattr(self, k, v) | ||
else: | ||
raise KeyError(f"Keyword :: {k}, not available in {self.__class__.__name__}") | ||
|
||
# Raise error/warnings if input is not as expected. Only a single input | ||
# must be provided to avoid conflicts. | ||
if not (bool(self.molecule) ^ bool(self.qubit_hamiltonian)): | ||
raise ValueError("A molecule OR qubit Hamiltonian object must be provided when instantiating " | ||
f"{self.__class__.__name__}.") | ||
|
||
if self.qubit_hamiltonian: | ||
if not (self.n_spinorbitals and self.n_electrons): | ||
raise ValueError("Expecting the number of spin-orbitals (n_spinorbitals) and the number of " | ||
"electrons (n_electrons) with a qubit_hamiltonian.") | ||
|
||
self.iteration = 0 | ||
self.energies = list() | ||
|
||
self.circuit_list = list() | ||
self.final_energy = None | ||
self.final_circuit = None | ||
self.final_statevector = None | ||
|
||
self.backend = None | ||
|
||
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. | ||
""" | ||
|
||
return get_reference_circuit(n_spinorbitals=self.n_spinorbitals, | ||
n_electrons=self.n_electrons, | ||
mapping=self.qubit_mapping, | ||
up_then_down=self.up_then_down, | ||
spin=0) | ||
|
||
def build(self): | ||
"""Builds the underlying objects required to run the QITE algorithm.""" | ||
|
||
# Building molecule data with a pyscf molecule. | ||
if self.molecule: | ||
|
||
self.n_spinorbitals = self.molecule.n_active_sos | ||
self.n_electrons = self.molecule.n_active_electrons | ||
|
||
# Compute qubit hamiltonian for the input molecular system | ||
qubit_op = fermion_to_qubit_mapping(fermion_operator=self.molecule.fermionic_hamiltonian, | ||
mapping=self.qubit_mapping, | ||
n_spinorbitals=self.n_spinorbitals, | ||
n_electrons=self.n_electrons, | ||
up_then_down=self.up_then_down, | ||
spin=0) | ||
|
||
self.qubit_hamiltonian = qubitop_to_qubitham(qubit_op, self.qubit_mapping, self.up_then_down) | ||
|
||
# Getting the pool of operators for the ansatz. If more functionalities | ||
# are added, this part must be modified and generalized. | ||
if self.pool_args is None: | ||
if self.pool == uccgsd_pool: | ||
self.pool_args = (self.n_spinorbitals,) | ||
else: | ||
raise KeyError("pool_args must be defined if using own pool function") | ||
# Check if pool function returns a QubitOperator or FermionOperator and populate variables | ||
pool_list = self.pool(*self.pool_args) | ||
if isinstance(pool_list[0], QubitOperator): | ||
self.pool_type = "qubit" | ||
self.full_pool_operators = pool_list | ||
elif isinstance(pool_list[0], (FermionOperator, ofFermionOperator)): | ||
self.pool_type = "fermion" | ||
self.fermionic_operators = pool_list | ||
self.full_pool_operators = [fermion_to_qubit_mapping(fermion_operator=fi, | ||
mapping=self.qubit_mapping, | ||
n_spinorbitals=self.n_spinorbitals, | ||
n_electrons=self.n_electrons, | ||
up_then_down=self.up_then_down) for fi in self.fermionic_operators] | ||
else: | ||
raise ValueError("pool function must return either QubitOperator or FermionOperator") | ||
|
||
# Cast all coefs to floats (rotations angles are real). | ||
for qubit_op in self.full_pool_operators: | ||
for term, coeff in qubit_op.terms.items(): | ||
qubit_op.terms[term] = math.copysign(1., coeff.imag) | ||
|
||
# Remove duplicates and only select terms with odd number of Y gates for all mappings except JKMN | ||
if self.qubit_mapping.upper() != "JKMN": | ||
reduced_pool_terms = set() | ||
for qubit_op in self.full_pool_operators: | ||
for term in qubit_op.terms: | ||
count_y = str(term).count("Y") | ||
if count_y % 2 == 1: | ||
reduced_pool_terms.add(term) | ||
else: | ||
reduced_pool_terms = set() | ||
for qubit_op in self.full_pool_operators: | ||
for term in qubit_op.terms.keys(): | ||
if term: | ||
reduced_pool_terms.add(term) | ||
|
||
# Generated list of pool_operators and full pool operator. | ||
self.pool_operators = [QubitOperator(term) for term in reduced_pool_terms] | ||
self.pool_qubit_op = QubitOperator() | ||
JamesB-1qbit marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for term in self.pool_operators: | ||
self.pool_qubit_op += term | ||
|
||
self.qubit_operator = self.qubit_hamiltonian.to_qubitoperator() | ||
|
||
# Obtain all qubit terms that need to be measured | ||
self.pool_h = [element*self.qubit_operator for element in self.pool_operators] | ||
self.pool_pool = [[element1*element2 for element2 in self.pool_operators] for element1 in self.pool_operators] | ||
|
||
# Obtain initial state preparation circuit | ||
self.circuit_list.append(self.prepare_reference_state()) | ||
self.final_circuit = copy(self.circuit_list[0]) | ||
|
||
# Quantum circuit simulation backend options | ||
self.backend = Simulator(target=self.backend_options["target"], n_shots=self.backend_options["n_shots"], | ||
noise_model=self.backend_options["noise_model"]) | ||
|
||
self.use_statevector = self.backend.statevector_available and self.backend._noise_model is None | ||
|
||
def simulate(self): | ||
"""Performs the QITE cycles. Each iteration, a linear system is | ||
solved to obtain the next unitary. The system to be solved can be found in | ||
section 3.5 of https://arxiv.org/pdf/2108.04413.pdf | ||
|
||
Returns: | ||
float: final energy after obtaining running QITE | ||
""" | ||
|
||
# Construction of the circuit. self.max_cycles terms are added, unless | ||
# the energy change is less than self.min_de. | ||
if self.use_statevector: | ||
self.update_statevector(self.backend, self.circuit_list[0]) | ||
self.final_energy = self.energy_expectation(self.backend) | ||
self.energies.append(self.final_energy) | ||
while self.iteration < self.max_cycles: | ||
self.iteration += 1 | ||
if self.verbose: | ||
print(f"Iteration {self.iteration} of QITE with starting energy {self.final_energy}") | ||
|
||
suv, bu = self.calculate_matrices(self.backend, self.final_energy) | ||
|
||
alphas_array = np.linalg.solve(suv.real, bu.real) | ||
# convert to dictionary with key as first (only) term of each pool_operator and value self.dt * alphas_array[i] | ||
alphas_dict = {next(iter(qu_op.terms)): self.dt * alphas_array[i] for i, qu_op in enumerate(self.pool_operators)} | ||
next_circuit = trotterize(self.pool_qubit_op, alphas_dict, trotter_order=1, n_trotter_steps=1) | ||
|
||
self.circuit_list.append(next_circuit) | ||
self.final_circuit += next_circuit | ||
|
||
if self.use_statevector: | ||
self.update_statevector(self.backend, self.circuit_list[self.iteration]) | ||
|
||
new_energy = self.energy_expectation(self.backend) | ||
self.energies.append(new_energy) | ||
|
||
if abs(new_energy - self.final_energy) < self.min_de and self.iteration > 1: | ||
self.final_energy = new_energy | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Take this statement out of the branches: it is not subject to the conditional statement. You then realize the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are doing The code snippet above is clearer to me, and the current code is unsatisfying... unless I'm missing something. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you do the declaration above, it will exit the loop immediately after two loops as |
||
break | ||
self.final_energy = new_energy | ||
|
||
if self.verbose: | ||
print(f"Final energy of QITE is {self.final_energy}") | ||
|
||
return self.energies[-1] | ||
|
||
def update_statevector(self, backend: Simulator, next_circuit: Circuit): | ||
r"""Update self.final_statevector by propagating with next_circuit using backend | ||
|
||
Args: | ||
Simulator: the backend to use for the statevector update | ||
Circuit: The circuit to apply to the statevector | ||
""" | ||
_, self.final_statevector = backend.simulate(next_circuit, | ||
return_statevector=True, | ||
initial_statevector=self.final_statevector) | ||
|
||
def calculate_matrices(self, backend: Simulator, new_energy: float): | ||
r"""Calculated matrix elements for imaginary time evolution. | ||
The matrices are defined in section 3.5 of https://arxiv.org/pdf/2108.04413.pdf | ||
|
||
Args: | ||
backend (Simulator): the backend from which the matrices are generated | ||
new_energy (float): the current energy_expectation of the Hamiltonian | ||
|
||
Returns: | ||
matrix float: The expectation values <\psi| pu^+ pv |\psi> | ||
array float: The expecation values <\psi| pu^+ H |\psi> | ||
""" | ||
|
||
circuit = Circuit(n_qubits=self.final_circuit.width) if self.use_statevector else self.final_circuit | ||
|
||
ndeltab = np.sqrt(1 - 2 * self.dt * new_energy) | ||
prefac = -1j/ndeltab | ||
bu = [prefac*backend.get_expectation_value(element, circuit, initial_statevector=self.final_statevector) | ||
for element in self.pool_h] | ||
bu = np.array(bu) | ||
pool_size = len(self.pool_h) | ||
suv = np.zeros((pool_size, pool_size), dtype=complex) | ||
for u in range(pool_size): | ||
for v in range(u+1, pool_size): | ||
suv[u, v] = backend.get_expectation_value(self.pool_pool[u][v], | ||
circuit, | ||
initial_statevector=self.final_statevector) | ||
suv[v, u] = suv[u, v] | ||
|
||
return suv, bu | ||
|
||
def energy_expectation(self, backend: Simulator): | ||
"""Estimate energy using the self.final_circuit, qubit hamiltonian and compute | ||
backend. | ||
|
||
Args: | ||
backend (Simulator): the backend one computes the energy expectation with | ||
|
||
Returns: | ||
float: energy computed by the backend | ||
""" | ||
circuit = Circuit(n_qubits=self.final_circuit.width) if self.use_statevector else self.final_circuit | ||
energy = backend.get_expectation_value(self.qubit_hamiltonian.to_qubitoperator(), | ||
circuit, | ||
initial_statevector=self.final_statevector) | ||
return energy | ||
|
||
def get_resources(self): | ||
"""Returns resources currently used in underlying state preparation i.e. self.final_circuit | ||
the number of pool operators, and the size of qubit_hamiltonian | ||
|
||
Returns: | ||
dict: Dictionary of various quantum resources required""" | ||
resources = dict() | ||
resources["qubit_hamiltonian_terms"] = len(self.qubit_hamiltonian.terms) | ||
resources["pool_size"] = len(self.pool_operators) | ||
resources["circuit_width"] = self.final_circuit.width | ||
resources["circuit_gates"] = self.final_circuit.size | ||
resources["circuit_2qubit_gates"] = self.final_circuit.counts.get("CNOT", 0) | ||
ValentinS4t1qbit marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return resources |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest leaving a short comment here so that users & developers can understand / remember the reason we froze a version number (qiskit moves a lot). Users will wonder why too, or have errors because we're not forcing them to install a particular version number.
A great example: I do not remember why we have a restriction on cirq below. Do you, @JamesB-1qbit @AlexandreF-1qbit ? If yes, leave a comment, or relax this with a recent cirq (e.g
cirq >= ....
). Even worse, cirq is installed automatically as a dependency of openfermion insetup.py
so there's a fair chance people who install Tangelo are not even running in the same conditions as our tests suite, and therefore it means we can't rely on this test suite to guarantee things work for our users. Whatever we do, this needs to be consistent.Ideal case: we realize everything works without this constraint on cirq version number.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a conversation that can happen independently of the PR if its actually non-trivial
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is due to a failed test due to the new version of Qiskit-aer. I opened an issue on the qiskit-aer GitHub to explain what is happening or revert to the old behaviour.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not aware for the cirq versioning. The new Qiskit-aer changed a bunch of things and we should look into it.