Skip to content

Commit

Permalink
[Feature] pyqtorch API (#214)
Browse files Browse the repository at this point in the history
Add API endpoints for run, sample and expectation to pyqtorch
  • Loading branch information
dominikandreasseitz authored Jul 3, 2024
1 parent 9d63f8d commit e2b4860
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 58 deletions.
71 changes: 71 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
`pyqtorch` exposes `run`, `sample` and `expectation` routines with the following interface:

## run
```python
def run(
circuit: QuantumCircuit,
state: Tensor = None,
values: dict[str, Tensor] = dict(),
) -> Tensor:
"""Sequentially apply each operation in `circuit.operations` to an input state `state`
given current parameter values `values`, perform an optional `embedding` on `values`
and return an output state.
Arguments:
circuit: A pyqtorch.QuantumCircuit instance.
state: A torch.Tensor of shape [2, 2, ..., batch_size].
values: A dictionary containing `parameter_name`: torch.Tensor key,value pairs denoting
the current parameter values for each parameter in `circuit`.
Returns:
A torch.Tensor of shape [2, 2, ..., batch_size]
"""
...
```

## sample
```python
def sample(
circuit: QuantumCircuit,
state: Tensor = None,
values: dict[str, Tensor] = dict(),
n_shots: int = 1000,
) -> list[Counter]:
"""Sample from `circuit` given an input state `state` given current parameter values `values`,
perform an optional `embedding` on `values` and return a list Counter objects mapping from
bitstring: num_samples.
Arguments:
circuit: A pyqtorch.QuantumCircuit instance.
state: A torch.Tensor of shape [2, 2, ..., batch_size].
values: A dictionary containing `parameter_name`: torch.Tensor key,value pairs
denoting the current parameter values for each parameter in `circuit`.
n_shots: A positive int denoting the number of requested samples.
Returns:
A list of Counter objects containing bitstring:num_samples pairs.
"""
...
```

## expectation

```python
def expectation(
circuit: QuantumCircuit,
state: Tensor,
values: dict[str, Tensor],
observable: Observable,
diff_mode: DiffMode = DiffMode.AD) -> torch.Tensor:
"""Compute the expectation value of `circuit` given a `state`, parameter values `values`
given an `observable` and optionally compute gradients using diff_mode.
Arguments:
circuit: A pyqtorch.QuantumCircuit instance.
state: A torch.Tensor of shape [2, 2, ..., batch_size].
values: A dictionary containing `parameter_name`: torch.Tensor key,value pairs
denoting the current parameter values for each parameter in `circuit`.
observable: A pyq.Observable instance.
diff_mode: The differentiation mode.
Returns:
An expectation value.
"""
...
```
15 changes: 14 additions & 1 deletion docs/adjoint.md → docs/differentiation.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
`pyqtorch` also offers a [adjoint differentiation mode](https://arxiv.org/abs/2009.02823) which can be used through the `expectation` method.
`pyqtorch` also offers several differentiation modes to compute gradients which can be accessed through the
`expectation` API. Simply pass one of three `DiffMode` options to the `diff_mode` argument.
The default is `ad`.

### Automatic Differentiation (DiffMode.AD)
The default differentation mode of `pyqtorch`, [torch.autograd](https://pytorch.org/docs/stable/autograd.html).
It uses the `torch` native automatic differentiation engine which tracks operations on `torch.Tensor` objects by constructing a computational graph to perform chain rules for derivatives calculations.

### Adjoint Differentiation (DiffMode.ADJOINT)
The [adjoint differentiation mode](https://arxiv.org/abs/2009.02823) computes first-order gradients by only requiring at most three states in memory in `O(P)` time where `P` is the number of parameters in a circuit.

### Generalized Parameter-Shift rules (DiffMode.GPSR)
To be added.

### Example
```python exec="on" source="material-block" html="1"
import pyqtorch as pyq
import torch
Expand Down
3 changes: 2 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ nav:
- Pyqtorch in a Nutshell: index.md
- How to Contribute: CONTRIBUTING.md
- Code of Conduct: CODE_OF_CONDUCT.md
- API: api.md
- Advanced Features:
- Differentiation: differentiation.md
- Analog Operations: analog.md
- Adjoint Differentiation: adjoint.md
- Digital noisy simulation: noise.md
- Quantum Dropout: dropout.md
- CUDA Profiling and debugging: cuda_debugging.md
Expand Down
4 changes: 2 additions & 2 deletions pyqtorch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,16 @@
logger.info(f"PyQTorch logger successfully setup with log level {LOG_LEVEL}")


from .adjoint import expectation
from .analog import (
Add,
DiagonalObservable,
HamiltonianEvolution,
Observable,
Scale,
)
from .api import expectation, run, sample
from .apply import apply_operator
from .circuit import Merge, QuantumCircuit, Sequence, run, sample
from .circuit import Merge, QuantumCircuit, Sequence
from .noise import (
AmplitudeDamping,
BitFlip,
Expand Down
34 changes: 1 addition & 33 deletions pyqtorch/adjoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from pyqtorch.circuit import QuantumCircuit
from pyqtorch.parametric import Parametric
from pyqtorch.primitive import Primitive
from pyqtorch.utils import DiffMode, inner_prod, param_dict
from pyqtorch.utils import inner_prod, param_dict

logger = getLogger(__name__)

Expand Down Expand Up @@ -122,35 +122,3 @@ def backward(ctx: Any, grad_out: Tensor) -> Tuple[None, ...]:
f"AdjointExpectation does not support operation: {type(op)}."
)
return (None, None, None, None, *grads_dict.values())


def expectation(
circuit: QuantumCircuit,
state: Tensor,
values: dict[str, Tensor],
observable: Observable,
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: Hamiltonian representing the observable
diff_mode: The differentiation mode
Returns:
A expectation value.
"""
if observable is None:
logger.error("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 inner_prod(state, observable.run(state, values)).real
elif diff_mode == DiffMode.ADJOINT:
return AdjointExpectation.apply(
circuit, observable, state, values.keys(), *values.values()
)
else:
logger.error(f"Requested diff_mode '{diff_mode}' not supported.")
99 changes: 99 additions & 0 deletions pyqtorch/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from __future__ import annotations

from collections import Counter
from logging import getLogger

from torch import Tensor

from pyqtorch.adjoint import AdjointExpectation
from pyqtorch.analog import Observable
from pyqtorch.circuit import QuantumCircuit
from pyqtorch.utils import DiffMode, inner_prod

logger = getLogger(__name__)


def run(
circuit: QuantumCircuit,
state: Tensor = None,
values: dict[str, Tensor] = dict(),
) -> Tensor:
"""Sequentially apply each operation in `circuit.operations` to an input state `state`
given current parameter values `values`, perform an optional `embedding` on `values`
and return an output state.
Arguments:
circuit: A pyqtorch.QuantumCircuit instance.
state: A torch.Tensor of shape [2, 2, ..., batch_size].
values: A dictionary containing `parameter_name`: torch.Tensor key,value pairs denoting
the current parameter values for each parameter in `circuit`.
Returns:
A torch.Tensor of shape [2, 2, ..., batch_size]
"""
logger.debug(f"Running circuit {circuit} on state {state} and values {values}.")
return circuit.run(state, values)


def sample(
circuit: QuantumCircuit,
state: Tensor = None,
values: dict[str, Tensor] = dict(),
n_shots: int = 1000,
) -> list[Counter]:
"""Sample from `circuit` given an input state `state` given current parameter values `values`,
perform an optional `embedding` on `values` and return a list Counter objects mapping from
bitstring: num_samples.
Arguments:
circuit: A pyqtorch.QuantumCircuit instance.
state: A torch.Tensor of shape [2, 2, ..., batch_size].
values: A dictionary containing `parameter_name`: torch.Tensor key,value pairs
denoting the current parameter values for each parameter in `circuit`.
n_shots: A positive int denoting the number of requested samples.
Returns:
A list of Counter objects containing bitstring:num_samples pairs.
"""
logger.debug(
f"Sampling circuit {circuit} on state {state} and values {values} with n_shots {n_shots}."
)
return circuit.sample(state, values, n_shots)


def expectation(
circuit: QuantumCircuit,
state: Tensor,
values: dict[str, Tensor],
observable: Observable,
diff_mode: DiffMode = DiffMode.AD,
) -> Tensor:
"""Compute the expectation value of `circuit` given a `state`, parameter values `values`
given an `observable` and optionally compute gradients using diff_mode.
Arguments:
circuit: A pyqtorch.QuantumCircuit instance.
state: A torch.Tensor of shape [2, 2, ..., batch_size].
values: A dictionary containing `parameter_name`: torch.Tensor key,value pairs
denoting the current parameter values for each parameter in `circuit`.
observable: A pyq.Observable instance.
diff_mode: The differentiation mode.
Returns:
An expectation value.
"""
logger.debug(
f"Computing expectation of circuit {circuit} on state {state}, values {values},\
given observable {observable} and diff_mode {diff_mode}."
)
if observable is None:
logger.error("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 inner_prod(state, observable.run(state, values)).real
elif diff_mode == DiffMode.ADJOINT:
return AdjointExpectation.apply(
circuit, observable, state, values.keys(), *values.values()
)
elif diff_mode == DiffMode.GPSR:
raise NotImplementedError("To be added.")
else:
logger.error(f"Requested diff_mode '{diff_mode}' not supported.")
17 changes: 0 additions & 17 deletions pyqtorch/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,20 +301,3 @@ def idxer() -> Generator[int, Any, None]:
{f"{param_name}_{n}": rand(1, requires_grad=True) for n in range(next(idx))}
)
return ops, params


def run(
circuit: QuantumCircuit,
state: Tensor = None,
values: dict[str, Tensor] = dict(),
) -> Tensor:
return circuit.run(state, values)


def sample(
circuit: QuantumCircuit,
state: Tensor = None,
values: dict[str, Tensor] = dict(),
n_shots: int = 1000,
) -> list[Counter]:
return circuit.sample(state, values, n_shots)
12 changes: 8 additions & 4 deletions pyqtorch/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,18 @@ class DiffMode(StrEnum):
"""
Which Differentiation method to use.
Options: Automatic Differentiation - Using torch.autograd.
Adjoint Differentiation - An implementation of "Efficient calculation of gradients
in classical simulations of variational quantum algorithms",
Jones & Gacon, 2020
Options: Automatic Differentiation - .
Adjoint Differentiation -
"""

AD = "ad"
"""torch.autograd"""
ADJOINT = "adjoint"
"""An implementation of "Efficient calculation of gradients
in classical simulations of variational quantum algorithms",
Jones & Gacon, 2020"""
GPSR = "gpsr"
"""To be added."""


def is_normalized(state: Tensor, atol: float = ATOL) -> bool:
Expand Down

0 comments on commit e2b4860

Please sign in to comment.