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.
- {qilisdk-0.1.3 → qilisdk-0.1.4}/.github/workflows/code_quality.yml +1 -4
- {qilisdk-0.1.3 → qilisdk-0.1.4}/.github/workflows/tests.yml +0 -3
- {qilisdk-0.1.3 → qilisdk-0.1.4}/.gitignore +4 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/CHANGELOG.md +9 -2
- {qilisdk-0.1.3 → qilisdk-0.1.4}/PKG-INFO +5 -6
- {qilisdk-0.1.3 → qilisdk-0.1.4}/README.md +3 -4
- {qilisdk-0.1.3 → qilisdk-0.1.4}/pyproject.toml +5 -2
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/quantum_objects.py +84 -21
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/__init__.py +1 -1
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/cuda/cuda_backend.py +2 -3
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/analog/test_quantum_objects.py +56 -12
- {qilisdk-0.1.3 → qilisdk-0.1.4}/uv.lock +1050 -739
- {qilisdk-0.1.3 → qilisdk-0.1.4}/.github/workflows/publish.yml +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/.pre-commit-config.yaml +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/.python-version +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/LICENCE +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/__init__.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/__init__.pyi +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/_optionals.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/__init__.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/algorithms.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/analog_backend.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/analog_result.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/exceptions.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/hamiltonian.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/analog/schedule.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/common/__init__.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/common/algorithm.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/common/backend.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/common/model.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/common/optimizer.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/common/optimizer_result.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/common/result.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/__init__.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/ansatz.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/circuit.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/digital_algorithm.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/digital_backend.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/digital_result.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/exceptions.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/gates.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/digital/vqe.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/__init__.pyi +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/cuda/__init__.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/cuda/cuda_analog_result.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/cuda/cuda_digital_result.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/__init__.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/keyring.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/models.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/qaas_analog_result.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/qaas_backend.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/qaas_digital_result.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/qaas_settings.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/qaas_time_evolution_result.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/extras/qaas/qaas_vqe_result.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/py.typed +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/utils/__init__.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/utils/openqasm2.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/utils/serialization.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/src/qilisdk/yaml.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/__init__.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/analog/__init__.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/analog/test_analog_result.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/analog/test_hamiltionian.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/analog/test_schedule.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/analog/test_time_evolution.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/common/__init__.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/common/test_scipy_optimizer.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/digital/__init__.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/digital/test_ansatz.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/digital/test_circuit.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/digital/test_gates.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/digital/test_vqe.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/extras/__init__.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/extras/test_cuda_backend.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/test_placeholder.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/utils/__init__.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/utils/test_openqasm2.py +0 -0
- {qilisdk-0.1.3 → qilisdk-0.1.4}/tests/utils/test_serialization.py +0 -0
|
@@ -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
|
+
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:
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
+
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
|
-
"
|
|
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,
|
|
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
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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=["
|
|
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
|
|
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
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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([
|
|
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
|
|