qilisdk 0.1.4__py3-none-any.whl → 0.1.5__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.
- qilisdk/__init__.py +11 -2
- qilisdk/__init__.pyi +2 -3
- qilisdk/_logging.py +135 -0
- qilisdk/_optionals.py +5 -7
- qilisdk/analog/__init__.py +3 -18
- qilisdk/analog/exceptions.py +2 -4
- qilisdk/analog/hamiltonian.py +455 -110
- qilisdk/analog/linear_schedule.py +118 -0
- qilisdk/analog/schedule.py +272 -79
- qilisdk/backends/__init__.py +45 -0
- qilisdk/{digital/digital_algorithm.py → backends/__init__.pyi} +3 -5
- qilisdk/backends/backend.py +117 -0
- qilisdk/{extras/cuda → backends}/cuda_backend.py +152 -159
- qilisdk/backends/qutip_backend.py +492 -0
- qilisdk/common/__init__.py +48 -2
- qilisdk/common/algorithm.py +2 -1
- qilisdk/{extras/qaas/qaas_settings.py → common/exceptions.py} +12 -6
- qilisdk/common/model.py +1019 -1
- qilisdk/common/parameterizable.py +75 -0
- qilisdk/common/qtensor.py +666 -0
- qilisdk/common/result.py +2 -1
- qilisdk/common/variables.py +1931 -0
- qilisdk/{extras/cuda/cuda_analog_result.py → cost_functions/__init__.py} +3 -4
- qilisdk/cost_functions/cost_function.py +77 -0
- qilisdk/cost_functions/model_cost_function.py +145 -0
- qilisdk/cost_functions/observable_cost_function.py +109 -0
- qilisdk/digital/__init__.py +3 -22
- qilisdk/digital/ansatz.py +203 -160
- qilisdk/digital/circuit.py +81 -9
- qilisdk/digital/exceptions.py +12 -6
- qilisdk/digital/gates.py +228 -85
- qilisdk/{extras/qaas/qaas_analog_result.py → functionals/__init__.py} +14 -5
- qilisdk/functionals/functional.py +39 -0
- qilisdk/{extras/cuda/cuda_digital_result.py → functionals/functional_result.py} +3 -4
- qilisdk/functionals/sampling.py +81 -0
- qilisdk/functionals/sampling_result.py +92 -0
- qilisdk/functionals/time_evolution.py +98 -0
- qilisdk/functionals/time_evolution_result.py +84 -0
- qilisdk/functionals/variational_program.py +80 -0
- qilisdk/functionals/variational_program_result.py +69 -0
- qilisdk/logging_config.yaml +16 -0
- qilisdk/{common/backend.py → optimizers/__init__.py} +2 -1
- qilisdk/optimizers/optimizer.py +39 -0
- qilisdk/{common → optimizers}/optimizer_result.py +3 -12
- qilisdk/{common/optimizer.py → optimizers/scipy_optimizer.py} +10 -28
- qilisdk/settings.py +78 -0
- qilisdk/{extras → speqtrum}/__init__.py +7 -8
- qilisdk/{extras → speqtrum}/__init__.pyi +3 -3
- qilisdk/speqtrum/experiments/__init__.py +25 -0
- qilisdk/speqtrum/experiments/experiment_functional.py +124 -0
- qilisdk/speqtrum/experiments/experiment_result.py +231 -0
- qilisdk/{extras/qaas → speqtrum}/keyring.py +8 -4
- qilisdk/speqtrum/speqtrum.py +432 -0
- qilisdk/speqtrum/speqtrum_models.py +300 -0
- qilisdk/utils/__init__.py +0 -14
- qilisdk/utils/openqasm2.py +1 -1
- qilisdk/utils/serialization.py +1 -1
- qilisdk/utils/visualization/PlusJakartaSans-SemiBold.ttf +0 -0
- qilisdk/utils/visualization/__init__.py +24 -0
- qilisdk/utils/visualization/circuit_renderers.py +781 -0
- qilisdk/utils/visualization/schedule_renderers.py +161 -0
- qilisdk/utils/visualization/style.py +154 -0
- qilisdk/utils/visualization/themes.py +76 -0
- qilisdk/yaml.py +126 -0
- {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/METADATA +180 -134
- qilisdk-0.1.5.dist-info/RECORD +69 -0
- qilisdk/analog/algorithms.py +0 -111
- qilisdk/analog/analog_backend.py +0 -43
- qilisdk/analog/analog_result.py +0 -114
- qilisdk/analog/quantum_objects.py +0 -596
- qilisdk/digital/digital_backend.py +0 -90
- qilisdk/digital/digital_result.py +0 -145
- qilisdk/digital/vqe.py +0 -166
- qilisdk/extras/cuda/__init__.py +0 -13
- qilisdk/extras/qaas/__init__.py +0 -13
- qilisdk/extras/qaas/models.py +0 -132
- qilisdk/extras/qaas/qaas_backend.py +0 -255
- qilisdk/extras/qaas/qaas_digital_result.py +0 -20
- qilisdk/extras/qaas/qaas_time_evolution_result.py +0 -20
- qilisdk/extras/qaas/qaas_vqe_result.py +0 -20
- qilisdk-0.1.4.dist-info/RECORD +0 -51
- {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/WHEEL +0 -0
- {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/licenses/LICENCE +0 -0
|
@@ -1,596 +0,0 @@
|
|
|
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 math
|
|
17
|
-
import string
|
|
18
|
-
from typing import Literal
|
|
19
|
-
|
|
20
|
-
import numpy as np
|
|
21
|
-
from scipy.sparse import csc_array, csr_matrix, issparse, kron, sparray, spmatrix
|
|
22
|
-
from scipy.sparse.linalg import expm
|
|
23
|
-
from scipy.sparse.linalg import norm as scipy_norm
|
|
24
|
-
|
|
25
|
-
from qilisdk.yaml import yaml
|
|
26
|
-
|
|
27
|
-
Complex = int | float | complex
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
###############################################################################
|
|
31
|
-
# Main Class Definition
|
|
32
|
-
###############################################################################
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@yaml.register_class
|
|
36
|
-
class QuantumObject:
|
|
37
|
-
"""
|
|
38
|
-
Represents a quantum state or operator using a sparse matrix representation.
|
|
39
|
-
|
|
40
|
-
The QuantumObject class is a wrapper around sparse matrices (or NumPy arrays,
|
|
41
|
-
which are converted to sparse matrices) that represent quantum states (kets, bras)
|
|
42
|
-
or operators. It provides utility methods for common quantum operations such as
|
|
43
|
-
taking the adjoint (dagger), computing tensor products, partial traces, and norms.
|
|
44
|
-
|
|
45
|
-
The internal data is stored as a SciPy CSR (Compressed Sparse Row) matrix for
|
|
46
|
-
efficient arithmetic and manipulation. The expected shapes for the data are:
|
|
47
|
-
- (2**N, 2**N) for operators or density matrices (or scalars),
|
|
48
|
-
- (2**N, 1) for ket states,
|
|
49
|
-
- (1, 2**N) or (2**N,) for bra states.
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
def __init__(self, data: np.ndarray | sparray | spmatrix) -> None:
|
|
53
|
-
"""
|
|
54
|
-
Initialize a QuantumObject with the given data.
|
|
55
|
-
|
|
56
|
-
Converts a NumPy array to a CSR matrix if needed and validates the shape of the input.
|
|
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).
|
|
59
|
-
|
|
60
|
-
Args:
|
|
61
|
-
data (np.ndarray | sparray | spmatrix): A dense NumPy array or a SciPy sparse matrix
|
|
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.
|
|
64
|
-
|
|
65
|
-
Raises:
|
|
66
|
-
ValueError: If the input data is not a NumPy array or a SciPy sparse matrix,
|
|
67
|
-
or if the data's shape does not correspond to a valid quantum state/operator.
|
|
68
|
-
"""
|
|
69
|
-
if isinstance(data, np.ndarray):
|
|
70
|
-
self._data = csr_matrix(data)
|
|
71
|
-
elif issparse(data):
|
|
72
|
-
self._data = data.tocsr()
|
|
73
|
-
else:
|
|
74
|
-
raise ValueError("Input must be a NumPy array or a SciPy sparse matrix")
|
|
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
|
|
80
|
-
raise ValueError(
|
|
81
|
-
"Dimension of data is wrong. expected data to have shape similar to (2**N, 2**N), (1, 2**N), (2**N, 1)",
|
|
82
|
-
f"but received {self._data.shape}",
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
# ------------- Properties --------------
|
|
86
|
-
|
|
87
|
-
@property
|
|
88
|
-
def data(self) -> csr_matrix:
|
|
89
|
-
"""
|
|
90
|
-
Get the internal sparse matrix representation of the QuantumObject.
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
csr_matrix: The internal representation as a CSR matrix.
|
|
94
|
-
"""
|
|
95
|
-
return self._data
|
|
96
|
-
|
|
97
|
-
@property
|
|
98
|
-
def dense(self) -> np.ndarray:
|
|
99
|
-
"""
|
|
100
|
-
Get the dense (NumPy array) representation of the QuantumObject.
|
|
101
|
-
|
|
102
|
-
Returns:
|
|
103
|
-
np.ndarray: The dense array representation.
|
|
104
|
-
"""
|
|
105
|
-
return self._data.toarray()
|
|
106
|
-
|
|
107
|
-
@property
|
|
108
|
-
def nqubits(self) -> int:
|
|
109
|
-
"""
|
|
110
|
-
Compute the number of qubits represented by the QuantumObject.
|
|
111
|
-
|
|
112
|
-
Returns:
|
|
113
|
-
int: The number of qubits if determinable; otherwise, -1.
|
|
114
|
-
"""
|
|
115
|
-
if self._data.shape[0] == self._data.shape[1]:
|
|
116
|
-
return int(np.log2(self._data.shape[0]))
|
|
117
|
-
if self._data.shape[0] == 1:
|
|
118
|
-
return int(np.log2(self._data.shape[1]))
|
|
119
|
-
if self._data.shape[1] == 1:
|
|
120
|
-
return int(np.log2(self._data.shape[0]))
|
|
121
|
-
return -1
|
|
122
|
-
|
|
123
|
-
@property
|
|
124
|
-
def shape(self) -> tuple[int, ...]:
|
|
125
|
-
"""
|
|
126
|
-
Get the shape of the QuantumObject's internal matrix.
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
tuple[int, ...]: The shape of the internal matrix.
|
|
130
|
-
"""
|
|
131
|
-
return self._data.shape
|
|
132
|
-
|
|
133
|
-
# ----------- Matrix Logic Operations ------------
|
|
134
|
-
|
|
135
|
-
def adjoint(self) -> QuantumObject:
|
|
136
|
-
"""
|
|
137
|
-
Compute the adjoint (conjugate transpose) of the QuantumObject.
|
|
138
|
-
|
|
139
|
-
Returns:
|
|
140
|
-
QuantumObject: A new QuantumObject that is the adjoint of this object.
|
|
141
|
-
"""
|
|
142
|
-
out = QuantumObject(self._data.conj().T)
|
|
143
|
-
return out
|
|
144
|
-
|
|
145
|
-
def ptrace(self, keep: list[int], dims: list[int] | None = None) -> "QuantumObject":
|
|
146
|
-
"""
|
|
147
|
-
Compute the partial trace over subsystems not in 'keep'.
|
|
148
|
-
|
|
149
|
-
This method calculates the reduced density matrix by tracing out
|
|
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.
|
|
155
|
-
|
|
156
|
-
Args:
|
|
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).
|
|
163
|
-
|
|
164
|
-
Raises:
|
|
165
|
-
ValueError: If the product of the dimensions in dims does not match the
|
|
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.
|
|
170
|
-
|
|
171
|
-
Returns:
|
|
172
|
-
QuantumObject: A new QuantumObject representing the reduced density matrix
|
|
173
|
-
for the subsystems specified in 'keep'.
|
|
174
|
-
"""
|
|
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.")
|
|
234
|
-
|
|
235
|
-
# Use letters from the ASCII alphabet (both cases) for einsum indices.
|
|
236
|
-
# For each subsystem, assign two letters: one for the row index and one for the column index.
|
|
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.
|
|
239
|
-
letters = iter(string.ascii_letters)
|
|
240
|
-
|
|
241
|
-
for i in range(len(dims)):
|
|
242
|
-
if i in keep:
|
|
243
|
-
# For a subsystem we want to keep, use two different letters (r, c)
|
|
244
|
-
r, c = next(letters), next(letters)
|
|
245
|
-
row_letters.append(r)
|
|
246
|
-
col_letters.append(c)
|
|
247
|
-
out_row.append(r)
|
|
248
|
-
out_col.append(c)
|
|
249
|
-
else:
|
|
250
|
-
# For subsystems to be traced out, assign the same letter (r, r) so that those indices are summed.
|
|
251
|
-
r = next(letters)
|
|
252
|
-
row_letters.append(r)
|
|
253
|
-
col_letters.append(r)
|
|
254
|
-
|
|
255
|
-
# Create the einsum subscript strings.
|
|
256
|
-
# The input tensor has 2*n indices (first n for rows, next n for columns).
|
|
257
|
-
input_subscript = "".join(row_letters + col_letters)
|
|
258
|
-
# The output will only contain the indices corresponding to the subsystems we keep.
|
|
259
|
-
output_subscript = "".join(out_row + out_col)
|
|
260
|
-
|
|
261
|
-
# Reshape rho into a tensor with shape dims + dims.
|
|
262
|
-
reshaped = rho.reshape(dims + dims)
|
|
263
|
-
# Use einsum to sum over the indices that appear twice (i.e. those being traced out).
|
|
264
|
-
return np.einsum(f"{input_subscript}->{output_subscript}", reshaped)
|
|
265
|
-
|
|
266
|
-
def norm(self, order: int | Literal["fro", "tr"] = 1) -> float:
|
|
267
|
-
"""
|
|
268
|
-
Compute the norm of the QuantumObject.
|
|
269
|
-
|
|
270
|
-
For density matrices, the norm order can be specified. For state vectors, the norm is computed accordingly.
|
|
271
|
-
|
|
272
|
-
Args:
|
|
273
|
-
order (int or {"fro", "tr"}, optional): The order of the norm.
|
|
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,
|
|
279
|
-
|
|
280
|
-
Returns:
|
|
281
|
-
float: The computed norm of the QuantumObject.
|
|
282
|
-
"""
|
|
283
|
-
if self.is_scalar():
|
|
284
|
-
return self.dense[0][0]
|
|
285
|
-
|
|
286
|
-
if self.is_density_matrix() or self.shape[0] == self.shape[1]:
|
|
287
|
-
if order == "tr":
|
|
288
|
-
return np.sum(np.abs(np.linalg.eigvalsh(self.dense)))
|
|
289
|
-
return scipy_norm(self._data, ord=order)
|
|
290
|
-
|
|
291
|
-
if self.is_bra():
|
|
292
|
-
return np.sqrt(self._data @ self._data.conj().T).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.")
|
|
298
|
-
|
|
299
|
-
def unit(self, order: int | Literal["fro", "tr"] = "tr") -> QuantumObject:
|
|
300
|
-
"""
|
|
301
|
-
Normalize the QuantumObject.
|
|
302
|
-
|
|
303
|
-
Scales the QuantumObject so that its norm becomes 1, according to the specified norm order.
|
|
304
|
-
|
|
305
|
-
Args:
|
|
306
|
-
order (int or {"fro", "tr"}, optional): The order of the norm to use for normalization.
|
|
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".
|
|
309
|
-
|
|
310
|
-
Raises:
|
|
311
|
-
ValueError: If the norm of the QuantumObject is 0, making normalization impossible.
|
|
312
|
-
|
|
313
|
-
Returns:
|
|
314
|
-
QuantumObject: A new QuantumObject that is the normalized version of this object.
|
|
315
|
-
"""
|
|
316
|
-
norm = self.norm(order=order)
|
|
317
|
-
if norm == 0:
|
|
318
|
-
raise ValueError("Cannot normalize a zero-norm Quantum Object")
|
|
319
|
-
|
|
320
|
-
return QuantumObject(self._data / norm)
|
|
321
|
-
|
|
322
|
-
def expm(self) -> QuantumObject:
|
|
323
|
-
"""
|
|
324
|
-
Compute the matrix exponential of the QuantumObject.
|
|
325
|
-
|
|
326
|
-
Returns:
|
|
327
|
-
QuantumObject: A new QuantumObject representing the matrix exponential.
|
|
328
|
-
"""
|
|
329
|
-
return QuantumObject(expm(self._data))
|
|
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
|
-
|
|
371
|
-
def is_ket(self) -> bool:
|
|
372
|
-
"""
|
|
373
|
-
Check if the QuantumObject represents a ket (column vector) state.
|
|
374
|
-
|
|
375
|
-
Returns:
|
|
376
|
-
bool: True if the QuantumObject is a ket state, False otherwise.
|
|
377
|
-
"""
|
|
378
|
-
return self.shape[1] == 1 and self.shape[0].bit_count() == 1
|
|
379
|
-
|
|
380
|
-
def is_bra(self) -> bool:
|
|
381
|
-
"""
|
|
382
|
-
Check if the QuantumObject represents a bra (row vector) state.
|
|
383
|
-
|
|
384
|
-
Returns:
|
|
385
|
-
bool: True if the QuantumObject is a bra state, False otherwise.
|
|
386
|
-
"""
|
|
387
|
-
return self.shape[0] == 1 and self.shape[1].bit_count() == 1
|
|
388
|
-
|
|
389
|
-
def is_scalar(self) -> bool:
|
|
390
|
-
"""
|
|
391
|
-
Check if the QuantumObject is a scalar (1x1 matrix).
|
|
392
|
-
|
|
393
|
-
Returns:
|
|
394
|
-
bool: True if the QuantumObject is a scalar, False otherwise.
|
|
395
|
-
"""
|
|
396
|
-
return self.shape == (1, 1)
|
|
397
|
-
|
|
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:
|
|
408
|
-
"""
|
|
409
|
-
Determine if the QuantumObject is a valid density matrix.
|
|
410
|
-
|
|
411
|
-
A valid density matrix must be square, Hermitian, positive semi-definite, and have a trace equal to 1.
|
|
412
|
-
|
|
413
|
-
Args:
|
|
414
|
-
tol (float, optional): The numerical tolerance for verifying Hermiticity,
|
|
415
|
-
eigenvalue non-negativity, and trace. Defaults to 1e-8.
|
|
416
|
-
|
|
417
|
-
Returns:
|
|
418
|
-
bool: True if the QuantumObject is a valid density matrix, False otherwise.
|
|
419
|
-
"""
|
|
420
|
-
# Check if rho is a square matrix
|
|
421
|
-
if not self.is_operator():
|
|
422
|
-
return False
|
|
423
|
-
|
|
424
|
-
# Check Hermitian condition: rho should be equal to its conjugate transpose
|
|
425
|
-
if not self.is_hermitian(tol=tol):
|
|
426
|
-
return False
|
|
427
|
-
|
|
428
|
-
# Check if eigenvalues are non-negative (positive semi-definite)
|
|
429
|
-
eigenvalues = np.linalg.eigvalsh(self.dense) # More stable for Hermitian matrices
|
|
430
|
-
if np.any(eigenvalues < -tol): # Allow small numerical errors
|
|
431
|
-
return False
|
|
432
|
-
|
|
433
|
-
# Check if the trace is 1
|
|
434
|
-
return np.isclose(self._data.trace(), 1, atol=tol)
|
|
435
|
-
|
|
436
|
-
def is_hermitian(self, tol: float = 1e-8) -> bool:
|
|
437
|
-
"""
|
|
438
|
-
Check if the QuantumObject is Hermitian.
|
|
439
|
-
|
|
440
|
-
Args:
|
|
441
|
-
tol (float, optional): The numerical tolerance for verifying Hermiticity.
|
|
442
|
-
Defaults to 1e-8.
|
|
443
|
-
|
|
444
|
-
Returns:
|
|
445
|
-
bool: True if the QuantumObject is Hermitian, False otherwise.
|
|
446
|
-
"""
|
|
447
|
-
return np.allclose(self.dense, self._data.conj().T.toarray(), atol=tol)
|
|
448
|
-
|
|
449
|
-
# ----------- Basic Arithmetic Operators ------------
|
|
450
|
-
|
|
451
|
-
def __add__(self, other: QuantumObject | Complex) -> QuantumObject:
|
|
452
|
-
if isinstance(other, QuantumObject):
|
|
453
|
-
return QuantumObject(self._data + other._data)
|
|
454
|
-
if isinstance(other, Complex) and other == 0:
|
|
455
|
-
return self
|
|
456
|
-
|
|
457
|
-
raise TypeError("Addition is only supported between QuantumState instances")
|
|
458
|
-
|
|
459
|
-
def __sub__(self, other: QuantumObject) -> QuantumObject:
|
|
460
|
-
if isinstance(other, QuantumObject):
|
|
461
|
-
return QuantumObject(self._data - other._data)
|
|
462
|
-
|
|
463
|
-
raise TypeError("Subtraction is only supported between QuantumState instances")
|
|
464
|
-
|
|
465
|
-
def __mul__(self, other: QuantumObject | Complex) -> QuantumObject:
|
|
466
|
-
if isinstance(other, (int, float, complex)):
|
|
467
|
-
return QuantumObject(self._data * other)
|
|
468
|
-
if isinstance(other, QuantumObject):
|
|
469
|
-
return QuantumObject(self._data * other._data)
|
|
470
|
-
|
|
471
|
-
raise TypeError("Unsupported multiplication type")
|
|
472
|
-
|
|
473
|
-
def __matmul__(self, other: QuantumObject) -> QuantumObject:
|
|
474
|
-
if isinstance(other, QuantumObject):
|
|
475
|
-
return QuantumObject(self._data @ other._data)
|
|
476
|
-
|
|
477
|
-
raise TypeError("Dot product is only supported between QuantumState instances")
|
|
478
|
-
|
|
479
|
-
def __rmul__(self, other: QuantumObject | Complex) -> QuantumObject:
|
|
480
|
-
return self.__mul__(other)
|
|
481
|
-
|
|
482
|
-
def __repr__(self) -> str:
|
|
483
|
-
return f"{self.dense}"
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
###############################################################################
|
|
487
|
-
# Outside class Function Definitions
|
|
488
|
-
###############################################################################
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
def basis_state(n: int, N: int) -> QuantumObject:
|
|
492
|
-
"""
|
|
493
|
-
Generate the n'th basis vector representation, on a N-size Hilbert space (N=2**num_qubits).
|
|
494
|
-
|
|
495
|
-
This function creates a column vector (ket) representing the Fock state |n⟩ in a Hilbert space of dimension N.
|
|
496
|
-
|
|
497
|
-
Args:
|
|
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.
|
|
500
|
-
|
|
501
|
-
Returns:
|
|
502
|
-
QuantumObject: A QuantumObject representing the |n⟩'th basis state on a N-size Hilbert space (N=2**num_qubits).
|
|
503
|
-
"""
|
|
504
|
-
return QuantumObject(csc_array(([1], ([n], [0])), shape=(N, 1)))
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
def ket(*state: int) -> QuantumObject:
|
|
508
|
-
"""
|
|
509
|
-
Generate a ket state for a multi-qubit system.
|
|
510
|
-
|
|
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⟩.
|
|
513
|
-
|
|
514
|
-
Args:
|
|
515
|
-
*state (int): A sequence of integers representing the state of each qubit (0 or 1).
|
|
516
|
-
|
|
517
|
-
Raises:
|
|
518
|
-
ValueError: If any of the provided qubit states is not 0 or 1.
|
|
519
|
-
|
|
520
|
-
Returns:
|
|
521
|
-
QuantumObject: A QuantumObject representing the multi-qubit ket state.
|
|
522
|
-
"""
|
|
523
|
-
if any(s not in {0, 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])
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
def bra(*state: int) -> QuantumObject:
|
|
530
|
-
"""
|
|
531
|
-
Generate a bra state for a multi-qubit system.
|
|
532
|
-
|
|
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|.
|
|
535
|
-
|
|
536
|
-
Args:
|
|
537
|
-
*state (int): A sequence of integers representing the state of each qubit (0 or 1).
|
|
538
|
-
|
|
539
|
-
Raises:
|
|
540
|
-
ValueError: If any of the provided qubit states is not 0 or 1.
|
|
541
|
-
|
|
542
|
-
Returns:
|
|
543
|
-
QuantumObject: A QuantumObject representing the multi-qubit bra state.
|
|
544
|
-
"""
|
|
545
|
-
if any(s not in {0, 1} for s in state):
|
|
546
|
-
raise ValueError(f"the state can only contain 1s or 0s. But received:: {state}")
|
|
547
|
-
|
|
548
|
-
return tensor_prod([QuantumObject(csc_array(([1], ([0], [s])), shape=(1, 2))) for s in state])
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
def tensor_prod(operators: list[QuantumObject]) -> QuantumObject:
|
|
552
|
-
"""
|
|
553
|
-
Calculate the tensor product of a list of QuantumObjects.
|
|
554
|
-
|
|
555
|
-
This function computes the tensor (Kronecker) product of all input QuantumObjects,
|
|
556
|
-
resulting in a composite QuantumObject that represents the combined state or operator.
|
|
557
|
-
|
|
558
|
-
Args:
|
|
559
|
-
operators (list[QuantumObject]): A list of QuantumObjects to be combined via tensor product.
|
|
560
|
-
|
|
561
|
-
Returns:
|
|
562
|
-
QuantumObject: A new QuantumObject representing the tensor product of the inputs.
|
|
563
|
-
"""
|
|
564
|
-
out = operators[0].data
|
|
565
|
-
if len(operators) > 1:
|
|
566
|
-
for i in range(1, len(operators)):
|
|
567
|
-
out = kron(out, operators[i].data)
|
|
568
|
-
|
|
569
|
-
return QuantumObject(out)
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
def expect_val(operator: QuantumObject, state: QuantumObject) -> Complex:
|
|
573
|
-
"""
|
|
574
|
-
Calculate the expectation value of an operator with respect to a quantum state.
|
|
575
|
-
|
|
576
|
-
Computes the expectation value ⟨state| operator |state⟩. The function handles both
|
|
577
|
-
pure state vectors and density matrices appropriately.
|
|
578
|
-
|
|
579
|
-
Args:
|
|
580
|
-
operator (QuantumObject): The quantum operator represented as a QuantumObject.
|
|
581
|
-
state (QuantumObject): The quantum state or density matrix represented as a QuantumObject.
|
|
582
|
-
|
|
583
|
-
Raises:
|
|
584
|
-
ValueError: If the operator is not a square matrix.
|
|
585
|
-
|
|
586
|
-
Returns:
|
|
587
|
-
Complex: The expectation value. The result is guaranteed to be real if the operator
|
|
588
|
-
is Hermitian, and may be complex otherwise.
|
|
589
|
-
"""
|
|
590
|
-
if not operator.is_operator():
|
|
591
|
-
raise ValueError("The operator must be a square matrix.")
|
|
592
|
-
|
|
593
|
-
if state.data.shape[1] == state.data.shape[0]:
|
|
594
|
-
return (operator @ state).dense.trace()
|
|
595
|
-
|
|
596
|
-
return (state.adjoint() @ operator @ state).dense[0, 0]
|
|
@@ -1,90 +0,0 @@
|
|
|
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 abc import ABC, abstractmethod
|
|
15
|
-
from enum import Enum
|
|
16
|
-
|
|
17
|
-
from qilisdk.digital.circuit import Circuit
|
|
18
|
-
from qilisdk.digital.digital_result import DigitalResult
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class DigitalSimulationMethod(str, Enum):
|
|
22
|
-
"""
|
|
23
|
-
Enumeration of available simulation methods for the CUDA backend.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
STATE_VECTOR = "state_vector"
|
|
27
|
-
TENSOR_NETWORK = "tensor_network"
|
|
28
|
-
MATRIX_PRODUCT_STATE = "matrix_product_state"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class DigitalBackend(ABC):
|
|
32
|
-
"""
|
|
33
|
-
Abstract base class for digital quantum circuit backends.
|
|
34
|
-
|
|
35
|
-
This abstract class defines the interface for a digital backend capable of executing a
|
|
36
|
-
quantum circuit. Subclasses must implement the execute method to run the circuit with a
|
|
37
|
-
specified number of measurement shots and return a DigitalResult encapsulating the measurement
|
|
38
|
-
outcomes.
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
def __init__(
|
|
42
|
-
self, digital_simulation_method: DigitalSimulationMethod = DigitalSimulationMethod.STATE_VECTOR
|
|
43
|
-
) -> None:
|
|
44
|
-
"""
|
|
45
|
-
Initialize the DigitalBackend.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
simulation_method (DigitalSimulationMethod, optional): The simulation method to use.
|
|
49
|
-
Options include STATE_VECTOR, TENSOR_NETWORK, or MATRIX_PRODUCT_STATE.
|
|
50
|
-
Defaults to STATE_VECTOR.
|
|
51
|
-
"""
|
|
52
|
-
self._digital_simulation_method = digital_simulation_method
|
|
53
|
-
|
|
54
|
-
@property
|
|
55
|
-
def digital_simulation_method(self) -> DigitalSimulationMethod:
|
|
56
|
-
"""
|
|
57
|
-
Get the simulation method currently configured for the backend.
|
|
58
|
-
|
|
59
|
-
Returns:
|
|
60
|
-
SimulationMethod: The simulation method to be used for circuit execution.
|
|
61
|
-
"""
|
|
62
|
-
return self._digital_simulation_method
|
|
63
|
-
|
|
64
|
-
@digital_simulation_method.setter
|
|
65
|
-
def digital_simulation_method(self, value: DigitalSimulationMethod) -> None:
|
|
66
|
-
"""
|
|
67
|
-
Set the simulation method for the backend.
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
value (SimulationMethod): The simulation method to set. Options include
|
|
71
|
-
STATE_VECTOR, TENSOR_NETWORK, or MATRIX_PRODUCT_STATE.
|
|
72
|
-
"""
|
|
73
|
-
self._digital_simulation_method = value
|
|
74
|
-
|
|
75
|
-
@abstractmethod
|
|
76
|
-
def execute(self, circuit: Circuit, nshots: int = 1000) -> DigitalResult:
|
|
77
|
-
"""
|
|
78
|
-
Execute the provided quantum circuit and return the measurement results.
|
|
79
|
-
|
|
80
|
-
This method should run the given circuit for the specified number of measurement shots and
|
|
81
|
-
produce a DigitalResult instance containing the raw measurement samples and any computed
|
|
82
|
-
probabilities.
|
|
83
|
-
|
|
84
|
-
Args:
|
|
85
|
-
circuit (Circuit): The quantum circuit to be executed.
|
|
86
|
-
nshots (int, optional): The number of measurement shots to perform. Defaults to 1000.
|
|
87
|
-
|
|
88
|
-
Returns:
|
|
89
|
-
DigitalResult: The result of executing the circuit, including measurement samples and probabilities.
|
|
90
|
-
"""
|