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,303 @@
1
+ import numpy as np
2
+ import scipy.linalg as LA
3
+ import scipy.fft as sfft
4
+ from qutip import mesolve, Qobj
5
+ from typing import List, Tuple, Callable, Optional, Any
6
+
7
+ from . import params as pa
8
+ from . import trans_basis as tb
9
+
10
+
11
+ class DynamicsOS:
12
+ """Class for open-system dynamics (Lindblad equation).
13
+
14
+ This class provides methods to simulate open-system dynamics described by the Lindblad equation.
15
+
16
+ Attributes:
17
+ Nsys (int): System Hilbert Space Dimension.
18
+ Hsys (np.ndarray): Hamiltonian of the system (shape (N, N)).
19
+ rho0 (np.ndarray): Initial density matrix (shape (N, N)).
20
+ c_ops (List[np.ndarray]): List of collapse operators (each of shape (N, N)).
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ Nsys: int,
26
+ Hsys: np.ndarray,
27
+ rho0: np.ndarray,
28
+ c_ops: Optional[List[np.ndarray]] = None
29
+ ) -> None:
30
+ """
31
+ Initialize the DynamicsOS instance.
32
+
33
+ Args:
34
+ Nsys (int): System Hilbert Space Dimension.
35
+ Hsys (np.ndarray): Hamiltonian of the system.
36
+ rho0 (np.ndarray): Initial density matrix.
37
+ c_ops (Optional[List[np.ndarray]]): List of collapse operators. Defaults to an empty list.
38
+ """
39
+ if c_ops is None:
40
+ c_ops = []
41
+ self.Nsys: int = Nsys
42
+ self.Hsys: np.ndarray = Hsys
43
+ self.rho0: np.ndarray = rho0
44
+ self.c_ops: List[np.ndarray] = c_ops
45
+
46
+ def Gt_matrix_expo(self, time_arr: List[float], Is_show_step: bool = False) -> List[np.ndarray]:
47
+ """
48
+ Compute the propagator of the Lindblad equation using the matrix exponential.
49
+
50
+ The propagator is computed by exponentiating the Liouvillian operator defined by the
51
+ system Hamiltonian and collapse operators.
52
+
53
+ Args:
54
+ time_arr (List[float]): Array of time values for the simulation.
55
+ Is_show_step (bool, optional): If True, prints the current simulation step. Defaults to False.
56
+
57
+ Returns:
58
+ List[np.ndarray]: List of propagators corresponding to each time in `time_arr`.
59
+ """
60
+ ident_h: np.ndarray = np.eye(self.Nsys, dtype=np.complex128)
61
+
62
+ # Build the A matrix for time-derivation of the vectorized density matrix.
63
+ Amat: np.ndarray = -1j * (np.kron(self.Hsys, ident_h) - np.kron(ident_h, self.Hsys.T))
64
+ for i in range(len(self.c_ops)):
65
+ Amat += 0.5 * (
66
+ 2.0 * np.kron(self.c_ops[i], self.c_ops[i].conj())
67
+ - np.kron(ident_h, (self.c_ops[i].T @ self.c_ops[i].conj()))
68
+ - np.kron((self.c_ops[i].T.conj() @ self.c_ops[i]), ident_h)
69
+ )
70
+
71
+ G_prop: List[np.ndarray] = []
72
+ for i, t in enumerate(time_arr):
73
+ if Is_show_step:
74
+ print("step", i, "time", t)
75
+ Gt: np.ndarray = LA.expm(Amat * t)
76
+ G_prop.append(Gt)
77
+ return G_prop
78
+
79
+ def propagate_matrix_exp(
80
+ self,
81
+ time_arr: List[float],
82
+ observable: np.ndarray,
83
+ Is_store_state: bool = False,
84
+ Is_show_step: bool = False,
85
+ Is_Gt: bool = False,
86
+ ) -> Any:
87
+ """
88
+ Solve the Lindblad equation using matrix exponential.
89
+
90
+ This method computes the propagator, evolves the initial density matrix, and calculates
91
+ the expectation value of the observable over time. Optionally, it stores the evolved density matrices.
92
+
93
+ Args:
94
+ time_arr (List[float]): Time array for dynamic simulation.
95
+ observable (np.ndarray): Observable matrix for which the expectation value is computed.
96
+ Is_store_state (bool, optional): If True, stores the density matrices at each time step.
97
+ Defaults to False.
98
+ Is_show_step (bool, optional): If True, prints the current simulation step. Defaults to False.
99
+ Is_Gt (bool, optional): If True, includes the propagators in the result. Defaults to False.
100
+
101
+ Returns:
102
+ Result: An object with the following attributes:
103
+ - expect (List[float]): List of expectation values over time.
104
+ - density_matrix (List[np.ndarray], optional): List of density matrices (if `Is_store_state` is True).
105
+ - Gprop (List[np.ndarray], optional): List of propagators (if `Is_Gt` is True).
106
+ """
107
+
108
+ class Result:
109
+ """Class for storing propagation results."""
110
+ def __init__(self, store_state: bool, include_Gt: bool) -> None:
111
+ self.expect: List[float] = []
112
+ if store_state:
113
+ self.density_matrix: List[np.ndarray] = []
114
+ if include_Gt:
115
+ self.Gprop: Optional[List[np.ndarray]] = None
116
+
117
+ result = Result(Is_store_state, Is_Gt)
118
+
119
+ # Compute the propagator of the Lindblad equation.
120
+ G_prop: List[np.ndarray] = self.Gt_matrix_expo(time_arr, Is_show_step)
121
+ if Is_Gt:
122
+ result.Gprop = G_prop
123
+
124
+ # Initialize the vectorized density matrix.
125
+ vec_rho0: np.ndarray = self.rho0.reshape(self.Nsys**2)
126
+
127
+ for i, _ in enumerate(time_arr):
128
+ vec_rhot: np.ndarray = G_prop[i] @ vec_rho0
129
+ # Reshape back to density matrix form.
130
+ rhot: np.ndarray = vec_rhot.reshape(self.Nsys, self.Nsys)
131
+
132
+ if Is_store_state:
133
+ result.density_matrix.append(rhot)
134
+ result.expect.append(np.trace(rhot @ observable).real)
135
+
136
+ return result
137
+
138
+ def propagate_qt(self, time_arr: List[float], observable: Any, **kwargs: Any) -> List[float]:
139
+ """
140
+ Propagate the system using QuTiP's `mesolve` function.
141
+
142
+ This method solves the Lindblad master equation using QuTiP's `mesolve` to compute the expectation
143
+ values of the observable over time.
144
+
145
+ Args:
146
+ time_arr (List[float]): Time array for dynamic simulation.
147
+ observable (Any): Observable operator(s) for which the expectation value is computed.
148
+ Can be a single operator or a list of operators.
149
+ **kwargs: Additional keyword arguments to pass to `mesolve`.
150
+
151
+ Returns:
152
+ List[float]: List of expectation values of the observable over time.
153
+ """
154
+ c_ops: List[Qobj] = [Qobj(c_op) for c_op in self.c_ops]
155
+
156
+ if isinstance(observable, list):
157
+ obs = [Qobj(op) for op in observable]
158
+ else:
159
+ obs = Qobj(observable)
160
+
161
+ result = mesolve(Qobj(self.Hsys), Qobj(self.rho0), time_arr, c_ops, obs, **kwargs)
162
+ return result.expect
163
+
164
+
165
+ class DVR_grid:
166
+ """Class for Discrete Variable Representation (DVR) grid methods.
167
+
168
+ This class handles grid-based representations for systems where the potential is expressed on grid points.
169
+
170
+ Attributes:
171
+ Ngrid (int): Number of grid points.
172
+ xmin (float): Minimum value of the grid.
173
+ xmax (float): Maximum value of the grid.
174
+ mass (float): Mass of the particle.
175
+ xgrid (np.ndarray): Array of grid points in position space.
176
+ dx (float): Spacing between grid points.
177
+ dk (float): Spacing in momentum space.
178
+ kgrid (np.ndarray): Array of grid points in momentum space.
179
+ ak2 (np.ndarray): Kinetic energy array in momentum space.
180
+ hamk (np.ndarray): Kinetic Hamiltonian matrix in position space.
181
+ potential (Optional[np.ndarray]): Potential energy array on the grid.
182
+ """
183
+
184
+ def __init__(self, xmin: float, xmax: float, Ngrid: int, mass: float) -> None:
185
+ """
186
+ Initialize the DVR_grid instance.
187
+
188
+ Args:
189
+ xmin (float): Minimum x-value.
190
+ xmax (float): Maximum x-value.
191
+ Ngrid (int): Number of grid points.
192
+ mass (float): Mass of the particle.
193
+ """
194
+ self.Ngrid: int = Ngrid
195
+ self.xmin: float = xmin
196
+ self.xmax: float = xmax
197
+ self.mass: float = mass
198
+
199
+ # Set up the position grid.
200
+ self.xgrid: np.ndarray = np.array([])
201
+ self._set_xgrid()
202
+ self.dx: float = self.xgrid[1] - self.xgrid[0]
203
+
204
+ # Set up the momentum grid.
205
+ self.dk: float = 2.0 * np.pi / (self.Ngrid * self.dx)
206
+ self.kgrid: np.ndarray = np.array([])
207
+ self.ak2: np.ndarray = np.array([]) # Kinetic energy array.
208
+ self.hamk: np.ndarray = np.array([]) # Kinetic Hamiltonian matrix.
209
+ self._set_kinet_ham()
210
+
211
+ # Potential energy array (to be set later).
212
+ self.potential: Optional[np.ndarray] = None
213
+
214
+ def _set_xgrid(self) -> None:
215
+ """
216
+ Set up the position space grid.
217
+
218
+ Initializes the `xgrid` attribute using a linear space between `xmin` and `xmax`.
219
+ """
220
+ self.xgrid = np.linspace(self.xmin, self.xmax, self.Ngrid)
221
+
222
+ def set_potential(self, func_pot: Callable[[float], float]) -> None:
223
+ """
224
+ Set up the potential energy array on the grid.
225
+
226
+ Args:
227
+ func_pot (Callable[[float], float]): Function that returns the potential value at a given x.
228
+ """
229
+ self.potential = np.zeros_like(self.xgrid)
230
+ for i in range(self.Ngrid):
231
+ self.potential[i] = func_pot(self.xgrid[i])
232
+
233
+ def _set_kinet_ham(self) -> None:
234
+ """
235
+ Set up the kinetic Hamiltonian matrix in position space.
236
+
237
+ This method computes the momentum grid and the corresponding kinetic energy values,
238
+ and constructs the kinetic Hamiltonian matrix in position space using a Fourier transform.
239
+ """
240
+ self.kgrid = np.zeros(self.Ngrid, dtype=np.float64)
241
+ self.ak2 = np.zeros(self.Ngrid, dtype=np.float64)
242
+
243
+ coef_k: float = pa.hbar**2 / (2.0 * self.mass)
244
+
245
+ for i in range(self.Ngrid):
246
+ if i < self.Ngrid // 2:
247
+ self.kgrid[i] = i * self.dk
248
+ else:
249
+ self.kgrid[i] = -(self.Ngrid - i) * self.dk
250
+ self.ak2[i] = coef_k * self.kgrid[i]**2
251
+
252
+ akx0: np.ndarray = sfft.ifft(self.ak2)
253
+ self.hamk = np.zeros((self.Ngrid, self.Ngrid), dtype=np.complex128)
254
+
255
+ for i in range(self.Ngrid):
256
+ for j in range(self.Ngrid):
257
+ if i < j:
258
+ self.hamk[i, j] = akx0[i - j].conj()
259
+ else:
260
+ self.hamk[i, j] = akx0[i - j]
261
+
262
+ def get_eig_state(self, Nstate: int) -> Tuple[np.ndarray, np.ndarray]:
263
+ """
264
+ Get the eigenstates for the potential in x-space.
265
+
266
+ Args:
267
+ Nstate (int): Number of eigenstates to output.
268
+
269
+ Returns:
270
+ Tuple[np.ndarray, np.ndarray]: A tuple containing:
271
+ - Eigenvalues (np.ndarray) for the first `Nstate` states.
272
+ - Eigenvectors (np.ndarray) for the first `Nstate` states, normalized by sqrt(dx).
273
+ """
274
+ Mata: np.ndarray = self.hamk.copy()
275
+ for i in range(self.Ngrid):
276
+ Mata[i, i] += self.potential[i]
277
+
278
+ val, arr = LA.eigh(Mata)
279
+ return val[:Nstate], arr[:, :Nstate] / np.sqrt(self.dx)
280
+
281
+ def x2k_wave(self, psi: np.ndarray) -> np.ndarray:
282
+ """
283
+ Transform the wavefunction from position space to momentum space.
284
+
285
+ Args:
286
+ psi (np.ndarray): Wavefunction in position space.
287
+
288
+ Returns:
289
+ np.ndarray: Wavefunction in momentum space.
290
+ """
291
+ return tb.x2k_wave(self.dx, psi)
292
+
293
+ def k2x_wave(self, psik: np.ndarray) -> np.ndarray:
294
+ """
295
+ Transform the wavefunction from momentum space to position space.
296
+
297
+ Args:
298
+ psik (np.ndarray): Wavefunction in momentum space.
299
+
300
+ Returns:
301
+ np.ndarray: Wavefunction in position space.
302
+ """
303
+ return tb.k2x_wave(self.dx, psik)
@@ -0,0 +1,29 @@
1
+ """
2
+ Define some Parameter and Constants
3
+ """
4
+
5
+ import numpy as np
6
+
7
+ # Pauli matrices
8
+ X = np.array([[0, 1], [1, 0]], dtype=np.complex128)
9
+ Y = np.array([[0, -1j], [1j, 0]], dtype=np.complex128)
10
+ Z = np.array([[1, 0], [0, -1]], dtype=np.complex128)
11
+ I = np.eye(2, dtype=np.complex128)
12
+
13
+
14
+ # sigma+ and sigma-
15
+ sigmap = 0.5*(X+1j*Y)
16
+ sigmam = 0.5*(X-1j*Y)
17
+
18
+ # Spin-up and spin-down states
19
+ spin_up = np.array([1.0, 0.0], dtype=np.float64)
20
+ spin_down = np.array([0.0, 1.0], dtype=np.float64)
21
+
22
+
23
+ #some constant to convert the units
24
+ au2fs = 2.418884254E-2
25
+ au2cm = 219474.63068
26
+ au2joule = 4.35974381E-18
27
+ bolz = 1.3806503E-23
28
+ au2ev = 27.2114
29
+ hbar = 1.0
@@ -0,0 +1,360 @@
1
+ import numpy as np
2
+ import scipy.linalg as LA
3
+ from typing import List, Tuple, Optional, Dict, Any, Union
4
+
5
+ from qiskit import transpile
6
+ from qiskit_aer import AerSimulator
7
+ #from qiskit.primitives import Estimator
8
+ from qiskit_ibm_runtime import EstimatorV2 as Estimator
9
+ from qiskit.quantum_info import SparsePauliOp
10
+ from qiskit.circuit import QuantumCircuit
11
+
12
+ from . import trans_basis as tb
13
+ from . import dilation_circuit as dc
14
+ from .numerical_methods import DynamicsOS
15
+
16
+
17
+ def expand(Gmat_org: np.ndarray, Norg: int, Nexpand: int) -> np.ndarray:
18
+ """
19
+ Expand the propagator in the vectorized density matrix representation.
20
+
21
+ This function expands the original propagator matrix to match the dimensions
22
+ of the vectorized density matrix representation (e.g., match the 2^N dimension).
23
+
24
+ Args:
25
+ Gmat_org (np.ndarray): Original propagator matrix.
26
+ Norg (int): Original system dimension.
27
+ Nexpand (int): Expanded system dimension.
28
+
29
+ Returns:
30
+ np.ndarray: The expanded propagator matrix with shape (Nexpand**2, Nexpand**2).
31
+ """
32
+ Gnew = np.zeros((Nexpand**2, Nexpand**2), dtype=np.complex128)
33
+ for i in range(Norg):
34
+ for j in range(Norg):
35
+ for k in range(Norg):
36
+ for l in range(Norg):
37
+ Gnew[i * Nexpand + j, k * Nexpand + l] = Gmat_org[i * Norg + j, k * Norg + l]
38
+ return Gnew
39
+
40
+
41
+ def gen_Kraus_list(Gmat: np.ndarray, N: int, tol: float = 1e-5) -> List[np.ndarray]:
42
+ """
43
+ Generate the Kraus operators from the propagator with a given tolerance.
44
+
45
+ This function computes the Kraus operator representation by constructing the Choi
46
+ matrix from the propagator matrix and performing an eigenvalue decomposition.
47
+
48
+ Args:
49
+ Gmat (np.ndarray): Matrix of the propagator with shape (N^2, N^2).
50
+ N (int): The system Hilbert space dimension.
51
+ tol (float, optional): Tolerance threshold for the Kraus operator representation.
52
+ Operators with eigenvalues below tol are ignored. Defaults to 1e-5.
53
+
54
+ Returns:
55
+ List[np.ndarray]: A list of Kraus operators.
56
+ """
57
+ # Define the Choi matrix from the propagator matrix
58
+ C_mat = np.zeros(Gmat.shape, dtype=np.complex128)
59
+ for i in range(N):
60
+ for j in range(N):
61
+ C_matij = np.zeros(Gmat.shape, dtype=np.complex128)
62
+ for k in range(N):
63
+ for l in range(N):
64
+ C_matij[i * N + k, l * N + j] = Gmat[j * N + k, l * N + i]
65
+ C_mat += C_matij
66
+
67
+ Kraus: List[np.ndarray] = []
68
+ eigenvalues, eigenvectors = LA.eigh(C_mat)
69
+ for idx in range(len(eigenvalues)):
70
+ if eigenvalues[idx] > tol:
71
+ Mi = np.sqrt(eigenvalues[idx]) * eigenvectors[:, idx].reshape(N, N)
72
+ Kraus.append(Mi.conj().T)
73
+ return Kraus
74
+
75
+
76
+ class QubitDynamicsOS(DynamicsOS):
77
+ """
78
+ Class for simulating quantum dynamics using either vectorized density matrix or Kraus operator representations.
79
+
80
+ This class provides methods to initialize state vectors, construct quantum circuits,
81
+ and perform quantum simulations using Qiskit backends.
82
+ """
83
+
84
+ def __init__(self, rep: str = 'Density', **kwargs: Any) -> None:
85
+ """
86
+ Initialize a QubitDynamicsOS instance.
87
+
88
+ Depending on the representation, either "Density" or "Kraus", the number of qubits is computed.
89
+ Additional keyword arguments are passed to the base DynamicsOS class.
90
+
91
+ Args:
92
+ rep (str, optional): Representation type, either 'Density' or 'Kraus'. Defaults to 'Density'.
93
+ **kwargs: Additional keyword arguments for the DynamicsOS initializer.
94
+ """
95
+ super().__init__(**kwargs)
96
+
97
+ if rep == 'Density':
98
+ # Vectorized density matrix representation
99
+ self.rep: str = 'Density'
100
+ self.Nqb: int = int(np.log2(self.Nsys**2))
101
+ elif rep == 'Kraus':
102
+ # Kraus operator representation
103
+ self.rep = 'Kraus'
104
+ self.Nqb = int(np.log2(self.Nsys))
105
+
106
+ # The counting qubits bit string and observable matrix are initialized to None.
107
+ self.count_str: Optional[List[str]] = None
108
+ self.observable: Optional[np.ndarray] = None
109
+
110
+ # Default dilation method for quantum simulation.
111
+ self.dilation_method: str = 'Sz-Nagy'
112
+
113
+ def set_dilation_method(self, method: str) -> None:
114
+ """
115
+ Set the dilation method for quantum simulation.
116
+
117
+ Args:
118
+ method (str): The dilation method, e.g., 'Sz-Nagy', 'SVD', or 'SVD-Walsh'.
119
+ """
120
+ self.dilation_method = method
121
+
122
+ def set_count_str(self, count_str: List[str]) -> None:
123
+ """
124
+ Set the counting bit string for measurement.
125
+
126
+ Args:
127
+ count_str (List[str]): The counting bit string.
128
+ """
129
+ self.count_str = count_str
130
+
131
+ def set_observable(self, observable: np.ndarray) -> None:
132
+ """
133
+ Set the observable for the quantum simulation.
134
+
135
+ Args:
136
+ observable (np.ndarray): The observable matrix.
137
+ """
138
+ self.observable = observable
139
+
140
+ def init_statevec_vecdens(self) -> Tuple[np.ndarray, float]:
141
+ """
142
+ Initialize the state vector from the initial density operator using vectorized representation.
143
+
144
+ The initial density matrix is reshaped into a vector and normalized.
145
+
146
+ Returns:
147
+ Tuple[np.ndarray, float]: A tuple containing the normalized state vector and the norm
148
+ of the original vectorized density matrix.
149
+ """
150
+ vec_rho0 = self.rho0.reshape(self.Nsys**2)
151
+ norm0 = LA.norm(vec_rho0, 2)
152
+ statevec = vec_rho0 / norm0
153
+ return statevec, norm0
154
+
155
+ def init_statevec_Kraus(self, tol: float = 1e-6) -> Tuple[List[np.ndarray], List[float]]:
156
+ """
157
+ Initialize state vectors from the initial density operator using the Kraus operator representation.
158
+
159
+ The density matrix is decomposed using eigenvalue decomposition, and eigenstates with eigenvalues
160
+ below the specified tolerance are ignored.
161
+
162
+ Args:
163
+ tol (float, optional): Tolerance for ignoring eigenstates with small eigenvalues. Defaults to 1e-6.
164
+
165
+ Returns:
166
+ Tuple[List[np.ndarray], List[float]]: A tuple containing a list of state vectors and a list of
167
+ corresponding probabilities.
168
+ """
169
+ eigenvalues, eigenvectors = LA.eigh(self.rho0)
170
+ statevec: List[np.ndarray] = []
171
+ prob: List[float] = []
172
+ for i in range(len(eigenvalues) - 1, -1, -1):
173
+ if abs(eigenvalues[i]) < tol:
174
+ break
175
+ prob.append(eigenvalues[i])
176
+ statevec.append(eigenvectors[:, i])
177
+ return statevec, prob
178
+
179
+ def _get_qiskit_observable(self, Isdilate: bool = False, tol: float = 5e-3) -> SparsePauliOp:
180
+ """
181
+ Prepare and return the Qiskit observable operator.
182
+
183
+ Converts the observable matrix to its Pauli representation and returns a SparsePauliOp.
184
+
185
+ Args:
186
+ Isdilate (bool, optional): Flag indicating whether to use the dilated observable.
187
+ Defaults to False.
188
+ tol (float, optional): Tolerance for the Pauli decomposition. Defaults to 5e-3.
189
+
190
+ Returns:
191
+ SparsePauliOp: The Qiskit representation of the observable.
192
+ """
193
+ if self.observable is None:
194
+ print('Error: observable is None')
195
+
196
+ if Isdilate:
197
+ num_qubits = self.Nqb + 1
198
+ Obs_mat = np.zeros((2 * self.Nsys, 2 * self.Nsys), dtype=np.complex128)
199
+ Obs_mat[:self.Nsys, :self.Nsys] = self.observable[:self.Nsys, :self.Nsys]
200
+ else:
201
+ num_qubits = self.Nqb
202
+ Obs_mat = self.observable
203
+
204
+ Obs_paulis_dic = tb.ham_to_pauli(Obs_mat, num_qubits, tol=tol)
205
+
206
+ # Prepare the Qiskit observable from the Pauli strings of the observable matrix.
207
+ data: List[str] = []
208
+ coef: List[float] = []
209
+ for key in Obs_paulis_dic:
210
+ data.append(key)
211
+ coef.append(Obs_paulis_dic[key])
212
+ obs_q = SparsePauliOp(data, coef)
213
+ return obs_q
214
+
215
+ def qc_simulation_kraus(
216
+ self,
217
+ time_arr: List[float],
218
+ shots: int = 1024,
219
+ Kraus: Optional[Dict[int, List[np.ndarray]]] = None,
220
+ Gprop: Optional[List[np.ndarray]] = None,
221
+ tolk: float = 1e-5,
222
+ tolo: float = 1e-5,
223
+ **kwargs: Any
224
+ ) -> np.ndarray:
225
+ """
226
+ Perform quantum simulation using the Kraus operator representation.
227
+
228
+ This method simulates the quantum system dynamics over a series of time steps using a Kraus operator-based approach.
229
+ It constructs quantum circuits for each Kraus operator and initial state, runs the simulation using Qiskit's Estimator,
230
+ and accumulates the measurement results.
231
+
232
+ Args:
233
+ time_arr (List[float]): List of time steps for simulation.
234
+ shots (int, optional): Number of shots for each measurement. Defaults to 1024.
235
+ Kraus (Optional[Dict[int, List[np.ndarray]]], optional): Dictionary mapping time step index to a list of Kraus operators.
236
+ If None, Kraus operators are generated from the propagator. Defaults to None.
237
+ Gprop (Optional[List[np.ndarray]], optional): Propagator matrix (or list of matrices) for simulation.
238
+ If None, it will be calculated. Defaults to None.
239
+ tolk (float, optional): Tolerance for generating Kraus operators. Defaults to 1e-5.
240
+ tolo (float, optional): Tolerance for observable decomposition. Defaults to 1e-5.
241
+ **kwargs: Additional keyword arguments for propagator calculation.
242
+
243
+ Returns:
244
+ np.ndarray: Array containing the quantum simulation results.
245
+ """
246
+ nsteps = len(time_arr)
247
+
248
+ # Generate Kraus operators if not provided.
249
+ if Kraus is None:
250
+ Kraus = {}
251
+ if Gprop is None:
252
+ print('Calculating the propagator')
253
+ Gprop = self.Gt_matrix_expo(time_arr, **kwargs)
254
+ print('Generating the Kraus operators')
255
+ for i in range(nsteps):
256
+ print('At step', i, 'of', nsteps)
257
+ Kraus[i] = gen_Kraus_list(Gprop[i], self.Nsys, tol=tolk)
258
+ print('Kraus operator generation complete')
259
+
260
+ # Perform Qiskit simulation using the Estimator.
261
+ estimator = Estimator()
262
+
263
+ statevec, prob = self.init_statevec_Kraus()
264
+ n_inistate = len(statevec)
265
+ print('Number of initial states in the density matrix:', n_inistate)
266
+ print('Probabilities:', prob)
267
+
268
+ # Obtain the Qiskit observable.
269
+ obs_q = self._get_qiskit_observable(Isdilate=True, tol=tolo)
270
+
271
+ print('Starting quantum simulation')
272
+ result_simulation = np.zeros(nsteps, dtype=np.float64)
273
+
274
+ for i in range(nsteps):
275
+ print('Simulation step', i, 'of', nsteps)
276
+ current_kraus_list = Kraus[i]
277
+ print('Number of Kraus operators:', len(current_kraus_list))
278
+ for kraus_op in current_kraus_list:
279
+ for istate in range(n_inistate):
280
+ qc = self._create_circuit(kraus_op, statevec[istate], Isscale=False)
281
+ result = estimator.run(qc, obs_q, shots=shots).result()
282
+ result_simulation[i] += result.values[0] * prob[istate]
283
+
284
+ return result_simulation
285
+
286
+ def qc_simulation_vecdens(
287
+ self,
288
+ time_arr: List[float],
289
+ shots: int = 1024,
290
+ backend: Any = AerSimulator(),
291
+ Gprop: Optional[List[np.ndarray]] = None,
292
+ **kwargs: Any
293
+ ) -> np.ndarray:
294
+ """
295
+ Perform quantum simulation using the vectorized density matrix representation.
296
+
297
+ This method simulates the quantum system dynamics over a series of time steps by constructing circuits
298
+ based on the vectorized density matrix representation, performing measurements, and processing the results.
299
+
300
+ Args:
301
+ time_arr (List[float]): List of time steps for simulation.
302
+ shots (int, optional): Number of measurement shots. Defaults to 1024.
303
+ backend (Any, optional): Quantum simulation backend. Defaults to AerSimulator().
304
+ Gprop (Optional[List[np.ndarray]], optional): Propagator matrix (or list of matrices) for simulation.
305
+ If None, it will be calculated. Defaults to None.
306
+ **kwargs: Additional keyword arguments for propagator calculation.
307
+
308
+ Returns:
309
+ np.ndarray: Array containing the quantum simulation results.
310
+ """
311
+ if Gprop is None:
312
+ Gprop = self.Gt_matrix_expo(time_arr, **kwargs)
313
+
314
+ nsteps = len(time_arr)
315
+
316
+ if self.count_str is None:
317
+ print("Error: count_str is not assigned")
318
+
319
+ n_bitstr = len(self.count_str)
320
+ statevec, norm0 = self.init_statevec_vecdens()
321
+ result = np.zeros((nsteps, n_bitstr), dtype=np.float64)
322
+
323
+ for i in range(nsteps):
324
+ if i % 100 == 0:
325
+ print('Quantum simulation step', i)
326
+ Gt = Gprop[i]
327
+ circuit, norm = self._create_circuit(Gt, statevec, Isscale=True)
328
+ circuit.measure(range(self.Nqb + 1), range(self.Nqb + 1))
329
+ if self.dilation_method == 'SVD-Walsh':
330
+ circuit = transpile(circuit, backend)
331
+ counts = backend.run(circuit, shots=shots).result().get_counts()
332
+ for j in range(n_bitstr):
333
+ bitstr = self.count_str[j]
334
+ if bitstr in counts:
335
+ result[i, j] = np.sqrt(counts[bitstr] / shots) * norm * norm0
336
+ else:
337
+ print('At time', i, 'with shots =', shots, "no counts for", bitstr)
338
+
339
+ return result
340
+
341
+ def _create_circuit(
342
+ self,
343
+ array: np.ndarray,
344
+ statevec: Union[np.ndarray, List[np.ndarray]],
345
+ Isscale: bool = True
346
+ ) -> QuantumCircuit:
347
+ """
348
+ Construct and return the quantum circuit.
349
+
350
+ This method wraps the call to the dilation circuit construction function.
351
+
352
+ Args:
353
+ array (np.ndarray): Array used for circuit construction (e.g., propagator or Kraus operator).
354
+ statevec (Union[np.ndarray, List[np.ndarray]]): State vector(s) to be used in the circuit.
355
+ Isscale (bool, optional): Flag indicating whether scaling should be applied. Defaults to True.
356
+
357
+ Returns:
358
+ QuantumCircuit: The constructed quantum circuit.
359
+ """
360
+ return dc.construct_circuit(self.Nqb, array, statevec, method=self.dilation_method, Isscale=Isscale)