qilisdk 0.1.3__tar.gz → 0.1.4__tar.gz

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 (79) hide show
  1. {qilisdk-0.1.3 → qilisdk-0.1.4}/.github/workflows/code_quality.yml +1 -4
  2. {qilisdk-0.1.3 → qilisdk-0.1.4}/.github/workflows/tests.yml +0 -3
  3. {qilisdk-0.1.3 → qilisdk-0.1.4}/.gitignore +4 -0
  4. {qilisdk-0.1.3 → qilisdk-0.1.4}/CHANGELOG.md +9 -2
  5. {qilisdk-0.1.3 → qilisdk-0.1.4}/PKG-INFO +5 -6
  6. {qilisdk-0.1.3 → qilisdk-0.1.4}/README.md +3 -4
  7. {qilisdk-0.1.3 → qilisdk-0.1.4}/pyproject.toml +5 -2
  8. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/quantum_objects.py +84 -21
  9. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/__init__.py +1 -1
  10. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/cuda/cuda_backend.py +2 -3
  11. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/analog/test_quantum_objects.py +56 -12
  12. {qilisdk-0.1.3 → qilisdk-0.1.4}/uv.lock +1050 -739
  13. {qilisdk-0.1.3 → qilisdk-0.1.4}/.github/workflows/publish.yml +0 -0
  14. {qilisdk-0.1.3 → qilisdk-0.1.4}/.pre-commit-config.yaml +0 -0
  15. {qilisdk-0.1.3 → qilisdk-0.1.4}/.python-version +0 -0
  16. {qilisdk-0.1.3 → qilisdk-0.1.4}/LICENCE +0 -0
  17. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/__init__.py +0 -0
  18. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/__init__.pyi +0 -0
  19. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/_optionals.py +0 -0
  20. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/__init__.py +0 -0
  21. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/algorithms.py +0 -0
  22. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/analog_backend.py +0 -0
  23. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/analog_result.py +0 -0
  24. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/exceptions.py +0 -0
  25. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/hamiltonian.py +0 -0
  26. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/schedule.py +0 -0
  27. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/common/__init__.py +0 -0
  28. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/common/algorithm.py +0 -0
  29. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/common/backend.py +0 -0
  30. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/common/model.py +0 -0
  31. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/common/optimizer.py +0 -0
  32. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/common/optimizer_result.py +0 -0
  33. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/common/result.py +0 -0
  34. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/__init__.py +0 -0
  35. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/ansatz.py +0 -0
  36. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/circuit.py +0 -0
  37. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/digital_algorithm.py +0 -0
  38. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/digital_backend.py +0 -0
  39. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/digital_result.py +0 -0
  40. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/exceptions.py +0 -0
  41. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/gates.py +0 -0
  42. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/vqe.py +0 -0
  43. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/__init__.pyi +0 -0
  44. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/cuda/__init__.py +0 -0
  45. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/cuda/cuda_analog_result.py +0 -0
  46. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/cuda/cuda_digital_result.py +0 -0
  47. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/__init__.py +0 -0
  48. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/keyring.py +0 -0
  49. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/models.py +0 -0
  50. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/qaas_analog_result.py +0 -0
  51. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/qaas_backend.py +0 -0
  52. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/qaas_digital_result.py +0 -0
  53. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/qaas_settings.py +0 -0
  54. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/qaas_time_evolution_result.py +0 -0
  55. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/qaas_vqe_result.py +0 -0
  56. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/py.typed +0 -0
  57. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/utils/__init__.py +0 -0
  58. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/utils/openqasm2.py +0 -0
  59. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/utils/serialization.py +0 -0
  60. {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/yaml.py +0 -0
  61. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/__init__.py +0 -0
  62. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/analog/__init__.py +0 -0
  63. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/analog/test_analog_result.py +0 -0
  64. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/analog/test_hamiltionian.py +0 -0
  65. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/analog/test_schedule.py +0 -0
  66. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/analog/test_time_evolution.py +0 -0
  67. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/common/__init__.py +0 -0
  68. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/common/test_scipy_optimizer.py +0 -0
  69. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/digital/__init__.py +0 -0
  70. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/digital/test_ansatz.py +0 -0
  71. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/digital/test_circuit.py +0 -0
  72. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/digital/test_gates.py +0 -0
  73. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/digital/test_vqe.py +0 -0
  74. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/extras/__init__.py +0 -0
  75. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/extras/test_cuda_backend.py +0 -0
  76. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/test_placeholder.py +0 -0
  77. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/utils/__init__.py +0 -0
  78. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/utils/test_openqasm2.py +0 -0
  79. {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/utils/test_serialization.py +0 -0
@@ -42,10 +42,7 @@ jobs:
42
42
 
43
43
  - name: Install the project
44
44
  run: uv sync --all-groups --all-extras
45
-
46
- - name: Explicit installation of cudaq
47
- run: uv pip install cudaq==0.10.0
48
-
45
+
49
46
  - name: Ruff
50
47
  run: uv run ruff check --output-format=github .
51
48
 
@@ -43,8 +43,5 @@ jobs:
43
43
  - name: Install qilisdk
44
44
  run: uv sync --all-groups --all-extras
45
45
 
46
- - name: Explicit installation of cudaq
47
- run: uv pip install cudaq==0.10.0
48
-
49
46
  - name: Run tests
50
47
  run: uv run pytest tests
@@ -14,3 +14,7 @@ wheels/
14
14
 
15
15
  # Folder for storing user files
16
16
  .tmp
17
+
18
+ # development environment files
19
+ .vscode
20
+ .coverage
@@ -1,8 +1,15 @@
1
+ # Qilisdk 0.1.4 (2025-06-18)
2
+
3
+ ### Bugfixes
4
+
5
+ - Removed manual installation of CUDA-Q for the tests and the code quality workflows. ([PR #40](https://github.com/qilimanjaro-tech/qilisdk/pulls/40))
6
+
7
+
1
8
  # Qilisdk 0.1.3 (2025-05-07)
2
9
 
3
10
  ### Bugfixes
4
11
 
5
- - Made `pydantic` pass to be a mandatory requirement, and not only for qaas as before. Solving a problem with installation overseen in previous PRs.
12
+ - Made `pydantic` pass to be a mandatory requirement, and not only for qaas as before. Solving a problem with installation overseen in previous PRs.
6
13
 
7
14
  ([PR #29](https://github.com/qilimanjaro-tech/qilisdk/pulls/29))
8
15
 
@@ -30,7 +37,7 @@
30
37
  ### Misc
31
38
 
32
39
  - Improved `QaaSBacked` functionality to include methods for executing digital and analog algorithms.
33
-
40
+
34
41
  [PR #27](https://github.com/qilimanjaro-tech/qilisdk/pulls/27)
35
42
 
36
43
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qilisdk
3
- Version: 0.1.3
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
@@ -26,7 +26,7 @@ Requires-Dist: pydantic>=2.10.6
26
26
  Requires-Dist: ruamel-yaml>=0.18.10
27
27
  Requires-Dist: scipy>=1.15.1
28
28
  Provides-Extra: cuda
29
- Requires-Dist: cudaq==0.10.0; extra == 'cuda'
29
+ Requires-Dist: cuda-quantum-cu12; extra == 'cuda'
30
30
  Provides-Extra: qaas
31
31
  Requires-Dist: httpx>=0.28.1; extra == 'qaas'
32
32
  Requires-Dist: keyring>=25.6.0; extra == 'qaas'
@@ -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
 
@@ -193,7 +193,7 @@ For analog simulations, the new `TimeEvolution` and `Schedule` classes allow you
193
193
 
194
194
  ```python
195
195
  import numpy as np
196
- from qilisdk.analog import TimeEvolution, Schedule, tensor, ket, X, Z, Y
196
+ from qilisdk.analog import Schedule, TimeEvolution, ket, X, Z, Y, tensor_prod
197
197
  from qilisdk.extras import CudaBackend
198
198
 
199
199
  T = 10 # Total evolution time
@@ -217,16 +217,15 @@ schedule = Schedule(
217
217
  )
218
218
 
219
219
  # Prepare an initial state (equal superposition)
220
- state = tensor([(ket(0) + ket(1)).unit() for _ in range(nqubits)]).unit()
220
+ state = tensor_prod([(ket(0) + ket(1)).unit() for _ in range(nqubits)]).unit()
221
221
 
222
222
  # Perform time evolution on the CUDA backend with observables to monitor
223
223
  time_evolution = TimeEvolution(
224
- backend=CudaBackend(),
225
224
  schedule=schedule,
226
225
  initial_state=state,
227
226
  observables=[Z(0), X(0), Y(0)],
228
227
  )
229
- results = time_evolution.evolve(store_intermediate_results=True)
228
+ results = time_evolution.evolve(backend=CudaBackend(), store_intermediate_results=True)
230
229
  print("Time Evolution Results:", results)
231
230
  ```
232
231
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qilisdk"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  description = "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
  readme = "README.md"
6
6
  authors = [{name = "Qilimanjaro Quantum Tech", email = "info@qilimanjaro.tech"}]
@@ -32,7 +32,7 @@ dependencies = [
32
32
 
33
33
  [project.optional-dependencies]
34
34
  cuda = [
35
- "cudaq==0.10.0",
35
+ "cuda-quantum-cu12",
36
36
  ]
37
37
  qaas = [
38
38
  "httpx>=0.28.1",
@@ -55,6 +55,9 @@ dev = [
55
55
  requires = ["hatchling"]
56
56
  build-backend = "hatchling.build"
57
57
 
58
+ [tool.uv.sources]
59
+ cudaq = { git = "https://github.com/NVIDIA/cuda-quantum", tag = "0.11.0"}
60
+
58
61
  [tool.ruff]
59
62
  line-length = 120
60
63
  output-format = "concise"
@@ -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
 
@@ -141,31 +142,95 @@ class QuantumObject:
141
142
  out = QuantumObject(self._data.conj().T)
142
143
  return out
143
144
 
144
- def ptrace(self, dims: list[int], keep: list[int]) -> "QuantumObject":
145
+ def ptrace(self, keep: list[int], dims: list[int] | None = None) -> "QuantumObject":
145
146
  """
146
147
  Compute the partial trace over subsystems not in 'keep'.
147
148
 
148
149
  This method calculates the reduced density matrix by tracing out
149
150
  the subsystems that are not specified in the 'keep' parameter.
150
- The input 'dims' represents the dimensions of each subsystem,
151
- and 'keep' indicates the indices of the subsystems to be retained.
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.
152
155
 
153
156
  Args:
154
- dims (list[int]): A list specifying the dimensions of each subsystem.
155
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).
156
163
 
157
164
  Raises:
158
165
  ValueError: If the product of the dimensions in dims does not match the
159
- 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.
160
170
 
161
171
  Returns:
162
172
  QuantumObject: A new QuantumObject representing the reduced density matrix
163
173
  for the subsystems specified in 'keep'.
164
174
  """
165
- rho = self.dense
166
- total_dim = np.prod(dims)
167
- if rho.shape != (total_dim, total_dim):
168
- 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.")
169
234
 
170
235
  # Use letters from the ASCII alphabet (both cases) for einsum indices.
171
236
  # For each subsystem, assign two letters: one for the row index and one for the column index.
@@ -196,15 +261,7 @@ class QuantumObject:
196
261
  # Reshape rho into a tensor with shape dims + dims.
197
262
  reshaped = rho.reshape(dims + dims)
198
263
  # Use einsum to sum over the indices that appear twice (i.e. those being traced out).
199
- reduced_tensor = np.einsum(f"{input_subscript}->{output_subscript}", reshaped)
200
-
201
- # The resulting tensor has separate indices for each subsystem kept.
202
- # Reshape it into a matrix (i.e. combine the row indices and column indices).
203
- dims_keep = [dims[i] for i in keep]
204
- new_dim = np.prod(dims_keep)
205
- reduced_matrix = reduced_tensor.reshape(new_dim, new_dim)
206
-
207
- return QuantumObject(reduced_matrix)
264
+ return np.einsum(f"{input_subscript}->{output_subscript}", reshaped)
208
265
 
209
266
  def norm(self, order: int | Literal["fro", "tr"] = 1) -> float:
210
267
  """
@@ -282,6 +339,7 @@ class QuantumObject:
282
339
 
283
340
  Raises:
284
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.
285
343
 
286
344
  Returns:
287
345
  QuantumObject: A new QuantumObject representing the density matrix.
@@ -289,15 +347,20 @@ class QuantumObject:
289
347
  if self.is_scalar():
290
348
  raise ValueError("Cannot make a density matrix from scalar.")
291
349
 
292
- if self.is_density_matrix():
293
- return self
294
-
295
350
  if self.is_bra():
296
351
  return (self.adjoint() @ self).unit()
297
352
 
298
353
  if self.is_ket():
299
354
  return (self @ self.adjoint()).unit()
300
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
+
301
364
  raise ValueError(
302
365
  "Cannot make a density matrix from this QuantumObject. "
303
366
  "It must be either a ket, a bra or already a density matrix."
@@ -20,7 +20,7 @@ __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(
@@ -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
@@ -85,18 +85,49 @@ def test_dag():
85
85
 
86
86
 
87
87
  def test_ptrace_valid():
88
- """Test partial trace on a valid 2-qubit density matrix.
89
-
90
- The test creates a 2-qubit state |00⟩, converts it to a density matrix,
91
- and then traces out the second qubit.
92
- """
93
- qket = ket(0, 0)
88
+ """Test partial trace on a valid 4-qubit density matrices."""
89
+ qket = ket(0, 1, 1, 0)
94
90
  rho = qket.to_density_matrix()
95
- # dims for a 2-qubit system are [2, 2]; keep the first qubit (index 0).
96
- reduced = rho.ptrace([2, 2], keep=[0])
97
- # Expected reduced density matrix is the pure state |0⟩.
98
- expected = ket(0).to_density_matrix()
99
- np.testing.assert_allclose(reduced.dense, expected.dense, atol=1e-8)
91
+
92
+ # Different combinations of partial traces.
93
+ reduced_single_qubit_ground = rho.ptrace(keep=[0], dims=[2, 2, 4])
94
+ reduced_single_qubit_excited = rho.ptrace(keep=[1], dims=[2, 2, 4])
95
+ reduced_double_qubit_1 = rho.ptrace(keep=[2], dims=[2, 2, 4])
96
+ reduced_double_qubit_2 = rho.ptrace(keep=[2, 3], dims=[2, 2, 2, 2])
97
+ reduced_double_qubit_3 = rho.ptrace(keep=[3, 2], dims=[2, 2, 2, 2])
98
+
99
+ # Expected reduced density matrices:
100
+ expected_single_qubit_ground = ket(0).to_density_matrix()
101
+ expected_single_qubit_excited = ket(1).to_density_matrix()
102
+ expected_double_qubit = ket(1, 0).to_density_matrix()
103
+
104
+ # Checks:
105
+ np.testing.assert_allclose(reduced_single_qubit_ground.dense, expected_single_qubit_ground.dense, atol=1e-8)
106
+ np.testing.assert_allclose(reduced_single_qubit_excited.dense, expected_single_qubit_excited.dense, atol=1e-8)
107
+ np.testing.assert_allclose(reduced_double_qubit_1.dense, expected_double_qubit.dense, atol=1e-8)
108
+ np.testing.assert_allclose(reduced_double_qubit_2.dense, expected_double_qubit.dense, atol=1e-8)
109
+ np.testing.assert_allclose(reduced_double_qubit_3.dense, expected_double_qubit.dense, atol=1e-8)
110
+
111
+
112
+ def test_ptrace_valid_keep_with_automatic_dims_and_density_matrix():
113
+ qket = ket(0, 0, 1, 0)
114
+ reduced_single_qubit = qket.ptrace(keep=[2, 3])
115
+ expected_single_qubit = ket(1, 0).to_density_matrix()
116
+ np.testing.assert_allclose(reduced_single_qubit.dense, expected_single_qubit.dense, atol=1e-8)
117
+
118
+
119
+ def test_ptrace_works_for_operators_which_are_not_density_matrices():
120
+ # Build a “diagonal” density matrix whose diagonal entries are 0…7. That way each composite basis |i0,i1,i2⟩ ↦
121
+ # flat index i = 4*i0 + 2*i1 + i2 carries a unique number. And the trace != 1, so not a density operator
122
+ dims = [2, 2, 2]
123
+ full_dim = np.prod(dims)
124
+ rho = np.diag(np.arange(full_dim, dtype=float))
125
+ q_obj = QuantumObject(rho)
126
+
127
+ # Pick an out of order keep list:
128
+ keep = [0, 2] # subspace 2 *then* subspace 0
129
+ expected_result = np.array([[2, 0, 0, 0], [0, 4, 0, 0], [0, 0, 10, 0], [0, 0, 0, 12]])
130
+ np.testing.assert_allclose(q_obj.ptrace(keep, dims).dense, expected_result, atol=1e-8)
100
131
 
101
132
 
102
133
  def test_ptrace_invalid_dims():
@@ -104,9 +135,22 @@ def test_ptrace_invalid_dims():
104
135
  arr = np.eye(2)
105
136
  qobj = QuantumObject(arr)
106
137
  with pytest.raises(ValueError): # noqa: PT011
107
- qobj.ptrace([2, 2], keep=[0])
138
+ qobj.ptrace(keep=[0], dims=[2, 2])
139
+ with pytest.raises(ValueError): # noqa: PT011
140
+ qobj.ptrace(keep=[0], dims=[1]) # too few dimensions
108
141
 
109
142
 
143
+ def test_ptrace_invalid_keep():
144
+ """Partial trace should raise ValueError if keep indices are out of bounds."""
145
+ arr = np.eye(2)
146
+ qobj = QuantumObject(arr)
147
+ with pytest.raises(ValueError): # noqa: PT011
148
+ qobj.ptrace(keep=[1], dims=[2])
149
+ with pytest.raises(ValueError): # noqa: PT011
150
+ qobj.ptrace(keep=[2], dims=[2]) # out of bounds index
151
+ with pytest.raises(ValueError): # noqa: PT011
152
+ qobj.ptrace(keep=[0, 1], dims=[2]) # too many indices
153
+
110
154
  # --- Arithmetic Operator Tests ---
111
155
 
112
156