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.

Files changed (38) hide show
  1. qflux/GQME/__init__.py +7 -0
  2. qflux/GQME/dynamics_GQME.py +438 -0
  3. qflux/GQME/params.py +62 -0
  4. qflux/GQME/readwrite.py +119 -0
  5. qflux/GQME/tdvp.py +233 -0
  6. qflux/GQME/tt_tfd.py +448 -0
  7. qflux/__init__.py +5 -0
  8. qflux/closed_systems/__init__.py +17 -0
  9. qflux/closed_systems/classical_methods.py +427 -0
  10. qflux/closed_systems/custom_execute.py +22 -0
  11. qflux/closed_systems/hamiltonians.py +88 -0
  12. qflux/closed_systems/qubit_methods.py +266 -0
  13. qflux/closed_systems/spin_dynamics_oo.py +371 -0
  14. qflux/closed_systems/spin_propagators.py +300 -0
  15. qflux/closed_systems/utils.py +205 -0
  16. qflux/open_systems/__init__.py +2 -0
  17. qflux/open_systems/dilation_circuit.py +183 -0
  18. qflux/open_systems/numerical_methods.py +303 -0
  19. qflux/open_systems/params.py +29 -0
  20. qflux/open_systems/quantum_simulation.py +360 -0
  21. qflux/open_systems/trans_basis.py +121 -0
  22. qflux/open_systems/walsh_gray_optimization.py +311 -0
  23. qflux/typing/__init__.py +0 -0
  24. qflux/typing/examples.py +24 -0
  25. qflux/utils/__init__.py +0 -0
  26. qflux/utils/io.py +16 -0
  27. qflux/utils/logging_config.py +61 -0
  28. qflux/variational_methods/__init__.py +1 -0
  29. qflux/variational_methods/qmad/__init__.py +0 -0
  30. qflux/variational_methods/qmad/ansatz.py +64 -0
  31. qflux/variational_methods/qmad/ansatzVect.py +61 -0
  32. qflux/variational_methods/qmad/effh.py +75 -0
  33. qflux/variational_methods/qmad/solver.py +356 -0
  34. qflux-0.0.1.dist-info/METADATA +144 -0
  35. qflux-0.0.1.dist-info/RECORD +38 -0
  36. qflux-0.0.1.dist-info/WHEEL +5 -0
  37. qflux-0.0.1.dist-info/licenses/LICENSE +674 -0
  38. 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,2 @@
1
+ from .numerical_methods import DynamicsOS, DVR_grid
2
+ from .quantum_simulation import QubitDynamicsOS
@@ -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
+