Quantum Numbers#

Overview#

In this notebook we will introduce the idea of quantum number conservation and how to enforce it in Renormalizer.

Quantum number conservation#

In the context of Shuai group’s research, the most common case of quantum number conservation is the conservation of electron/exciton in the Holstein model.

\[\hat H = \sum_{ij} J_{ij} \hat a^\dagger_i \hat a_{i+1} + \sum_{ik}\frac{1}{2} (\hat p_{ik}^2 + \omega_k^2 \hat x_{ik}^2) + \sum_{ik} \hat a^\dagger_i \hat a_i \hat x_{ik}\]

Here, each electronic degree of freedom, described by \(\hat a^\dagger_i\) and \(\hat a\), is coupled with a set of harmonic oscillators indexed by \(k\).

For the demonstration purposes, we may neglect the vibrations and focus only on the electronic part, which leads us to the Hückel model (or tight binding model)

\[\hat H = \sum_{ij} J_{ij} \hat a^\dagger_i \hat a_{i+1}\]

This is a model that conserves the total particle number. Formally speaking, this means \(\hat H\) commutes with the total particle number operator \(\hat N = \sum_i \hat a^\dagger_i \hat a_i\). More intuitively, for any state \(|\psi\rangle\) with a particular particle number, \(\hat H |\psi\rangle\) has the same particle number. For example, suppose there are only two sites, and

\[|\psi\rangle = |01\rangle+|10\rangle\]

Thus,

\[\hat H |\psi\rangle = J_{12}|10\rangle+J_{21}|01\rangle\]

The two states with different quantum numbers, \(|00\rangle\) and \(|11\rangle\), will not present in \(\hat H |\psi\rangle\). In this model, the total particle number, is called a (good) quantum number.

There are two main reasons why we should care about quantum number - For many chemical applications the quantum number can be considered as an input to the model, just as the coefficients \(J_{ij}\). For example, the total number of exciton in a excitonic coupling model should be 1. If the conservation of quantum number is not enforced, numerical error may eventually lead us to incorrect solution. - Another advantage of using quantum number is that it saves memory and accelerates calculation. By looking at quantum numbers we can assert some of the matrix/vector elements must be zero. The sparsity can then be exploited for more efficient tensor manipulation, such as SVD.

Setting quantum number for states#

In renormalizer, the first place to set the quantum number is basis sets. Most BasisSet classes have the sigmaqn argument that determines the quantum number of each basis.

[1]:
from renormalizer import BasisSimpleElectron
2024-11-19 03:14:21,870[INFO] Use NumPy as backend
2024-11-19 03:14:21,871[INFO] numpy random seed is 9012
2024-11-19 03:14:21,872[INFO] random seed is 1092
2024-11-19 03:14:21,882[INFO] Git Commit Hash: 5d9832501dbae153174503f00b3479337588b7f1
2024-11-19 03:14:21,883[INFO] use 64 bits

Here we set up two basis sets and sets the quantum number for \(|0\rangle\) to 0 and \(|1\rangle\) to 1.

[2]:
b1 = BasisSimpleElectron(0, sigmaqn=[0, 1])
b2 = BasisSimpleElectron(1, sigmaqn=[0, 1])
basis = [b1, b2]

We next build a random MPS based on the basis sets and see its effect

[3]:
from renormalizer import Mps, Model
[4]:
model = Model(basis, ham_terms=[])
[5]:
mps = Mps.random(model, qntot=1, m_max=2)
[6]:
mps[0].array
[6]:
array([[[1., 0.],
        [0., 1.]]])
[7]:
mps[1].array
[7]:
array([[[ 0.        ],
        [-0.7089592 ]],

       [[-0.70524949],
        [ 0.        ]]])
[8]:
mps.todense()
[8]:
array([ 0.        , -0.7089592 , -0.70524949,  0.        ])

We can see that although the MPS is random, half of the matrix elements are zero, due to particle number conservation. As a result, the overall dense state vector has a well-defined particle number of 1.

For comparison, in the following the MPS when quantum number is not activated is shown

[9]:
model2 = Model([BasisSimpleElectron(i, sigmaqn=[0, 0]) for i in range(2)], ham_terms=[])
mps2 = Mps.random(model2, qntot=0, m_max=2)
[10]:
mps2[0].array
[10]:
array([[[-0.27201201, -0.96229386],
        [ 0.96229386, -0.27201201]]])
[11]:
mps2[1].array
[11]:
array([[[-0.56196169],
        [ 0.61166976]],

       [[ 0.34668986],
        [ 0.43573537]]])
[12]:
mps2.todense()
[12]:
array([-0.18075719, -0.58568699, -0.63507609,  0.47008079])

Setting quantum number for operators#

Just as states, operators are also associated with quantum number. The quantum number of an operator shows the change of quantum number if the operator is applied to a state. For example, the quantum number of \(\hat a^\dagger\) is 1, since

\[\hat a^\dagger |0\rangle = |1\rangle\]

Similarly, the quantum number of \(\hat a\) is -1.

In Renormalizer, to fully take advantage of quatnum number conservation, we need to set the quantum number for the operators. The Op class accepts the qn argument for the quantum number of each elementary operator.

[13]:
from renormalizer import Op, Mpo

Apply the creation operator to the MPS. The total quantum number of the MPS increases from 1 to 2, and the resulting state is \(|11\rangle\).

[14]:
mps3 = Mpo(model, Op(r"a^\dagger", 0, qn=1)) @ mps
2024-11-19 03:14:21,992[DEBUG] # of operator terms: 1
2024-11-19 03:14:21,992[DEBUG] Input operator terms: 1
2024-11-19 03:14:21,994[DEBUG] After combination of the same terms: 1
[15]:
mps3.qntot
[15]:
array([2])
[16]:
mps3.todense()
[16]:
array([ 0.       ,  0.       ,  0.       , -0.7089592])

For complex symbols the quantum number for each elementary symbol should be specified.

[17]:
ham_terms = Op(r"a^\dagger a", [0, 1], qn=[1, -1]) + Op(r"a^\dagger a", [1, 0], qn=[1, -1])

To summarize, three places are relevant to the setup of quantum number - The basis sets - The operators - Total quantum number in MPS

Notes#

Setting quantum number is not necessary. When quantum number is not conserved or for some reason quantum number conservation is not desired, set all quantum numbers to 0.

Renormalizer supports the conservation of multiple quantum numbers. For example, in ab initio electronic structure calculations, both the number of alpha spin electrons and the number of beta spin electrons are conserved. In such cases, quantum numbers should be set to a numpy array of integers.

[18]:
import numpy as np
[19]:
# spin up creation operator
Op(r"a^\dagger", "up", qn=np.array([1, 0]))
[19]:
Op('a^\\dagger', ['up'], 1.0, [[1, 0]])
[20]:
# spin up annihilation operator
Op(r"a", "up", qn=np.array([-1, 0]))
[20]:
Op('a', ['up'], 1.0, [[-1, 0]])
[21]:
# spin down creation operator
Op(r"a^\dagger", "down", qn=np.array([0, 1]))
[21]:
Op('a^\\dagger', ['down'], 1.0, [[0, 1]])
[22]:
# spin down annihilation operator
Op(r"a^\dagger", "down", qn=np.array([0, -1]))
[22]:
Op('a^\\dagger', ['down'], 1.0, [[0, -1]])
[ ]: