Table of Contents

Python lib qmodel

GitHub: https://github.com/magmage/qmodel

Basic information

A lightweight Python library for discrete quantum models centered on Hilbert space bases and operators. The main classes are Basis, Operator and Vector correspondingly, with derived classes for specialized bases and a class for vectors of operators. Here, “basis” should always be read as “Hilbert space with a fixed basis”. An operator is then always defined with respect to such a fixed basis and carries a reference to the Basis object it belongs to. The basis elements are indexed by quantum numbers, “qn” for short, like $(n,\ell,m)$ for atomic orbitals or $(i,\sigma)$ for a Hubbard lattice. Those are realized as dictionaries. A basis can be tensorized to represent multiple particles, the corresponding basis elements are then realized as tuples of the constituent basis elements. Next to the full tensor product (for distinguishable particles, qubits) also symmetric and anti-symmetric tensor products are supported.

First import the necessary classes.

from qmodel import *

Getting started

A Basis object is initialized with a qn key (string) and a range of quantum numbers (float-valued iterable). The example initializes a 3-dimensional space with a basis that is numbered with a qn called “i” and that has the values 1,2,3. Some information about the basis is then printed.

b = Basis('i', (1,2,3))
print(b)

One of the most basic operators in this basis is the mapping $e_i \mapsto i e_i$ when $e_i$ are the respective basis vectors. It thus always has a diagonal matrix in this basis. The corresponding method is diag() which yields an Operator object. Printing an Operator gives the matrix which just has $(1,2,3)$ in the diagonal in this case.

A = b.diag()
print(A)

Operators can be arithmetically manipulated which always creates new instances.

B = 3*A - A/2 + 1
print(A) # stays the same
print(B)

Tensor products

Tensor products between different spaces are created by calling b1.tensor(b2). The two Basis objects have to have disjoint qn keys. Consequently, b2.tensor(b1) gives a different order in the basis but is effectively the same, since anyway the quantum numbers keep track of the basis elements. Let's create the space for a single particle on a 3-site lattice labelled by qn “i” with 2 internal degrees of freedom valued $(-1,0,+1)$ and labelled by qn “f”. If the basis is printed, all basis elements are listed with their respective qns collected in a dictionary.

bi = Basis("i", (1,2,3))
bf = Basis("f", (-1,0,1))
b1 = bi.tensor(bf)
b2 = bf.tensor(bi)
print(b1)
print(b2)

The operator $\hat A$ from before must then be extended to act on the full tensor space. This corresponds to the operator $\hat A\otimes \hat I$ on b1 and $\hat I\otimes \hat A$ on b2. The method extend(bx) with the respective extended basis bx then returns a new extension of an operator acting on the larger basis.

A = bi.diag()
print(A.extend(b1))
print(A.extend(b2))

We can also directly tensorize operators. For this define $\hat A$ on bi and $\hat B$ on bf and get $\hat A\otimes \hat B$ with A.tensor(B). The corresponding tensor basis is then automatically formed but can also be provided as an optional argument.

A = bi.diag()
B = bf.diag()
T = A.tensor(B)
print(T)
print(T.basis)

Note that this kind of tensor products of bases are called “one-particle basis” even though they might represent multiple, different and distinguishable particles, since they can equally well be understood as a single particle with multiple degrees-of-freedom (qns).

Distinguishable many-particle (qubit) systems

There are three different possibilities to form an $N$-fold tensor product of a given space: A full tensor product of all basis elements, a symmetric, or an antisymmetric tensor product. For the state space of distinguishable particles of the same kind the full tensor product is needed. If every such particle has just one internal degree of freedom with values $\pm 1$ (e.g. the spin in one fixed direction, always for Spin-$\tfrac{1}{2}$ particles and in the $z$-direction) then we talk about a qubit system. We first define the state space for a single system for which the basis class SpinBasis is provided and then form the $N$-fold tensor product with ntensor(N).

N = 2
b_single = SpinBasis()
b_many = b_single.ntensor(N)
print(b_many)

When printing this basis, we notice that the basis elements look differently now. They are given as a tuple where the first element always signifies the type of tensor product, where x stands for the full tensor product, s for symmetric, and a for antisymmetric. This is followed by a dictionary for the qn of each particle. If the $N$-particle basis is tensorized again, say $2$-fold, this just yields a $2N$ particle space.

b_many_many = b_many.ntensor(2)
print(b_many_many)

The method sigma_z() applied to the single-particle SpinBasis gives the $\hat\sigma_z$ Pauli operator that measures the spin in $z$-directions with eigenvalues $\pm 1$. We can extend this operator to the $N$-particle basis to form $\hat\sigma_z\otimes\hat I\otimes\hat I\otimes\ldots + \hat I\otimes\hat\sigma_z\otimes\hat I\otimes\ldots + \ldots$ which measures the total $z$-spin of all particles. The same operator is given by calling diag('s') on the many-particle basis, which sums all values of the given qn for each basis element and returns a diagonal operator.

Sz = b_single.sigma_z().extend(b_many)
print(Sz)
print(b_many.diag('s'))

Note that in this way it is impossible to address the individual qubits because they share the same qn label. To keep them addressable separately we have to create one SpinBasis for each particle and give them different labels, say s1 and s2, then form the tensor product. This way we can get sigma_z() for just one particle (e.g. the first) and subsequently extend this operator to the $2$-particle basis. The same result is achieved by calling diag('s1') on the $2$-particle basis.

b_spin1 = SpinBasis('s1')
b_spin2 = SpinBasis('s2')
b_many = b_spin1.tensor(b_spin2)
print(b_many)
print(b_spin1.sigma_z().extend(b_many))
print(b_many.diag('s1'))

Fermionic many-particle systems

If the particles are indistinguishable and described by an antisymmetric wavefunction (fermions) then the many-particle space must be formed with ntensor(N, 'a') or, as an alias, wedge(N). Again, our particles have just an internal spin degree-of-freedom and we construct the $N$-particle basis and an operator to measure total spin.

N = 2
b_single = SpinBasis()
b_many = b_single.wedge(N)
print(b_many)
print(b_many.diag('s'))

The result for $N=2$ is just a one-dimensional Hilbert space, since the only possible state is $\chi_+\otimes\chi_- - \chi_-\otimes\chi_+$. For $N=3$ the state space is even empty, since we just have two different internal states for each particle. We thus add a further degree-of-freedom to the individual particles, say a location labelled by x that has valued in $\{ 1,2,3 \}$. Now, the antisymmetric wedge product results in an 15-dimensional $2$-particle space.

b_single = Basis('x', (1,2,3)).tensor(SpinBasis())
print(b_single)
b_many = b_single.wedge(2)
print(b_many)

Notice, that like this we already created a spin-lattice system, yet without using the LatticeBasis class discussed below.

Bosonic many-particle systems

If a many-particle system is described by a symmetric wavefunction (bosons), the many-particle basis must be created with ntensor(N, 's'). With the SpinBasis from before (although a boson should not have Spin-$\tfrac{1}{2}$), a $2$-particle system then has three basis states, $\chi_+\otimes\chi_+$, $\chi_-\otimes\chi_-$, and $\chi_+\otimes\chi_- + \chi_-\otimes\chi_+$. We also print the operator that measures the total spin of the system.

N = 2
b_single = SpinBasis()
b_many = b_single.ntensor(N, 's')
print(b_many)
print(b_many.diag('s'))

We can also combine different symmetries in a single, larger space. Say we want to describe the interaction between two fermions with qn f that can take values $\{1,2,3\}$ and two bosons with qn b and values $\{1,2\}$. Then this is how to construct the space for the whole system.

b_fermion = Basis('f', (1,2,3)).wedge(2)
b_boson = Basis('b', (1,2)).ntensor(2, 's')
print(b_fermion.tensor(b_boson))

We see that the basis elements now get nested, with the outer combination of fermions and bosons marked with x (full tensor product), and then the basis elements for the antisymmetric and symmetric component systems.

Since usually the boson number is not fixed within a system, we should instead use a different description that models a Fock space. The qn b with values $\{1,2\}$ is replaced by two modes, i.e. bosons of different kinds, and each mode has the number of particles $\{0,1,2,\ldots\}$ as qn. This is implemented as NumberBasis and they are constructed by (optionally) setting the maximal number (cutoff, the maximal number is max_num-1) and the qn label. The bases for two different modes (with different labels) can then be combined into one space with the usual tensor product.

b1_boson = NumberBasis(max_num=5, qn_key='n1')
print(b1_boson)
b2_boson = NumberBasis(max_num=5, qn_key='n2')
b_two_modes = b1_boson.tensor(b2_boson)
print(b_two_modes)

The NumberBasis also implements creation and annihilation operators $\hat a^\dagger, \hat a$ and the operator for the displacement coordinate and its derivative, $\hat x = \tfrac{1}{\sqrt{2}}(\hat a^\dagger + \hat a), \hat \partial_x = \tfrac{1}{\sqrt{2}}(-\hat a^\dagger + \hat a)$. Finally, we test if $\hat a^\dagger\hat a$ gives the number operator, that we also get from diag(), as expected.

print(b1_boson.creator())
print(b1_boson.annihilator())
print(b1_boson.x_operator())
print(b1_boson.dx_operator())
print(b1_boson.creator()*b1_boson.annihilator())
print(b1_boson.diag())

Spin-lattice example

We create a basis for an $M$-site lattice and a separate one for the spin degree-of-freedom $s\in{\pm 1}$. Although this is easy to achieve with the discussed Basis class, we can use the specialized LatticeBasis and SpinBasis for this that offer additional functionality.

M = 3
b_lattice = LatticeBasis(M)
b_spin = SpinBasis()
b_onepart = b_lattice.tensor(b_spin) # one-particle space
print(b_onepart)

We then put $N=2$ fermionic particles on this lattice which means the space is spanned by all 2-fold antisymmetric combinations (combinatorial product without doubles) of the 1-particle space basis vectors. The method wedge(n) (antisymmetric tensor product, wedge product) provides this functionality and gives a space of dimension ${2M \choose N} = 15$.

N = 2
b = b_onepart.wedge(N)
print(b)

We now want to create an operator for the Hubbard Hamiltonian \begin{equation} \hat H = -t \sum_{i,s} \left( \hat a^\dagger_{i+1,s} \hat a_{i,s} + \text{h.c.} \right) + U \sum_i \hat n_{i\uparrow} \hat n_{i\downarrow}, \end{equation} where we assume a periodic chain, i.e., $i=M+1$ corresponds to $i=1$ again. Summation over all lattice sites is achieved with b_lattice.sum() that takes a callable as argument that itself has the qn as argument. For the sum over all lattice sites and spin states, b_onepart.sum() can be used instead.

b_lattice.sum(lambda qn: print(qn))
b_onepart.sum(lambda qn: print(qn))

The library allows us to combine the parts of the operator just like in mathematical notation, it is just important to create the operators on the full many-particle basis b. The operator $\hat a^\dagger_{i1,s1} \hat a_{i2,s2}$ is given by the method hop(qn1, qn2) (fermion one-body operator) where qnx = {'i': ix, 's': sx}. For $\hat n_{i,s} = \hat a^\dagger_{i,s} \hat a_{i,s}$ a single argument is enough, hop(qn) (fermion number operator). We create a little helper function that always gets the qn for the next site $i+1$ by changing just the index “i” in a dictionary. The method add_adj() adds the adjoint (hermitian conjugate) to an operator and returns the result.

t = 1
U = 1
next_site = lambda qn: dict(qn, i=qn['i']%M+1)
H = -t * b_onepart.sum(lambda qn: b.hop(next_site(qn), qn).add_adj()) \
    +U * b_lattice.sum(lambda qni: b.hop(dict(qni, s=1)) * b.hop(dict(qni, s=-1))) 
print(H)

The correct Fermi statistics is automatically obeyed when the operators are created on an antisymmetric tensor space.

Next, the method H.eig() gives the full spectrum and eigensystem of the Hamiltonian. Yet, here it is important to add the information that the operator is self-adjoint (hermitian) with H.eig(hermitian = True), so that only real eigenvalues can be expected and spectrum and eigensystem get sorted (lowest first). The information is stored in a dictionary from which we retrieve the ground-state energy and the ground-state vector as well as degeneracy information. The special method trace(b_lattice) can then be used to sum the modulus square of the vector coefficients over all quantum numbers except the ones of basis b_lattice (which can also be a single one). Consequently, trace(b_lattice) gives the lattice density of the ground state when applied to the ground-state vector.

E = H.eig(hermitian = True)
gs_energy = E['eigenvalues'][0]
gs_vector = E['eigenvectors'][0]
gs_degeneracy = E['degeneracy'][0]
gs_density = gs_vector.trace(b_lattice)
print(gs_energy)
print(gs_density)
print(gs_degeneracy)

The ground state for $N=2$ is non-degenerate, so for a Hamiltonian that is symmetric under lattice-site permutations the density is uniform. Next, add a non-uniform potential to get some variation. The potential is given as a list (NB: index starting at 0) and multiplied pointwise with the operator b.hop(qni) where now qni = {'i': *} is a dictionary just including the lattice index. This means the spin index gets automatically summed over all possibilities, thus b.hop({'i': 1}) corresponds to $\hat n_{1\uparrow}+\hat n_{1\downarrow}$.

pot = [0, -1, 0]
H = H + b_lattice.sum(lambda qni: pot[qni['i']-1] * b.hop(qni))
E = H.eig(hermitian = True)
gs_energy = E['eigenvalues'][0]
gs_vector = E['eigenvectors'][0]
gs_density = gs_vector.trace(b_lattice)
print(gs_energy)
print(gs_density)

The ground-state energy gets lowered, as expected, and the probability of finding a particle at the favorable lattice position $i=2$ is increased, while the other ones still have equal probability.

Operators can also be organized and manipulated as vectors. The SpinBasis offers a special method to create the Pauli-operator vector $\hat S = (\hat\sigma_x,\hat\sigma_y,\hat\sigma_z)$ where $\hat\sigma_*$ are the usual Pauli operators. We can use this to evaluate the spin expectation value of the ground state from the above example which correctly yields (approximately) zero in all three components, because no spin direction is favored by $\hat H$.

S = b_spin.sigma_vec().extend(b)
print(S.expval(gs_vector))

Dicke model example

In this Dicke model example, two qubits (distinguishable Spin-$\tfrac{1}{2}$ particles) are coupled to a single photon mode. The Hamiltonian is \begin{equation} \hat H = \hat a^\dagger \hat a + \frac{1}{2} - t (\hat\sigma_x^1 + \hat\sigma_x^2) + \lambda \hat x \otimes (\hat\sigma_z^1 + \hat\sigma_z^2). \end{equation} In the coupling term the displacement coordinate of the photon mode, measured by $\hat x$, couples to the total spin in $z$-direction. The coupling strength is controlled by $\lambda$ and we give a plot of the 10 lowest eigenvalues of $\hat H$ for different values of $\lambda$.

import numpy as np
import matplotlib.pyplot as plt 
 
max_photon_num = 10
b_mode = NumberBasis(max_num=max_photon_num)
# define qubits separately so operators can act separately on them
b1_spin = SpinBasis('s1')
b2_spin = SpinBasis('s2')
b = b_mode.tensor(b1_spin).tensor(b2_spin)
 
num_op = b_mode.diag().extend(b) # number operator for photons
x_op = b_mode.x_operator().extend(b)
sigma_z1 = b1_spin.sigma_z().extend(b)
sigma_z2 = b2_spin.sigma_z().extend(b)
sigma_x1 = b1_spin.sigma_x().extend(b)
sigma_x2 = b2_spin.sigma_x().extend(b)
 
t = 1
H0 = num_op + 1/2 - t*(sigma_x1 + sigma_x2)
coupling_op = x_op*(sigma_z1 + sigma_z2)
 
spec = []
lam_span = np.linspace(0,5,50)
for lam in lam_span:
    H = H0 + lam*coupling_op
    res = H.eig(hermitian = True)
    spec.append(res['eigenvalues'][0:10])
plt.plot(lam_span, spec, 'b')
plt.show()

API reference

Basis class

Properties

Basis.dim dimension, number of basis elements
Basis.qn_keys the labels of the used qns
Basis.part_num number of particles when tuples are used (only with symmetry)
Basis.part_basis reference to the particle basis, or self
Basis.symmetry symmetry marker (None | 'x' | 's' | 'a' )
Basis.el list of all elements (as dicts or tuples)

Methods

Basis([qn_key: str], [qn_range: Iterable]) → Basis

The constructor returns a basis object with a single qn. If qn_key or qn_range is not provided, an empty basis is returned. The qn_range must be an Iterable of type int or float that lists the possible values for the qn.

Basis.tensor(basis: Basis) → Basis

Return the tensor product of the basis object with another basis. This method cannot be used for a tensor product with the same basis or any basis with the same qn labels. For a tensor product with the same basis, Basis.ntensor() must be used.

Basis.ntensor(n: int, [symmetry]) → Basis

Return the $n$-fold tensor product of the basis object as a new basis. The optional argument symmetry allows to define a special symmetry for the new basis:

  • symmetry = 'x' (default): no special symmetry (distinguishable particles, qubits)
  • symmetry = 's' : symmetric space (bosons)
  • symmetry = 'a' : anti-symmetric space (fermions)

Basis.wedge(n: int) → Basis

Alias for Basis.ntensor(n, symmetry = 'a').

Basis.sum(func: Callable) → float

Iterates all basis elements, calls func (a Callable that returns a summable value) on each basis element (dictionary valued) and returns the sum of the results.

Basis.id() → Operator

Returns the identity operator in this basis.

Basis.diag([qn_key: str]) → Operator

Returns a diagonal operator $x\mapsto x$ for a given qn. In a many-particle basis the values of all single-particle basis elements are summed.

qn_key is a qn of the basis. If the basis has only a single qn then this argument can be omitted.

Basis.hop(qn1: dict, [qn2: dict]) → Operator

The method creates an one-body (hopping) operator with matrix $M_{ij}$ on the given basis $\{b_i\}$ after rules below.

If qn2 is not given then qn2 = qn1. qn1 and qn1 must contain the same qn keys.

  • One-particle basis: If qn1 is found in the qn of $b_i$ and qn2 is found in the qn of $b_j$ and the other qns all agree between $b_i$ and $b_j$ then $M_{ij}=1$, else $0$.
  • Symmetric many-particle basis: qn1 can be found in multiple particle sectors of $b_i$ and likewise qn2 in $b_j$, all combinations of those are iterated, the one-particle basis elements are compared like in the one-particle basis above, and the results added.
  • Antisymmetric many-particle basis: Same as in the symmetric case, but the sign is set $(-1)^{p_1+p_2}$, where $p_1$ and $p_2$ are the index of the respective particle sectors where qn1 and qn2 are found.
  • Non-symmetric many-particle basis: Same as in the symmetric case, but additionally those positions have to agree, $p_1=p_2$.

This means the qns can, but most not, fully qualify a one-particle basis element. If they do not then this method implies a summation over the remaining part of the qn. In the (anti-)symmetric case the hopping operator corresponds to $\hat a^\dagger_i\hat a_j$ if qn1 represents $b_i$ and qn2 represents $b_j$.

LatticeBasis class

Derived from Basis.

LatticeBasis(vertex_num: int, [qn_key: str])

The constructor returns a lattice basis object with vertex_num vertices numbered with a qn labelled qn_key from 1 onward. If qn_key is omitted, the qn label is “i”.

LatticeBasis.graph_laplacian(graph_edges: set, [qn_key: str], [hopping: float], [include_degree: bool]) → Operator

Return an operator for the graph Laplacian on this lattice basis. The graph is specified by a set of pairs (tuples) that define the edges on the graph. If the vertices are not labelled by “i”, then the qn_key must be provided.

hopping default is 1. This value is inserted at the position of the matrix where an edge connects two vertices.

include_degree default is True. If not False then the diagonal includes the negative degree of the vertex, i.e., the number of other vertices connecting to it.

Example: b.graph_laplacian({(1,2), (2,3), (1,3)})

Note that in graph theory usually an opposite sign convention is employed. We used this definition such that the negative graph Laplacian fulfills the same semi-positivity condition as the continuum Laplacian.

LatticeBasis.graph_pot(pot: dict|list|ndarray, [qn_key: str]) → Operator

Return a diagonal operator for a potential on a lattice (graph).

pot can be of type dict, list, or NumPy type ndarray. A dict must be of form {1: p1, 2: p2, …} where pi is the potential acting on vertex i. Alternatively, it can be a list/one-dimensional array [p1, p2, …].

qn_key default is “i” and contains the corresponding qn label for the vertices.

VacuumBasis class

Derived from Basis.

VacuumBasis([qn_key: str])

The constructor returns a basis for the vaccum which is just the one-dimension Hilbert space $\mathbb{C}$. If qn_key is omitted, the qn label is “vac”.

NumberBasis class

Derived from Basis.

NumberBasis(max_num: int, [qn_key: str])

The constructor returns a basis for the single-mode Fock space with number basis $|0\rangle, |1\rangle, |2\rangle, \ldots$ up to max_num-1. If qn_key is omitted, the qn label is “n”.

NumberBasis.creator() → Operator

Return the creation operator $\hat a^\dagger$ for the number basis.

NumberBasis.annihilator() → Operator

Return the annihilation operator $\hat a$ for the number basis.

NumberBasis.x_operator() → Operator

Return the operator for the displacement coordinate, $\hat x = \tfrac{1}{\sqrt{2}}(\hat a^\dagger + \hat a)$.

NumberBasis.dx_operator() → Operator

Return the operator for the derivative w.r.t. the displacement coordinate, $\hat \partial_x = \tfrac{1}{\sqrt{2}}(-\hat a^\dagger + \hat a)$.

SpinBasis class

Derived from Basis.

SpinBasis([qn_key: str])

The constructor returns a basis for a Spin-$\tfrac{1}{2}$ particle with qn values $\{+1,-1\}$ (in that order; spin measured in $z$-direction). If qn_key is omitted, the qn label is “s”.

SpinBasis.sigma_x() → Operator

Return the $\hat\sigma_x = \begin{pmatrix}0 & 1 \\ 1 & 0\end{pmatrix}$ Pauli operator.

SpinBasis.sigma_y() → Operator

Return the $\hat\sigma_y = \begin{pmatrix}0 & -\mathrm{i} \\ \mathrm{i} & 0\end{pmatrix}$ Pauli operator.

SpinBasis.sigma_z() → Operator

Return the $\hat\sigma_z = \begin{pmatrix}1 & 0 \\ 0 & -1\end{pmatrix}$ Pauli operator.

SpinBasis.sigma_vec() → OperatorList

Return an OperatorList object containing the vector $(\hat\sigma_x,\hat\sigma_y,\hat\sigma_z)$.

SpinBasis.sigma_plus() → Operator

Return the operator $\hat\sigma_+ = \begin{pmatrix}0 & 1 \\ 0 & 0\end{pmatrix}$.

SpinBasis.sigma_minus() → Operator

Return the operator $\hat\sigma_- = \begin{pmatrix}0 & 0 \\ 1 & 0\end{pmatrix}$.

SpinBasis.proj_plus() → Operator

Return the projection operator on the $+1$ eigenspace, $\hat P_+ = \begin{pmatrix}1 & 0 \\ 0 & 0\end{pmatrix}$.

SpinBasis.proj_minus() → Operator

Return the projection operator on the $-1$ eigenspace, $\hat P_- = \begin{pmatrix}0 & 0 \\ 0 & 1\end{pmatrix}$.

Vector class

Operators

The following operators are overloaded to work with Vector objects:
* with scalars from left/right
(See also overloading in the Operator class.)

Properties

Vector.basis reference to the basis of the vector
Vector.col NumPy ndarray that stores the numerical values of the vector

Methods

Vector(basis: Basis, [col: list|ndarray])

The constructor returns a vector object belonging to basis that can be optionally initialized with col of type list or NumPy type ndarray (one-dimensional).

Vector.copy() → Vector

Return a copy of the object.

Vector.inner(X: Vector) → complex

Return the inner product of the vector with another vector. Note that the two vectors need to belong to the same basis and that the entries from the object the method is called on get complex conjugated.

Vector.prob() → ndarray

Return the components $|x_i|^2$ (probabilities) of the vector as an array.

Vector.norm() → float

Return the $2$-norm of the vector.

Vector.trace(subbasis: Basis) → ndarray

Trace out all qn except those contained in subbasis, which has to be a “primitive” basis without any special symmetry (elements are only dicts), and return a density w.r.t. subbasis.

Example: Let lb be a LatticeBasis and X a vector on a many-particle basis that includes particles on the lattice (possibly with additional degrees-of-freedom). Then X.trace(lb) gives the density on the lattice.

Operator class

Operators

The following operators are overloaded to work with Operator objects:
- for sign change
+/- addition and subtraction between operators and between operators and scalars (times identity) from left/right
* multiplication between operators (non-commutative)
* multiplication between operators and scalars from left/right
* multiplication between operator and vector (from left)
* multiplication between operators and lists of scalars (from left/right) $(a_i)_i$ give an OperatorList object $(a_i \hat A)_i$
/ division of an operator by a scalar
[double asterisk] integer power of an operator
(Note that operations only work between operators (and vectors) defined on the same basis, this means the same instance of an Basis object.)

Properties

Operator.basis reference to the basis of the operator
Operator.matrix NumPy ndarray that stores the numerical matrix values of the operator

Methods

Operator(basis: Basis)

The constructor returns an operator object belonging to basis with a zero matrix.

Operator.copy() → Operator

Return a copy of the object.

Operator.conj() → Operator

Return an operator that is the complex conjugate.

Operator.T() → Operator

Return an operator that is the transpose.

Operator.adj() → Operator

Return an operator that is the Hermitian adjoint (complex conjugate and transpose).

Operator.add_adj() → Operator

Return an operator where the Hermitian adjoint is added, $\hat A + \hat A^\dagger$.

Operator.comm(A: Operator) → Operator

Return the commutator of the operator with another operator (first comes the operator where this method is called upon).

Operator.acomm(A: Operator) → Operator

Return the anticommutator of the operator with another operator.

Operator.expval(vec: Vector, [check_real: bool], [transform_real: bool]) → float|complex

Return the expectation value of the operator w.r.t. the given vector.

check_real: If True returns an error if the result has an imaginary part. Default is False.

transform_real: If True the result just returns the real part. This can be useful to also discard a +0*1j or very small imaginary part. Default is False.

Operator.norm() → float

Return the 2-norm (largest singular value) of an operator.

Operator.eig([hermitian: bool]) → dict

Get the eigenvalues and the eigenvectors of the operator. Returns a dictionary with keys 'eigenvalues' and 'eigenvectors' where the first is a NumPy ndarray and the second a list of Vector objects.

If hermitian is set to True (default is False) then a self-adjoint operator is assumed and the eigenvalues are returned in ascending order. Also, the returned dictionary includes a further key 'degeneracy' which is a NumPy ndarray with the dimension of the eigenspaces ordered after the eigenvalues (e.g. [2,1,3] means a 2-fold degenerate groundstate, a non-degenerate 1st excited state and a 3-fold degenerate 2nd excited state). Note that even if hermitian is set to True the operator is not checked for self-adjointness (see method test_hermiticity()).

Operator.test_hermiticity()

Raises an error if the operator is not self-adjoint (hermitian). No return value.

Operator.diag() → dict

The method first checks for self-adjointness, then returns the diagonal of the diagonalized operator $\hat D$ and the corresponding transformation matrix $\hat U$ as NumPy ndarray in a dictionary with keys 'diag' and 'transform' . The relation to the operator is $\hat D = \hat U \hat A \hat U^\dagger$.

Operator.extend(ext_basis: Basis) → Operator

Extend the operator to a larger basis that includes the previous qn. If $\hat A$ was defined on $V$ and is extended to $V\otimes W$, the returned operator is $\hat A\otimes\hat I$.

Operator.tensor(A: Operator|OperatorList, [tensor_basis: Basis]) → Operator|OperatorList

Tensor product of the operator with another one. If the argument is an OperatorList then the tensor product is taken with every operator in the list and an OperatorList is returned.

tensor_basis is an optional argument to fix a basis for the returned operator. If it is not given, a new basis is constructed as a tensor product itself. It might be necessary to specify tensor_basis in many cases, since the new operator should be defined on a particular basis object.

OperatorList class

Operators

The following operators are overloaded to work with OperatorList objects:
- for sign change of all operators in the list (vector)
+/- vector addition and subtraction between operator lists and between operator lists and scalars (in all components) from left/right
* inner product between two operator lists (non-commutative) or between an operator list and a list of numbers or ndarray
* multiplication between operator list and scalars (in all components) from left/right
/ division of the operator list by a scalar (in all components)
len() gives the number of operators in the list
[i] returns the ith operator in the list

Properties

OperatorList.basis reference to the basis of the operator list (all operators in the list must have the same basis)
OperatorList.operators the list of Operator objects

Methods

OperatorList(basis: Basis, [operators: list])

The constructor returns an operator list belonging to basis that can already be initialized with a given list of Operator objects.

OperatorList.append(operators: Operator|list)

Append an operator or all operators in a list to the operator list.

OperatorList.copy() → OperatorList

Return a copy of the object.

OperatorList.conj() → OperatorList

Return an operator list with each operator complex conjugated.

OperatorList.T() → OperatorList

Return an operator list with each operator transposed.

OperatorList.adj() → OperatorList

Return an operator list with each operator Hermitian adjoined (complex conjugated and transposed).

OperatorList.expval(vec: Vector, [check_real: bool], [transform_real: bool]) → float|complex

Return the expectation values of each operator in the list w.r.t. the given vector as a NumPy ndarray. Flags are like in Operator.expval().

OperatorList.norm() → float

Return the Euclidean norm of the list of operators, $\sqrt{\sum_i \|\hat A_i\|^2}$.

OperatorList.sum() → Operator

Return the sum of all operators in the list, $\sum_i \hat A_i$.

OperatorList.diag([trials: int]) → dict

Simultaneous diagonalization of the operators in the list. Since this method uses randomization, a numer of trials can be specified. If the method is not successful in any trial, an error is raised. Returns the diagonals of the diagonalized operators as a list and the corresponding transformation matrix as NumPy ndarray in a dictionary with keys 'diag' and 'transform' . The relation to the operators is $\hat D_i = \hat U \hat A_i \hat U^\dagger$.

OperatorList.extend(ext_basis: Basis) → OperatorList

Extend each operator in the list to a larger basis that includes the previous qn.

Operator.tensor(A: Operator, [tensor_basis: Basis]) → OperatorList

Tensor product of each operator in the list with another operator that returns an OperatorList. The optional argument tensor_basis is like for Operator.tensor().