diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index edf83de8..00000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,33 +0,0 @@ -# Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment include: - -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -- The use of sexualized language or imagery and unwelcome sexual attention or advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 91129226..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,105 +0,0 @@ -# How to Contribute - -We're grateful for your interest in participating in pyqtorch! Please follow our guidelines to ensure a smooth contribution process. - -## Reporting an Issue or Proposing a Feature - -Your course of action will depend on your objective, but generally, you should start by creating an issue. If you've discovered a bug or have a feature you'd like to see added to **PyQ**, feel free to create an issue on [pyqtorch's GitHub issue tracker](https://github.com/pasqal-io/pyqtorch/issues). Here are some steps to take: - -1. Quickly search the existing issues using relevant keywords to ensure your issue hasn't been addressed already. -2. If your issue is not listed, create a new one. Try to be as detailed and clear as possible in your description. - -- If you're merely suggesting an improvement or reporting a bug, that's already excellent! We thank you for it. Your issue will be listed and, hopefully, addressed at some point. -- However, if you're willing to be the one solving the issue, that would be even better! In such instances, you would proceed by preparing a [Pull Request](#submitting-a-pull-request). - -## Submitting a Pull Request - -We're excited that you're eager to contribute to pyqtorch! To contribute, fork the `main` branch of pyqtorch repository and once you are satisfied with your feature and all the tests pass create a [Pull Request](https://github.com/pasqal-io/pyqtorch/pulls). - -Here's the process for making a contribution: - -Click the "Fork" button at the upper right corner of the [repo page](https://github.com/pasqal-io/pyqtorch) to create a new GitHub repo at `https://github.com/USERNAME/pyqtorch`, where `USERNAME` is your GitHub ID. Then, `cd` into the directory where you want to place your new fork and clone it: - -```shell -git clone https://github.com/USERNAME/pyqtorch.git -``` - -Next, navigate to your new pyqtorch fork directory and mark the main pyqtorch repository as the `upstream`: - -```shell -git remote add upstream https://github.com/pasqal-io/pyqtorch.git -``` - -## Setting up your development environment - -We recommended to use `hatch` for managing environments: - -To develop within pyqtorch, use: -```shell -pip install hatch -hatch -v shell -``` - -To run pyqtorch tests, use: - -```shell -hatch -e tests run test -``` - -If you don't want to use `hatch`, you can use the environment manager of your -choice (e.g. Conda) and execute the following: - -```shell -pip install pytest - -pip install -e . -pytest -``` - -### Useful Things for your workflow: Linting and Testing - -Use `pre-commit` hooks to make sure that the code is properly linted before pushing a new commit. Make sure that the unit tests and type checks are passing since the merge request will not be accepted if the automatic CI/CD pipeline do not pass. - -Without `hatch`: - -```shell -pip install pytest - -pip install -e . -pip install pre-commit -pre-commit install -pre-commit run --all-files -pytest -``` - -And with `hatch`: - -```shell -hatch -e tests run pre-commit run --all-files -hatch -e tests run test -``` - -Make sure your docs build too! - -With `hatch`: - -```shell -hatch -e docs run mkdocs build --clean --strict -``` - -Without `hatch`, `pip` install those libraries first: -"mkdocs", -"mkdocs-material", -"mkdocstrings", -"mkdocstrings-python", -"mkdocs-section-index", -"mkdocs-jupyter", -"mkdocs-exclude", -"markdown-exec" - - -And then: - -```shell - mkdocs build --clean --strict -``` diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md deleted file mode 120000 index 0400d574..00000000 --- a/docs/CODE_OF_CONDUCT.md +++ /dev/null @@ -1 +0,0 @@ -../CODE_OF_CONDUCT.md \ No newline at end of file diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..edf83de8 --- /dev/null +++ b/docs/CODE_OF_CONDUCT.md @@ -0,0 +1,33 @@ +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md deleted file mode 120000 index 44fcc634..00000000 --- a/docs/CONTRIBUTING.md +++ /dev/null @@ -1 +0,0 @@ -../CONTRIBUTING.md \ No newline at end of file diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 00000000..91129226 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,105 @@ +# How to Contribute + +We're grateful for your interest in participating in pyqtorch! Please follow our guidelines to ensure a smooth contribution process. + +## Reporting an Issue or Proposing a Feature + +Your course of action will depend on your objective, but generally, you should start by creating an issue. If you've discovered a bug or have a feature you'd like to see added to **PyQ**, feel free to create an issue on [pyqtorch's GitHub issue tracker](https://github.com/pasqal-io/pyqtorch/issues). Here are some steps to take: + +1. Quickly search the existing issues using relevant keywords to ensure your issue hasn't been addressed already. +2. If your issue is not listed, create a new one. Try to be as detailed and clear as possible in your description. + +- If you're merely suggesting an improvement or reporting a bug, that's already excellent! We thank you for it. Your issue will be listed and, hopefully, addressed at some point. +- However, if you're willing to be the one solving the issue, that would be even better! In such instances, you would proceed by preparing a [Pull Request](#submitting-a-pull-request). + +## Submitting a Pull Request + +We're excited that you're eager to contribute to pyqtorch! To contribute, fork the `main` branch of pyqtorch repository and once you are satisfied with your feature and all the tests pass create a [Pull Request](https://github.com/pasqal-io/pyqtorch/pulls). + +Here's the process for making a contribution: + +Click the "Fork" button at the upper right corner of the [repo page](https://github.com/pasqal-io/pyqtorch) to create a new GitHub repo at `https://github.com/USERNAME/pyqtorch`, where `USERNAME` is your GitHub ID. Then, `cd` into the directory where you want to place your new fork and clone it: + +```shell +git clone https://github.com/USERNAME/pyqtorch.git +``` + +Next, navigate to your new pyqtorch fork directory and mark the main pyqtorch repository as the `upstream`: + +```shell +git remote add upstream https://github.com/pasqal-io/pyqtorch.git +``` + +## Setting up your development environment + +We recommended to use `hatch` for managing environments: + +To develop within pyqtorch, use: +```shell +pip install hatch +hatch -v shell +``` + +To run pyqtorch tests, use: + +```shell +hatch -e tests run test +``` + +If you don't want to use `hatch`, you can use the environment manager of your +choice (e.g. Conda) and execute the following: + +```shell +pip install pytest + +pip install -e . +pytest +``` + +### Useful Things for your workflow: Linting and Testing + +Use `pre-commit` hooks to make sure that the code is properly linted before pushing a new commit. Make sure that the unit tests and type checks are passing since the merge request will not be accepted if the automatic CI/CD pipeline do not pass. + +Without `hatch`: + +```shell +pip install pytest + +pip install -e . +pip install pre-commit +pre-commit install +pre-commit run --all-files +pytest +``` + +And with `hatch`: + +```shell +hatch -e tests run pre-commit run --all-files +hatch -e tests run test +``` + +Make sure your docs build too! + +With `hatch`: + +```shell +hatch -e docs run mkdocs build --clean --strict +``` + +Without `hatch`, `pip` install those libraries first: +"mkdocs", +"mkdocs-material", +"mkdocstrings", +"mkdocstrings-python", +"mkdocs-section-index", +"mkdocs-jupyter", +"mkdocs-exclude", +"markdown-exec" + + +And then: + +```shell + mkdocs build --clean --strict +``` diff --git a/docs/docsutils.py b/docs/docsutils.py deleted file mode 100644 index c78fab7b..00000000 --- a/docs/docsutils.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations - -from io import StringIO - -from matplotlib.figure import Figure - - -def fig_to_html(fig: Figure) -> str: - buffer = StringIO() - fig.savefig(buffer, format="svg") - return buffer.getvalue() diff --git a/docs/index.md b/docs/index.md index b68a85a9..a4e1942e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,7 +11,7 @@ choice and install it normally with `pip`: pip install pyqtorch ``` -## Digital +## Digital Operations `pyqtorch` implements a large selection of both primitive and parametric single to n-qubit, digital quantum gates. @@ -60,7 +60,7 @@ rx = RX(0) new_state = rx(state, torch.rand(1)) ``` -## Analog +## Analog Operations `pyqtorch` also contains a `analog` module which allows for global state evolution through the `HamiltonianEvolution` class. Note that it only accepts a `torch.Tensor` as a generator which is expected to be an Hermitian matrix. To build arbitrary Pauli hamiltonians, we recommend using [Qadence](https://pasqal-io.github.io/qadence/v1.0.3/tutorials/hamiltonians/). @@ -116,142 +116,124 @@ def _fwd(phi: torch.Tensor) -> torch.Tensor: assert torch.autograd.gradcheck(_fwd, theta) ``` +## Efficient Computation of Derivatives + +`pyqtorch` also offers a [adjoint differentiation mode](https://arxiv.org/abs/2009.02823) which can be used through the `expectation` method of `QuantumCircuit`. + +```python exec="on" source="material-block" +import pyqtorch as pyq +import torch +from pyqtorch.utils import DiffMode + +n_qubits = 3 +batch_size = 1 + +rx = pyq.RX(0, param_name="x") +cnot = pyq.CNOT(1, 2) +ops = [rx, cnot] +n_qubits = 3 +circ = pyq.QuantumCircuit(n_qubits, ops) + +obs = pyq.QuantumCircuit(n_qubits, [pyq.Z(0)]) +state = pyq.zero_state(n_qubits) + +values_ad = {"x": torch.tensor([torch.pi / 2], requires_grad=True)} +values_adjoint = {"x": torch.tensor([torch.pi / 2], requires_grad=True)} +exp_ad = pyq.expectation(circ, state, values_ad, obs, DiffMode.AD) +exp_adjoint = pyq.expectation(circ, state, values_adjoint, obs, DiffMode.ADJOINT) + +dfdx_ad = torch.autograd.grad(exp_ad, tuple(values_ad.values()), torch.ones_like(exp_ad)) + +dfdx_adjoint = torch.autograd.grad( + exp_adjoint, tuple(values_adjoint.values()), torch.ones_like(exp_adjoint) +) + +assert len(dfdx_ad) == len(dfdx_adjoint) +for i in range(len(dfdx_ad)): + assert torch.allclose(dfdx_ad[i], dfdx_adjoint[i]) +``` + ## Fitting a function -Let's have a look at how the `QuantumCircuit` can be used to implement a Quantum Neural Network and fit a simple function. +Let's have a look at how the `QuantumCircuit` can be used to fit a function. ```python exec="on" source="material-block" html="1" from __future__ import annotations +from operator import add +from functools import reduce import torch import pyqtorch as pyq +from pyqtorch.utils import DiffMode from pyqtorch.parametric import Parametric -import numpy as np import matplotlib.pyplot as plt -import torch.nn.functional as F +from torch.nn.functional import mse_loss -def target_function(x: torch.Tensor, degree: int = 3) -> torch.Tensor: - result = 0 - for i in range(degree): - result += torch.cos(i*x) + torch.sin(i*x) - return .05 * result +# We can train on GPU if available +DEVICE = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu') -x = torch.tensor(np.linspace(0, 10, 100)) -y = target_function(x, 5) +# Target function and some training data +fn = lambda x, degree: .05 * reduce(add, (torch.cos(i*x) + torch.sin(i*x) for i in range(degree)), 0) +x = torch.linspace(0, 10, 100) +y = fn(x, 5) -def HEA(n_qubits: int, n_layers: int, param_name: str) -> pyq.QuantumCircuit: +def hea(n_qubits: int, n_layers: int, param_name: str) -> list: ops = [] for l in range(n_layers): ops += [pyq.RX(i, f'{param_name}_0_{l}_{i}') for i in range(n_qubits)] ops += [pyq.RY(i, f'{param_name}_1_{l}_{i}') for i in range(n_qubits)] ops += [pyq.RX(i, f'{param_name}_2_{l}_{i}') for i in range(n_qubits)] ops += [pyq.CNOT(i % n_qubits, (i+1) % n_qubits) for i in range(n_qubits)] - return pyq.QuantumCircuit(n_qubits, ops) - - -class QNN(pyq.QuantumCircuit): - - def __init__(self, n_qubits, n_layers): - super().__init__(n_qubits, []) - self.n_qubits = n_qubits - self.feature_map = pyq.QuantumCircuit(n_qubits, [pyq.RX(i, f'phi') for i in range(n_qubits)]) - self.hea = HEA(n_qubits, n_layers, 'theta') - self.observable = pyq.Z(0) - self.param_dict = torch.nn.ParameterDict({op.param_name: torch.rand(1, requires_grad=True) for op in self.hea.operations if isinstance(op, Parametric)}) - def forward(self, phi: torch.Tensor): - batch_size = len(phi) - state = self.feature_map.init_state(batch_size) - state = self.feature_map(state, {'phi': phi}) - state = self.hea(state, self.param_dict) - new_state = self.observable(state, self.param_dict) - return pyq.overlap(state, new_state) + return ops n_qubits = 5 n_layers = 3 -model = QNN(n_qubits, n_layers) +diff_mode = DiffMode.ADJOINT +# Lets define a feature map to encode our 'x' values +feature_map = [pyq.RX(i, f'x') for i in range(n_qubits)] +# To fit the function, we define a hardware-efficient ansatz with tunable parameters +ansatz = hea(n_qubits, n_layers, 'theta') +observable = pyq.QuantumCircuit(n_qubits, [pyq.Z(0)]) +param_dict = torch.nn.ParameterDict({op.param_name: torch.rand(1, requires_grad=True) for op in ansatz if isinstance(op, Parametric)}) +circ = pyq.QuantumCircuit(n_qubits, feature_map + ansatz) +# Lets move all necessary components to the DEVICE +circ = circ.to(DEVICE) +observable = observable.to(DEVICE) +param_dict = param_dict.to(DEVICE) +x, y = x.to(DEVICE), y.to(DEVICE) +state = circ.init_state() + +def exp_fn(param_dict: dict[str, torch.Tensor], inputs: dict[str, torch.Tensor]) -> torch.Tensor: + return pyq.expectation(circ, state, {**param_dict,**inputs}, observable, diff_mode) with torch.no_grad(): - y_init = model(x) + y_init = exp_fn(param_dict, {'x': x}) -optimizer = torch.optim.Adam(model.parameters(), lr=.01) -epochs = 200 +# We need to set 'foreach' False since Adam doesnt support float64 on CUDA devices +optimizer = torch.optim.Adam(param_dict.values(), lr=.01, foreach=False) +epochs = 300 for epoch in range(epochs): optimizer.zero_grad() - y_pred = model(x) - loss = F.mse_loss(y, y_pred) + y_pred = exp_fn(param_dict, {'x': x}) + loss = mse_loss(y, y_pred) loss.backward() optimizer.step() - with torch.no_grad(): - y_final = model(x) + y_final = exp_fn(param_dict, {'x': x}) plt.plot(x.numpy(), y.numpy(), label="truth") plt.plot(x.numpy(), y_init.numpy(), label="initial") plt.plot(x.numpy(), y_final.numpy(), "--", label="final", linewidth=3) plt.legend() -from docs import docsutils # markdown-exec: hide -print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide -``` - -## First Order Adjoint Differentiation - -`pyqtorch` also offers a [adjoint differentiation mode](https://arxiv.org/abs/2009.02823) which can be used through the `expectation` method of `QuantumCircuit`. - -```python exec="on" source="material-block" -import pyqtorch as pyq -import torch -from pyqtorch.utils import DiffMode - -n_qubits = 3 -batch_size = 1 -diff_mode = DiffMode.ADJOINT - - -rx = pyq.RX(0, param_name="theta_0") -cry = pyq.CPHASE(0, 1, param_name="theta_1") -rz = pyq.RZ(2, param_name="theta_2") -cnot = pyq.CNOT(1, 2) -ops = [rx, cry, rz, cnot] -n_qubits = 3 -adjoint_circ = pyq.QuantumCircuit(n_qubits, ops, DiffMode.ADJOINT) -ad_circ = pyq.QuantumCircuit(n_qubits, ops, DiffMode.AD) -obs = pyq.QuantumCircuit(n_qubits, [pyq.Z(0)]) - -theta_0_value = torch.pi / 2 -theta_1_value = torch.pi -theta_2_value = torch.pi / 4 - -state = pyq.zero_state(n_qubits) - -theta_0_ad = torch.tensor([theta_0_value], requires_grad=True) -thetas_0_adjoint = torch.tensor([theta_0_value], requires_grad=True) - -theta_1_ad = torch.tensor([theta_1_value], requires_grad=True) -thetas_1_adjoint = torch.tensor([theta_1_value], requires_grad=True) - -theta_2_ad = torch.tensor([theta_2_value], requires_grad=True) -thetas_2_adjoint = torch.tensor([theta_2_value], requires_grad=True) - -values_ad = {"theta_0": theta_0_ad, "theta_1": theta_1_ad, "theta_2": theta_2_ad} -values_adjoint = { - "theta_0": thetas_0_adjoint, - "theta_1": thetas_1_adjoint, - "theta_2": thetas_2_adjoint, -} -exp_ad = ad_circ.expectation(values_ad, obs, state) -exp_adjoint = adjoint_circ.expectation(values_adjoint, obs, state) - -grad_ad = torch.autograd.grad(exp_ad, tuple(values_ad.values()), torch.ones_like(exp_ad)) - -grad_adjoint = torch.autograd.grad( - exp_adjoint, tuple(values_adjoint.values()), torch.ones_like(exp_adjoint) -) - -assert len(grad_ad) == len(grad_adjoint) -for i in range(len(grad_ad)): - assert torch.allclose(grad_ad[i], grad_adjoint[i]) +from io import StringIO # markdown-exec: hide +from matplotlib.figure import Figure # markdown-exec: hide +def fig_to_html(fig: Figure) -> str: # markdown-exec: hide + buffer = StringIO() # markdown-exec: hide + fig.savefig(buffer, format="svg") # markdown-exec: hide + return buffer.getvalue() # markdown-exec: hide +print(fig_to_html(plt.gcf())) # markdown-exec: hide ``` diff --git a/pyproject.toml b/pyproject.toml index dc4639a1..46b72d25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ authors = [ ] requires-python = ">=3.8,<3.12" license = {text = "Apache 2.0"} -version = "1.0.3" +version = "1.0.4" classifiers=[ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", @@ -28,14 +28,10 @@ classifiers=[ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] -dependencies = [ - "numpy", - "torch", -] +dependencies = ["torch", "numpy"] [project.optional-dependencies] -dev = ["black", "pytest", "pytest-xdist", "pytest-cov", "flake8", "mypy", "pre-commit", "ruff", "nbconvert", - "ipykernel", "matplotlib"] +dev = ["black", "pytest", "pytest-xdist", "pytest-cov", "flake8", "mypy", "pre-commit", "ruff", "nbconvert", "matplotlib"] [tool.hatch.envs.tests] features = [ diff --git a/pyqtorch/__init__.py b/pyqtorch/__init__.py index 76294179..6b23b303 100644 --- a/pyqtorch/__init__.py +++ b/pyqtorch/__init__.py @@ -13,7 +13,7 @@ from .analog import HamiltonianEvolution from .apply import apply_operator -from .circuit import QuantumCircuit +from .circuit import QuantumCircuit, expectation from .parametric import CPHASE, CRX, CRY, CRZ, PHASE, RX, RY, RZ, U from .primitive import ( CNOT, diff --git a/pyqtorch/abstract.py b/pyqtorch/abstract.py deleted file mode 100644 index f5ff00ad..00000000 --- a/pyqtorch/abstract.py +++ /dev/null @@ -1,57 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import Any, Tuple - -import torch -from torch.nn import Module - -import pyqtorch as pyq -from pyqtorch.utils import Operator, State - - -class AbstractOperator(ABC, Module): - def __init__(self, target: int): - super().__init__() - self.target: int = target - self.qubit_support: Tuple[int, ...] = (target,) - self.n_qubits: int = max(self.qubit_support) - - def __mul__(self, other: AbstractOperator | pyq.QuantumCircuit) -> pyq.QuantumCircuit: - if isinstance(other, AbstractOperator): - ops = torch.nn.ModuleList([self, other]) - return pyq.QuantumCircuit(max(self.target, other.target), ops) - elif isinstance(other, pyq.QuantumCircuit): - ops = torch.nn.ModuleList([self]) + other.operations - return pyq.QuantumCircuit(max(self.target, other.target), ops) - else: - raise TypeError(f"Unable to compose {type(self)} with {type(other)}") - - def __key(self) -> tuple: - return self.qubit_support - - def __eq__(self, other: Any) -> bool: - if isinstance(other, type(self)): - return self.__key() == other.__key() - else: - return False - - def __hash__(self) -> int: - return hash(self.qubit_support) - - @abstractmethod - def unitary(self, values: dict[str, torch.Tensor] | torch.Tensor = {}) -> Operator: - ... - - @abstractmethod - def dagger(self, values: dict[str, torch.Tensor] | torch.Tensor = {}) -> Operator: - ... - - @abstractmethod - def forward( - self, state: torch.Tensor, values: dict[str, torch.Tensor] | torch.Tensor = {} - ) -> State: - ... - - def extra_repr(self) -> str: - return f"qubit_support={self.qubit_support}" diff --git a/pyqtorch/adjoint.py b/pyqtorch/adjoint.py index 803e75b5..f6ac2475 100644 --- a/pyqtorch/adjoint.py +++ b/pyqtorch/adjoint.py @@ -37,13 +37,19 @@ def forward( def backward(ctx: Any, grad_out: Tensor) -> tuple: param_values = ctx.saved_tensors values = param_dict(ctx.param_names, param_values) - grads: list = [] + grads_dict = values.copy() for op in ctx.circuit.reverse(): ctx.out_state = apply_operator(ctx.out_state, op.dagger(values), op.qubit_support) if isinstance(op, Parametric): - mu = apply_operator(ctx.out_state, op.jacobian(values), op.qubit_support) - grads = [grad_out * 2 * overlap(ctx.projected_state, mu)] + grads + if values[op.param_name].requires_grad: + mu = apply_operator(ctx.out_state, op.jacobian(values), op.qubit_support) + grad = grad_out * 2 * overlap(ctx.projected_state, mu) + else: + grad = torch.zeros(1) + + grads_dict[op.param_name] = grad + ctx.projected_state = apply_operator( ctx.projected_state, op.dagger(values), op.qubit_support ) - return (None, None, None, None, *grads) + return (None, None, None, None, *grads_dict.values()) diff --git a/pyqtorch/apply.py b/pyqtorch/apply.py index f61b3f16..859f7b82 100644 --- a/pyqtorch/apply.py +++ b/pyqtorch/apply.py @@ -19,6 +19,26 @@ def apply_operator( n_qubits: int = None, batch_size: int = None, ) -> State: + """Applies an operator, i.e. a single tensor of shape [2, 2, ...], on a given state + of shape [2 for _ in range(n_qubits)] for a given set of (target and control) qubits. + + Since dimension 'i' in 'state' corresponds to all amplitudes where qubit 'i' is 1, + target and control qubits represent the dimensions over which to contract the 'operator'. + Contraction means applying the 'dot' operation between the operator array and dimension 'i' + of 'state, resulting in a new state where the result of the 'dot' operation has been moved to + dimension 'i' of 'state'. To restore the former order of dimensions, the affected dimensions + are moved to their original positions and the state is returned. + + Arguments: + state: State to operate on. + operator: Tensor to contract over 'state'. + qubits: Tuple of qubits on which to apply the 'operator' to. + n_qubits: The number of qubits of the full system. + batch_size: Batch size of either state and or operators. + + Returns: + State after applying 'operator'. + """ qubits = list(qubits) if n_qubits is None: n_qubits = len(state.size()) - 1 diff --git a/pyqtorch/circuit.py b/pyqtorch/circuit.py index ee504d92..f3abac88 100644 --- a/pyqtorch/circuit.py +++ b/pyqtorch/circuit.py @@ -1,28 +1,28 @@ from __future__ import annotations +from logging import getLogger from typing import Any, Iterator -import torch +from torch import Tensor, device +from torch.nn import Module, ModuleList -from pyqtorch.abstract import AbstractOperator from pyqtorch.utils import DiffMode, State, overlap, zero_state +logger = getLogger(__name__) -class QuantumCircuit(torch.nn.Module): - def __init__( - self, n_qubits: int, operations: list[AbstractOperator], diff_mode: DiffMode = DiffMode.AD - ): + +class QuantumCircuit(Module): + def __init__(self, n_qubits: int, operations: list[Module]): super().__init__() self.n_qubits = n_qubits - self.operations = torch.nn.ModuleList(operations) - self.diff_mode = diff_mode + self.operations = ModuleList(operations) - def __mul__(self, other: AbstractOperator | QuantumCircuit) -> QuantumCircuit: + def __mul__(self, other: Module | QuantumCircuit) -> QuantumCircuit: n_qubits = max(self.n_qubits, other.n_qubits) if isinstance(other, QuantumCircuit): return QuantumCircuit(n_qubits, self.operations.extend(other.operations)) - elif isinstance(other, AbstractOperator): + elif isinstance(other, Module): return QuantumCircuit(n_qubits, self.operations.append(other)) else: @@ -43,46 +43,73 @@ def __eq__(self, other: Any) -> bool: def __hash__(self) -> int: return hash(self.__key()) - def run(self, state: State = None, values: dict[str, torch.Tensor] = {}) -> State: + def run(self, state: State = None, values: dict[str, Tensor] = {}) -> State: if state is None: state = self.init_state() for op in self.operations: state = op(state, values) return state - def forward(self, state: State, values: dict[str, torch.Tensor] = {}) -> State: + def forward(self, state: State, values: dict[str, Tensor] = {}) -> State: return self.run(state, values) - def expectation( - self, - values: dict[str, torch.Tensor], - observable: QuantumCircuit, - state: State = None, - ) -> torch.Tensor: - if observable is None: - raise ValueError("Please provide an observable to compute expectation.") - if state is None: - state = self.init_state(batch_size=1) - if self.diff_mode == DiffMode.AD: - state = self.run(state, values) - return overlap(state, observable.forward(state, values)) + @property + def _device(self) -> device | None: + devices = set() + for op in self.operations: + if isinstance(op, QuantumCircuit): + devices.add(op._device) + elif isinstance(op, Module): + devices.update([b.device for b in op.buffers()]) + if len(devices) == 1 and None not in devices: + _device = next(iter(devices)) + logger.debug(f"Found device {_device}.") + return _device else: - from pyqtorch.adjoint import AdjointExpectation - - return AdjointExpectation.apply( - self, observable, state, values.keys(), *values.values() + logger.warning( + f"Unable to determine device of module {self}.\ + Found {devices}, however expected exactly one device." ) + return None - @property - def _device(self) -> torch.device: - try: - (_, buffer) = next(self.named_buffers()) - return buffer.device - except StopIteration: - return torch.device("cpu") - - def init_state(self, batch_size: int = 1) -> torch.Tensor: + def init_state(self, batch_size: int = 1) -> Tensor: return zero_state(self.n_qubits, batch_size, device=self._device) def reverse(self) -> QuantumCircuit: - return QuantumCircuit(self.n_qubits, torch.nn.ModuleList(list(reversed(self.operations)))) + return QuantumCircuit(self.n_qubits, ModuleList(list(reversed(self.operations)))) + + def to(self, device: device) -> QuantumCircuit: + self.operations = ModuleList([op.to(device) for op in self.operations]) + return self + + +def expectation( + circuit: QuantumCircuit, + state: State, + values: dict[str, Tensor], + observable: QuantumCircuit, + diff_mode: DiffMode = DiffMode.AD, +) -> Tensor: + """Compute the expectation value of the circuit given a state and observable. + Arguments: + circuit: QuantumCircuit instance + state: An input state + values: A dictionary of parameter values + observable: QuantumCircuit representing the observable + diff_mode: The differentiation mode + Returns: + A expectation value. + """ + if observable is None: + raise ValueError("Please provide an observable to compute expectation.") + if state is None: + state = circuit.init_state(batch_size=1) + if diff_mode == DiffMode.AD: + state = circuit.run(state, values) + return overlap(state, observable.forward(state, values)) + elif diff_mode == DiffMode.ADJOINT: + from pyqtorch.adjoint import AdjointExpectation + + return AdjointExpectation.apply(circuit, observable, state, values.keys(), *values.values()) + else: + raise ValueError(f"Requested diff_mode '{diff_mode}' not supported.") diff --git a/pyqtorch/primitive.py b/pyqtorch/primitive.py index 636a7be2..b60512e4 100644 --- a/pyqtorch/primitive.py +++ b/pyqtorch/primitive.py @@ -5,18 +5,35 @@ import torch -from pyqtorch.abstract import AbstractOperator from pyqtorch.apply import apply_operator from pyqtorch.matrices import OPERATIONS_DICT, _controlled, _dagger from pyqtorch.utils import Operator, State, product_state -class Primitive(AbstractOperator): - def __init__(self, pauli: torch.Tensor, target: int): - super().__init__(target) +class Primitive(torch.nn.Module): + def __init__(self, pauli: torch.Tensor, target: int) -> None: + super().__init__() + self.target: int = target + self.qubit_support: Tuple[int, ...] = (target,) + self.n_qubits: int = max(self.qubit_support) self.register_buffer("pauli", pauli) self._param_type = None + def __key(self) -> tuple: + return self.qubit_support + + def __eq__(self, other: object) -> bool: + if isinstance(other, type(self)): + return self.__key() == other.__key() + else: + return False + + def __hash__(self) -> int: + return hash(self.qubit_support) + + def extra_repr(self) -> str: + return f"qubit_support={self.qubit_support}" + @property def param_type(self) -> None: return self._param_type diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 54538f77..d97b7475 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -4,7 +4,7 @@ import torch import pyqtorch as pyq -from pyqtorch.circuit import DiffMode +from pyqtorch.circuit import DiffMode, expectation def test_adjoint_diff() -> None: @@ -14,8 +14,7 @@ def test_adjoint_diff() -> None: cnot = pyq.CNOT(1, 2) ops = [rx, cry, rz, cnot] n_qubits = 3 - adjoint_circ = pyq.QuantumCircuit(n_qubits, ops, DiffMode.ADJOINT) - ad_circ = pyq.QuantumCircuit(n_qubits, ops, DiffMode.AD) + circ = pyq.QuantumCircuit(n_qubits, ops) obs = pyq.QuantumCircuit(n_qubits, [pyq.Z(0)]) theta_0_value = torch.pi / 2 @@ -39,8 +38,8 @@ def test_adjoint_diff() -> None: "theta_1": thetas_1_adjoint, "theta_2": thetas_2_adjoint, } - exp_ad = ad_circ.expectation(values_ad, obs, state) - exp_adjoint = adjoint_circ.expectation(values_adjoint, obs, state) + exp_ad = expectation(circ, state, values_ad, obs, DiffMode.AD) + exp_adjoint = expectation(circ, state, values_adjoint, obs, DiffMode.ADJOINT) grad_ad = torch.autograd.grad(exp_ad, tuple(values_ad.values()), torch.ones_like(exp_ad)) @@ -65,7 +64,7 @@ def test_differentiate_circuit(diff_mode: DiffMode, batch_size: int, n_qubits: i pyq.CNOT(0, 1), pyq.Toffoli((2, 1), 0), ] - circ = pyq.QuantumCircuit(n_qubits, ops, diff_mode=diff_mode) + circ = pyq.QuantumCircuit(n_qubits, ops) state = pyq.random_state(n_qubits, batch_size) phi = torch.rand(batch_size, requires_grad=True) theta = torch.rand(batch_size, requires_grad=True) @@ -78,3 +77,10 @@ def _fwd(phi: torch.Tensor, theta: torch.Tensor, epsilon: torch.Tensor) -> torch return circ(state, {"phi": phi, "theta": theta, "epsilon": epsilon}) assert torch.autograd.gradcheck(_fwd, (phi, theta, epsilon)) + + +def test_device_inference() -> None: + ops = [pyq.RX(0), pyq.RX(0)] + circ = pyq.QuantumCircuit(2, ops) + nested_circ = pyq.QuantumCircuit(2, [circ, circ]) + assert nested_circ._device is not None diff --git a/tests/test_digital.py b/tests/test_digital.py index dae6da33..0fd6bda0 100644 --- a/tests/test_digital.py +++ b/tests/test_digital.py @@ -236,7 +236,7 @@ def test_dagger_single_qubit() -> None: if issubclass(cls, Parametric): op = cls(target, param_name) # type: ignore[arg-type] else: - op = cls(target) # type: ignore[misc] + op = cls(target) # type: ignore[assignment, call-arg] values = {param_name: torch.rand(1)} if param_name == "theta" else torch.rand(1) new_state = apply_operator(state, op.unitary(values), [target]) daggered_back = apply_operator(new_state, op.dagger(values), [target]) @@ -257,7 +257,7 @@ def test_dagger_nqubit() -> None: op = cls(target - 1, target, param_name) # type: ignore[arg-type] qubit_support = (target + 1, target) else: - op = cls(target - 1, target) # type: ignore[misc] + op = cls(target - 1, target) # type: ignore[call-arg] qubit_support = (target + 1, target) values = {param_name: torch.rand(1)} if param_name == "theta" else torch.rand(1) new_state = apply_operator(state, op.unitary(values), qubit_support)