Time Evolution
Contents
7. Time Evolution#
Time evolutions in quimb
are handled by the class Evolution
, which is initialized with a starting state and hamiltonian.
7.1. Basic Usage#
Set up the Evolution
object with a initial state and hamiltonian.
[1]:
from quimb import *
p0 = rand_ket(2**10)
h = ham_heis(10, sparse=True)
evo = Evolution(p0, h)
Update it in a single shot to a new time and get the state,
[2]:
evo.update_to(1)
evo.pt
[2]:
[[ 0.001538+0.012379j]
[-0.00541 +0.02611j ]
[-0.012026+0.015335j]
...
[-0.041381+0.02054j ]
[-0.004961-0.001546j]
[-0.019127+0.002967j]]
Lazily generate the state at multiple times:
[3]:
for pt in evo.at_times([2, 3, 4]):
print(expec(pt, p0))
0.0005106405568608293
0.009995197474010302
0.00525130789898347
7.2. Methods of Updating#
There are three methods of updating the state:
Evolution(..., method='integrate')
: use definite integration. Get system at each time step, only need action of Hamiltonian on state. Generally efficient. For pure and mixed states. The additional optionint_small_step={False, True}
determines whether a low or high order adaptive stepping scheme is used, giving naturally smaller or larger times steps. Seescipy.integrate.ode
for details,False
corresponds to"dop853"
,True
to"dopri5"
.
Evolution(..., method='solve')
. Diagonalize the hamiltonian, which once done, allows quickly updating to arbitrary times. Supports pure and mixed states, recomended for small systems.
Evolution(..., method='expm')
: compute the evolved state using the action of the matrix exponential in a ‘single shot’ style. Only needs action of Hamiltonian, for very large systems can use distributed MPI. Only for pure states.
7.3. Computing on the fly#
Sometimes, if integrating, it is best to just query the state at time-steps chosen dynamically by the adaptive scheme. This is achieved using the compute
keyword supplied to Evolution
. It can also just be a convenient way to set up calculations as well:
[4]:
p0 = rand_product_state(10)
h = ham_heis(10, sparse=True)
dims = [2] * 10
sysa, sysb = (0, 1), (2, 3)
def calc_t_and_logneg(t, pt):
ln = logneg_subsys(pt, dims, sysa, sysb)
return t, ln
evo = Evolution(p0, h, compute=calc_t_and_logneg, progbar=True)
evo.update_to(1)
ts, lns = zip(*evo.results)
100%|##########| 100/100 [00:00<00:00, 6697.06%/s]
[5]:
ts
[5]:
(0.0,
0.2312290367760555,
0.4377093533282588,
0.6399835221505082,
0.8549423071920399,
1.0)
[6]:
lns
[6]:
(0.0,
0.16992190183490394,
0.2865537529923966,
0.3808698480199881,
0.4716243573297176,
0.5295261025477127)
If a dict of callables is supplied to compute
, (each should take two arguments, the time, and the state, as above), Evolution.results
will itself be a dictionary containing the results of each function at each time step, under the respective key. This can be more convenient:
[7]:
def calc_t(t, _):
return t
def calc_logneg(_, pt):
return logneg_subsys(pt, [2]*10, 0, 1)
evo = Evolution(p0, h, compute={'t': calc_t, 'ln': calc_logneg}, progbar=True)
evo.update_to(1)
100%|##########| 100/100 [00:00<00:00, 4243.44%/s]
[8]:
evo.results
[8]:
{'t': [0.0,
0.2312290367760555,
0.4377093533282588,
0.6399835221505082,
0.8549423071920399,
1.0],
'ln': [0.0,
0.15268909733170494,
0.2827184207625697,
0.40032861740248604,
0.5081441062956807,
0.5669364519710873]}
7.4. Time-Dependent Evolutions#
If you are using method='integrate'
you can supply a callable to ham
to evolve the state with a time dependent Hamiltonian. It should take a single argument t
and return the Hamiltonian at the time. It probably makes sense to use a custom class here to avoid reconstructing as much of the Hamiltonian as possible at each step.
Here we’ll evolve the Neel state:
with the Hamiltonian:
[9]:
class MyTimeDepIsingHam:
def __init__(self, L):
self.h_interaction = ham_ising(L, sparse=True, jz=1.0, bx=0.0, cyclic=False)
self.h_field = ham_ising(L, sparse=True, jz=0.0, bx=1.0, cyclic=False)
def __call__(self, t):
return self.h_interaction + cos(t) * self.h_field
[10]:
L = 16
# our initial state
psi0 = neel_state(L)
# instantiate the ham object, it's __call__ method will be used by Evolution
fn_ham_t = MyTimeDepIsingHam(L)
We still want to compute some properties during the evolution:
[11]:
compute = {
'time': lambda t, p: t,
'entropy': lambda t, p: entropy_subsys(p, dims=[2] * L, sysa=range(L // 2))
}
Now we set up the evolution object again:
[12]:
evo = Evolution(psi0, fn_ham_t, progbar=True, compute=compute)
[13]:
evo.update_to(10)
100%|##########| 100/100 [00:11<00:00, 8.77%/s]
We can plot the half chain entropy that we computed on the fly:
[14]:
%matplotlib inline
from matplotlib import pyplot as plt
plt.plot(evo.results['time'], evo.results['entropy'])
[14]:
[<matplotlib.lines.Line2D at 0x7f2501956fd0>]

Or we can use the final state:
[15]:
fidelity(psi0, evo.pt)
[15]:
0.003302180752068526