qilisdk 0.1.2__py3-none-any.whl → 0.1.4__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.
@@ -14,7 +14,7 @@
14
14
 
15
15
  from .algorithms import TimeEvolution
16
16
  from .hamiltonian import Hamiltonian, I, X, Y, Z
17
- from .quantum_objects import QuantumObject, basis, bra, expect, ket, tensor
17
+ from .quantum_objects import QuantumObject, basis_state, bra, expect_val, ket, tensor_prod
18
18
  from .schedule import Schedule
19
19
 
20
20
  __all__ = [
@@ -26,9 +26,9 @@ __all__ = [
26
26
  "X",
27
27
  "Y",
28
28
  "Z",
29
- "basis",
29
+ "basis_state",
30
30
  "bra",
31
- "expect",
31
+ "expect_val",
32
32
  "ket",
33
- "tensor",
33
+ "tensor_prod",
34
34
  ]
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
  from __future__ import annotations
15
15
 
16
+ import math
16
17
  import string
17
18
  from typing import Literal
18
19
 
@@ -24,7 +25,11 @@ from scipy.sparse.linalg import norm as scipy_norm
24
25
  from qilisdk.yaml import yaml
25
26
 
26
27
  Complex = int | float | complex
27
- TWO = 2
28
+
29
+
30
+ ###############################################################################
31
+ # Main Class Definition
32
+ ###############################################################################
28
33
 
29
34
 
30
35
  @yaml.register_class
@@ -39,9 +44,9 @@ class QuantumObject:
39
44
 
40
45
  The internal data is stored as a SciPy CSR (Compressed Sparse Row) matrix for
41
46
  efficient arithmetic and manipulation. The expected shapes for the data are:
42
- - (2**N, 2**N) for operators or density matrices,
47
+ - (2**N, 2**N) for operators or density matrices (or scalars),
43
48
  - (2**N, 1) for ket states,
44
- - (1, 2**N) for bra states.
49
+ - (1, 2**N) or (2**N,) for bra states.
45
50
  """
46
51
 
47
52
  def __init__(self, data: np.ndarray | sparray | spmatrix) -> None:
@@ -50,10 +55,12 @@ class QuantumObject:
50
55
 
51
56
  Converts a NumPy array to a CSR matrix if needed and validates the shape of the input.
52
57
  The input must represent a valid quantum state or operator with appropriate dimensions.
58
+ Notice that 1D arrays of shape (2N,) are considered/transformed to bras with shape (1, 2N).
53
59
 
54
60
  Args:
55
61
  data (np.ndarray | sparray | spmatrix): A dense NumPy array or a SciPy sparse matrix
56
- representing a quantum state or operator.
62
+ representing a quantum state or operator. Should be of shape: (2**N, 2**N) for operators
63
+ (1, 2**N) for ket states, (2**N, 1) or (2**N,) for bra states, or (1, 1) for scalars.
57
64
 
58
65
  Raises:
59
66
  ValueError: If the input data is not a NumPy array or a SciPy sparse matrix,
@@ -65,21 +72,18 @@ class QuantumObject:
65
72
  self._data = data.tocsr()
66
73
  else:
67
74
  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:
75
+
76
+ # Valid shapes are operators = (2**N, 2**N) (scalars included), bra's = (1, 2**N) / (2**N,), or ket's =(2**N, 1):
77
+ valid_shape = self.is_operator() or self.is_ket() or self.is_bra()
78
+
79
+ if len(self._data.shape) != 2 or not valid_shape: # noqa: PLR2004
78
80
  raise ValueError(
79
81
  "Dimension of data is wrong. expected data to have shape similar to (2**N, 2**N), (1, 2**N), (2**N, 1)",
80
82
  f"but received {self._data.shape}",
81
83
  )
82
84
 
85
+ # ------------- Properties --------------
86
+
83
87
  @property
84
88
  def data(self) -> csr_matrix:
85
89
  """
@@ -126,7 +130,9 @@ class QuantumObject:
126
130
  """
127
131
  return self._data.shape
128
132
 
129
- def dag(self) -> QuantumObject:
133
+ # ----------- Matrix Logic Operations ------------
134
+
135
+ def adjoint(self) -> QuantumObject:
130
136
  """
131
137
  Compute the adjoint (conjugate transpose) of the QuantumObject.
132
138
 
@@ -136,52 +142,112 @@ class QuantumObject:
136
142
  out = QuantumObject(self._data.conj().T)
137
143
  return out
138
144
 
139
- def ptrace(self, dims: list[int], keep: list[int]) -> "QuantumObject":
145
+ def ptrace(self, keep: list[int], dims: list[int] | None = None) -> "QuantumObject":
140
146
  """
141
147
  Compute the partial trace over subsystems not in 'keep'.
142
148
 
143
149
  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.
150
+ the subsystems that are not specified in the 'keep' parameter.
151
+ The input 'dims' represents the dimensions of each subsystem (optional),
152
+ and 'keep' indicates the indices of those subsystems to be retained.
153
+
154
+ If the QuantumObject is a ket or bra, it will first be converted to a density matrix.
147
155
 
148
156
  Args:
149
- dims (list[int]): A list specifying the dimensions of each subsystem.
150
157
  keep (list[int]): A list of indices corresponding to the subsystems to retain.
158
+ The order of the indices in 'keep' is not important, since dimensions will
159
+ be returned in the tensor original order, but the indices must be unique.
160
+ dims (list[int], optional): A list specifying the dimensions of each subsystem.
161
+ If not specified, a density matrix of qubit states is assumed, and the
162
+ dimensions are inferred accordingly (i.e. we split the state in dim 2 states).
151
163
 
152
164
  Raises:
153
165
  ValueError: If the product of the dimensions in dims does not match the
154
- shape of the QuantumObject's dense representation.
166
+ shape of the QuantumObject's dense representation or if any dimension is non-positive.
167
+ ValueError: If the indices in 'keep' are not unique or are out of range.
168
+ ValueError: If the QuantumObject is not a valid density matrix or state vector.
169
+ ValueError: If the number of subsystems exceeds the available ASCII letters.
155
170
 
156
171
  Returns:
157
172
  QuantumObject: A new QuantumObject representing the reduced density matrix
158
173
  for the subsystems specified in 'keep'.
159
174
  """
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")
175
+ # 1) Get the density matrix representation:
176
+ rho = self.dense if self.is_operator() else self.to_density_matrix().dense
177
+
178
+ # 2.a) If `dims` is not provided, we assume a density matrix of qubit states (we split in subsystems of dim = 2):
179
+ if dims is None:
180
+ # The to_density_matrix() should check its a square matrix, with size being a power of 2, so we can do:
181
+ number_of_qubits_in_state = int(math.log2(rho.shape[0]))
182
+ dims = [2 for _ in range(number_of_qubits_in_state)]
183
+ # 2.b) If `dims` is provided, we run checks on it:
184
+ else:
185
+ total_dim = int(np.prod(dims))
186
+ if rho.shape != (total_dim, total_dim):
187
+ raise ValueError(
188
+ f"Dimension mismatch: QuantumObject shape {rho.shape} does not match the expected shape ({total_dim}, {total_dim}), given by the product of all passed `dims`: (np.prod(dims), np.prod(dims))."
189
+ )
190
+ if any(d <= 0 for d in dims):
191
+ raise ValueError("All subsystem dimensions must be positive")
192
+
193
+ # 3) Validate & sort `keep`
194
+ keep_set = set(keep)
195
+ if any(i < 0 or i >= len(dims) for i in keep_set):
196
+ raise ValueError("keep indices out of range (0, len(dims))")
197
+ if len(keep_set) != len(keep):
198
+ raise ValueError("duplicate indices in keep")
199
+
200
+ # 4) Trace out the subsystems not in `keep`.
201
+ rho_t = self._compute_traced_tensor_via_einstein_summation(rho, keep_set, dims)
202
+
203
+ # 5) The resulting tensor has separate indices for each subsystem kept.
204
+ # Reshape it into a matrix (i.e. combine the row indices and column indices).
205
+ dims_keep = [dims[i] for i in keep_set]
206
+ new_dim = int(np.prod(dims_keep)) if dims_keep else 1
207
+
208
+ return QuantumObject(rho_t.reshape((new_dim, new_dim)))
209
+
210
+ @staticmethod
211
+ def _compute_traced_tensor_via_einstein_summation(rho: np.ndarray, keep: set[int], dims: list[int]) -> np.ndarray:
212
+ """Helper function called in `ptrace`, which computes the partial trace over subsystems not in 'keep'.
213
+
214
+ This function generates the appropriate einsum subscript strings for the input tensor
215
+ and performs the summation over the indices corresponding to the subsystems being traced out.
216
+
217
+ Args:
218
+ rho (np.ndarray): The input density matrix to be traced out.
219
+ keep (set[int]): A list of indices corresponding to the subsystems to retain.
220
+ The order of the indices in 'keep' is not important, since dimensions will
221
+ be returned in the tensor original order, but the indices must be unique.
222
+ dims (list[int]): A list specifying the dimensions of each subsystem.
223
+
224
+ Returns:
225
+ np.ndarray: The resulting tensor after tracing out the specified subsystems.
226
+
227
+ Raises:
228
+ ValueError: If the number of subsystems exceeds the available ASCII letters.
229
+ """
230
+ # Check that the number of subsystems is not too large, that we run out of ascii letters.
231
+ needed, MAX_LABELS = len(dims) + len(keep), len(string.ascii_letters)
232
+ if needed > MAX_LABELS:
233
+ raise ValueError(f"Not enough einsum labels (dims + keep): need {needed}, but only {MAX_LABELS} available.")
164
234
 
165
- n = len(dims)
166
235
  # Use letters from the ASCII alphabet (both cases) for einsum indices.
167
236
  # 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.
237
+ row_letters, col_letters = [], []
238
+ out_row, out_col = [], [] # Letters that will remain in the output for the row part and for the column part.
172
239
  letters = iter(string.ascii_letters)
173
240
 
174
- for i in range(n):
241
+ for i in range(len(dims)):
175
242
  if i in keep:
176
- # For a subsystem we want to keep, use two different letters
177
- r = next(letters)
178
- c = next(letters)
243
+ # For a subsystem we want to keep, use two different letters (r, c)
244
+ r, c = next(letters), next(letters)
179
245
  row_letters.append(r)
180
246
  col_letters.append(c)
181
247
  out_row.append(r)
182
248
  out_col.append(c)
183
249
  else:
184
- # For subsystems to be traced out, assign the same letter so that those indices are summed.
250
+ # For subsystems to be traced out, assign the same letter (r, r) so that those indices are summed.
185
251
  r = next(letters)
186
252
  row_letters.append(r)
187
253
  col_letters.append(r)
@@ -195,40 +261,40 @@ class QuantumObject:
195
261
  # Reshape rho into a tensor with shape dims + dims.
196
262
  reshaped = rho.reshape(dims + dims)
197
263
  # 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)
264
+ return np.einsum(f"{input_subscript}->{output_subscript}", reshaped)
207
265
 
208
266
  def norm(self, order: int | Literal["fro", "tr"] = 1) -> float:
209
267
  """
210
268
  Compute the norm of the QuantumObject.
211
269
 
212
- For density matrices, the norm order can be specified. For state vectors,
213
- the norm is computed accordingly.
270
+ For density matrices, the norm order can be specified. For state vectors, the norm is computed accordingly.
214
271
 
215
272
  Args:
216
273
  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.
274
+ Only applies if the QuantumObject represents a density matrix. Other than all the
275
+ orders accepted by scipy, it also accepts 'tr' for the trace norm. Defaults to 1.
276
+
277
+ Raises:
278
+ ValueError: If the QuantumObject is not a valid density matrix or state vector,
219
279
 
220
280
  Returns:
221
281
  float: The computed norm of the QuantumObject.
222
282
  """
223
283
  if self.is_scalar():
224
284
  return self.dense[0][0]
225
- if self.is_dm() or self.shape[0] == self.shape[1]:
285
+
286
+ if self.is_density_matrix() or self.shape[0] == self.shape[1]:
226
287
  if order == "tr":
227
- return self._data.trace()
288
+ return np.sum(np.abs(np.linalg.eigvalsh(self.dense)))
228
289
  return scipy_norm(self._data, ord=order)
290
+
229
291
  if self.is_bra():
230
292
  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]
293
+
294
+ if self.is_ket():
295
+ return np.sqrt(self._data.conj().T @ self._data).toarray()[0, 0]
296
+
297
+ raise ValueError("The QuantumObject is not a valid density matrix or state vector. Cannot compute the norm.")
232
298
 
233
299
  def unit(self, order: int | Literal["fro", "tr"] = "tr") -> QuantumObject:
234
300
  """
@@ -238,8 +304,8 @@ class QuantumObject:
238
304
 
239
305
  Args:
240
306
  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".
307
+ Only applies if the QuantumObject represents a density matrix. Other than all the
308
+ orders accepted by scipy, it also accepts 'tr' for the trace norm. Defaults to "tr".
243
309
 
244
310
  Raises:
245
311
  ValueError: If the norm of the QuantumObject is 0, making normalization impossible.
@@ -250,6 +316,7 @@ class QuantumObject:
250
316
  norm = self.norm(order=order)
251
317
  if norm == 0:
252
318
  raise ValueError("Cannot normalize a zero-norm Quantum Object")
319
+
253
320
  return QuantumObject(self._data / norm)
254
321
 
255
322
  def expm(self) -> QuantumObject:
@@ -261,6 +328,46 @@ class QuantumObject:
261
328
  """
262
329
  return QuantumObject(expm(self._data))
263
330
 
331
+ def to_density_matrix(self) -> QuantumObject:
332
+ """
333
+ Convert the QuantumObject to a density matrix.
334
+
335
+ If the QuantumObject represents a state vector (ket or bra), this method
336
+ calculates the corresponding density matrix by taking the outer product.
337
+ If the QuantumObject is already a density matrix, it is returned unchanged.
338
+ The resulting density matrix is normalized.
339
+
340
+ Raises:
341
+ ValueError: If the QuantumObject is a scalar, as a density matrix cannot be derived.
342
+ ValueError: If the QuantumObject is an operator that is not a density matrix.
343
+
344
+ Returns:
345
+ QuantumObject: A new QuantumObject representing the density matrix.
346
+ """
347
+ if self.is_scalar():
348
+ raise ValueError("Cannot make a density matrix from scalar.")
349
+
350
+ if self.is_bra():
351
+ return (self.adjoint() @ self).unit()
352
+
353
+ if self.is_ket():
354
+ return (self @ self.adjoint()).unit()
355
+
356
+ if self.is_density_matrix():
357
+ return self
358
+
359
+ if self.is_operator():
360
+ raise ValueError(
361
+ "Cannot make a density matrix from an operator, which is not a density matrix already (trace=1 and hermitian)."
362
+ )
363
+
364
+ raise ValueError(
365
+ "Cannot make a density matrix from this QuantumObject. "
366
+ "It must be either a ket, a bra or already a density matrix."
367
+ )
368
+
369
+ # ----------- Checks for Matrices ------------
370
+
264
371
  def is_ket(self) -> bool:
265
372
  """
266
373
  Check if the QuantumObject represents a ket (column vector) state.
@@ -268,7 +375,7 @@ class QuantumObject:
268
375
  Returns:
269
376
  bool: True if the QuantumObject is a ket state, False otherwise.
270
377
  """
271
- return self.shape[0] % 2 == 0 and self.shape[1] == 1
378
+ return self.shape[1] == 1 and self.shape[0].bit_count() == 1
272
379
 
273
380
  def is_bra(self) -> bool:
274
381
  """
@@ -277,7 +384,7 @@ class QuantumObject:
277
384
  Returns:
278
385
  bool: True if the QuantumObject is a bra state, False otherwise.
279
386
  """
280
- return self.shape[1] % 2 == 0 and self.shape[0] == 1
387
+ return self.shape[0] == 1 and self.shape[1].bit_count() == 1
281
388
 
282
389
  def is_scalar(self) -> bool:
283
390
  """
@@ -286,14 +393,22 @@ class QuantumObject:
286
393
  Returns:
287
394
  bool: True if the QuantumObject is a scalar, False otherwise.
288
395
  """
289
- return self.data.shape == (1, 1)
396
+ return self.shape == (1, 1)
290
397
 
291
- def is_dm(self, tol: float = 1e-8) -> bool:
398
+ def is_operator(self) -> bool:
399
+ """
400
+ Check if the QuantumObject is an operator (square matrix).
401
+
402
+ Returns:
403
+ bool: True if the QuantumObject is an operator, False otherwise.
404
+ """
405
+ return self._data.shape[1] == self._data.shape[0] and self._data.shape[0].bit_count() == 1
406
+
407
+ def is_density_matrix(self, tol: float = 1e-8) -> bool:
292
408
  """
293
409
  Determine if the QuantumObject is a valid density matrix.
294
410
 
295
- A valid density matrix must be square, Hermitian, positive semi-definite,
296
- and have a trace equal to 1.
411
+ A valid density matrix must be square, Hermitian, positive semi-definite, and have a trace equal to 1.
297
412
 
298
413
  Args:
299
414
  tol (float, optional): The numerical tolerance for verifying Hermiticity,
@@ -303,11 +418,11 @@ class QuantumObject:
303
418
  bool: True if the QuantumObject is a valid density matrix, False otherwise.
304
419
  """
305
420
  # Check if rho is a square matrix
306
- if self.shape[0] != self.shape[1]:
421
+ if not self.is_operator():
307
422
  return False
308
423
 
309
424
  # 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):
425
+ if not self.is_hermitian(tol=tol):
311
426
  return False
312
427
 
313
428
  # Check if eigenvalues are non-negative (positive semi-definite)
@@ -318,7 +433,7 @@ class QuantumObject:
318
433
  # Check if the trace is 1
319
434
  return np.isclose(self._data.trace(), 1, atol=tol)
320
435
 
321
- def is_herm(self, tol: float = 1e-8) -> bool:
436
+ def is_hermitian(self, tol: float = 1e-8) -> bool:
322
437
  """
323
438
  Check if the QuantumObject is Hermitian.
324
439
 
@@ -331,39 +446,20 @@ class QuantumObject:
331
446
  """
332
447
  return np.allclose(self.dense, self._data.conj().T.toarray(), atol=tol)
333
448
 
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()
449
+ # ----------- Basic Arithmetic Operators ------------
356
450
 
357
451
  def __add__(self, other: QuantumObject | Complex) -> QuantumObject:
358
452
  if isinstance(other, QuantumObject):
359
453
  return QuantumObject(self._data + other._data)
360
454
  if isinstance(other, Complex) and other == 0:
361
455
  return self
456
+
362
457
  raise TypeError("Addition is only supported between QuantumState instances")
363
458
 
364
459
  def __sub__(self, other: QuantumObject) -> QuantumObject:
365
460
  if isinstance(other, QuantumObject):
366
461
  return QuantumObject(self._data - other._data)
462
+
367
463
  raise TypeError("Subtraction is only supported between QuantumState instances")
368
464
 
369
465
  def __mul__(self, other: QuantumObject | Complex) -> QuantumObject:
@@ -371,11 +467,13 @@ class QuantumObject:
371
467
  return QuantumObject(self._data * other)
372
468
  if isinstance(other, QuantumObject):
373
469
  return QuantumObject(self._data * other._data)
470
+
374
471
  raise TypeError("Unsupported multiplication type")
375
472
 
376
473
  def __matmul__(self, other: QuantumObject) -> QuantumObject:
377
474
  if isinstance(other, QuantumObject):
378
475
  return QuantumObject(self._data @ other._data)
476
+
379
477
  raise TypeError("Dot product is only supported between QuantumState instances")
380
478
 
381
479
  def __rmul__(self, other: QuantumObject | Complex) -> QuantumObject:
@@ -385,19 +483,23 @@ class QuantumObject:
385
483
  return f"{self.dense}"
386
484
 
387
485
 
388
- def basis(N: int, n: int) -> QuantumObject:
486
+ ###############################################################################
487
+ # Outside class Function Definitions
488
+ ###############################################################################
489
+
490
+
491
+ def basis_state(n: int, N: int) -> QuantumObject:
389
492
  """
390
- Generate the basis vector representation of a Fock state.
493
+ Generate the n'th basis vector representation, on a N-size Hilbert space (N=2**num_qubits).
391
494
 
392
- This function creates a column vector (ket) representing the Fock state |n⟩
393
- in a Hilbert space of dimension N.
495
+ This function creates a column vector (ket) representing the Fock state |n⟩ in a Hilbert space of dimension N.
394
496
 
395
497
  Args:
396
- N (int): The dimension of the Hilbert space (number of Fock states).
397
- n (int): The desired number state.
498
+ n (int): The desired number state (from 0 to N-1).
499
+ N (int): The dimension of the Hilbert space, has a value 2**num_qubits.
398
500
 
399
501
  Returns:
400
- QuantumObject: A QuantumObject representing the Fock state |n⟩.
502
+ QuantumObject: A QuantumObject representing the |n⟩'th basis state on a N-size Hilbert space (N=2**num_qubits).
401
503
  """
402
504
  return QuantumObject(csc_array(([1], ([n], [0])), shape=(N, 1)))
403
505
 
@@ -406,9 +508,8 @@ def ket(*state: int) -> QuantumObject:
406
508
  """
407
509
  Generate a ket state for a multi-qubit system.
408
510
 
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⟩.
511
+ This function creates a tensor product of individual qubit states (kets) based on the input values.
512
+ Each input must be either 0 or 1. For example, ket(0, 1) creates a two-qubit ket state |0⟩ ⊗ |1⟩.
412
513
 
413
514
  Args:
414
515
  *state (int): A sequence of integers representing the state of each qubit (0 or 1).
@@ -420,17 +521,17 @@ def ket(*state: int) -> QuantumObject:
420
521
  QuantumObject: A QuantumObject representing the multi-qubit ket state.
421
522
  """
422
523
  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])
524
+ raise ValueError(f"the state can only contain 1s or 0s. But received: {state}")
525
+
526
+ return tensor_prod([QuantumObject(csc_array(([1], ([s], [0])), shape=(2, 1))) for s in state])
425
527
 
426
528
 
427
529
  def bra(*state: int) -> QuantumObject:
428
530
  """
429
531
  Generate a bra state for a multi-qubit system.
430
532
 
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|.
533
+ This function creates a tensor product of individual qubit states (bras) based on the input values.
534
+ Each input must be either 0 or 1. For example, bra(0, 1) creates a two-qubit bra state ⟨0| ⊗ ⟨1|.
434
535
 
435
536
  Args:
436
537
  *state (int): A sequence of integers representing the state of each qubit (0 or 1).
@@ -442,11 +543,12 @@ def bra(*state: int) -> QuantumObject:
442
543
  QuantumObject: A QuantumObject representing the multi-qubit bra state.
443
544
  """
444
545
  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])
546
+ raise ValueError(f"the state can only contain 1s or 0s. But received:: {state}")
447
547
 
548
+ return tensor_prod([QuantumObject(csc_array(([1], ([0], [s])), shape=(1, 2))) for s in state])
448
549
 
449
- def tensor(operators: list[QuantumObject]) -> QuantumObject:
550
+
551
+ def tensor_prod(operators: list[QuantumObject]) -> QuantumObject:
450
552
  """
451
553
  Calculate the tensor product of a list of QuantumObjects.
452
554
 
@@ -463,10 +565,11 @@ def tensor(operators: list[QuantumObject]) -> QuantumObject:
463
565
  if len(operators) > 1:
464
566
  for i in range(1, len(operators)):
465
567
  out = kron(out, operators[i].data)
568
+
466
569
  return QuantumObject(out)
467
570
 
468
571
 
469
- def expect(operator: QuantumObject, state: QuantumObject) -> Complex:
572
+ def expect_val(operator: QuantumObject, state: QuantumObject) -> Complex:
470
573
  """
471
574
  Calculate the expectation value of an operator with respect to a quantum state.
472
575
 
@@ -477,10 +580,17 @@ def expect(operator: QuantumObject, state: QuantumObject) -> Complex:
477
580
  operator (QuantumObject): The quantum operator represented as a QuantumObject.
478
581
  state (QuantumObject): The quantum state or density matrix represented as a QuantumObject.
479
582
 
583
+ Raises:
584
+ ValueError: If the operator is not a square matrix.
585
+
480
586
  Returns:
481
587
  Complex: The expectation value. The result is guaranteed to be real if the operator
482
588
  is Hermitian, and may be complex otherwise.
483
589
  """
590
+ if not operator.is_operator():
591
+ raise ValueError("The operator must be a square matrix.")
592
+
484
593
  if state.data.shape[1] == state.data.shape[0]:
485
594
  return (operator @ state).dense.trace()
486
- return (state.dag() @ operator @ state).dense[0, 0]
595
+
596
+ return (state.adjoint() @ operator @ state).dense[0, 0]
@@ -20,12 +20,12 @@ __all__ = []
20
20
  OPTIONAL_FEATURES: list[OptionalFeature] = [
21
21
  OptionalFeature(
22
22
  name="cuda",
23
- dependencies=["cudaq"],
23
+ dependencies=["cuda-quantum-cu12"],
24
24
  symbols=[Symbol(path="qilisdk.extras.cuda.cuda_backend", name="CudaBackend")],
25
25
  ),
26
26
  OptionalFeature(
27
27
  name="qaas",
28
- dependencies=["httpx", "keyring", "pydantic", "pydantic-settings"],
28
+ dependencies=["httpx", "keyring"],
29
29
  symbols=[Symbol(path="qilisdk.extras.qaas.qaas_backend", name="QaaSBackend")],
30
30
  ),
31
31
  ]
@@ -17,9 +17,8 @@ from typing import TYPE_CHECKING, Callable, Type, TypeVar
17
17
 
18
18
  import cudaq
19
19
  import numpy as np
20
- from cudaq import State
21
- from cudaq.operator import ElementaryOperator, OperatorSum, ScalarOperator, evolve, spin
22
- from cudaq.operator import Schedule as cuda_schedule
20
+ from cudaq import ElementaryOperator, OperatorSum, ScalarOperator, State, evolve, spin
21
+ from cudaq import Schedule as cuda_schedule
23
22
 
24
23
  from qilisdk.analog.analog_backend import AnalogBackend
25
24
  from qilisdk.analog.hamiltonian import Hamiltonian, PauliI, PauliOperator, PauliX, PauliY, PauliZ
@@ -277,7 +276,7 @@ class CudaBackend(DigitalBackend, AnalogBackend):
277
276
 
278
277
  return CudaAnalogResult(
279
278
  final_expected_values=np.array(
280
- [exp_val.expectation() for exp_val in evolution_result.final_expectation_values()]
279
+ [exp_val.expectation() for exp_val in evolution_result.final_expectation_values()[0]]
281
280
  ),
282
281
  expected_values=(
283
282
  np.array(
@@ -287,12 +286,12 @@ class CudaBackend(DigitalBackend, AnalogBackend):
287
286
  else None
288
287
  ),
289
288
  final_state=(
290
- QuantumObject(np.array(evolution_result.final_state())).dag()
289
+ QuantumObject(np.array(evolution_result.final_state())).adjoint()
291
290
  if evolution_result.final_state() is not None
292
291
  else None
293
292
  ),
294
293
  intermediate_states=(
295
- [QuantumObject(np.array(state)).dag() for state in evolution_result.intermediate_states()]
294
+ [QuantumObject(np.array(state)).adjoint() for state in evolution_result.intermediate_states()]
296
295
  if evolution_result.intermediate_states() is not None
297
296
  else None
298
297
  ),
@@ -17,6 +17,7 @@ import json
17
17
  import logging
18
18
  from base64 import urlsafe_b64encode
19
19
  from datetime import datetime, timezone
20
+ from os import environ
20
21
  from typing import TYPE_CHECKING, cast
21
22
 
22
23
  import httpx
@@ -65,7 +66,7 @@ class QaaSBackend(DigitalBackend, AnalogBackend):
65
66
  c) keyring (fallback).
66
67
  """
67
68
 
68
- _api_url: str = "https://qilimanjaroqaas.ddns.net:8080/api/v1"
69
+ _api_url: str = environ.get("PUBLIC_API_URL", "https://qilimanjaroqaas.ddns.net:8080/api/v1")
69
70
 
70
71
  def __init__(self) -> None:
71
72
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qilisdk
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: qilisdk is a Python framework for writing digital and analog quantum algorithms and executing them across multiple quantum backends. Its modular design streamlines the development process and enables easy integration with a variety of quantum platforms.
5
5
  Author-email: Qilimanjaro Quantum Tech <info@qilimanjaro.tech>
6
6
  License-File: LICENCE
@@ -21,15 +21,15 @@ Classifier: Topic :: Scientific/Engineering :: Quantum Computing
21
21
  Requires-Python: >=3.10
22
22
  Requires-Dist: dill>=0.3.9
23
23
  Requires-Dist: numpy>=2.2.4
24
+ Requires-Dist: pydantic-settings>=2.8.0
25
+ Requires-Dist: pydantic>=2.10.6
24
26
  Requires-Dist: ruamel-yaml>=0.18.10
25
27
  Requires-Dist: scipy>=1.15.1
26
28
  Provides-Extra: cuda
27
- Requires-Dist: cudaq==0.9.1; extra == 'cuda'
29
+ Requires-Dist: cuda-quantum-cu12; extra == 'cuda'
28
30
  Provides-Extra: qaas
29
31
  Requires-Dist: httpx>=0.28.1; extra == 'qaas'
30
32
  Requires-Dist: keyring>=25.6.0; extra == 'qaas'
31
- Requires-Dist: pydantic-settings>=2.8.0; extra == 'qaas'
32
- Requires-Dist: pydantic>=2.10.6; extra == 'qaas'
33
33
  Description-Content-Type: text/markdown
34
34
 
35
35
  # QiliSDK
@@ -227,7 +227,7 @@ For analog simulations, the new `TimeEvolution` and `Schedule` classes allow you
227
227
 
228
228
  ```python
229
229
  import numpy as np
230
- from qilisdk.analog import TimeEvolution, Schedule, tensor, ket, X, Z, Y
230
+ from qilisdk.analog import Schedule, TimeEvolution, ket, X, Z, Y, tensor_prod
231
231
  from qilisdk.extras import CudaBackend
232
232
 
233
233
  T = 10 # Total evolution time
@@ -251,16 +251,15 @@ schedule = Schedule(
251
251
  )
252
252
 
253
253
  # Prepare an initial state (equal superposition)
254
- state = tensor([(ket(0) + ket(1)).unit() for _ in range(nqubits)]).unit()
254
+ state = tensor_prod([(ket(0) + ket(1)).unit() for _ in range(nqubits)]).unit()
255
255
 
256
256
  # Perform time evolution on the CUDA backend with observables to monitor
257
257
  time_evolution = TimeEvolution(
258
- backend=CudaBackend(),
259
258
  schedule=schedule,
260
259
  initial_state=state,
261
260
  observables=[Z(0), X(0), Y(0)],
262
261
  )
263
- results = time_evolution.evolve(store_intermediate_results=True)
262
+ results = time_evolution.evolve(backend=CudaBackend(), store_intermediate_results=True)
264
263
  print("Time Evolution Results:", results)
265
264
  ```
266
265
 
@@ -3,13 +3,13 @@ qilisdk/__init__.pyi,sha256=iwacKJYttlh9DgQgNmnAjkU9CyjDQG42X3gr9Dg1kME,710
3
3
  qilisdk/_optionals.py,sha256=gl6yZ2sROY-USvNkXy6964OBOrDNKJAIyBU8D2H13z4,3594
4
4
  qilisdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  qilisdk/yaml.py,sha256=QTDiLB8-3GWOfFKxvKwfEtJjTYXRxGP3ldUAEERB51E,4055
6
- qilisdk/analog/__init__.py,sha256=ExHaNL6pYH9HPY1ahskbPHqVyN9BnH8_ZxLCBS1HYp0,974
6
+ qilisdk/analog/__init__.py,sha256=0J2XPJKMrvBM8jRa4nhxMKTB0fQ5kt4emb4lp4uv3Qs,1004
7
7
  qilisdk/analog/algorithms.py,sha256=OiQma6y5ueNE1buXXbNexme1k1aQaIg1A00C6mX10JQ,4621
8
8
  qilisdk/analog/analog_backend.py,sha256=GRCqlCslo8F0iB8N6Mn-_0IFJos6xRbadwVvk3lYilc,1753
9
9
  qilisdk/analog/analog_result.py,sha256=aaTGQmKm8-cuFvs_lhCAP0JOaUxsmsNcVbn_M5rvkm4,4705
10
10
  qilisdk/analog/exceptions.py,sha256=66nF1b3God6LgJ1IQd0thC3iyr6z1iMcZTfmDsSr4LE,686
11
11
  qilisdk/analog/hamiltonian.py,sha256=ELQXIauk_foOQbOChTFIV4fYzTwqHhSP1tMw8PwytaQ,25126
12
- qilisdk/analog/quantum_objects.py,sha256=_M7hkZOD-NW0FjoTtkrFFDVICxQ2cH7OP0xlfqjtuTE,18833
12
+ qilisdk/analog/quantum_objects.py,sha256=toYJZmgnAq06vHSpJbyl6bXEC1jRLjBrN9tJ2lV_9bc,24478
13
13
  qilisdk/analog/schedule.py,sha256=AHpiBPN1YZb9J2WNw6MyRPjRWAyqLVIiGFj5y_rwDmA,12807
14
14
  qilisdk/common/__init__.py,sha256=sB9yxB1512KdJuWGSHx7TWvIxLMFBX1m7zm6NVkWEAA,657
15
15
  qilisdk/common/algorithm.py,sha256=_MBdMHIJogTPnR8QRCvpf364lxQNDqGWWyZpsgplcyI,636
@@ -27,17 +27,17 @@ qilisdk/digital/digital_result.py,sha256=PmqVOp4W1COZOFYcROxuLG7WAYjBAB6EM5BeY0J
27
27
  qilisdk/digital/exceptions.py,sha256=21RnaSBkzZOuQWcIWVB303F4nyU1ABOykhAYow4m7lA,871
28
28
  qilisdk/digital/gates.py,sha256=Ylf9iwDNONaatzIrrPpXi6H59C7bMvrq-tWyIK5ZykU,29614
29
29
  qilisdk/digital/vqe.py,sha256=HdxueeBO6MQYKyy8xo2_x50hqHd693RPvXaxEFIcEJI,6648
30
- qilisdk/extras/__init__.py,sha256=kki6xpa5TInawohC_vNacHZ4UAWxrEoK_lpl5eUkRpk,1543
30
+ qilisdk/extras/__init__.py,sha256=MdNUZ8Fd0hYFcRzUQWrhHGGEhdHlldd61fWL8iIPe_Y,1522
31
31
  qilisdk/extras/__init__.pyi,sha256=xQ4zOPeSaNM5zGfb07fK4lcbAu4y95YfgSlHs5TkR3Y,717
32
32
  qilisdk/extras/cuda/__init__.py,sha256=LDSTo7NstSbHMvvqvx7ZLxh0HFTEXg1O_B76SyEEHk8,588
33
33
  qilisdk/extras/cuda/cuda_analog_result.py,sha256=0IoBZpkPfxS5CDps1Z6uRH1ATWG689klYR1RvGyUhBU,737
34
- qilisdk/extras/cuda/cuda_backend.py,sha256=ew8Dethe1bIFrSRd0rv-cl7joozrHrPNV1IBn7LtOhQ,16403
34
+ qilisdk/extras/cuda/cuda_backend.py,sha256=AuoWuwlFQfloEE6krMP_1ZHsQZJyxwcnB7g56YTW1U0,16379
35
35
  qilisdk/extras/cuda/cuda_digital_result.py,sha256=bgHCrmcUVCBngo443PhfLkMxAT8Kk24FRubU3bJsdsQ,742
36
36
  qilisdk/extras/qaas/__init__.py,sha256=LDSTo7NstSbHMvvqvx7ZLxh0HFTEXg1O_B76SyEEHk8,588
37
37
  qilisdk/extras/qaas/keyring.py,sha256=xjJj6wrRALj31foctYTGZ07wgusVyIGuzeKxU-MNAA0,1774
38
38
  qilisdk/extras/qaas/models.py,sha256=Phf_j8LPmBUeUbe_StgM-aSz8o1HecCnfsjFbvMAlTY,3801
39
39
  qilisdk/extras/qaas/qaas_analog_result.py,sha256=MLWKtRWLwKZl7Q-tWv615-P_LKFxJIRrK_ni_jQvaP8,738
40
- qilisdk/extras/qaas/qaas_backend.py,sha256=PvXvuL-c5VtPv0IAt2z8AS8witUbVJbVnmxJMwKFiBw,9976
40
+ qilisdk/extras/qaas/qaas_backend.py,sha256=2S953of7KUZ_JBNuqPBHCGa3uOu6Jve3CQxoppJvZyc,10030
41
41
  qilisdk/extras/qaas/qaas_digital_result.py,sha256=9EaZujlXvdmqdfVD-laDJq_I8YO5bc3XHRvEobOYR1U,743
42
42
  qilisdk/extras/qaas/qaas_settings.py,sha256=Vl-OPs0ijht7GqxjztY-Id3nEinfE48j_J-d9mJ4Ctk,935
43
43
  qilisdk/extras/qaas/qaas_time_evolution_result.py,sha256=sj9-xe1CbuO-yOkG5ac-f49CpIM4-X6tKnd9RBVfRMU,745
@@ -45,7 +45,7 @@ qilisdk/extras/qaas/qaas_vqe_result.py,sha256=tQUJ904aW7BzSzd-Dxi8TvL6AIQAnDsiOG
45
45
  qilisdk/utils/__init__.py,sha256=cFdezrFwesp9azZEBG_CWq3_Qp1yH8do_PyJbIIdSkU,920
46
46
  qilisdk/utils/openqasm2.py,sha256=QGQi2rrkYB_cqRgCyp3V3uDyfnVvcdMxykIiK0-sqXM,8316
47
47
  qilisdk/utils/serialization.py,sha256=vp-q2SZ9cBq3NX-gfT3ZDt0tzF3KnxkhV0cM7imJ2zo,3870
48
- qilisdk-0.1.2.dist-info/METADATA,sha256=Pr59Q2KtieNfdr9qiOac3B1Bin1bzbbapoajmejEFy0,18039
49
- qilisdk-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
50
- qilisdk-0.1.2.dist-info/licenses/LICENCE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
51
- qilisdk-0.1.2.dist-info/RECORD,,
48
+ qilisdk-0.1.4.dist-info/METADATA,sha256=TyXixdKgm05tUHh5HArhbZh5U2cUE63hGTFPY6msU1M,18016
49
+ qilisdk-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
50
+ qilisdk-0.1.4.dist-info/licenses/LICENCE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
51
+ qilisdk-0.1.4.dist-info/RECORD,,