Symmetry
The use of symmetry to simplify computational problems in quantum chemistry is well-established. A symmetry of an operator \(\hat{O}\) can be described by an operator \(\hat{S}\) on the same Hilbert space, where the two operators \(\hat{O}\) and \(\hat{S}\) commute. InQuanto contains special purpose classes to represent symmetry operators in both the fermionic (inquanto.operators.SymmetryOperatorFermionic
) and qubit (inquanto.operators.SymmetryOperatorPauli
) spaces. These subclass their respective operator classes, and as such may otherwise be constructed and used in the same way. Both classes provide additional functionality related to symmetry, above the normal inquanto.operators.FermionOperator
and inquanto.operators.QubitOperator
classes. Class method is_symmetry_of()
can verify (by commutation) that a given symmetry operator is indeed a symmetry of a given operator within the same space. Class method symmetry_sector()
yields the symmetry sector that a provided state is in - i.e. the expectation value of the symmetry operator with the provided state.
from inquanto.operators import SymmetryOperatorPauli
from inquanto.operators import QubitOperator
from inquanto.states import QubitState
symmetry = SymmetryOperatorPauli("Z0 Z1 Z2 Z3")
operator = QubitOperator("X0 X1 X2 X3")
state = QubitState([1,1,0,0])
print("Is it a symmetry? ", symmetry.is_symmetry_of(operator))
print("Symmetry sector with state: ", symmetry.symmetry_sector(state))
Is it a symmetry? True
Symmetry sector with state: 1.0
Warning
Calculating a sector is performed by determining the expectation value of the state with the symmetry operator. For typical purposes (for instance, calculating the sector of a Hartree-Fock state with Z2 symmetry operators) this will be both efficient and produce predictable results. However, validity checking is not performed and the use of symmetry-inconsistent states may lead to inconsistent results, long execution times, or both.
For computational purposes, it is common to express fermionic symmetry operators as products of operators, rather than linear combinations. For example, the fermionic parity operator \((-1)^{\sum_i{\hat{N}_i}}\) may be expressed as a product \(\prod_i{1 - 2\hat{N}_i}\). The product form can be efficiently stored, whereas expanding out the product into a single linear combination may invoke computational difficulty. It is particularly useful when converting to a qubit representation, where individual multiplicands may be converted to single Pauli strings, thus avoiding any exponential growth in the number of terms.
In order to allow for symmetry operators in this form to be used in InQuanto, a inquanto.operators.SymmetryOperatorFermionicFactorised
class is provided. These are a subclass of FermionOperatorList
, and provide similar is_symmetry_of()
and symmetry_sector()
functionality to the main symmetry classes. Conversion is also possible with the to_symmetry_operator_fermionic()
method. In all cases, care should again be taken to ensure excessive computational difficulty is not incurred.
Finding symmetries
InQuanto is capable of automatically generating symmetry operators on both fermionic and qubit spaces; however, the method of generation is different in each case. For fermionic Hilbert spaces, it is common to have some prior knowledge regarding the physical point group, electron number and spin symmetries. This information is typically provided by a precursor classical calculation and is associated with the FermionSpace
class. InQuanto uses this information to generate the Z2 symmetry operators corresponding to the Z2 point group symmetries (using orbital irreducible representation information), the electron number parity and spin parity symmetries. This can be performed using the symmetry_operators_z2()
method:
from inquanto.spaces import FermionSpace
point_group_label = "D2h"
irrep_labels = ["Ag", "Ag", "B1u", "B1u"]
fermion_space = FermionSpace(4, point_group_label, irrep_labels)
symmetry_operators_fermion = fermion_space.symmetry_operators_z2()
for x in symmetry_operators_fermion:
print(x)
(1.0, ), (-2.0, F3^ F3 ), (-2.0, F2^ F2 ), (4.0, F2^ F2 F3^ F3 )
(1.0, ), (-2.0, F3^ F3 ), (-2.0, F2^ F2 ), (4.0, F2^ F2 F3^ F3 ), (-2.0, F1^ F1 ), (4.0, F1^ F1 F3^ F3 ), (4.0, F1^ F1 F2^ F2 ), (-8.0, F1^ F1 F2^ F2 F3^ F3 ), (-2.0, F0^ F0 ), (4.0, F0^ F0 F3^ F3 ), (4.0, F0^ F0 F2^ F2 ), (-8.0, F0^ F0 F2^ F2 F3^ F3 ), (4.0, F0^ F0 F1^ F1 ), (-8.0, F0^ F0 F1^ F1 F3^ F3 ), (-8.0, F0^ F0 F1^ F1 F2^ F2 ), (16.0, F0^ F0 F1^ F1 F2^ F2 F3^ F3 )
(1.0, ), (-2.0, F2^ F2 ), (-2.0, F0^ F0 ), (4.0, F0^ F0 F2^ F2 )
FermionSpace
objects without point group symmetry information will generate symmetry operators corresponding to electron number parity and spin parity only.
Warning
Symmetry operators generated in this way may exponentially blow up in the number of terms. For advanced usage, the parameter return_factorized
can be set to True
to return operators as a inquanto.operators.SymmetryOperatorFermionicFactorised
, as discussed above.
Conversely, QubitSpace
objects in InQuanto do not store any information with regards to the physical symmetries of the system. Here, Z2 symmetries can be determined by providing an operator which preserves the relevant symmetries – typically the Hamiltonian. Symmetries are found by finding a set of independent generators for operators that commute with the provided operator. [54]
from inquanto.spaces import QubitSpace
from inquanto.express import load_h5
h2_sto3g = load_h5("h2_sto3g.h5", as_tuple=True)
h2_hamiltonian = h2_sto3g.hamiltonian_operator.to_FermionOperator().qubit_encode()
qubit_space = QubitSpace(4)
symmetry_operators_qubit = qubit_space.symmetry_operators_z2(h2_hamiltonian)
for x in symmetry_operators_qubit:
print(x)
(1.0, Z0 Z1 I2 I3)
(1.0, Z0 I1 Z2 I3)
(1.0, Z0 I1 I2 Z3)
Point Group Symmetry
In quantum chemistry, it is common to explicitly consider molecular point group symmetries. While the symmetry_operators_z2()
method of a FermionSpace
forms a convenient way to generate Z2 symmetries (including Z2 point group symmetries), InQuanto contains tools for the explicit analysis of the molecular point group. The spatial symmetries of molecules and their molecular orbitals can be manipulated by using the inquanto.symmetry.PointGroup
class. For example, a list of supported point groups can be obtained:
from inquanto.symmetry import PointGroup
print(PointGroup.supported_groups())
['C1', 'C2', 'C2h', 'C2v', 'C3v', 'Ci', 'Cs', 'D2', 'D2h', 'D3h', 'Oh', 'Td']
If the point group of interest is supported, the corresponding character table can be printed in a human-readable format:
from inquanto.symmetry import PointGroup
pg = PointGroup("C2v")
pg.print_character_table()
C2v E C2 (z) σ_v (xz) σ_v (yz)
A1 1 1 1 1
A2 1 1 -1 -1
B1 1 -1 1 -1
B2 1 -1 -1 1
Components of representations expressible by the group can also be evaluated:
from inquanto.symmetry import PointGroup
pg = PointGroup("C2v")
print("[1, -1, -1, 1] components:", pg.compute_representation_components([1, -1, -1, 1]))
[1, -1, -1, 1] components: [(0, 'A1'), (0, 'A2'), (0, 'B1'), (1, 'B2')]
Similarly, the results of direct products of irreps can be obtained, both as a list of characters and as a decomposition into irreducible components.
from inquanto.symmetry import PointGroup
pg = PointGroup("D2h")
components, direct_product = pg.irrep_direct_product(["Ag", "B1u"])
print("Ag, B1u direct product:", direct_product)
print("Ag, B1u direct product components:", components)
Ag, B1u direct product: [ 1 1 -1 -1 -1 -1 1 1]
Ag, B1u direct product components: [(0, 'Ag'), (0, 'B1g'), (0, 'B2g'), (0, 'B3g'), (0, 'Au'), (1, 'B1u'), (0, 'B2u'), (0, 'B3u')]
Z2 Tapering
Z2 qubit tapering [54] is a method of reducing the number of qubits needed to simulate a given fermionic Hamiltonian. Each independent Z2 symmetry divides the full \(2^N\) dimensional qubit state space into two \(2^{N-1}\) dimensional sectors - one where the expectation value of the symmetry operator is \(1\), and one where it is \(-1\). As the symmetry operator commutes with the Hamiltonian, each eigenstate of the Hamiltonian must have support exclusively on states within one of these sectors. By transforming the Hamiltonian such that the symmetry operators are mapped to single qubit operators (note that this is a Clifford operation and as such can be performed classically efficiently), the state of \(N_s\) qubits is fixed and can be inferred from the symmetry sector that the desired eigenstate is in. In ground state problems, this symmetry sector is known a priori – it will be the symmetry sector of the Hartree-Fock state, assuming that the true eigenstate has nonzero overlap with the Hartree-Fock state. As the state of these \(N_s\) qubits is fixed and known, there is no need to include them in the quantum calculation.
Z2 qubit tapering is implemented in InQuanto by the inquanto.symmetry.TapererZ2
class. To taper an operator, symmetry operators and the symmetry sectors of a known reference state must be provided. Symmetry operators may be obtained from either the fermionic or qubit space; however, in the former case the operators must be mapped to qubit operators using the same fermion-to-qubit encoding scheme as the qubit operator. Once the TapererZ2
object is instantiated for a given set of symmetry operators and sectors, it may be used to find tapered operators and states as below. The object may be reused, if tapering multiple operators with the same symmetries is required.
from inquanto.symmetry import TapererZ2
hf_state = QubitState([1,1,0,0])
symmetry_sectors = [x.symmetry_sector(hf_state) for x in symmetry_operators_qubit]
taperer = TapererZ2(symmetry_operators_qubit, symmetry_sectors)
tapered_hamiltonian = taperer.tapered_operator(h2_hamiltonian)
print(tapered_hamiltonian)
(-0.29264467227722657, I0), (0.8248612119271037, Z0), (0.17966867956301552, X0)
Note that if using a tapered Hamiltonian in a variational algorithm, the ansatz state must also be tapered. This is performed using additional functionality of the ansatz classes.
from inquanto.ansatzes import FermionSpaceAnsatzUCCSD
from inquanto.states import FermionState
reference_state = FermionState([1,1,0,0])
ansatz = FermionSpaceAnsatzUCCSD(
fermion_space,
reference_state,
taperer=taperer,
tapering_exponent_check_behaviour="discard",
)
Tip
For this to work, ansatz excitations must preserve symmetry. By default, FermionSpaceAnsatzUCCSD
will check for symmetry violating excitations and throw an error if any are found. This behavior can be changed using the tapering_exponent_check_behaviour
parameter, which is described in the API reference documentation for FermionSpaceStateExp
, the base class of FermionSpaceAnsatzUCCSD
.
Tip
Note that in the example above, taperer
and tapering_exponent_check_behaviour
are passed as **kwargs
to FermionSpaceAnsatzUCCSD
as it doesn’t have them as positional parameters. These are forwarded to its parent FermionSpaceStateExp
class which handles the tapering.