-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathevolution.py
492 lines (444 loc) · 21.1 KB
/
evolution.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
"""Contains functions for creating initial and equilibrium density
matrices and evolving them in time."""
from scipy import linalg, constants
import numpy as np
from qutip.nonmarkov.heom import HSolverDL
from qutip import Qobj
from quantum_heom import utilities as util
TEMP_INDEP_MODELS = ['local dephasing lindblad']
TEMP_DEP_MODELS = ['global thermalising lindblad',
'local thermalising lindblad',
'HEOM']
DYNAMICS_MODELS = TEMP_INDEP_MODELS + TEMP_DEP_MODELS
def initial_density_matrix(dims: int, init_site_pop: list) -> np.ndarray:
"""
Returns an N x N 2D array corresponding to the density matrix
of the system at time t=0, where N is the number of sites. Site
populations are split equally between the sites specified in
'init_site_pop'.
Parameters
----------
dims : int
The dimension (i.e. the number of sites) of the quantum
system.
init_site_pop : list of int
The sites in which to place initial population. For
example, to place population in a superposition of sites 1
and 6 (in a 7-site system), the user should pass [1, 6].
To place twice as much initial population in 3 as in 4 pass
[3, 3, 4]. Default value is [1], which populates only site 1.
Example: user passes for a N=3 site system [1, 3].
Superposition state given by (1 / sqrt(2)) (|1> + |3>).
Density matrix given by outer product of this state:
rho (0) = (1/2) (|1><1| + |1><6| + |6><1| + |6><6|)
Note that there will be coherences present in the inital
state.
Returns
-------
np.ndarray
N x N 2D array (where N is the number of sites)
for the initial density matrix.
"""
assert isinstance(dims, int), 'dims must be passed as an int'
assert dims > 1, 'Must specify the dimensions as at least 2.'
assert isinstance(init_site_pop, list), 'init_site_pop must be a list.'
for site in init_site_pop:
if site < 1 or site > dims:
raise ValueError('Invalid site number.')
num_states = len(init_site_pop)
normalisation = 1. / np.sqrt(num_states)
# Construct the base states
state = np.zeros(dims)
for site in init_site_pop:
state[site - 1] += 1
state *= normalisation
return np.outer(state, state)
def equilibrium_state(dynamics_model: str, dims: int, hamiltonian: np.ndarray,
temperature: float) -> np.ndarray:
"""
Returns the equilibirum density matrix of the system. For
systems described by thermalising models, this is the
themal equilibirum state, given by:
.. math::
\\rho^{(eq)}
= \\frac{e^{- H / k_B T}}{tr(e^{- H / k_B T})}
however non-thermalising models ('local dephasing lindblad'),
this is the maximally mixed state corresponding to a diagonal
matrix with elements equal to 1/N. This also corresponds to the
thermal equilibrium state in the infinite temperature limit.
Parameters
----------
dynamics_model : str
The model used to describe the system dynamics. Must be
one of 'local dephasing lindblad', 'local thermalising
lindblad', 'global thermalising lindblad', or 'HEOM'.
dims : int
The dimension (i.e. the number of sites) of the quantum
system.
hamiltonian : np.ndarray
The system Hamiltonian for the open quantum system, with
dimensions (dims x dims), in units of rad ps^-1. Only
needs to be passed if dynamics_model is a thermalising
model.
temperature : float
The temperature of the bath, in Kelvin. Need only be
passed if dynamics_model is a thermalising model.
Returns
-------
np.ndarray
A 2D square density matrix for the system's equilibrium
state.
"""
assert dynamics_model in DYNAMICS_MODELS, (
'Must choose a dynamics_model from ' + str(DYNAMICS_MODELS))
if dynamics_model in TEMP_DEP_MODELS:
assert isinstance(hamiltonian, np.ndarray) and (
hamiltonian.shape[0] == hamiltonian.shape[1] == dims), (
'Hamiltonian must be a square np.ndarray with same dimensions'
' as specified in "dims", units of rad ps^-1')
assert isinstance(temperature, float) and temperature > 0., (
'Must pass temperature as a positive float for thermalising models'
' in Kelvin.')
if dynamics_model in TEMP_INDEP_MODELS:
# Maximally-mixed state for dephasing model:
return np.eye(dims, dtype=complex) * 1. / dims
eigv, eigs = util.eigv(hamiltonian), util.eigs(hamiltonian)
eq_values = np.zeros_like(eigv)
for idx, _ in enumerate(eq_values):
eq_values[idx] = np.exp(- eigv[idx] * 1e12 * constants.hbar
/ (constants.k * temperature))
# Divide each exponentiated eigenvalue by the total sum.
# Forms a vector representation of the diagonalised equilibrium state,
# in the eigenbasis.
eq_values = eq_values / np.sum(eq_values)
# Diagonalize into a sqaure matrix
eq_values = np.diag(eq_values)
# Convert from the eigenbasis to site basis.
return util.basis_change(eq_values, eigs, False)
def evolve_matrix_one_step(dens_mat: np.ndarray, superop: np.ndarray,
time_interval: float) -> np.ndarray:
"""
Evolves a density matrix at time t to time (t + dt), where dt
is specified by 'time_interval', using an exponential
propagator, given by:
\\rho(t + dt) = exp(superop * dt) \\rho(t)
Typically, 'superop' will be the sum of Hamiltonian and
Lindbladian superoperators. Assumes quantities passed in
correct and consistent units; i.e. that the superop is in
angular frequency units of rad ps^-1, and the time_interval
is in time units ps.
Parameters
----------
dens_mat : np.ndarray
The density matrix to evolve forward in time.
superop : np.ndarray
The superoperator that governs the dynamics of the quantum
system, typically formed from the sum of Hamiltonian and
Lindbladian superoperators.
time_interval : float
The step forward in time to which the density matrix
will be evolved.
Returns
-------
evolved : np.ndarray
The input density matrix evolved forward in time by
time_interval.
"""
assert isinstance(dens_mat, np.ndarray), 'Input matrix must be a np.ndarray'
dims = dens_mat.shape[0]
assert dims == dens_mat.shape[1], 'Input matrix must be square'
assert isinstance(superop, np.ndarray), 'Superoperator must be a np.ndarray'
dims = int(np.sqrt(superop.shape[0]))
# Build the N^2 x N^2 propagator
propa = linalg.expm(superop * time_interval)
# Propagate vectorised density matrix
evolved = np.matmul(propa, dens_mat.flatten('C'))
# Reshape back to square and return
return evolved.reshape((dims, dims), order='C')
def time_evo_lindblad(dens_mat: np.ndarray, superop: np.ndarray,
timesteps: int, time_interval: float,
dynamics_model: str, hamiltonian: np.ndarray,
temperature: float) -> np.ndarray:
"""
Evaluates the time evolution of a starting density matrix over
multiple time steps for the Lindblad models. Returns an array
containing the times, density matrices at each timestep, and
the trace of the density matrix squared and/or the trace
distance at each step, if specified in 'trace_measure'. Assumes
quantities passed in correct and consistent units; i.e. that
the superop is in angular frequency units of rad ps^-1, and the
time_interval is in time units ps.
Parameters
----------
dens_mat : np.ndarray
The initial density matrix to evolve forward in time.
superop : np.ndarray
The superoperator that governs the dynamics of the quantum
system, in units of rad ps^-1. Typically formed from the
sum of Hamiltonian and Lindbladian superoperators.
timesteps : int
The number of timesteps over which to evaluate the density
matrix.
time_interval : float
The step forward in time to which the density matrix
will be evolved, in fs.
dynamics_model : str
The model used to describe the system dynamics. Must be one
of 'local dephasing lindblad','local thermalising
lindblad', 'global thermalising lindblad'.
hamiltonian : np.ndarray
The system Hamiltonian for the open quantum system, with
dimensions (dims x dims), in rad ps^-1. Only needs to be
passed if dynamics_model is a thermalising model.
temperature : float
The temperature of the bath, in K. Need only be passed if
dynamics_model is a thermalising model.
Returns
-------
np.array
An array where each element corresponds to a timestep in the
evolution of the density matrix, containing the following
info, respectively; time, density matrix at time, trace
squared, trace distance.
"""
# Check inputs
assert isinstance(dens_mat, np.ndarray), 'Input matrix must be a np.ndarray'
dims = dens_mat.shape[0]
assert dims == dens_mat.shape[1], 'Input matrix must be square'
assert isinstance(superop, np.ndarray), 'Superoperator must be a np.ndarray'
assert superop.shape[0] == superop.shape[1], 'Superoperator must be square'
assert superop.shape[0] == dims**2, (
'Superoperator dimensions must be the square of the density matrix'
' dims.')
assert isinstance(timesteps, int), 'timesteps must be passed as an int.'
assert isinstance(time_interval, float), 'time_interval must be a float.'
assert isinstance(dynamics_model, str), (
'Must provide the dynamics model to calculate the trace'
' distance.')
if dynamics_model in TEMP_DEP_MODELS:
assert isinstance(hamiltonian, np.ndarray), (
'Must provide the system Hamiltonian as a numpy ndarray to'
' calculate the trace distance for thermalising models.')
assert isinstance(temperature, float), (
'Must provide the temperature as a positive float in order to'
' calculate the trace distance for thermalising models.')
# Convert time fs --> ps to match superoperator units
time_interval = time_interval * 1e-3
# Produce time evolution data
time, evolved = 0., dens_mat
squared = util.trace_matrix_squared(evolved)
eq_state = equilibrium_state(dynamics_model, dims, hamiltonian, temperature)
distance = util.trace_distance(evolved, eq_state)
evolution = np.empty(timesteps + 1, dtype=np.ndarray)
evolution[0] = np.array([time, evolved, squared, distance])
for step in range(1, timesteps + 1):
time += time_interval
evolved = evolve_matrix_one_step(evolved, superop, time_interval)
evolved = util.renormalise_matrix(evolved)
squared = util.trace_matrix_squared(evolved)
eq_state = equilibrium_state(dynamics_model, dims,
hamiltonian, temperature)
distance = util.trace_distance(evolved, eq_state)
# Add quantities in quantum_HEOM units; i.e. convert time back ps --> fs
evolution[step] = np.array([time * 1e3, evolved, squared, distance])
return evolution
def time_evo_heom(dens_mat: np.ndarray, timesteps: int, time_interval: float,
hamiltonian: np.ndarray, coupling_op: np.ndarray,
reorg_energy: float, temperature: float, bath_cutoff: int,
matsubara_terms: int, cutoff_freq: float,
matsubara_coeffs: np.ndarray, matsubara_freqs: np.ndarray
) -> tuple:
"""
Evaluates the time evolution of a starting density matrix over
multiple time steps for the HEOM model, interfacing with
QuTiP's in-built HEOMSolver. Returns a 3-ple (tuple) containing
the time evolution data, the matsubara coefficients, and the
matsubara frequencies (respectively) used in the calculation.
Parameters
----------
dens_mat : np.ndarray
The initial density matrix to evolve forward in time.
timesteps : int
The number of timesteps over which to evaluate the density
matrix.
time_interval : float
The step forward in time to which the density matrix
will be evolved, in units of ps.
hamiltonian : np.ndarray
The system Hamiltonian for the open quantum system, with
dimensions (dims x dims), in units of rad ps^-1.
coupling_op : np.ndarray
The coupling operator for the system-bath interaction.
reorg_energy : float
The factor by which the spectral density should be scaled
by. Should be passed in units of rad ps^-1. Must be a
non-negative float.
temperature : float
The temperature of the bath, in units of rad ps^-1.
bath_cutoff : int
The number of bath terms to include in the HEOM evaluation
of the system dynamics.
mastsubara_terms : int
The number of matsubara terms to include in the HEOM
evaluation of the system dynamics.
cutoff_freq : float
The cutoff frequency used in calculating the spectral
density, in units of rad ps^-1.
matsubara_coeffs : np.ndarray
The matsubara coefficients c_k used in calculating the
spectral density for the HEOM approach. Must be in
order (largest -> smallest), where the nth coefficient
corresponds to the nth matsubara term. If None QuTiP's
HEOMSolver automatically generates them.
matsubara_freqs: np.ndarray
The matsubara frequencies v_k used in calculating the
spectral density for the HEOM approach, in units of rad
ps^-1.
Must be in order (smallest -> largest), where the nth
frequency corresponds to the nth matsubara term. If None;
QuTiP's HEOMSolver automatically generates them.
Returns
-------
np.array
An array where each element corresponds to a timestep in the
evolution of the density matrix, containing the following
info, respectively; time, density matrix at time, trace
squared, trace distance.
"""
assert isinstance(dens_mat, np.ndarray), 'Input matrix must be a np.ndarray'
dims = dens_mat.shape[0]
assert dims == dens_mat.shape[1], 'Initial density matrix must be square.'
assert isinstance(timesteps, int), 'timesteps must be passed as an int.'
assert isinstance(time_interval, float), 'time_interval must be a float.'
assert (isinstance(hamiltonian, np.ndarray)
and hamiltonian.shape[0] == hamiltonian.shape[1]
and hamiltonian.shape[0] == dims), (
'Must provide the system Hamiltonian as a square np.ndarray'
' with dimensions matching the density matrix.')
assert (isinstance(coupling_op, np.ndarray)
and coupling_op.shape[0] == coupling_op.shape[1]
and coupling_op.shape[0] == dims), (
'Must provide the coupling operator as a square np.ndarray'
' with dimensions matching the density matrix.')
assert (isinstance(reorg_energy, (int, float)) and reorg_energy >= 0.), (
'Must provide the coupling strength of the system as a positive float.')
assert (isinstance(temperature, (int, float)) and temperature > 0.), (
'Must provide the temperature of the system as a positive float.')
assert (isinstance(bath_cutoff, int) and bath_cutoff >= 0), (
'Bath cutoff must be a positive int')
assert (isinstance(matsubara_terms, int) and matsubara_terms >= 0), (
'matsubara_terms must be a positive int')
assert (isinstance(cutoff_freq, (int, float)) and cutoff_freq > 0.), (
'Must provide the cutoff_freq as a positive float.')
if matsubara_coeffs is not None:
check = [isinstance(i, (float, complex)) for i in matsubara_coeffs]
assert (isinstance(matsubara_coeffs, np.ndarray)
and check.count(True) == len(check)), (
'matsubara_coeffs must be passed as a np.ndarray with all'
' elements as positive floats.')
if matsubara_freqs is not None:
check = [isinstance(i, (float, complex)) for i in matsubara_freqs]
assert (isinstance(matsubara_freqs, np.ndarray)
and check.count(True) == len(check)), (
'matsubara_freqs must be passed as a np.ndarray with all'
' elements as floats.')
# Build HEOM Solver
hsolver = HSolverDL(Qobj(hamiltonian), # rad ps^-1
Qobj(coupling_op),
reorg_energy, # rad ps^-1
temperature, # rad ps^-1
bath_cutoff,
matsubara_terms,
cutoff_freq, # ps^-1
planck=1.0,
boltzmann=1.0,
renorm=True,
stats=False)
# Set the matsubara coeffs and freqs to those passed (if actually passed)
if matsubara_coeffs is not None:
hsolver.exp_coeff = matsubara_coeffs
if matsubara_freqs is not None:
hsolver.exp_freq = matsubara_freqs
# Run the simulation over the time interval.
times = np.array(range(timesteps + 1)) * time_interval # ps
result = hsolver.run(Qobj(dens_mat), times)
# CONVERT BACK TO QUANTUM_HEOM UNITS
conv_kelvin_to_rad_per_ps = constants.k / (constants.hbar * 1e12)
temperature = temperature / conv_kelvin_to_rad_per_ps # Kelvin
hamiltonian = hamiltonian # rad ps^-1
# RETRIEVE THERMAL EQUILIBRIUM STATE
# equilibrium_state() method requires Hamiltonian in rad ps^-1 and T in K
eq_state = equilibrium_state('HEOM', dims, hamiltonian, temperature)
# PROCESS TIME EVOLUTION DATA
evolution = np.empty(len(result.states), dtype=np.ndarray)
for i in range(0, len(result.states)):
dens_matrix = np.array(result.states[i]).T
dens_matrix = util.renormalise_matrix(dens_matrix)
evolution[i] = np.array([float(result.times[i]) * 1e3, # ps --> fs
dens_matrix,
util.trace_matrix_squared(dens_matrix),
util.trace_distance(dens_matrix, eq_state)])
return evolution, np.array(hsolver.exp_coeff), np.array(hsolver.exp_freq)
def process_evo_data(time_evolution: np.array, elements: [list, None],
trace_measure: list):
"""
Processes a QuantumSystem's time evolution data as produced
by its time_evolution() method. Returns the time, matrix,
and trace measure (trace of the matrix squared, and trace
distance) as separate numpy arrays, ready for use in plotting.
Parameters
----------
time_evolution : np.array
As produced by the QuantumSystem's time_evolution() method,
containing the time, density matrix, and trace measures
at each timestep in the evolution.
elements : list
The elements of the density matrix to extract and return,
in the format i.e. ['11', '21', ...]. Can also take the
value None.
trace_measure : list of str
The trace measures to extract from the time evolution data.
Must be a list containing either, both, or neither of
'squared', 'distance'.
Returns
-------
times : np.array of float
All the times in the evolution of the system.
matrix_data : dict of np.array
Contains {str: np.array} pairs where the str corresponds
to each of the elements of the density matrix specified
in elements (i.e. '21'), while the np.array contains all
the value of that matrix elements at each time step.
If elements is passed as None, matrix_data is returned as
None.
squared : np.array of float
The value of the trace of the density matrix squared at
each timestep, if 'squared' was specified in trace_measure.
If not specified, squared is returned as None.
distance : np.array of float
The value of the trace distance of the density matrix at
each timestep, if 'distance' was specified in
trace_measure. If not specified, distance is returned as
None.
"""
times = np.empty(len(time_evolution), dtype=float)
matrix_data = ({element: np.empty(len(time_evolution), dtype=complex)
for element in elements} if elements else None)
squared = (np.empty(len(time_evolution), dtype=float)
if 'squared' in trace_measure else None)
distance = (np.empty(len(time_evolution), dtype=float)
if 'distance' in trace_measure else None)
for idx, (time, rho_t, squ, dist) in enumerate(time_evolution, start=0):
# Retrieve time
times[idx] = time # already in fs
# Process density matrix data
if matrix_data is not None:
for element in elements:
n, m = int(element[0]) - 1, int(element[1]) - 1
matrix_data[element][idx] = rho_t[n][m]
# Process trace measure data
if squared is not None:
squared[idx] = squ
if distance is not None:
distance[idx] = dist
return times, matrix_data, squared, distance