qilisdk 0.1.0__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.
Files changed (47) hide show
  1. qilisdk/__init__.py +47 -0
  2. qilisdk/__init__.pyi +30 -0
  3. qilisdk/_optionals.py +105 -0
  4. qilisdk/analog/__init__.py +17 -0
  5. qilisdk/analog/algorithms.py +111 -0
  6. qilisdk/analog/analog_backend.py +43 -0
  7. qilisdk/analog/analog_result.py +114 -0
  8. qilisdk/analog/exceptions.py +19 -0
  9. qilisdk/analog/hamiltonian.py +706 -0
  10. qilisdk/analog/quantum_objects.py +486 -0
  11. qilisdk/analog/schedule.py +311 -0
  12. qilisdk/common/__init__.py +20 -0
  13. qilisdk/common/algorithm.py +17 -0
  14. qilisdk/common/backend.py +16 -0
  15. qilisdk/common/model.py +16 -0
  16. qilisdk/common/optimizer.py +136 -0
  17. qilisdk/common/optimizer_result.py +110 -0
  18. qilisdk/common/result.py +17 -0
  19. qilisdk/digital/__init__.py +66 -0
  20. qilisdk/digital/ansatz.py +143 -0
  21. qilisdk/digital/circuit.py +106 -0
  22. qilisdk/digital/digital_algorithm.py +20 -0
  23. qilisdk/digital/digital_backend.py +90 -0
  24. qilisdk/digital/digital_result.py +145 -0
  25. qilisdk/digital/exceptions.py +31 -0
  26. qilisdk/digital/gates.py +989 -0
  27. qilisdk/digital/vqe.py +165 -0
  28. qilisdk/extras/__init__.py +13 -0
  29. qilisdk/extras/cuda/__init__.py +18 -0
  30. qilisdk/extras/cuda/cuda_analog_result.py +19 -0
  31. qilisdk/extras/cuda/cuda_backend.py +398 -0
  32. qilisdk/extras/cuda/cuda_digital_result.py +19 -0
  33. qilisdk/extras/qaas/__init__.py +13 -0
  34. qilisdk/extras/qaas/keyring.py +54 -0
  35. qilisdk/extras/qaas/models.py +57 -0
  36. qilisdk/extras/qaas/qaas_backend.py +154 -0
  37. qilisdk/extras/qaas/qaas_digital_result.py +20 -0
  38. qilisdk/extras/qaas/qaas_settings.py +23 -0
  39. qilisdk/py.typed +0 -0
  40. qilisdk/utils/__init__.py +27 -0
  41. qilisdk/utils/openqasm2.py +215 -0
  42. qilisdk/utils/serialization.py +128 -0
  43. qilisdk/yaml.py +71 -0
  44. qilisdk-0.1.0.dist-info/METADATA +237 -0
  45. qilisdk-0.1.0.dist-info/RECORD +47 -0
  46. qilisdk-0.1.0.dist-info/WHEEL +4 -0
  47. qilisdk-0.1.0.dist-info/licenses/LICENCE +201 -0
@@ -0,0 +1,486 @@
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from __future__ import annotations
15
+
16
+ import string
17
+ from typing import Literal
18
+
19
+ import numpy as np
20
+ from scipy.sparse import csc_array, csr_matrix, issparse, kron, sparray, spmatrix
21
+ from scipy.sparse.linalg import expm
22
+ from scipy.sparse.linalg import norm as scipy_norm
23
+
24
+ from qilisdk.yaml import yaml
25
+
26
+ Complex = int | float | complex
27
+ TWO = 2
28
+
29
+
30
+ @yaml.register_class
31
+ class QuantumObject:
32
+ """
33
+ Represents a quantum state or operator using a sparse matrix representation.
34
+
35
+ The QuantumObject class is a wrapper around sparse matrices (or NumPy arrays,
36
+ which are converted to sparse matrices) that represent quantum states (kets, bras)
37
+ or operators. It provides utility methods for common quantum operations such as
38
+ taking the adjoint (dagger), computing tensor products, partial traces, and norms.
39
+
40
+ The internal data is stored as a SciPy CSR (Compressed Sparse Row) matrix for
41
+ efficient arithmetic and manipulation. The expected shapes for the data are:
42
+ - (2**N, 2**N) for operators or density matrices,
43
+ - (2**N, 1) for ket states,
44
+ - (1, 2**N) for bra states.
45
+ """
46
+
47
+ def __init__(self, data: np.ndarray | sparray | spmatrix) -> None:
48
+ """
49
+ Initialize a QuantumObject with the given data.
50
+
51
+ Converts a NumPy array to a CSR matrix if needed and validates the shape of the input.
52
+ The input must represent a valid quantum state or operator with appropriate dimensions.
53
+
54
+ Args:
55
+ data (np.ndarray | sparray | spmatrix): A dense NumPy array or a SciPy sparse matrix
56
+ representing a quantum state or operator.
57
+
58
+ Raises:
59
+ ValueError: If the input data is not a NumPy array or a SciPy sparse matrix,
60
+ or if the data's shape does not correspond to a valid quantum state/operator.
61
+ """
62
+ if isinstance(data, np.ndarray):
63
+ self._data = csr_matrix(data)
64
+ elif issparse(data):
65
+ self._data = data.tocsr()
66
+ else:
67
+ raise ValueError("Input must be a NumPy array or a SciPy sparse matrix")
68
+ invalid_shape = (
69
+ len(self._data.shape) > TWO
70
+ or (self._data.shape[0] == 1 and self._data.shape[1] != 1 and self._data.shape[1] % 2 != 0)
71
+ or (self._data.shape[1] == 1 and self._data.shape[0] != 1 and self._data.shape[0] % 2 != 0)
72
+ or (self._data.shape[0] != self._data.shape[1] and self._data.shape[0] != 1 and self._data.shape[1] != 1)
73
+ or (
74
+ self._data.shape[1] == self._data.shape[0] and self._data.shape[0] % 2 != 0 and self._data.shape[0] != 1
75
+ )
76
+ )
77
+ if invalid_shape:
78
+ raise ValueError(
79
+ "Dimension of data is wrong. expected data to have shape similar to (2**N, 2**N), (1, 2**N), (2**N, 1)",
80
+ f"but received {self._data.shape}",
81
+ )
82
+
83
+ @property
84
+ def data(self) -> csr_matrix:
85
+ """
86
+ Get the internal sparse matrix representation of the QuantumObject.
87
+
88
+ Returns:
89
+ csr_matrix: The internal representation as a CSR matrix.
90
+ """
91
+ return self._data
92
+
93
+ @property
94
+ def dense(self) -> np.ndarray:
95
+ """
96
+ Get the dense (NumPy array) representation of the QuantumObject.
97
+
98
+ Returns:
99
+ np.ndarray: The dense array representation.
100
+ """
101
+ return self._data.toarray()
102
+
103
+ @property
104
+ def nqubits(self) -> int:
105
+ """
106
+ Compute the number of qubits represented by the QuantumObject.
107
+
108
+ Returns:
109
+ int: The number of qubits if determinable; otherwise, -1.
110
+ """
111
+ if self._data.shape[0] == self._data.shape[1]:
112
+ return int(np.log2(self._data.shape[0]))
113
+ if self._data.shape[0] == 1:
114
+ return int(np.log2(self._data.shape[1]))
115
+ if self._data.shape[1] == 1:
116
+ return int(np.log2(self._data.shape[0]))
117
+ return -1
118
+
119
+ @property
120
+ def shape(self) -> tuple[int, ...]:
121
+ """
122
+ Get the shape of the QuantumObject's internal matrix.
123
+
124
+ Returns:
125
+ tuple[int, ...]: The shape of the internal matrix.
126
+ """
127
+ return self._data.shape
128
+
129
+ def dag(self) -> QuantumObject:
130
+ """
131
+ Compute the adjoint (conjugate transpose) of the QuantumObject.
132
+
133
+ Returns:
134
+ QuantumObject: A new QuantumObject that is the adjoint of this object.
135
+ """
136
+ out = QuantumObject(self._data.conj().T)
137
+ return out
138
+
139
+ def ptrace(self, dims: list[int], keep: list[int]) -> "QuantumObject":
140
+ """
141
+ Compute the partial trace over subsystems not in 'keep'.
142
+
143
+ This method calculates the reduced density matrix by tracing out
144
+ the subsystems that are not specified in the 'keep' parameter. The
145
+ input 'dims' represents the dimensions of each subsystem, and 'keep'
146
+ indicates the indices of the subsystems to be retained.
147
+
148
+ Args:
149
+ dims (list[int]): A list specifying the dimensions of each subsystem.
150
+ keep (list[int]): A list of indices corresponding to the subsystems to retain.
151
+
152
+ Raises:
153
+ ValueError: If the product of the dimensions in dims does not match the
154
+ shape of the QuantumObject's dense representation.
155
+
156
+ Returns:
157
+ QuantumObject: A new QuantumObject representing the reduced density matrix
158
+ for the subsystems specified in 'keep'.
159
+ """
160
+ rho = self.dense
161
+ total_dim = np.prod(dims)
162
+ if rho.shape != (total_dim, total_dim):
163
+ raise ValueError("Dimension mismatch between provided dims and QuantumObject shape")
164
+
165
+ n = len(dims)
166
+ # Use letters from the ASCII alphabet (both cases) for einsum indices.
167
+ # For each subsystem, assign two letters: one for the row index and one for the column index.
168
+ row_letters = []
169
+ col_letters = []
170
+ out_row = [] # Letters that will remain in the output for the row part.
171
+ out_col = [] # Letters for the column part.
172
+ letters = iter(string.ascii_letters)
173
+
174
+ for i in range(n):
175
+ if i in keep:
176
+ # For a subsystem we want to keep, use two different letters
177
+ r = next(letters)
178
+ c = next(letters)
179
+ row_letters.append(r)
180
+ col_letters.append(c)
181
+ out_row.append(r)
182
+ out_col.append(c)
183
+ else:
184
+ # For subsystems to be traced out, assign the same letter so that those indices are summed.
185
+ r = next(letters)
186
+ row_letters.append(r)
187
+ col_letters.append(r)
188
+
189
+ # Create the einsum subscript strings.
190
+ # The input tensor has 2*n indices (first n for rows, next n for columns).
191
+ input_subscript = "".join(row_letters + col_letters)
192
+ # The output will only contain the indices corresponding to the subsystems we keep.
193
+ output_subscript = "".join(out_row + out_col)
194
+
195
+ # Reshape rho into a tensor with shape dims + dims.
196
+ reshaped = rho.reshape(dims + dims)
197
+ # Use einsum to sum over the indices that appear twice (i.e. those being traced out).
198
+ reduced_tensor = np.einsum(f"{input_subscript}->{output_subscript}", reshaped)
199
+
200
+ # The resulting tensor has separate indices for each subsystem kept.
201
+ # Reshape it into a matrix (i.e. combine the row indices and column indices).
202
+ dims_keep = [dims[i] for i in keep]
203
+ new_dim = np.prod(dims_keep)
204
+ reduced_matrix = reduced_tensor.reshape(new_dim, new_dim)
205
+
206
+ return QuantumObject(reduced_matrix)
207
+
208
+ def norm(self, order: int | Literal["fro", "tr"] = 1) -> float:
209
+ """
210
+ Compute the norm of the QuantumObject.
211
+
212
+ For density matrices, the norm order can be specified. For state vectors,
213
+ the norm is computed accordingly.
214
+
215
+ Args:
216
+ order (int or {"fro", "tr"}, optional): The order of the norm.
217
+ If the QuantumObject represents a density matrix and 'tr' is specified,
218
+ the trace norm is returned. Defaults to 1.
219
+
220
+ Returns:
221
+ float: The computed norm of the QuantumObject.
222
+ """
223
+ if self.is_scalar():
224
+ return self.dense[0][0]
225
+ if self.is_dm() or self.shape[0] == self.shape[1]:
226
+ if order == "tr":
227
+ return self._data.trace()
228
+ return scipy_norm(self._data, ord=order)
229
+ if self.is_bra():
230
+ return np.sqrt(self._data @ self._data.conj().T).toarray()[0, 0]
231
+ return np.sqrt(self._data.conj().T @ self._data).toarray()[0, 0]
232
+
233
+ def unit(self, order: int | Literal["fro", "tr"] = "tr") -> QuantumObject:
234
+ """
235
+ Normalize the QuantumObject.
236
+
237
+ Scales the QuantumObject so that its norm becomes 1, according to the specified norm order.
238
+
239
+ Args:
240
+ order (int or {"fro", "tr"}, optional): The order of the norm to use for normalization.
241
+ If the QuantumObject represents a density matrix and 'tr' is specified,
242
+ the trace norm is used. Defaults to "tr".
243
+
244
+ Raises:
245
+ ValueError: If the norm of the QuantumObject is 0, making normalization impossible.
246
+
247
+ Returns:
248
+ QuantumObject: A new QuantumObject that is the normalized version of this object.
249
+ """
250
+ norm = self.norm(order=order)
251
+ if norm == 0:
252
+ raise ValueError("Cannot normalize a zero-norm Quantum Object")
253
+ return QuantumObject(self._data / norm)
254
+
255
+ def expm(self) -> QuantumObject:
256
+ """
257
+ Compute the matrix exponential of the QuantumObject.
258
+
259
+ Returns:
260
+ QuantumObject: A new QuantumObject representing the matrix exponential.
261
+ """
262
+ return QuantumObject(expm(self._data))
263
+
264
+ def is_ket(self) -> bool:
265
+ """
266
+ Check if the QuantumObject represents a ket (column vector) state.
267
+
268
+ Returns:
269
+ bool: True if the QuantumObject is a ket state, False otherwise.
270
+ """
271
+ return self.shape[0] % 2 == 0 and self.shape[1] == 1
272
+
273
+ def is_bra(self) -> bool:
274
+ """
275
+ Check if the QuantumObject represents a bra (row vector) state.
276
+
277
+ Returns:
278
+ bool: True if the QuantumObject is a bra state, False otherwise.
279
+ """
280
+ return self.shape[1] % 2 == 0 and self.shape[0] == 1
281
+
282
+ def is_scalar(self) -> bool:
283
+ """
284
+ Check if the QuantumObject is a scalar (1x1 matrix).
285
+
286
+ Returns:
287
+ bool: True if the QuantumObject is a scalar, False otherwise.
288
+ """
289
+ return self.data.shape == (1, 1)
290
+
291
+ def is_dm(self, tol: float = 1e-8) -> bool:
292
+ """
293
+ Determine if the QuantumObject is a valid density matrix.
294
+
295
+ A valid density matrix must be square, Hermitian, positive semi-definite,
296
+ and have a trace equal to 1.
297
+
298
+ Args:
299
+ tol (float, optional): The numerical tolerance for verifying Hermiticity,
300
+ eigenvalue non-negativity, and trace. Defaults to 1e-8.
301
+
302
+ Returns:
303
+ bool: True if the QuantumObject is a valid density matrix, False otherwise.
304
+ """
305
+ # Check if rho is a square matrix
306
+ if self.shape[0] != self.shape[1]:
307
+ return False
308
+
309
+ # Check Hermitian condition: rho should be equal to its conjugate transpose
310
+ if not np.allclose(self.dense, self._data.conj().T.toarray(), atol=tol):
311
+ return False
312
+
313
+ # Check if eigenvalues are non-negative (positive semi-definite)
314
+ eigenvalues = np.linalg.eigvalsh(self.dense) # More stable for Hermitian matrices
315
+ if np.any(eigenvalues < -tol): # Allow small numerical errors
316
+ return False
317
+
318
+ # Check if the trace is 1
319
+ return np.isclose(self._data.trace(), 1, atol=tol)
320
+
321
+ def is_herm(self, tol: float = 1e-8) -> bool:
322
+ """
323
+ Check if the QuantumObject is Hermitian.
324
+
325
+ Args:
326
+ tol (float, optional): The numerical tolerance for verifying Hermiticity.
327
+ Defaults to 1e-8.
328
+
329
+ Returns:
330
+ bool: True if the QuantumObject is Hermitian, False otherwise.
331
+ """
332
+ return np.allclose(self.dense, self._data.conj().T.toarray(), atol=tol)
333
+
334
+ def to_density_matrix(self) -> QuantumObject:
335
+ """
336
+ Convert the QuantumObject to a density matrix.
337
+
338
+ If the QuantumObject represents a state vector (ket or bra), this method
339
+ calculates the corresponding density matrix by taking the outer product.
340
+ If the QuantumObject is already a density matrix, it is returned unchanged.
341
+ The resulting density matrix is normalized.
342
+
343
+ Raises:
344
+ ValueError: If the QuantumObject is a scalar, as a density matrix cannot be derived.
345
+
346
+ Returns:
347
+ QuantumObject: A new QuantumObject representing the density matrix.
348
+ """
349
+ if self.is_scalar():
350
+ raise ValueError("Cannot make a density matrix from scalar.")
351
+ if self.is_dm():
352
+ return self
353
+ if self.is_bra():
354
+ return (self.dag() @ self).unit()
355
+ return (self @ self.dag()).unit()
356
+
357
+ def __add__(self, other: QuantumObject | Complex) -> QuantumObject:
358
+ if isinstance(other, QuantumObject):
359
+ return QuantumObject(self._data + other._data)
360
+ if isinstance(other, Complex) and other == 0:
361
+ return self
362
+ raise TypeError("Addition is only supported between QuantumState instances")
363
+
364
+ def __sub__(self, other: QuantumObject) -> QuantumObject:
365
+ if isinstance(other, QuantumObject):
366
+ return QuantumObject(self._data - other._data)
367
+ raise TypeError("Subtraction is only supported between QuantumState instances")
368
+
369
+ def __mul__(self, other: QuantumObject | Complex) -> QuantumObject:
370
+ if isinstance(other, (int, float, complex)):
371
+ return QuantumObject(self._data * other)
372
+ if isinstance(other, QuantumObject):
373
+ return QuantumObject(self._data * other._data)
374
+ raise TypeError("Unsupported multiplication type")
375
+
376
+ def __matmul__(self, other: QuantumObject) -> QuantumObject:
377
+ if isinstance(other, QuantumObject):
378
+ return QuantumObject(self._data @ other._data)
379
+ raise TypeError("Dot product is only supported between QuantumState instances")
380
+
381
+ def __rmul__(self, other: QuantumObject | Complex) -> QuantumObject:
382
+ return self.__mul__(other)
383
+
384
+ def __repr__(self) -> str:
385
+ return f"{self.dense}"
386
+
387
+
388
+ def basis(N: int, n: int) -> QuantumObject:
389
+ """
390
+ Generate the basis vector representation of a Fock state.
391
+
392
+ This function creates a column vector (ket) representing the Fock state |n⟩
393
+ in a Hilbert space of dimension N.
394
+
395
+ Args:
396
+ N (int): The dimension of the Hilbert space (number of Fock states).
397
+ n (int): The desired number state.
398
+
399
+ Returns:
400
+ QuantumObject: A QuantumObject representing the Fock state |n⟩.
401
+ """
402
+ return QuantumObject(csc_array(([1], ([n], [0])), shape=(N, 1)))
403
+
404
+
405
+ def ket(*state: int) -> QuantumObject:
406
+ """
407
+ Generate a ket state for a multi-qubit system.
408
+
409
+ This function creates a tensor product of individual qubit states (kets)
410
+ based on the input values. Each input must be either 0 or 1. For example,
411
+ ket(0, 1) creates a two-qubit ket state |0⟩ ⊗ |1⟩.
412
+
413
+ Args:
414
+ *state (int): A sequence of integers representing the state of each qubit (0 or 1).
415
+
416
+ Raises:
417
+ ValueError: If any of the provided qubit states is not 0 or 1.
418
+
419
+ Returns:
420
+ QuantumObject: A QuantumObject representing the multi-qubit ket state.
421
+ """
422
+ if any(s not in {0, 1} for s in state):
423
+ raise ValueError("the state can only be 1 or 0.")
424
+ return tensor([QuantumObject(csc_array(([1], ([s], [0])), shape=(2, 1))) for s in state])
425
+
426
+
427
+ def bra(*state: int) -> QuantumObject:
428
+ """
429
+ Generate a bra state for a multi-qubit system.
430
+
431
+ This function creates a tensor product of individual qubit states (bras)
432
+ based on the input values. Each input must be either 0 or 1. For example,
433
+ bra(0, 1) creates a two-qubit bra state ⟨0| ⊗ ⟨1|.
434
+
435
+ Args:
436
+ *state (int): A sequence of integers representing the state of each qubit (0 or 1).
437
+
438
+ Raises:
439
+ ValueError: If any of the provided qubit states is not 0 or 1.
440
+
441
+ Returns:
442
+ QuantumObject: A QuantumObject representing the multi-qubit bra state.
443
+ """
444
+ if any(s not in {0, 1} for s in state):
445
+ raise ValueError("the state can only be 1 or 0.")
446
+ return tensor([QuantumObject(csc_array(([1], ([0], [s])), shape=(1, 2))) for s in state])
447
+
448
+
449
+ def tensor(operators: list[QuantumObject]) -> QuantumObject:
450
+ """
451
+ Calculate the tensor product of a list of QuantumObjects.
452
+
453
+ This function computes the tensor (Kronecker) product of all input QuantumObjects,
454
+ resulting in a composite QuantumObject that represents the combined state or operator.
455
+
456
+ Args:
457
+ operators (list[QuantumObject]): A list of QuantumObjects to be combined via tensor product.
458
+
459
+ Returns:
460
+ QuantumObject: A new QuantumObject representing the tensor product of the inputs.
461
+ """
462
+ out = operators[0].data
463
+ if len(operators) > 1:
464
+ for i in range(1, len(operators)):
465
+ out = kron(out, operators[i].data)
466
+ return QuantumObject(out)
467
+
468
+
469
+ def expect(operator: QuantumObject, state: QuantumObject) -> Complex:
470
+ """
471
+ Calculate the expectation value of an operator with respect to a quantum state.
472
+
473
+ Computes the expectation value ⟨state| operator |state⟩. The function handles both
474
+ pure state vectors and density matrices appropriately.
475
+
476
+ Args:
477
+ operator (QuantumObject): The quantum operator represented as a QuantumObject.
478
+ state (QuantumObject): The quantum state or density matrix represented as a QuantumObject.
479
+
480
+ Returns:
481
+ Complex: The expectation value. The result is guaranteed to be real if the operator
482
+ is Hermitian, and may be complex otherwise.
483
+ """
484
+ if state.data.shape[1] == state.data.shape[0]:
485
+ return (operator @ state).dense.trace()
486
+ return (state.dag() @ operator @ state).dense[0, 0]