Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typing type deprecation #50

Merged
merged 5 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
413 changes: 413 additions & 0 deletions notebooks/Training Ansatzes.ipynb

Large diffs are not rendered by default.

11 changes: 5 additions & 6 deletions quick/backend/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import numpy as np
from numpy.typing import NDArray
from types import NotImplementedType
from typing import Type

from quick.circuit import Circuit

Expand Down Expand Up @@ -66,7 +65,7 @@ def __init__(
if device not in ["CPU", "GPU"]:
raise ValueError(f"Invalid device: {device}. Must be either 'CPU' or 'GPU'.")
self.device = device
self._qc_framework: Type[Circuit]
self._qc_framework: type[Circuit]

@staticmethod
def backendmethod(method):
Expand Down Expand Up @@ -316,7 +315,7 @@ class NoisyBackend(Backend, ABC):
`device` : str
The device to use for simulating the circuit.
This can be either "CPU", or "GPU".
`_qc_framework` : Type[quick.circuit.Circuit]
`_qc_framework` : type[quick.circuit.Circuit]
The quantum computing framework to use.
`noisy` : bool
Whether the simulation is noisy or not.
Expand Down Expand Up @@ -348,7 +347,7 @@ def __init__(

self.noisy = self.single_qubit_error > 0.0 or self.two_qubit_error > 0.0

self._qc_framework: Type[Circuit]
self._qc_framework: type[Circuit]


class FakeBackend(Backend, ABC):
Expand All @@ -367,7 +366,7 @@ class FakeBackend(Backend, ABC):
`device` : str
The device to use for simulating the circuit.
This can be either "CPU", or "GPU".
`_qc_framework` : Type[quick.circuit.Circuit]
`_qc_framework` : type[quick.circuit.Circuit]
The quantum computing framework to use.
`_backend_name` : str
The name of the backend to use (usually the name of the backend being emulated).
Expand All @@ -386,7 +385,7 @@ def __init__(
""" Initialize a `quick.backend.FakeBackend` instance.
"""
super().__init__(device=device)
self._qc_framework: Type[Circuit]
self._qc_framework: type[Circuit]
self._backend_name: str
self._max_num_qubits: int

Expand Down
2 changes: 2 additions & 0 deletions quick/circuit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"dag",
"gate_matrix",
"from_framework",
"Ansatz",
"Circuit",
"CirqCircuit",
"PennylaneCircuit",
Expand All @@ -32,5 +33,6 @@
from quick.circuit.pennylanecircuit import PennylaneCircuit
from quick.circuit.quimbcircuit import QuimbCircuit
from quick.circuit.tketcircuit import TKETCircuit
from quick.circuit.ansatz import Ansatz
import quick.circuit.from_framework as from_framework
import quick.circuit.dag as dag
255 changes: 255 additions & 0 deletions quick/circuit/ansatz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
# Copyright 2023-2025 Qualition Computing LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://github.com/Qualition/quick/blob/main/LICENSE
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

""" Ansatz for variational quantum circuits.
"""

from __future__ import annotations

__all__ = ["Ansatz"]

from quick.circuit import Circuit

# Type hint for nested lists of floats
Params = list[list[float] | float] | list[float]


class Ansatz:
""" `quick.circuit.Ansatz` class for parameterized quantum circuits
which can be used as variational ansatz for quantum machine learning
models.

Notes
-----
The `Ansatz` class is a wrapper around the `quick.circuit.Circuit` class
which provides a more user-friendly interface for parameterized quantum
circuits, and means for variationally updating them.

Use-cases of variational quantum circuits include:
- Supervised QML
- Approximate Quantum Compilation (AQC)
- Quantum Approximate Optimization Algorithm (QAOA)
- Variational Quantum Eigensolver (VQE)

This class is meant to provide a simple interface for specifically updating
the rotation angles of a parameterized quantum circuit. Users can use the class
in the following manner:

```python
from quick.circuit import Ansatz, QiskitCircuit
from quick.circuit.circuit_utils import reshape, flatten
from quick.random import generate_random_state
from scipy.optimize import minimize

# Create a parameterized quantum circuit with
# random state initialization
circuit = QiskitCircuit(2)
circuit.initialize(generate_random_state(2), [0, 1])

# Define a target state
target_state = generate_random_state(2)

# Create an ansatz object
ansatz = Ansatz(circuit)

# Define the cost function
def cost_function(thetas, shape):
ansatz.thetas = reshape(thetas, shape)
return 1 - target_state.conj().T @ ansatz.ansatz.state

initial_thetas, shape = flatten(ansatz.thetas)

# Optimize the ansatz circuit
result = minimize(cost_function, initial_thetas, args=(shape), method="BFGS")
```

This example demonstrates how one can use the Ansatz class to perform approximate
state preparation by variationally updating the circuit where the cost function is
simply the fidelity between the target state and the state prepared by the ansatz.

For simplicity, we do not bother with defining specific optimization interfaces so
users can use whatever means of optimization they prefer as long as they update the
parameters of the ansatz circuit per iteration. Our recommendation is to use the
`scipy.optimize.minimize` function which provides a minimalistic and elegant interface
to access a variety of optimization algorithms. It should however be noted that if
the use-case requires more sophisticated optimization techniques depending on the
cost landscape, users are urged to implement their own optimization routines and/or
use more advanced optimization libraries.

Lastly, `flatten` and `reshape` functions are used to convert the parameters of the
ansatz circuit to a 1D array and vice versa. This is necessary because the optimization
routine expects a 1D array of parameters to optimize over, while the ansatz circuit
requires the original shape to update its definition. Feel free to use these functions
or implement your own as needed.

You may also make a PR to exclude the need for `flatten` and `reshape` by providing a
more elegant way of handling the parameter updates.

Parameters
----------
`ansatz` : quick.circuit.Circuit
The parameterized quantum circuit to be used as the ansatz.
`ignore_global_phase` : bool, optional, default=True
Whether to ignore the global phase when setting the parameters
of the ansatz.

Attributes
----------
`ansatz` : quick.circuit.Circuit
The parameterized quantum circuit to be used as the ansatz.
`thetas` : numpy.ndarray
The parameters of the ansatz circuit.
`num_params` : int
The number of parameters in the ansatz circuit.
`num_parameterized_gates` : int
The number of parameterized gates in the ansatz circuit.

Raises
------
TypeError
- If the `ansatz` is not an instance of the `quick.circuit.Circuit`.

Usage
-----
>>> from quick.circuit import QiskitCircuit
>>> circuit = QiskitCircuit(2)
>>> circuit.H(0)
>>> circuit.CX(0, 1)
>>> ansatz = Ansatz(circuit)
"""
def __init__(
self,
ansatz: Circuit,
ignore_global_phase: bool = True
) -> None:
""" Initialize a `quick.circuit.Ansatz` instance.
"""
if not isinstance(ansatz, Circuit):
raise TypeError(
"The `ansatz` must be an instance of the `quick.circuit.Circuit`. "
f"Received {type(ansatz)} instead."
)

self.ansatz = ansatz
self.ignore_global_phase = ignore_global_phase

if not self.is_parameterized:
raise ValueError("The `ansatz` must contain parameterized gates.")

@property
def thetas(self) -> Params:
""" The parameters of the ansatz circuit.

Returns
-------
Params
The parameters of the ansatz circuit.

Usage
-----
>>> ansatz.thetas
"""
thetas: Params = []

for gate in self.ansatz.circuit_log:
if "angles" in gate:
thetas.append(gate["angles"])
elif "angle" in gate:
if gate["gate"] == "GlobalPhase" and self.ignore_global_phase:
continue
thetas.append(gate["angle"])

return thetas

@thetas.setter
def thetas(
self,
thetas: Params
) -> None:
""" Set the parameters of the ansatz circuit.

Parameters
----------
`thetas` : Params
The parameters to set for the ansatz circuit.

Usage
-----
>>> # Set the parameters of the ansatz circuit
... # with one U3 gate
>>> ansatz.thetas = np.array([0.1, 0.2, 0.3])
"""
for gate in self.ansatz.circuit_log:
if "angles" in gate:
gate["angles"] = thetas.pop(0)
elif "angle" in gate:
if gate["gate"] == "GlobalPhase" and self.ignore_global_phase:
continue
gate["angle"] = thetas.pop(0)

self.ansatz.update()

@property
def num_params(self) -> int:
""" The number of parameters in the ansatz circuit.

Returns
-------
int
The number of parameters in the ansatz circuit.

Usage
-----
>>> ansatz.num_params
"""
flattened_thetas: list[float] = []

for theta in self.thetas:
if isinstance(theta, list):
flattened_thetas.extend(theta)
else:
flattened_thetas.append(theta)

return len(flattened_thetas)

@property
def num_parameterized_gates(self) -> int:
""" The number of parameterized gates in the ansatz circuit.

Returns
-------
int
The number of parameterized gates in the ansatz circuit.

Usage
-----
>>> ansatz.num_parameterized_gates
"""
return len(self.thetas)

@property
def is_parameterized(self) -> bool:
""" Check if the ansatz circuit contains parameterized gates.

Returns
-------
bool
True if the ansatz circuit contains parameterized gates,
False otherwise.

Usage
-----
>>> ansatz.is_parameterized
"""
return len(self.thetas) > 0
16 changes: 8 additions & 8 deletions quick/circuit/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from numpy.typing import NDArray
from types import NotImplementedType
from typing import (
Any, Callable, Literal, overload, SupportsFloat, SupportsIndex, Type, TYPE_CHECKING
Any, Callable, Literal, overload, SupportsFloat, SupportsIndex, TYPE_CHECKING
)

import qiskit # type: ignore
Expand Down Expand Up @@ -5797,7 +5797,7 @@ def change_mapping(

def convert(
self,
circuit_framework: Type[Circuit]
circuit_framework: type[Circuit]
) -> Circuit:
""" Convert the circuit to another circuit framework.

Expand Down Expand Up @@ -5973,7 +5973,7 @@ def to_qasm(
@staticmethod
def from_cirq(
cirq_circuit: cirq.Circuit,
output_framework: Type[Circuit]
output_framework: type[Circuit]
) -> Circuit:
""" Create a `quick.Circuit` from a `cirq.Circuit`.

Expand Down Expand Up @@ -6005,7 +6005,7 @@ def from_cirq(
@staticmethod
def from_pennylane(
pennylane_circuit: qml.QNode,
output_framework: Type[Circuit]
output_framework: type[Circuit]
) -> Circuit:
""" Create a `quick.circuit.Circuit` from a `qml.QNode`.

Expand Down Expand Up @@ -6043,7 +6043,7 @@ def from_pennylane(
@staticmethod
def from_qiskit(
qiskit_circuit: qiskit.QuantumCircuit,
output_framework: Type[Circuit]
output_framework: type[Circuit]
) -> Circuit:
""" Create a `quick.circuit.Circuit` from a `qiskit.QuantumCircuit`.

Expand Down Expand Up @@ -6075,7 +6075,7 @@ def from_qiskit(
@staticmethod
def from_tket(
tket_circuit: pytket.Circuit,
output_framework: Type[Circuit]
output_framework: type[Circuit]
) -> Circuit:
""" Create a `quick.circuit.Circuit` from a `tket.Circuit`.

Expand Down Expand Up @@ -6107,7 +6107,7 @@ def from_tket(
@staticmethod
def from_qasm(
qasm: str,
output_framework: Type[Circuit]
output_framework: type[Circuit]
) -> Circuit:
""" Create a `quick.circuit.Circuit` from a QASM string.

Expand Down Expand Up @@ -6146,7 +6146,7 @@ def from_qasm(
@staticmethod
def from_quimb(
quimb_circuit: qtn.Circuit,
output_framework: Type[Circuit]
output_framework: type[Circuit]
) -> Circuit:
""" Create a `quick.circuit.Circuit` from a `qtn.Circuit`.

Expand Down
Loading