qilisdk 0.1.2__tar.gz → 0.1.3__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.2 → qilisdk-0.1.3}/.github/workflows/code_quality.yml +3 -0
  2. {qilisdk-0.1.2 → qilisdk-0.1.3}/.github/workflows/tests.yml +3 -0
  3. {qilisdk-0.1.2 → qilisdk-0.1.3}/CHANGELOG.md +29 -0
  4. {qilisdk-0.1.2 → qilisdk-0.1.3}/PKG-INFO +4 -4
  5. {qilisdk-0.1.2 → qilisdk-0.1.3}/pyproject.toml +4 -4
  6. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/analog/__init__.py +4 -4
  7. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/analog/quantum_objects.py +135 -88
  8. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/__init__.py +1 -1
  9. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/cuda/cuda_backend.py +3 -3
  10. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/qaas/qaas_backend.py +2 -1
  11. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/analog/test_quantum_objects.py +11 -11
  12. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/utils/test_serialization.py +2 -2
  13. {qilisdk-0.1.2 → qilisdk-0.1.3}/uv.lock +762 -1066
  14. {qilisdk-0.1.2 → qilisdk-0.1.3}/.github/workflows/publish.yml +0 -0
  15. {qilisdk-0.1.2 → qilisdk-0.1.3}/.gitignore +0 -0
  16. {qilisdk-0.1.2 → qilisdk-0.1.3}/.pre-commit-config.yaml +0 -0
  17. {qilisdk-0.1.2 → qilisdk-0.1.3}/.python-version +0 -0
  18. {qilisdk-0.1.2 → qilisdk-0.1.3}/LICENCE +0 -0
  19. {qilisdk-0.1.2 → qilisdk-0.1.3}/README.md +0 -0
  20. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/__init__.py +0 -0
  21. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/__init__.pyi +0 -0
  22. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/_optionals.py +0 -0
  23. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/analog/algorithms.py +0 -0
  24. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/analog/analog_backend.py +0 -0
  25. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/analog/analog_result.py +0 -0
  26. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/analog/exceptions.py +0 -0
  27. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/analog/hamiltonian.py +0 -0
  28. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/analog/schedule.py +0 -0
  29. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/common/__init__.py +0 -0
  30. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/common/algorithm.py +0 -0
  31. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/common/backend.py +0 -0
  32. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/common/model.py +0 -0
  33. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/common/optimizer.py +0 -0
  34. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/common/optimizer_result.py +0 -0
  35. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/common/result.py +0 -0
  36. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/digital/__init__.py +0 -0
  37. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/digital/ansatz.py +0 -0
  38. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/digital/circuit.py +0 -0
  39. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/digital/digital_algorithm.py +0 -0
  40. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/digital/digital_backend.py +0 -0
  41. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/digital/digital_result.py +0 -0
  42. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/digital/exceptions.py +0 -0
  43. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/digital/gates.py +0 -0
  44. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/digital/vqe.py +0 -0
  45. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/__init__.pyi +0 -0
  46. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/cuda/__init__.py +0 -0
  47. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/cuda/cuda_analog_result.py +0 -0
  48. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/cuda/cuda_digital_result.py +0 -0
  49. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/qaas/__init__.py +0 -0
  50. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/qaas/keyring.py +0 -0
  51. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/qaas/models.py +0 -0
  52. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/qaas/qaas_analog_result.py +0 -0
  53. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/qaas/qaas_digital_result.py +0 -0
  54. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/qaas/qaas_settings.py +0 -0
  55. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/qaas/qaas_time_evolution_result.py +0 -0
  56. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/extras/qaas/qaas_vqe_result.py +0 -0
  57. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/py.typed +0 -0
  58. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/utils/__init__.py +0 -0
  59. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/utils/openqasm2.py +0 -0
  60. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/utils/serialization.py +0 -0
  61. {qilisdk-0.1.2 → qilisdk-0.1.3}/src/qilisdk/yaml.py +0 -0
  62. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/__init__.py +0 -0
  63. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/analog/__init__.py +0 -0
  64. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/analog/test_analog_result.py +0 -0
  65. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/analog/test_hamiltionian.py +0 -0
  66. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/analog/test_schedule.py +0 -0
  67. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/analog/test_time_evolution.py +0 -0
  68. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/common/__init__.py +0 -0
  69. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/common/test_scipy_optimizer.py +0 -0
  70. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/digital/__init__.py +0 -0
  71. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/digital/test_ansatz.py +0 -0
  72. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/digital/test_circuit.py +0 -0
  73. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/digital/test_gates.py +0 -0
  74. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/digital/test_vqe.py +0 -0
  75. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/extras/__init__.py +0 -0
  76. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/extras/test_cuda_backend.py +0 -0
  77. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/test_placeholder.py +0 -0
  78. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/utils/__init__.py +0 -0
  79. {qilisdk-0.1.2 → qilisdk-0.1.3}/tests/utils/test_openqasm2.py +0 -0
@@ -43,6 +43,9 @@ jobs:
43
43
  - name: Install the project
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
+
46
49
  - name: Ruff
47
50
  run: uv run ruff check --output-format=github .
48
51
 
@@ -43,5 +43,8 @@ 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
+
46
49
  - name: Run tests
47
50
  run: uv run pytest tests
@@ -1,3 +1,30 @@
1
+ # Qilisdk 0.1.3 (2025-05-07)
2
+
3
+ ### Bugfixes
4
+
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.
6
+
7
+ ([PR #29](https://github.com/qilimanjaro-tech/qilisdk/pulls/29))
8
+
9
+ - Made several small changes to the `QuantumObject` class and logic. The two main changes are:
10
+ - The first concerns the trace norm which was incorrectly implemented before.
11
+ - The second concerns changing the modulus of 2 check for the Hilbert Space size, to a a pow(2) check.
12
+
13
+ ([PR #30](https://github.com/qilimanjaro-tech/qilisdk/pulls/30))
14
+
15
+ - Solved problems with ``Cudaq`` backend:
16
+ - Updated ``Cudaq`` to version 0.10.0 to fix issues encountered in version 0.9.1
17
+ - Migrated ``CudaBackend`` to use the new version of ``Cudaq``
18
+
19
+ ([PR #31](https://github.com/qilimanjaro-tech/qilisdk/pulls/31))
20
+
21
+ ### Misc
22
+
23
+ - Transformed hardcoded `PUBLIC URL` into an environment variable lookup that defaults to the hardcoded value
24
+
25
+ ([PR #32](https://github.com/qilimanjaro-tech/qilisdk/pulls/32))
26
+
27
+
1
28
  # Qilisdk 0.1.2 (2025-04-22)
2
29
 
3
30
  ### Misc
@@ -66,6 +93,7 @@
66
93
  ```
67
94
 
68
95
  ([PR #2](https://github.com/qilimanjaro-tech/qilisdk/pulls/2))
96
+
69
97
  - Introduces the `Hamiltonian` class as a central component for Pauli-based operator arithmetic, with a flyweight pattern for single-qubit operators. Internally stores terms as a dictionary mapping tuples of `PauliOperator` objects to complex coefficients.
70
98
 
71
99
  **Key Features**
@@ -130,6 +158,7 @@
130
158
  This release provides a robust framework for Pauli-operator arithmetic, scalar integration, and canonical simplification, forming a foundation for higher-level quantum analog functionality.
131
159
 
132
160
  ([PR #3](https://github.com/qilimanjaro-tech/qilisdk/pulls/3))
161
+
133
162
  - Added the `Optimizer` abstract base class and its concrete subclass `SciPyOptimizer`. The `SciPyOptimizer` class wraps `scipy.optimize.minimize` to optimize cost functions while supporting extra keyword arguments such as Jacobian, bounds, etc. This implementation provides a structured way to perform optimization and access optimal parameters via the `optimal_parameters` property.
134
163
 
135
164
  ### Code Example
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qilisdk
3
- Version: 0.1.2
3
+ Version: 0.1.3
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: cudaq==0.10.0; 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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qilisdk"
3
- version = "0.1.2"
3
+ version = "0.1.3"
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"}]
@@ -26,17 +26,17 @@ dependencies = [
26
26
  "numpy>=2.2.4",
27
27
  "ruamel-yaml>=0.18.10",
28
28
  "scipy>=1.15.1",
29
+ "pydantic>=2.10.6",
30
+ "pydantic-settings>=2.8.0",
29
31
  ]
30
32
 
31
33
  [project.optional-dependencies]
32
34
  cuda = [
33
- "cudaq==0.9.1",
35
+ "cudaq==0.10.0",
34
36
  ]
35
37
  qaas = [
36
38
  "httpx>=0.28.1",
37
39
  "keyring>=25.6.0",
38
- "pydantic>=2.10.6",
39
- "pydantic-settings>=2.8.0",
40
40
  ]
41
41
 
42
42
  [dependency-groups]
@@ -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
  ]
@@ -24,7 +24,11 @@ from scipy.sparse.linalg import norm as scipy_norm
24
24
  from qilisdk.yaml import yaml
25
25
 
26
26
  Complex = int | float | complex
27
- TWO = 2
27
+
28
+
29
+ ###############################################################################
30
+ # Main Class Definition
31
+ ###############################################################################
28
32
 
29
33
 
30
34
  @yaml.register_class
@@ -39,9 +43,9 @@ class QuantumObject:
39
43
 
40
44
  The internal data is stored as a SciPy CSR (Compressed Sparse Row) matrix for
41
45
  efficient arithmetic and manipulation. The expected shapes for the data are:
42
- - (2**N, 2**N) for operators or density matrices,
46
+ - (2**N, 2**N) for operators or density matrices (or scalars),
43
47
  - (2**N, 1) for ket states,
44
- - (1, 2**N) for bra states.
48
+ - (1, 2**N) or (2**N,) for bra states.
45
49
  """
46
50
 
47
51
  def __init__(self, data: np.ndarray | sparray | spmatrix) -> None:
@@ -50,10 +54,12 @@ class QuantumObject:
50
54
 
51
55
  Converts a NumPy array to a CSR matrix if needed and validates the shape of the input.
52
56
  The input must represent a valid quantum state or operator with appropriate dimensions.
57
+ Notice that 1D arrays of shape (2N,) are considered/transformed to bras with shape (1, 2N).
53
58
 
54
59
  Args:
55
60
  data (np.ndarray | sparray | spmatrix): A dense NumPy array or a SciPy sparse matrix
56
- representing a quantum state or operator.
61
+ representing a quantum state or operator. Should be of shape: (2**N, 2**N) for operators
62
+ (1, 2**N) for ket states, (2**N, 1) or (2**N,) for bra states, or (1, 1) for scalars.
57
63
 
58
64
  Raises:
59
65
  ValueError: If the input data is not a NumPy array or a SciPy sparse matrix,
@@ -65,21 +71,18 @@ class QuantumObject:
65
71
  self._data = data.tocsr()
66
72
  else:
67
73
  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:
74
+
75
+ # Valid shapes are operators = (2**N, 2**N) (scalars included), bra's = (1, 2**N) / (2**N,), or ket's =(2**N, 1):
76
+ valid_shape = self.is_operator() or self.is_ket() or self.is_bra()
77
+
78
+ if len(self._data.shape) != 2 or not valid_shape: # noqa: PLR2004
78
79
  raise ValueError(
79
80
  "Dimension of data is wrong. expected data to have shape similar to (2**N, 2**N), (1, 2**N), (2**N, 1)",
80
81
  f"but received {self._data.shape}",
81
82
  )
82
83
 
84
+ # ------------- Properties --------------
85
+
83
86
  @property
84
87
  def data(self) -> csr_matrix:
85
88
  """
@@ -126,7 +129,9 @@ class QuantumObject:
126
129
  """
127
130
  return self._data.shape
128
131
 
129
- def dag(self) -> QuantumObject:
132
+ # ----------- Matrix Logic Operations ------------
133
+
134
+ def adjoint(self) -> QuantumObject:
130
135
  """
131
136
  Compute the adjoint (conjugate transpose) of the QuantumObject.
132
137
 
@@ -141,9 +146,9 @@ class QuantumObject:
141
146
  Compute the partial trace over subsystems not in 'keep'.
142
147
 
143
148
  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.
149
+ 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.
147
152
 
148
153
  Args:
149
154
  dims (list[int]): A list specifying the dimensions of each subsystem.
@@ -162,26 +167,22 @@ class QuantumObject:
162
167
  if rho.shape != (total_dim, total_dim):
163
168
  raise ValueError("Dimension mismatch between provided dims and QuantumObject shape")
164
169
 
165
- n = len(dims)
166
170
  # Use letters from the ASCII alphabet (both cases) for einsum indices.
167
171
  # For each subsystem, assign two letters: one for the row index and one for the column index.
168
- row_letters = []
169
- col_letters = []
170
- out_row = [] # Letters that will remain in the output for the row part.
171
- out_col = [] # Letters for the column part.
172
+ row_letters, col_letters = [], []
173
+ out_row, out_col = [], [] # Letters that will remain in the output for the row part and for the column part.
172
174
  letters = iter(string.ascii_letters)
173
175
 
174
- for i in range(n):
176
+ for i in range(len(dims)):
175
177
  if i in keep:
176
- # For a subsystem we want to keep, use two different letters
177
- r = next(letters)
178
- c = next(letters)
178
+ # For a subsystem we want to keep, use two different letters (r, c)
179
+ r, c = next(letters), next(letters)
179
180
  row_letters.append(r)
180
181
  col_letters.append(c)
181
182
  out_row.append(r)
182
183
  out_col.append(c)
183
184
  else:
184
- # For subsystems to be traced out, assign the same letter so that those indices are summed.
185
+ # For subsystems to be traced out, assign the same letter (r, r) so that those indices are summed.
185
186
  r = next(letters)
186
187
  row_letters.append(r)
187
188
  col_letters.append(r)
@@ -209,26 +210,34 @@ class QuantumObject:
209
210
  """
210
211
  Compute the norm of the QuantumObject.
211
212
 
212
- For density matrices, the norm order can be specified. For state vectors,
213
- the norm is computed accordingly.
213
+ For density matrices, the norm order can be specified. For state vectors, the norm is computed accordingly.
214
214
 
215
215
  Args:
216
216
  order (int or {"fro", "tr"}, optional): The order of the norm.
217
- If the QuantumObject represents a density matrix and 'tr' is specified,
218
- the trace norm is returned. Defaults to 1.
217
+ Only applies if the QuantumObject represents a density matrix. Other than all the
218
+ orders accepted by scipy, it also accepts 'tr' for the trace norm. Defaults to 1.
219
+
220
+ Raises:
221
+ ValueError: If the QuantumObject is not a valid density matrix or state vector,
219
222
 
220
223
  Returns:
221
224
  float: The computed norm of the QuantumObject.
222
225
  """
223
226
  if self.is_scalar():
224
227
  return self.dense[0][0]
225
- if self.is_dm() or self.shape[0] == self.shape[1]:
228
+
229
+ if self.is_density_matrix() or self.shape[0] == self.shape[1]:
226
230
  if order == "tr":
227
- return self._data.trace()
231
+ return np.sum(np.abs(np.linalg.eigvalsh(self.dense)))
228
232
  return scipy_norm(self._data, ord=order)
233
+
229
234
  if self.is_bra():
230
235
  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]
236
+
237
+ if self.is_ket():
238
+ return np.sqrt(self._data.conj().T @ self._data).toarray()[0, 0]
239
+
240
+ raise ValueError("The QuantumObject is not a valid density matrix or state vector. Cannot compute the norm.")
232
241
 
233
242
  def unit(self, order: int | Literal["fro", "tr"] = "tr") -> QuantumObject:
234
243
  """
@@ -238,8 +247,8 @@ class QuantumObject:
238
247
 
239
248
  Args:
240
249
  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".
250
+ Only applies if the QuantumObject represents a density matrix. Other than all the
251
+ orders accepted by scipy, it also accepts 'tr' for the trace norm. Defaults to "tr".
243
252
 
244
253
  Raises:
245
254
  ValueError: If the norm of the QuantumObject is 0, making normalization impossible.
@@ -250,6 +259,7 @@ class QuantumObject:
250
259
  norm = self.norm(order=order)
251
260
  if norm == 0:
252
261
  raise ValueError("Cannot normalize a zero-norm Quantum Object")
262
+
253
263
  return QuantumObject(self._data / norm)
254
264
 
255
265
  def expm(self) -> QuantumObject:
@@ -261,6 +271,40 @@ class QuantumObject:
261
271
  """
262
272
  return QuantumObject(expm(self._data))
263
273
 
274
+ def to_density_matrix(self) -> QuantumObject:
275
+ """
276
+ Convert the QuantumObject to a density matrix.
277
+
278
+ If the QuantumObject represents a state vector (ket or bra), this method
279
+ calculates the corresponding density matrix by taking the outer product.
280
+ If the QuantumObject is already a density matrix, it is returned unchanged.
281
+ The resulting density matrix is normalized.
282
+
283
+ Raises:
284
+ ValueError: If the QuantumObject is a scalar, as a density matrix cannot be derived.
285
+
286
+ Returns:
287
+ QuantumObject: A new QuantumObject representing the density matrix.
288
+ """
289
+ if self.is_scalar():
290
+ raise ValueError("Cannot make a density matrix from scalar.")
291
+
292
+ if self.is_density_matrix():
293
+ return self
294
+
295
+ if self.is_bra():
296
+ return (self.adjoint() @ self).unit()
297
+
298
+ if self.is_ket():
299
+ return (self @ self.adjoint()).unit()
300
+
301
+ raise ValueError(
302
+ "Cannot make a density matrix from this QuantumObject. "
303
+ "It must be either a ket, a bra or already a density matrix."
304
+ )
305
+
306
+ # ----------- Checks for Matrices ------------
307
+
264
308
  def is_ket(self) -> bool:
265
309
  """
266
310
  Check if the QuantumObject represents a ket (column vector) state.
@@ -268,7 +312,7 @@ class QuantumObject:
268
312
  Returns:
269
313
  bool: True if the QuantumObject is a ket state, False otherwise.
270
314
  """
271
- return self.shape[0] % 2 == 0 and self.shape[1] == 1
315
+ return self.shape[1] == 1 and self.shape[0].bit_count() == 1
272
316
 
273
317
  def is_bra(self) -> bool:
274
318
  """
@@ -277,7 +321,7 @@ class QuantumObject:
277
321
  Returns:
278
322
  bool: True if the QuantumObject is a bra state, False otherwise.
279
323
  """
280
- return self.shape[1] % 2 == 0 and self.shape[0] == 1
324
+ return self.shape[0] == 1 and self.shape[1].bit_count() == 1
281
325
 
282
326
  def is_scalar(self) -> bool:
283
327
  """
@@ -286,14 +330,22 @@ class QuantumObject:
286
330
  Returns:
287
331
  bool: True if the QuantumObject is a scalar, False otherwise.
288
332
  """
289
- return self.data.shape == (1, 1)
333
+ return self.shape == (1, 1)
334
+
335
+ def is_operator(self) -> bool:
336
+ """
337
+ Check if the QuantumObject is an operator (square matrix).
338
+
339
+ Returns:
340
+ bool: True if the QuantumObject is an operator, False otherwise.
341
+ """
342
+ return self._data.shape[1] == self._data.shape[0] and self._data.shape[0].bit_count() == 1
290
343
 
291
- def is_dm(self, tol: float = 1e-8) -> bool:
344
+ def is_density_matrix(self, tol: float = 1e-8) -> bool:
292
345
  """
293
346
  Determine if the QuantumObject is a valid density matrix.
294
347
 
295
- A valid density matrix must be square, Hermitian, positive semi-definite,
296
- and have a trace equal to 1.
348
+ A valid density matrix must be square, Hermitian, positive semi-definite, and have a trace equal to 1.
297
349
 
298
350
  Args:
299
351
  tol (float, optional): The numerical tolerance for verifying Hermiticity,
@@ -303,11 +355,11 @@ class QuantumObject:
303
355
  bool: True if the QuantumObject is a valid density matrix, False otherwise.
304
356
  """
305
357
  # Check if rho is a square matrix
306
- if self.shape[0] != self.shape[1]:
358
+ if not self.is_operator():
307
359
  return False
308
360
 
309
361
  # 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):
362
+ if not self.is_hermitian(tol=tol):
311
363
  return False
312
364
 
313
365
  # Check if eigenvalues are non-negative (positive semi-definite)
@@ -318,7 +370,7 @@ class QuantumObject:
318
370
  # Check if the trace is 1
319
371
  return np.isclose(self._data.trace(), 1, atol=tol)
320
372
 
321
- def is_herm(self, tol: float = 1e-8) -> bool:
373
+ def is_hermitian(self, tol: float = 1e-8) -> bool:
322
374
  """
323
375
  Check if the QuantumObject is Hermitian.
324
376
 
@@ -331,39 +383,20 @@ class QuantumObject:
331
383
  """
332
384
  return np.allclose(self.dense, self._data.conj().T.toarray(), atol=tol)
333
385
 
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()
386
+ # ----------- Basic Arithmetic Operators ------------
356
387
 
357
388
  def __add__(self, other: QuantumObject | Complex) -> QuantumObject:
358
389
  if isinstance(other, QuantumObject):
359
390
  return QuantumObject(self._data + other._data)
360
391
  if isinstance(other, Complex) and other == 0:
361
392
  return self
393
+
362
394
  raise TypeError("Addition is only supported between QuantumState instances")
363
395
 
364
396
  def __sub__(self, other: QuantumObject) -> QuantumObject:
365
397
  if isinstance(other, QuantumObject):
366
398
  return QuantumObject(self._data - other._data)
399
+
367
400
  raise TypeError("Subtraction is only supported between QuantumState instances")
368
401
 
369
402
  def __mul__(self, other: QuantumObject | Complex) -> QuantumObject:
@@ -371,11 +404,13 @@ class QuantumObject:
371
404
  return QuantumObject(self._data * other)
372
405
  if isinstance(other, QuantumObject):
373
406
  return QuantumObject(self._data * other._data)
407
+
374
408
  raise TypeError("Unsupported multiplication type")
375
409
 
376
410
  def __matmul__(self, other: QuantumObject) -> QuantumObject:
377
411
  if isinstance(other, QuantumObject):
378
412
  return QuantumObject(self._data @ other._data)
413
+
379
414
  raise TypeError("Dot product is only supported between QuantumState instances")
380
415
 
381
416
  def __rmul__(self, other: QuantumObject | Complex) -> QuantumObject:
@@ -385,19 +420,23 @@ class QuantumObject:
385
420
  return f"{self.dense}"
386
421
 
387
422
 
388
- def basis(N: int, n: int) -> QuantumObject:
423
+ ###############################################################################
424
+ # Outside class Function Definitions
425
+ ###############################################################################
426
+
427
+
428
+ def basis_state(n: int, N: int) -> QuantumObject:
389
429
  """
390
- Generate the basis vector representation of a Fock state.
430
+ Generate the n'th basis vector representation, on a N-size Hilbert space (N=2**num_qubits).
391
431
 
392
- This function creates a column vector (ket) representing the Fock state |n⟩
393
- in a Hilbert space of dimension N.
432
+ This function creates a column vector (ket) representing the Fock state |n⟩ in a Hilbert space of dimension N.
394
433
 
395
434
  Args:
396
- N (int): The dimension of the Hilbert space (number of Fock states).
397
- n (int): The desired number state.
435
+ n (int): The desired number state (from 0 to N-1).
436
+ N (int): The dimension of the Hilbert space, has a value 2**num_qubits.
398
437
 
399
438
  Returns:
400
- QuantumObject: A QuantumObject representing the Fock state |n⟩.
439
+ QuantumObject: A QuantumObject representing the |n⟩'th basis state on a N-size Hilbert space (N=2**num_qubits).
401
440
  """
402
441
  return QuantumObject(csc_array(([1], ([n], [0])), shape=(N, 1)))
403
442
 
@@ -406,9 +445,8 @@ def ket(*state: int) -> QuantumObject:
406
445
  """
407
446
  Generate a ket state for a multi-qubit system.
408
447
 
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⟩.
448
+ This function creates a tensor product of individual qubit states (kets) based on the input values.
449
+ Each input must be either 0 or 1. For example, ket(0, 1) creates a two-qubit ket state |0⟩ ⊗ |1⟩.
412
450
 
413
451
  Args:
414
452
  *state (int): A sequence of integers representing the state of each qubit (0 or 1).
@@ -420,17 +458,17 @@ def ket(*state: int) -> QuantumObject:
420
458
  QuantumObject: A QuantumObject representing the multi-qubit ket state.
421
459
  """
422
460
  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])
461
+ raise ValueError(f"the state can only contain 1s or 0s. But received: {state}")
462
+
463
+ return tensor_prod([QuantumObject(csc_array(([1], ([s], [0])), shape=(2, 1))) for s in state])
425
464
 
426
465
 
427
466
  def bra(*state: int) -> QuantumObject:
428
467
  """
429
468
  Generate a bra state for a multi-qubit system.
430
469
 
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|.
470
+ This function creates a tensor product of individual qubit states (bras) based on the input values.
471
+ Each input must be either 0 or 1. For example, bra(0, 1) creates a two-qubit bra state ⟨0| ⊗ ⟨1|.
434
472
 
435
473
  Args:
436
474
  *state (int): A sequence of integers representing the state of each qubit (0 or 1).
@@ -442,11 +480,12 @@ def bra(*state: int) -> QuantumObject:
442
480
  QuantumObject: A QuantumObject representing the multi-qubit bra state.
443
481
  """
444
482
  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])
483
+ raise ValueError(f"the state can only contain 1s or 0s. But received:: {state}")
447
484
 
485
+ return tensor_prod([QuantumObject(csc_array(([1], ([0], [s])), shape=(1, 2))) for s in state])
448
486
 
449
- def tensor(operators: list[QuantumObject]) -> QuantumObject:
487
+
488
+ def tensor_prod(operators: list[QuantumObject]) -> QuantumObject:
450
489
  """
451
490
  Calculate the tensor product of a list of QuantumObjects.
452
491
 
@@ -463,10 +502,11 @@ def tensor(operators: list[QuantumObject]) -> QuantumObject:
463
502
  if len(operators) > 1:
464
503
  for i in range(1, len(operators)):
465
504
  out = kron(out, operators[i].data)
505
+
466
506
  return QuantumObject(out)
467
507
 
468
508
 
469
- def expect(operator: QuantumObject, state: QuantumObject) -> Complex:
509
+ def expect_val(operator: QuantumObject, state: QuantumObject) -> Complex:
470
510
  """
471
511
  Calculate the expectation value of an operator with respect to a quantum state.
472
512
 
@@ -477,10 +517,17 @@ def expect(operator: QuantumObject, state: QuantumObject) -> Complex:
477
517
  operator (QuantumObject): The quantum operator represented as a QuantumObject.
478
518
  state (QuantumObject): The quantum state or density matrix represented as a QuantumObject.
479
519
 
520
+ Raises:
521
+ ValueError: If the operator is not a square matrix.
522
+
480
523
  Returns:
481
524
  Complex: The expectation value. The result is guaranteed to be real if the operator
482
525
  is Hermitian, and may be complex otherwise.
483
526
  """
527
+ if not operator.is_operator():
528
+ raise ValueError("The operator must be a square matrix.")
529
+
484
530
  if state.data.shape[1] == state.data.shape[0]:
485
531
  return (operator @ state).dense.trace()
486
- return (state.dag() @ operator @ state).dense[0, 0]
532
+
533
+ return (state.adjoint() @ operator @ state).dense[0, 0]
@@ -25,7 +25,7 @@ OPTIONAL_FEATURES: list[OptionalFeature] = [
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
  ]
@@ -277,7 +277,7 @@ class CudaBackend(DigitalBackend, AnalogBackend):
277
277
 
278
278
  return CudaAnalogResult(
279
279
  final_expected_values=np.array(
280
- [exp_val.expectation() for exp_val in evolution_result.final_expectation_values()]
280
+ [exp_val.expectation() for exp_val in evolution_result.final_expectation_values()[0]]
281
281
  ),
282
282
  expected_values=(
283
283
  np.array(
@@ -287,12 +287,12 @@ class CudaBackend(DigitalBackend, AnalogBackend):
287
287
  else None
288
288
  ),
289
289
  final_state=(
290
- QuantumObject(np.array(evolution_result.final_state())).dag()
290
+ QuantumObject(np.array(evolution_result.final_state())).adjoint()
291
291
  if evolution_result.final_state() is not None
292
292
  else None
293
293
  ),
294
294
  intermediate_states=(
295
- [QuantumObject(np.array(state)).dag() for state in evolution_result.intermediate_states()]
295
+ [QuantumObject(np.array(state)).adjoint() for state in evolution_result.intermediate_states()]
296
296
  if evolution_result.intermediate_states() is not None
297
297
  else None
298
298
  ),
@@ -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
  """