2. Generating Objects

2.1. States

2.2. Operators

Gate operators:

Most of these are cached (and immutable), so can be called repeatedly without creating any new objects:

>>> pauli('Z') is pauli('Z')

Hamiltonians and related operators:


The Hamiltonians are generally defined using spin operators rather than Pauli matrices. Thus for example, the following spin-1/2 Hamiltonians would be equivalent

  • in spin-operators:

\[ \hat{H} = \sum J S^X_i S^X_{i + 1} + B S^Z_i \]
  • and in Pauli operators (with \(S^X=\dfrac{\sigma^X}{2}\) etc.):

\[ \hat{H} = \sum \dfrac{J}{4} \sigma^X_i \sigma^X_{i + 1} + \dfrac{B}{2} \sigma^Z_{i} \]

note that interaction terms are scaled different than the single site terms.

2.3. Random States & Operators

Random pure states:

Random operators:

All of these functions accept a seed argument for replicability:

>>> rand_rho(2, seed=42)
qarray([[ 0.196764+7.758223e-19j, -0.08442 +2.133635e-01j],
        [-0.08442 -2.133635e-01j,  0.803236-2.691589e-18j]])

>>> rand_rho(2, seed=42)
qarray([[ 0.196764+7.758223e-19j, -0.08442 +2.133635e-01j],
        [-0.08442 -2.133635e-01j,  0.803236-2.691589e-18j]])

For some applications, generating random numbers with a single thread can be a bottleneck, though since version 1.17 numpy itself enables parallel streams of random numbers to be generated. quimb handles setting up the bit generators and multi-threading the creation of random arrays, with potentially large performance gains. While the random number sequences can be still replicated using the seed argument, they also depend (deterministically) on the number of threads used, so may vary across machines unless this is set (e.g. with 'OMP_NUM_THREADS').


Previously, randomgen was needed for this functionality, and its bit generators can still be specified to set_rand_bitgen() if installed.

The following gives a quick idea of the speed-ups possible. First random, complex, normally distributed array generation with a naive numpy method:

>>> import numpy as np
>>> %timeit np.random.randn(2**22) + 1j * np.random.randn(2**22)
394 ms ± 2.93 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

And generation with quimb:

>>> import quimb as qu
>>> %timeit qu.randn(2**22, dtype=complex)
45.8 ms ± 2.08 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

>>> # try a randomgen bit generator
>>> qu.set_rand_bitgen('Xoshiro256')
>>> %timeit qu.randn(2**22, dtype=complex)
41.2 ms ± 2.02 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

>>> # use the default numpy bit generator
>>> qu.set_rand_bitgen(None)