qflux 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of qflux might be problematic. Click here for more details.
- qflux/GQME/__init__.py +7 -0
- qflux/GQME/dynamics_GQME.py +438 -0
- qflux/GQME/params.py +62 -0
- qflux/GQME/readwrite.py +119 -0
- qflux/GQME/tdvp.py +233 -0
- qflux/GQME/tt_tfd.py +448 -0
- qflux/__init__.py +5 -0
- qflux/closed_systems/__init__.py +17 -0
- qflux/closed_systems/classical_methods.py +427 -0
- qflux/closed_systems/custom_execute.py +22 -0
- qflux/closed_systems/hamiltonians.py +88 -0
- qflux/closed_systems/qubit_methods.py +266 -0
- qflux/closed_systems/spin_dynamics_oo.py +371 -0
- qflux/closed_systems/spin_propagators.py +300 -0
- qflux/closed_systems/utils.py +205 -0
- qflux/open_systems/__init__.py +2 -0
- qflux/open_systems/dilation_circuit.py +183 -0
- qflux/open_systems/numerical_methods.py +303 -0
- qflux/open_systems/params.py +29 -0
- qflux/open_systems/quantum_simulation.py +360 -0
- qflux/open_systems/trans_basis.py +121 -0
- qflux/open_systems/walsh_gray_optimization.py +311 -0
- qflux/typing/__init__.py +0 -0
- qflux/typing/examples.py +24 -0
- qflux/utils/__init__.py +0 -0
- qflux/utils/io.py +16 -0
- qflux/utils/logging_config.py +61 -0
- qflux/variational_methods/__init__.py +1 -0
- qflux/variational_methods/qmad/__init__.py +0 -0
- qflux/variational_methods/qmad/ansatz.py +64 -0
- qflux/variational_methods/qmad/ansatzVect.py +61 -0
- qflux/variational_methods/qmad/effh.py +75 -0
- qflux/variational_methods/qmad/solver.py +356 -0
- qflux-0.0.1.dist-info/METADATA +144 -0
- qflux-0.0.1.dist-info/RECORD +38 -0
- qflux-0.0.1.dist-info/WHEEL +5 -0
- qflux-0.0.1.dist-info/licenses/LICENSE +674 -0
- qflux-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from qiskit.circuit.library import PauliEvolutionGate
|
|
2
|
+
# Trotter-Suzuki implementation for decomposition of exponentials
|
|
3
|
+
# of matrices
|
|
4
|
+
from qiskit.synthesis import SuzukiTrotter
|
|
5
|
+
from qiskit.quantum_info import SparsePauliOp
|
|
6
|
+
from qiskit import QuantumCircuit, QuantumRegister
|
|
7
|
+
import numpy as np
|
|
8
|
+
from itertools import groupby
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# -----------------------------------------------------------------
|
|
14
|
+
# Hamiltonian Functions
|
|
15
|
+
# -----------------------------------------------------------------
|
|
16
|
+
def get_hamiltonian_n_site_terms(n, coeff, n_qubits):
|
|
17
|
+
'''
|
|
18
|
+
Assembles each term in the Hamiltonian based on their Pauli string
|
|
19
|
+
representation and multiplying by the respective coefficient.
|
|
20
|
+
'''
|
|
21
|
+
XX_coeff = coeff[0]
|
|
22
|
+
YY_coeff = coeff[1]
|
|
23
|
+
ZZ_coeff = coeff[2]
|
|
24
|
+
Z_coeff = coeff[3]
|
|
25
|
+
|
|
26
|
+
XX_term = SparsePauliOp(("I" * n + "XX" + "I" * (n_qubits - 2 - n)))
|
|
27
|
+
XX_term *= XX_coeff
|
|
28
|
+
YY_term = SparsePauliOp(("I" * n + "YY" + "I" * (n_qubits - 2 - n)))
|
|
29
|
+
YY_term *= YY_coeff
|
|
30
|
+
ZZ_term = SparsePauliOp(("I" * n + "ZZ" + "I" * (n_qubits - 2 - n)))
|
|
31
|
+
ZZ_term *= ZZ_coeff
|
|
32
|
+
Z_term = SparsePauliOp(("I" * n + "Z" + "I" * (n_qubits - 1 - n)))
|
|
33
|
+
Z_term *= Z_coeff
|
|
34
|
+
|
|
35
|
+
return (XX_term + YY_term + ZZ_term + Z_term)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_heisenberg_hamiltonian(n_qubits, coeff=None):
|
|
39
|
+
r'''
|
|
40
|
+
Takes an integer number corresponding to number of spins/qubits
|
|
41
|
+
and a list of sublists containing the necessary coefficients
|
|
42
|
+
to assemble the complete Hamiltonian:
|
|
43
|
+
$$
|
|
44
|
+
H = \sum _i ^N h_z Z_i
|
|
45
|
+
+ \sum _i ^{N-1} (h_xx X_i X_{i+1}
|
|
46
|
+
+ h_yy Y_i Y_{i+1}
|
|
47
|
+
+ h_zz Z_i Z_{i+1}
|
|
48
|
+
)
|
|
49
|
+
$$
|
|
50
|
+
Each sublist contains the [XX, YY, ZZ, Z] coefficients in this order.
|
|
51
|
+
The last sublist should have the same shape, but only the Z component
|
|
52
|
+
is used.
|
|
53
|
+
If no coefficient list is provided, all are set to 1.
|
|
54
|
+
'''
|
|
55
|
+
|
|
56
|
+
# Three qubits because for 2 we get H_O = 0
|
|
57
|
+
assert n_qubits >= 3
|
|
58
|
+
|
|
59
|
+
if coeff == None:
|
|
60
|
+
'Setting default values for the coefficients'
|
|
61
|
+
coeff = [[1.0, 1.0, 1.0, 1.0] for i in range(n_qubits)]
|
|
62
|
+
|
|
63
|
+
# Even terms of the Hamiltonian
|
|
64
|
+
# (summing over individual pair-wise elements)
|
|
65
|
+
H_E = sum((get_hamiltonian_n_site_terms(i, coeff[i], n_qubits)
|
|
66
|
+
for i in range(0, n_qubits-1, 2)))
|
|
67
|
+
|
|
68
|
+
# Odd terms of the Hamiltonian
|
|
69
|
+
# (summing over individual pair-wise elements)
|
|
70
|
+
H_O = sum((get_hamiltonian_n_site_terms(i, coeff[i], n_qubits)
|
|
71
|
+
for i in range(1, n_qubits-1, 2)))
|
|
72
|
+
|
|
73
|
+
# adding final Z term at the Nth site
|
|
74
|
+
final_term = SparsePauliOp("I" * (n_qubits - 1) + "Z")
|
|
75
|
+
final_term *= coeff[n_qubits-1][3]
|
|
76
|
+
if (n_qubits % 2) == 0:
|
|
77
|
+
H_E += final_term
|
|
78
|
+
else:
|
|
79
|
+
H_O += final_term
|
|
80
|
+
|
|
81
|
+
# Returns the list of the two sets of terms
|
|
82
|
+
return [H_E, H_O]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_time_evolution_operator(num_qubits, tau, trotter_steps, coeff=None):
|
|
86
|
+
'''
|
|
87
|
+
Given a number of qubits, generates the corresponding time-evolution for
|
|
88
|
+
the Ising model with the same number of sites.
|
|
89
|
+
|
|
90
|
+
Input:
|
|
91
|
+
num_qubits (int): number of qubits, which should be equal to the
|
|
92
|
+
number of spins in the chain
|
|
93
|
+
evo_time (float): time parameter in time-evolution operator
|
|
94
|
+
trotter_steps (int): number of time steps for the Suzuki-Trotter
|
|
95
|
+
decomposition
|
|
96
|
+
coeff (list of lists): parameters for each term in the Hamiltonian
|
|
97
|
+
for each site ie ([[XX0, YY0, ZZ0, Z0], [XX1, YY1, ZZ1, Z1], ...])
|
|
98
|
+
Returns:
|
|
99
|
+
evo_op.definition: Trotterized time-evolution operator
|
|
100
|
+
'''
|
|
101
|
+
# Constructing the Hamiltonian here;
|
|
102
|
+
# heisenberg_hamiltonian = [H_E, H_O]
|
|
103
|
+
heisenberg_hamiltonian = get_heisenberg_hamiltonian(num_qubits,
|
|
104
|
+
coeff)
|
|
105
|
+
|
|
106
|
+
# e^ (-i*H*evo_time), with Trotter decomposition
|
|
107
|
+
# exp[(i * evo_time)*(IIIIXXIIII + IIIIYYIIII + IIIIZZIIII + IIIIZIIIII)]
|
|
108
|
+
evo_op = PauliEvolutionGate(heisenberg_hamiltonian, tau,
|
|
109
|
+
synthesis=SuzukiTrotter(order=2,
|
|
110
|
+
reps=trotter_steps))
|
|
111
|
+
# The Trotter order=2 applies one set of the operators for
|
|
112
|
+
# half a timestep, then the other set for a full timestep,
|
|
113
|
+
# then the first step for another half a step note that reps
|
|
114
|
+
# includes the number of repetitions of the Trotterized
|
|
115
|
+
# operator higher number means more repetitions, and thus
|
|
116
|
+
# allowing larger timestep
|
|
117
|
+
return evo_op.definition
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def find_string_pattern(pattern, string):
|
|
121
|
+
match_list = []
|
|
122
|
+
for m in re.finditer(pattern, string):
|
|
123
|
+
match_list.append(m.start())
|
|
124
|
+
return match_list
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# efficient propagator for pauli hamiltonians
|
|
128
|
+
def sort_Pauli_by_symmetry(ham):
|
|
129
|
+
'''
|
|
130
|
+
Separates a qiskit PauliOp object terms into 1 and 2-qubit
|
|
131
|
+
operators. Furthermore, 2-qubit operators are separated according
|
|
132
|
+
to the parity of the index first non-identity operation.
|
|
133
|
+
'''
|
|
134
|
+
one_qubit_terms = []
|
|
135
|
+
two_qubit_terms = []
|
|
136
|
+
# separating the one-qubit from two-qubit terms
|
|
137
|
+
for term in ham:
|
|
138
|
+
matches = find_string_pattern('X|Y|Z', str(term.paulis[0]))
|
|
139
|
+
pauli_string = term.paulis[0]
|
|
140
|
+
coeff = np.real(term.coeffs[0])
|
|
141
|
+
str_tag = pauli_string.to_label().replace('I', '')
|
|
142
|
+
if len(matches) == 2:
|
|
143
|
+
two_qubit_terms.append((pauli_string, coeff, matches, str_tag))
|
|
144
|
+
elif len(matches) == 1:
|
|
145
|
+
one_qubit_terms.append((pauli_string, coeff, matches, str_tag))
|
|
146
|
+
|
|
147
|
+
# sorting the two-qubit terms according to index on which they act
|
|
148
|
+
two_qubit_terms = sorted(two_qubit_terms, key=lambda x: x[2])
|
|
149
|
+
# separating the even from the odd two-qubit terms
|
|
150
|
+
even_two_qubit_terms = list(filter(lambda x: not x[2][0]%2, two_qubit_terms))
|
|
151
|
+
odd_two_qubit_terms = list(filter(lambda x: x[2][0]%2, two_qubit_terms))
|
|
152
|
+
|
|
153
|
+
even_two_qubit_terms = [list(v) for i, v in groupby(even_two_qubit_terms, lambda x: x[2][0])]
|
|
154
|
+
odd_two_qubit_terms = [list(v) for i, v in groupby(odd_two_qubit_terms, lambda x: x[2][0])]
|
|
155
|
+
|
|
156
|
+
return one_qubit_terms, even_two_qubit_terms, odd_two_qubit_terms
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def generate_circ_pattern_1qubit(circ, term, delta_t):
|
|
160
|
+
'''
|
|
161
|
+
General 1-qubit gate for exponential of product identity and
|
|
162
|
+
a single pauli gate.
|
|
163
|
+
Only a single rotation operation is required, with the angle
|
|
164
|
+
being related to the exponential argument:
|
|
165
|
+
|
|
166
|
+
R_P(coeff) = exp(-i * coeff * P / 2)
|
|
167
|
+
|
|
168
|
+
Where P is the Pauli gate and coeff encompasses the constant
|
|
169
|
+
coefficient term
|
|
170
|
+
'''
|
|
171
|
+
coeff = 2 * term[1] * delta_t
|
|
172
|
+
if term[3] == 'X':
|
|
173
|
+
circ.rx(coeff, term[2])
|
|
174
|
+
elif term[3] == 'Y':
|
|
175
|
+
circ.ry(coeff, term[2])
|
|
176
|
+
elif term[3] == 'Z':
|
|
177
|
+
circ.rz(coeff, term[2])
|
|
178
|
+
|
|
179
|
+
return circ
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def generate_circ_pattern_2qubit(circ, term, delta_t):
|
|
183
|
+
r'''
|
|
184
|
+
General 2-qubit gate for exponential of Paulis. This is the
|
|
185
|
+
optimal decomposition, based on a component of a U(4) operator.
|
|
186
|
+
(see )
|
|
187
|
+
|
|
188
|
+
The circuit structure is as follows:
|
|
189
|
+
|
|
190
|
+
- ---- I ---- C - Rz(o) - X --- I --- C - Rz(pi/2) -
|
|
191
|
+
- Rz(-pi/2) - X - Ry(p) - C - Ry(l) - X ---- I -----
|
|
192
|
+
|
|
193
|
+
Where CX represent CNOT operations, R are rotation gates with angles,
|
|
194
|
+
and I is the identity matrix. The angles are parameterized as follows:
|
|
195
|
+
|
|
196
|
+
$ o = \theta = (\pi/2 - A) $
|
|
197
|
+
$ p = \phi = (A - \pi/2) $
|
|
198
|
+
$ l = \lambda = (\pi/2 - A) $
|
|
199
|
+
|
|
200
|
+
Where A is the exponential argument.
|
|
201
|
+
'''
|
|
202
|
+
# wires to which to apply the operation
|
|
203
|
+
wires = term[0][2]
|
|
204
|
+
|
|
205
|
+
# angles to parameterize the circuit,
|
|
206
|
+
# based on exponential argument
|
|
207
|
+
if any('XX' in sublist for sublist in term):
|
|
208
|
+
g_phi = ( 2 * (-1) * term[0][1] * delta_t - np.pi / 2)
|
|
209
|
+
else:
|
|
210
|
+
g_phi = - np.pi / 2
|
|
211
|
+
if any('YY' in sublist for sublist in term):
|
|
212
|
+
g_lambda = (np.pi/2 - 2 * (-1) * term[1][1] * delta_t)
|
|
213
|
+
else:
|
|
214
|
+
g_lambda = np.pi/2
|
|
215
|
+
if any('ZZ' in sublist for sublist in term):
|
|
216
|
+
g_theta = (np.pi/2 - 2 * (-1) * term[2][1] * delta_t)
|
|
217
|
+
else:
|
|
218
|
+
g_theta = np.pi/2
|
|
219
|
+
|
|
220
|
+
# circuit
|
|
221
|
+
circ.rz(-np.pi/2, wires[1])
|
|
222
|
+
circ.cx(wires[1], wires[0])
|
|
223
|
+
circ.rz(g_theta, wires[0])
|
|
224
|
+
circ.ry(g_phi, wires[1])
|
|
225
|
+
circ.cx(wires[0], wires[1])
|
|
226
|
+
circ.ry(g_lambda, wires[1])
|
|
227
|
+
circ.cx(wires[1], wires[0])
|
|
228
|
+
circ.rz(np.pi/2, wires[0])
|
|
229
|
+
return circ
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_manual_Trotter(num_q, pauli_ops, timestep, n_trotter=1,
|
|
233
|
+
trotter_type='basic', reverse_bits=True):
|
|
234
|
+
# sorts the Pauli strings according to qubit number they affect and symmetry
|
|
235
|
+
one_q, even_two_q, odd_two_q = sort_Pauli_by_symmetry(pauli_ops)
|
|
236
|
+
# scales the timestep according to the number of trotter steps
|
|
237
|
+
timestep_even_two_q = timestep / n_trotter
|
|
238
|
+
timestep_odd_two_q = timestep / n_trotter
|
|
239
|
+
timestep_one_q = timestep / n_trotter
|
|
240
|
+
# symmetric places 1/2 of one_q and odd_two_q before and after even_two_q
|
|
241
|
+
if trotter_type == 'symmetric':
|
|
242
|
+
timestep_odd_two_q /= 2
|
|
243
|
+
timestep_one_q /= 2
|
|
244
|
+
# constructs circuits for each segment of the operators
|
|
245
|
+
qc_odd_two_q, qc_even_two_q, qc_one_q = QuantumCircuit(num_q), QuantumCircuit(num_q), QuantumCircuit(num_q)
|
|
246
|
+
for i in even_two_q:
|
|
247
|
+
qc_even_two_q = generate_circ_pattern_2qubit(qc_even_two_q, i, timestep_even_two_q)
|
|
248
|
+
for i in odd_two_q:
|
|
249
|
+
qc_odd_two_q = generate_circ_pattern_2qubit(qc_odd_two_q, i, timestep_odd_two_q)
|
|
250
|
+
for i in one_q:
|
|
251
|
+
qc_one_q = generate_circ_pattern_1qubit(qc_one_q, i, timestep_one_q)
|
|
252
|
+
# assembles the circuit for Trotter decomposition of exponential
|
|
253
|
+
qr = QuantumRegister(num_q)
|
|
254
|
+
qc = QuantumCircuit(qr)
|
|
255
|
+
if trotter_type == 'basic':
|
|
256
|
+
qc = qc.compose(qc_even_two_q)
|
|
257
|
+
qc = qc.compose(qc_odd_two_q)
|
|
258
|
+
qc = qc.compose(qc_one_q)
|
|
259
|
+
elif trotter_type == 'symmetric':
|
|
260
|
+
qc = qc.compose(qc_one_q)
|
|
261
|
+
qc = qc.compose(qc_odd_two_q)
|
|
262
|
+
qc = qc.compose(qc_even_two_q)
|
|
263
|
+
qc = qc.compose(qc_odd_two_q)
|
|
264
|
+
qc = qc.compose(qc_one_q)
|
|
265
|
+
# repeats the single_trotter circuit several times to match n_trotter
|
|
266
|
+
for i in range(n_trotter-1):
|
|
267
|
+
qc = qc.compose(qc)
|
|
268
|
+
if reverse_bits:
|
|
269
|
+
return qc.reverse_bits()
|
|
270
|
+
else:
|
|
271
|
+
return qc
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
if __name__ == '__main__':
|
|
275
|
+
num_shots = 100
|
|
276
|
+
num_q = 3
|
|
277
|
+
evolution_timestep = 0.1
|
|
278
|
+
n_trotter_steps = 1
|
|
279
|
+
# XX YY ZZ, Z
|
|
280
|
+
ham_coeffs = ([[0.75/2, 0.75/2, 0.0, 0.65]]
|
|
281
|
+
+ [[0.5, 0.5, 0.0, 1.0]
|
|
282
|
+
for i in range(num_q-1)])
|
|
283
|
+
time_evo_op = get_time_evolution_operator(
|
|
284
|
+
num_qubits=num_q, tau=evolution_timestep,
|
|
285
|
+
trotter_steps=n_trotter_steps, coeff=ham_coeffs)
|
|
286
|
+
print(time_evo_op)
|
|
287
|
+
|
|
288
|
+
spin_chain_hamiltonian = get_heisenberg_hamiltonian(num_q,
|
|
289
|
+
ham_coeffs)
|
|
290
|
+
|
|
291
|
+
spin_chain_hamiltonian = sum(spin_chain_hamiltonian)
|
|
292
|
+
print(get_manual_Trotter(num_q, spin_chain_hamiltonian,
|
|
293
|
+
0.1).draw())
|
|
294
|
+
print(get_manual_Trotter(num_q, spin_chain_hamiltonian, 0.1,
|
|
295
|
+
n_trotter=2).draw())
|
|
296
|
+
print(get_manual_Trotter(num_q, spin_chain_hamiltonian, 0.1,
|
|
297
|
+
trotter_type='symmetric').draw())
|
|
298
|
+
print(get_manual_Trotter(num_q, spin_chain_hamiltonian, 0.1,
|
|
299
|
+
n_trotter=2,
|
|
300
|
+
trotter_type='symmetric').draw())
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Utilities
|
|
2
|
+
# Plotting utilities, conversion factors, etc.
|
|
3
|
+
import numpy as np
|
|
4
|
+
import qiskit_aer
|
|
5
|
+
from qiskit.compiler import transpile
|
|
6
|
+
from qiskit_ibm_runtime import Sampler
|
|
7
|
+
from qiskit.quantum_info import SparsePauliOp
|
|
8
|
+
import itertools
|
|
9
|
+
|
|
10
|
+
# Conversion Factors:
|
|
11
|
+
def convert_au_to_eV(input_val):
|
|
12
|
+
au2ev = 27.21138602
|
|
13
|
+
return(au2ev * input_val)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def convert_eV_to_au(input_val):
|
|
17
|
+
au2ev = 27.21138602
|
|
18
|
+
ev2au = 1/au2ev
|
|
19
|
+
return(ev2au * input_val)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def convert_bohr_to_au(input_val):
|
|
23
|
+
bohr2au = 0.52917721092
|
|
24
|
+
return(bohr2au * input_val)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def convert_au_to_bohr(input_val):
|
|
28
|
+
bohr2au = 0.52917721092
|
|
29
|
+
au2bohr = 1/bohr2au
|
|
30
|
+
return(input_val * au2bohr)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def convert_fs_to_au(input_val):
|
|
34
|
+
fs2au = 41.3414
|
|
35
|
+
return(fs2au * input_val)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def convert_au_to_fs(input_val):
|
|
39
|
+
fs2au = 41.3414
|
|
40
|
+
au2fs = 1/fs2au
|
|
41
|
+
return(au2fs * input_val)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_proton_mass():
|
|
45
|
+
proton_mass = 1836.15267343 # proton-electron mass ratio
|
|
46
|
+
return(proton_mass)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def execute(QCircuit, backend=None, shots=None, real_backend=False):
|
|
50
|
+
'''
|
|
51
|
+
Function to replace the now-deprecated Qiskit
|
|
52
|
+
`QuantumCircuit.execute()` method.
|
|
53
|
+
|
|
54
|
+
Input:
|
|
55
|
+
- `QCircuit`: qiskit.QuantumCircuit object
|
|
56
|
+
- `Backend`: qiskit.Backend instance
|
|
57
|
+
- `shots`: int specifying the number of shots
|
|
58
|
+
- `real_backend`: bool specifying whether the provided backend is
|
|
59
|
+
a real device (True) or not (False)
|
|
60
|
+
'''
|
|
61
|
+
if shots:
|
|
62
|
+
n_shots = shots
|
|
63
|
+
else:
|
|
64
|
+
n_shots = 1024 # Use the qiskit default if not specified
|
|
65
|
+
|
|
66
|
+
if real_backend:
|
|
67
|
+
QCircuit.measure_all()
|
|
68
|
+
qc = transpile(QCircuit, backend=backend)
|
|
69
|
+
sampler = Sampler(backend)
|
|
70
|
+
job = sampler.run([qc], shots=n_shots)
|
|
71
|
+
else:
|
|
72
|
+
# Transpile circuit with statevector backend
|
|
73
|
+
tmp_circuit = transpile(QCircuit, backend)
|
|
74
|
+
# Run the transpiled circuit
|
|
75
|
+
job = backend.run(tmp_circuit, n_shots=shots)
|
|
76
|
+
return(job)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Calculation of Expectation Value:
|
|
80
|
+
def calculate_expectation_values(dynamics_results, observable_grid, do_FFT=False, dx=None):
|
|
81
|
+
'''
|
|
82
|
+
Function to calculate the time-dependent expectation value of an observable O defined on a grid.
|
|
83
|
+
Inputs:
|
|
84
|
+
|
|
85
|
+
- `dynamics_results`: np.ndarray of wavefunctions/propagated states with shape: (n_steps, nx)
|
|
86
|
+
- `observable_grid`: np.array of observable
|
|
87
|
+
'''
|
|
88
|
+
if dx:
|
|
89
|
+
d_observable = dx
|
|
90
|
+
else:
|
|
91
|
+
d_observable = observable_grid[1] - observable_grid[0]
|
|
92
|
+
if do_FFT:
|
|
93
|
+
psi_list = np.fft.fft(dynamics_results, axis=1, norm='ortho')
|
|
94
|
+
else:
|
|
95
|
+
psi_list = dynamics_results
|
|
96
|
+
# Compute the expectation value.
|
|
97
|
+
expectation = np.real(np.sum(psi_list.conj()*observable_grid*psi_list*d_observable, axis=1))
|
|
98
|
+
|
|
99
|
+
return(expectation)
|
|
100
|
+
|
|
101
|
+
# Pauli Decomposition Utilities
|
|
102
|
+
|
|
103
|
+
def vec_query(arr, my_dict):
|
|
104
|
+
'''
|
|
105
|
+
This function vectorizes dictionary querying, allowing us to query `my_dict` with a np.array `arr` of keys.
|
|
106
|
+
'''
|
|
107
|
+
|
|
108
|
+
return np.vectorize(my_dict.__getitem__, otypes=[tuple])(arr)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def nested_kronecker_product(a):
|
|
112
|
+
'''
|
|
113
|
+
Handles Kronecker Products for list (i.e., a = [Z, Z, Z] will evaluate Z ⊗ Z ⊗ Z)
|
|
114
|
+
'''
|
|
115
|
+
if len(a) == 2:
|
|
116
|
+
return np.kron(a[0],a[1])
|
|
117
|
+
else:
|
|
118
|
+
return np.kron(a[0], nested_kronecker_product(a[1:]))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def Hilbert_Schmidt(mat1, mat2):
|
|
122
|
+
'Return the Hilbert-Schmidt Inner Product of two matrices.'
|
|
123
|
+
return np.trace(mat1.conj().T * mat2)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def decompose(Ham_arr, tol=12, subset = None):
|
|
127
|
+
'''
|
|
128
|
+
Function that takes an input matrix `H` and decomposes into a sum of tensor products of Pauli Matrices.
|
|
129
|
+
- The expectation is that `H` will have a shape of 2^n, where n is the number of qubits needed to represent
|
|
130
|
+
the matrix in this form. n determines the number of terms in the tensor product composing the Pauli strings.
|
|
131
|
+
- Ex: If H has shape of (16, 16), n = log2(H) = 4 qubits.
|
|
132
|
+
The Pauli strings will then take the form of ['IIII', 'ZIII', 'ZZII', etc.]
|
|
133
|
+
Has the option to toggle verbose output, which prints the terms of the pauli sum.
|
|
134
|
+
'''
|
|
135
|
+
|
|
136
|
+
X = np.asarray([[0,1],[1,0]])
|
|
137
|
+
Y = np.asarray([[0,complex(0,-1)],[complex(0,1),0]])
|
|
138
|
+
Z = np.asarray([[1,0],[0,-1]])
|
|
139
|
+
I = Z@Z
|
|
140
|
+
# Define a dictionary with the four Pauli matrices:
|
|
141
|
+
global_pms = {'I': I,'X': X,'Y': Y,'Z': Z}
|
|
142
|
+
|
|
143
|
+
# The next few lines allow for the function use a reduced basis.
|
|
144
|
+
pm_keys = []
|
|
145
|
+
pm_vals = []
|
|
146
|
+
if subset:
|
|
147
|
+
for tmp_key in subset:
|
|
148
|
+
pm_keys.append(tmp_key)
|
|
149
|
+
pm_vals.append(global_pms[tmp_key])
|
|
150
|
+
pms = dict(zip(pm_keys, pm_vals))
|
|
151
|
+
else:
|
|
152
|
+
pms = global_pms
|
|
153
|
+
|
|
154
|
+
pauli_keys = list(pms.keys()) # Keys of the dictionary
|
|
155
|
+
|
|
156
|
+
nqb = int(np.log2(Ham_arr.shape[0])) # Determine the # of qubits needed
|
|
157
|
+
|
|
158
|
+
# Make all possible tensor products of Pauli matrices sigma
|
|
159
|
+
sigma_combinations = list(itertools.product(pauli_keys, repeat=nqb))
|
|
160
|
+
|
|
161
|
+
output_string = '' # Initialize an empty string to which we can add our terms
|
|
162
|
+
for ii in range(len(sigma_combinations)):
|
|
163
|
+
pauli_str = ''.join(sigma_combinations[ii])
|
|
164
|
+
|
|
165
|
+
# Convert the Pauli string into a list of matrices
|
|
166
|
+
tmp_mat_list = vec_query(np.array(sigma_combinations[ii]), pms)
|
|
167
|
+
|
|
168
|
+
# Evaluate the Kronecker product of the matrix array
|
|
169
|
+
tmp_p_matrix = nested_kronecker_product(tmp_mat_list)
|
|
170
|
+
|
|
171
|
+
# Compute the coefficient for each Pauli string
|
|
172
|
+
a_coeff = (1/(2**nqb)) * Hilbert_Schmidt(tmp_p_matrix, Ham_arr)
|
|
173
|
+
|
|
174
|
+
# If the coefficient is non-zero, we want to use it!
|
|
175
|
+
if abs(a_coeff) > 10**(-tol):
|
|
176
|
+
output_string += str(np.round(a_coeff.real, tol))+'*'+pauli_str
|
|
177
|
+
output_string += '+' # Add a plus sign for the next term!
|
|
178
|
+
|
|
179
|
+
return output_string[:-1] # To ignore that extra plus sign
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def build_pauli_dict(decomposed_operator):
|
|
183
|
+
'''
|
|
184
|
+
Build a Pauli dictionary from the output of the `decompose` function above.
|
|
185
|
+
This is done by converting the unique Pauli Strings ['IIII', 'IIIZ', etc.] to keys,
|
|
186
|
+
where the values of the dictionary are the numerical coefficients.
|
|
187
|
+
'''
|
|
188
|
+
single_terms = decomposed_operator.split('+')
|
|
189
|
+
pauli_dict_out = {}
|
|
190
|
+
for term in single_terms:
|
|
191
|
+
coeff, pauli_str = term.split('*')
|
|
192
|
+
pauli_dict_out[pauli_str] = float(coeff)
|
|
193
|
+
return pauli_dict_out
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def pauli_strings_2_pauli_sum(operator):
|
|
197
|
+
'''
|
|
198
|
+
Function to convert the output of the `decompose` function above into a Qiskit-recognized PauliSumOp.
|
|
199
|
+
The string representing the Hamiltonian as a sum of product of Paulis is converted into a dictionary where
|
|
200
|
+
the keys are the Pauli Strings (ex: 'IIII' or 'IXYZ') and the values are the coefficients.
|
|
201
|
+
'''
|
|
202
|
+
tmp_pauli_dict = build_pauli_dict(operator)
|
|
203
|
+
# Convert the dict to a list of tuples of the form [('Pauli String', float_coeff), ...]
|
|
204
|
+
tmp_pauli_sum = SparsePauliOp(data=list(tmp_pauli_dict.keys()), coeffs=list(tmp_pauli_dict.values()))
|
|
205
|
+
return tmp_pauli_sum
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy.linalg as LA
|
|
3
|
+
|
|
4
|
+
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
|
|
5
|
+
from qiskit.quantum_info.operators import Operator
|
|
6
|
+
|
|
7
|
+
from . import walsh_gray_optimization as wo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def scale_array(array,scale=1.1):
|
|
11
|
+
"""
|
|
12
|
+
renormalize an array to make sure it is a contraction
|
|
13
|
+
"""
|
|
14
|
+
# Normalization factor, divide by martix's norm to ensure contraction
|
|
15
|
+
# may divide a larger scaling factor controlled by the number (scale)
|
|
16
|
+
norm = LA.norm(array,2)*scale
|
|
17
|
+
array_new = array/norm
|
|
18
|
+
|
|
19
|
+
return array_new, norm
|
|
20
|
+
|
|
21
|
+
def dilate_Sz_Nagy(array):
|
|
22
|
+
"""
|
|
23
|
+
dilate the non-unitary array (should be a contraction) to a unitary matrix
|
|
24
|
+
array: ndarray of N*N
|
|
25
|
+
"""
|
|
26
|
+
ident = np.eye(array.shape[0])
|
|
27
|
+
|
|
28
|
+
# Calculate the conjugate transpose of the G propagator
|
|
29
|
+
fcon = (array.conjugate()).T
|
|
30
|
+
|
|
31
|
+
# Calculate the defect matrix for dilation
|
|
32
|
+
fdef = LA.sqrtm(ident - np.dot(fcon, array))
|
|
33
|
+
|
|
34
|
+
# Calculate the defect matrix for the conjugate of the G propagator
|
|
35
|
+
fcondef = LA.sqrtm(ident - np.dot(array, fcon))
|
|
36
|
+
|
|
37
|
+
# Dilate the G propagator to create a unitary operator
|
|
38
|
+
array_dilated = np.block([[array, fcondef], [fdef, -fcon]])
|
|
39
|
+
|
|
40
|
+
return array_dilated
|
|
41
|
+
|
|
42
|
+
#generate the quantum gate matrices for SVD-dilation
|
|
43
|
+
def dilate_SVD(array):
|
|
44
|
+
"""
|
|
45
|
+
dilate the non-unitary array to unitary matrices using SVD technique by Schlimgen et al
|
|
46
|
+
(Phys. Rev. A 2022, 106, 022414.)
|
|
47
|
+
|
|
48
|
+
array: ndarray of N*N (should be a contraction)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
#get the dimension of the array
|
|
52
|
+
Nvec = array.shape[0]
|
|
53
|
+
|
|
54
|
+
#Performing SVD to the array
|
|
55
|
+
U1,S1,V1 = LA.svd(array)
|
|
56
|
+
|
|
57
|
+
Mzero = np.zeros((Nvec,Nvec),dtype=np.complex128)
|
|
58
|
+
fk=np.zeros(2*Nvec,dtype=np.float64)
|
|
59
|
+
|
|
60
|
+
Sig_p = np.zeros(Nvec,dtype=np.complex128)
|
|
61
|
+
Sig_m = np.zeros(Nvec,dtype=np.complex128)
|
|
62
|
+
for i in range(len(S1)):
|
|
63
|
+
|
|
64
|
+
Sig_p[i] = S1[i]+1j*np.sqrt((1-S1[i]**2))
|
|
65
|
+
Sig_m[i] = S1[i]-1j*np.sqrt((1-S1[i]**2))
|
|
66
|
+
|
|
67
|
+
#here U_Sigma = e^{i f_k}
|
|
68
|
+
fk[i] = (-1j*np.log(Sig_p[i])).real
|
|
69
|
+
fk[Nvec+i] = (-1j*np.log(Sig_m[i])).real
|
|
70
|
+
|
|
71
|
+
SG0 = np.block([[np.diag(Sig_p),Mzero],\
|
|
72
|
+
[Mzero,np.diag(Sig_m)]])
|
|
73
|
+
|
|
74
|
+
return U1, V1, SG0, fk
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cons_SVD_cirq(Nqb, array, ini_vec, Iswalsh = True):
|
|
78
|
+
"""
|
|
79
|
+
Construct the SVD-dilation circuit
|
|
80
|
+
Nqb: number of qubits
|
|
81
|
+
array: the non-unitary array (should be a contraction)
|
|
82
|
+
ini_vec: initial qubit state vector (in the dilation space)
|
|
83
|
+
Iswalsh: If True, then combine with Walsh operator representation to reduce the circuit depth
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
qc = QuantumCircuit(Nqb,Nqb)
|
|
87
|
+
qc.initialize(ini_vec,range(0,Nqb))
|
|
88
|
+
|
|
89
|
+
U_matu,U_matv,S_mat0,fk = dilate_SVD(array)
|
|
90
|
+
|
|
91
|
+
qc.append(Operator(U_matv),range(0,Nqb-1))
|
|
92
|
+
qc.h(Nqb-1)
|
|
93
|
+
|
|
94
|
+
if(Iswalsh):
|
|
95
|
+
#the walsh coeff
|
|
96
|
+
arr_a = wo.walsh_coef(fk,Nqb)
|
|
97
|
+
|
|
98
|
+
Ulist_diag0 = wo.cirq_list_walsh(arr_a,Nqb,1E-5)
|
|
99
|
+
Ulist_diag = wo.optimize(Ulist_diag0)
|
|
100
|
+
qc_diag = wo.cirq_from_U(Ulist_diag,Nqb)
|
|
101
|
+
|
|
102
|
+
qc.append(qc_diag.to_gate(),range(Nqb))
|
|
103
|
+
|
|
104
|
+
else:
|
|
105
|
+
qc.append(Operator(S_mat0),range(Nqb))
|
|
106
|
+
|
|
107
|
+
qc.append(Operator(U_matu),range(0,Nqb-1))
|
|
108
|
+
qc.h(Nqb-1)
|
|
109
|
+
|
|
110
|
+
return qc
|
|
111
|
+
|
|
112
|
+
def cons_SzNagy_cirq(Nqb, array, ini_vec):
|
|
113
|
+
"""
|
|
114
|
+
Construct the Sz-Nagy-dilation circuit
|
|
115
|
+
Nqb: number of qubits
|
|
116
|
+
array: the non-unitary array (should be a contraction)
|
|
117
|
+
ini_vec: initial qubit state vector (in the dilation space)
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
qr = QuantumRegister(Nqb) # Create a quantum register
|
|
121
|
+
cr = ClassicalRegister(Nqb) # Create a classical register to store measurement results
|
|
122
|
+
qc = QuantumCircuit(qr, cr) # Combine the quantum and classical registers to create the quantum circuit
|
|
123
|
+
|
|
124
|
+
# Initialize the quantum circuit with the initial state
|
|
125
|
+
qc.initialize(ini_vec, qr)
|
|
126
|
+
|
|
127
|
+
# Create a custom unitary operator with the dilated propagator
|
|
128
|
+
U_dil = dilate_Sz_Nagy(array)
|
|
129
|
+
U_dil_op = Operator(U_dil)
|
|
130
|
+
|
|
131
|
+
# Apply the unitary operator to the quantum circuit's qubits
|
|
132
|
+
qc.unitary(U_dil_op, qr)
|
|
133
|
+
|
|
134
|
+
return qc
|
|
135
|
+
|
|
136
|
+
def construct_circuit(Nqb, array,statevec,method = 'Sz-Nagy',Isscale = True):
|
|
137
|
+
"""
|
|
138
|
+
construct the quantum circuit
|
|
139
|
+
|
|
140
|
+
method: specify the dilation method, can be 'Sz-Nagy' or 'SVD' or 'SVD-Walsh'
|
|
141
|
+
'Sz-Nagy': using Sz-Nagy method do dilation
|
|
142
|
+
'SVD': using SVD-dilation method
|
|
143
|
+
'SVD-Walsh': SVD-dilation combine with Walsh operator representation to reduce the circuit depth
|
|
144
|
+
|
|
145
|
+
Nqb: number of qubits in the original space, should match the dimension of array (N=2^Nqb)
|
|
146
|
+
array: ndarray of N*N
|
|
147
|
+
statevec: ndarray of N*1 (initial statevector)
|
|
148
|
+
|
|
149
|
+
Isscale:
|
|
150
|
+
if Ture, renormalize the array to make sure it is a contraction
|
|
151
|
+
if False, do not renormalize.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
#the number of qubits for the original space
|
|
155
|
+
if(2**Nqb != array.shape[0]):
|
|
156
|
+
print('error, array dimension not matched!')
|
|
157
|
+
|
|
158
|
+
#state vector in the dilated space
|
|
159
|
+
statevec_dil = np.concatenate((statevec, np.zeros_like(statevec)))
|
|
160
|
+
|
|
161
|
+
#first scale the array to make it a contraction
|
|
162
|
+
if(Isscale):
|
|
163
|
+
array_new, normfac = scale_array(array)
|
|
164
|
+
else:
|
|
165
|
+
array_new = array
|
|
166
|
+
|
|
167
|
+
if(method == 'Sz-Nagy'):
|
|
168
|
+
qc = cons_SzNagy_cirq(Nqb+1, array_new, statevec_dil)
|
|
169
|
+
elif(method == 'SVD'):
|
|
170
|
+
Iswalsh = False
|
|
171
|
+
qc = cons_SVD_cirq(Nqb+1, array_new, statevec_dil, Iswalsh)
|
|
172
|
+
elif(method == 'SVD-Walsh'):
|
|
173
|
+
Iswalsh = True
|
|
174
|
+
qc = cons_SVD_cirq(Nqb+1, array_new, statevec_dil, Iswalsh)
|
|
175
|
+
else:
|
|
176
|
+
print('method error in construct circuit')
|
|
177
|
+
|
|
178
|
+
if(Isscale):
|
|
179
|
+
return qc,normfac
|
|
180
|
+
else:
|
|
181
|
+
return qc
|
|
182
|
+
|
|
183
|
+
|