diff --git a/.github/workflows/test_docs.yml b/.github/workflows/test_docs.yml
index d131ade9e..f018bb9f4 100644
--- a/.github/workflows/test_docs.yml
+++ b/.github/workflows/test_docs.yml
@@ -7,8 +7,6 @@ on:
pull_request:
branches:
- main
- paths:
- - docs
workflow_dispatch: {}
concurrency:
@@ -17,11 +15,11 @@ concurrency:
jobs:
test_qadence_ubuntu:
- name: Test Qadence (ubuntu)
+ name: Test Qadence docs (ubuntu)
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- - name: Select Python 3.10
+ - name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: '3.10'
diff --git a/docs/digital_analog_qc/daqc-basics.md b/docs/digital_analog_qc/daqc-basics.md
index b7e61450b..d20bf9ad9 100644
--- a/docs/digital_analog_qc/daqc-basics.md
+++ b/docs/digital_analog_qc/daqc-basics.md
@@ -1,30 +1,28 @@
# Digital-Analog Quantum Computation
_**Digital-analog quantum computation**_ (DAQC) is a universal quantum computing
-paradigm [^1]. The main ingredients of a DAQC program are:
+paradigm[^1], based on two primary computations:
- Fast single-qubit operations (digital).
- Multi-partite entangling operations acting on all qubits (analog).
-Analog operations are typically assumed to follow device-specific interacting qubit Hamiltonians, such as the Ising Hamiltonian [^2]. The most common realization of the DAQC paradigm is on neutral atoms quantum computing platforms.
+The DAQC paradigm is typically implemented on quantum computing hardware based on neutral-atoms where both these computations are realizable.
## Digital-Analog Emulation
-Qadence simplifies the execution of DAQC programs on neutral-atom devices
-by providing a simplified interface for adding interaction and interfacing
-with pulse-level programming in `pulser`[^3].
+Qadence simplifies the execution of DAQC programs on either emulated or real neutral-atom devices
+by providing a simplified interface for customizing interactions and interfacing
+with pulse-level programming in `Pulser`[^3].
+## Digital-Analog Transformation
-## DAQC Transform
-
-Furthermore, essential to digital-analog computation is the ability to represent an arbitrary Hamiltonian
-with the evolution of a fixed and device-amenable Hamiltonian. Such a transform was described in the
-DAQC[^2] paper for ZZ interactions, which is natively implemented in Qadence.
+Furthermore, the essence of digital-analog computation is the ability to represent any analog operation, _i.e._ any arbitrary Hamiltonian, using an
+auxiliary device-amenable Hamiltonian, such as the ubiquitous Ising model[^2]. This is at the core of the DAQC implementation in Qadence.
## References
-[^1]: [Dodd et al., Universal quantum computation and simulation using any entangling Hamiltonian and local unitaries, PRA 65, 040301 (2002).](https://arxiv.org/abs/quant-ph/0106064)
+[^1]: [Dodd _et al._, Universal quantum computation and simulation using any entangling Hamiltonian and local unitaries, PRA 65, 040301 (2002).](https://arxiv.org/abs/quant-ph/0106064)
-[^2]: [Parra-Rodriguez et al., Digital-Analog Quantum Computation, PRA 101, 022305 (2020).](https://arxiv.org/abs/1812.03637)
+[^2]: [Pulser: An open-source package for the design of pulse sequences in programmable neutral-atom arrays](https://pulser.readthedocs.io/en/stable/)
-[^3]: [Pulser: An open-source package for the design of pulse sequences in programmable neutral-atom arrays](https://pulser.readthedocs.io/en/stable/)
+[^3]: [Parra-Rodriguez _et al._, Digital-Analog Quantum Computation, PRA 101, 022305 (2020).](https://arxiv.org/abs/1812.03637)
diff --git a/docs/digital_analog_qc/pulser-basic.md b/docs/digital_analog_qc/pulser-basic.md
index 80b4350b5..87d24caf6 100644
--- a/docs/digital_analog_qc/pulser-basic.md
+++ b/docs/digital_analog_qc/pulser-basic.md
@@ -28,7 +28,7 @@ The current backend has the following operations:
| gate | description | trainable parameter |
|-------------|--------------------------------------------------------------------------------------------------|---------------------|
-| `Rot` | Single qubit rotations. | rotation angle |
+| `RX`, `RY` | Single qubit rotations. | rotation angle |
| `AnalogRot` | Span a single qubit rotation among the entire register. | rotation angle |
| `entangle` | Fully entangle the register. | interaction time |
| `wait` | An idle block to wait for the system to evolve for a specific time according to the interaction. | free evolution time |
@@ -67,15 +67,15 @@ To run the pulse sequence we have to provide values for the parametrized block w
import torch
params = {
- "wait": torch.tensor([383]), # ns
- "y": torch.tensor([torch.pi/2]),
+ "t": torch.tensor([383]), # ns
+ "y": torch.tensor([3*torch.pi/2]),
}
# Visualise the final state vector
final_vector = model.run(params)
print(final_vector)
-sample = model.sample(params, n_shots=50)[0]
+sample = model.sample(params, n_shots=500)[0]
print(sample)
```
```python exec="on" source="material-block" html="1" session="pulser-basic"
@@ -97,151 +97,47 @@ print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide
```
-## Create your own gate
-A big advantage of the `chain` block is it makes it easy to create complex
-operations from simple ones. Take the entanglement operation as an example.
-
-The operation consists of moving _all_ the qubits to the `X` basis having the
-atoms' interaction perform a controlled-Z operation during the free evolution.
-And we can easily recreate this pattern using the `AnFreeEvo` and `AnRY` blocks.
-
-```python exec="on" source="material-block" session="pulser-basic"
-from qadence import AnalogRY, chain, wait
-
-def my_entanglement(duration):
- return chain(
- AnalogRY(-torch.pi / 2),
- wait(duration)
- )
-```
+## Working with observables
-Then we proceed as before.
+You can calculate expectation value of `Observables` in the Pulser backend the same way as in other backends by using the `expectation` method.
+First we create the desired observables using Qadence blocks.
-```python exec="on" source="material-block" session="pulser-basic" html="1"
-protocol = chain(
- my_entanglement("t"),
- RY(0, "y"),
-)
-register = Register(2)
-circuit = QuantumCircuit(register, protocol)
-model = QuantumModel(circuit, backend="pulser", diff_mode='gpsr')
+```python exec="on" source="material-block" session="pulser-basic"
+from qadence.operations import I, X, Y, Z, kron
-params = {
- "t": torch.tensor([383]), # ns
- "y": torch.tensor([torch.pi / 2]),
-}
+zz = kron(I(0), Z(1), I(2), Z(3))
+xy = kron(I(0), X(1), I(2), Y(3))
+yx = kron(I(0), Y(1), I(2), X(3))
-sample = model.sample(params, n_shots=50)[0]
+obs = [zz, xy + yx]
-fig, ax = plt.subplots()
-plt.bar(sample.keys(), sample.values())
-from docs import docsutils # markdown-exec: hide
-print(docsutils.fig_to_html(fig)) # markdown-exec: hide
-```
-
-```python exec="on" source="material-block" html="1" session="pulser-basic"
-model.assign_parameters(params).draw(draw_phase_area=True, show=False)
-from docs import docsutils # markdown-exec: hide
-print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide
```
+Now we define the `QuantumModel` and pass the observable list to it together with the constructed circuit.
-## Large qubits registers
-The constructor `Register(n_qubits)` generates a linear register that works fine
-with two or three qubits. But for the blocks we have so far, large registers
-work better with a square loop layout like the following.
-
-```python exec="on" source="material-block" html="1" session="pulser-basic"
-register = Register.square(qubits_side=4)
-register.draw(show=False)
-from docs import docsutils # markdown-exec: hide
-print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide
-```
-
-In those cases, global pulses are preferred to generate entanglement to avoid
-changing the addressing pattern on the fly.
+```python exec="on" source="material-block" result="json" session="pulser-basic"
+from qadence import RX, AnalogRot
-```python exec="on" source="material-block" html="1" session="pulser-basic"
-protocol = chain(
- entangle("t"),
- AnalogRY(torch.pi / 2),
+blocks = chain(
+ RX(0, "x"),
+ RX(2, "x"),
+ AnalogRot(duration=300, omega=5*torch.pi)
)
register = Register.square(qubits_side=2)
-circuit = QuantumCircuit(register, protocol)
-model = QuantumModel(circuit, backend="pulser", diff_mode='gpsr')
-model.backend.backend.config.with_modulation = True
+circuit = QuantumCircuit(register, blocks)
+model = QuantumModel(circuit, observable=obs, backend="pulser", diff_mode="gpsr")
params = {
- "t": torch.tensor([1956]), # ns
+ "x": torch.tensor([3*torch.pi/2]), # ns
}
-sample = model.sample(params, n_shots=500)[0]
-
-fig, ax = plt.subplots()
-ax.bar(sample.keys(), sample.values())
-plt.xticks(rotation='vertical')
-from docs import docsutils # markdown-exec: hide
-print(docsutils.fig_to_html(fig)) # markdown-exec: hide
-```
-```python exec="on" source="material-block" html="1" session="pulser-basic"
-model.assign_parameters(params).draw(draw_phase_area=True, show=False)
-from docs import docsutils # markdown-exec: hide
-print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide
-```
-
-!!! note
- The gates shown here don't work with arbitrary registers since they rely on
- the registered geometry to work properly.
-
-## Working with observables
-
-The current backend version does not support Qadence `Observables`. However, it's
-still possible to use the regular Pulser simulations to calculate expected
-values.
-
-To do so, we use the `assign_parameters` property to recover the Pulser sequence.
-
-```python exec="on" source="material-block" session="pulser-basic"
-params = {
- "t": torch.tensor([383]), # ns
-}
-
-built_sequence = model.assign_parameters(params)
+final_result = model.expectation(values=params)
```
-
-Next, we create the desired observables using `Qutip` [^2].
-
-
-```python exec="on" source="material-block" session="pulser-basic"
-import qutip
-
-zz = qutip.tensor([qutip.qeye(2), qutip.sigmaz(), qutip.qeye(2), qutip.sigmaz()])
-xy = qutip.tensor([qutip.qeye(2), qutip.sigmax(), qutip.qeye(2), qutip.sigmay()])
-yx = qutip.tensor([qutip.qeye(2), qutip.sigmay(), qutip.qeye(2), qutip.sigmax()])
-```
-
-
-We use the Pulser `Simulation` class to run the sequence and call the method `expect` over our observables.
-
-```python exec="on" source="material-block" result="json" session="pulser-basic"
-from pulser_simulation import Simulation
-
-sim = Simulation(built_sequence)
-result = sim.run()
-
-final_result = result.expect([zz, xy + yx])
-print(final_result[0][-1], final_result[1][-1])
-```
-
-We use the Pulser `Simulation` class to run the sequence and call the method
-`expect` over our observables.
-
-Here the `final_result` contains the expected values during the evolution (one
-point per nanosecond). In this case, looking only at the final values, we see
-the qubits `q0` and `q2` are on the *Bell-diagonal* state $\Big(|00\rangle -i |11\rangle\Big)/\sqrt{2}$.
+We use the `expectation` method of the `QuantumModel` instance to calculate the expectation values.
+Here the `final_result` contains the expected values of observables in `obs` list.
```python exec="on" source="material-block" html="1" session="pulser-basic"
from qadence import fourier_feature_map, RX, RY
@@ -272,4 +168,3 @@ print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide
## References
[^1]: [Pulser: An open-source package for the design of pulse sequences in programmable neutral-atom arrays](https://pulser.readthedocs.io/en/stable/)
-[^2]: [Qutip](https://qutip.org)
diff --git a/docs/environment.yml b/docs/environment.yml
new file mode 100644
index 000000000..52d7743a8
--- /dev/null
+++ b/docs/environment.yml
@@ -0,0 +1,19 @@
+name: readthedocs
+channels:
+ - defaults
+dependencies:
+ - python=3.10
+ - python-graphviz
+ - pip
+ - pip:
+ - markdown-exec
+ - mkdocs-exclude
+ - mkdocs-jupyter
+ - mkdocs-material
+ - mkdocs-section-index==0.3.6
+ - mkdocs==1.5.2
+ - mkdocstrings
+ - mkdocstrings-python
+ - -e ../
+ - pulser>=0.12.0
+ - amazon-braket-sdk
diff --git a/docs/index.md b/docs/index.md
index 29da41a27..f60fbd156 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,155 +1,142 @@
-
noheading
-
-!!! warning "Large Logo"
- Put a large verion of the logo herec.
-
-Qadence is a Python package that provides a simple interface to build _**digital-analog quantum
-programs**_ with tunable interaction defined on _**arbitrary qubit register layouts**_.
+**Qadence** is a Python package that provides a simple interface to build _**digital-analog quantum
+programs**_ with tunable qubit interaction defined on _**arbitrary register topologies**_ realizable on neutral atom devices.
## Feature highlights
* A [block-based system](tutorials/getting_started.md) for composing _**complex digital-analog
- programs**_ in a flexible and extensible manner. Heavily inspired by
- [`Yao.jl`](https://github.com/QuantumBFS/Yao.jl) and functional programming concepts.
+ programs**_ in a flexible and scalable manner, inspired by the Julia quantum SDK
+ [Yao.jl](https://github.com/QuantumBFS/Yao.jl) and functional programming concepts.
* A [simple interface](digital_analog_qc/analog-basics.md) to work with _**interacting qubit systems**_
- using [arbitrary qubit registers](tutorials/register.md).
-
-* Intuitive, [expression-based system](tutorials/parameters.md) built on top of `sympy` to construct
- _**parametric quantum programs**_.
+ using [arbitrary registers topologies](tutorials/register.md).
-* [Higher-order generalized parameter shift](link to psr tutorial) rules for _**differentiating
- arbitrary quantum operations**_ on real hardware.
+* An intuitive [expression-based system](tutorials/parameters.md) developed on top of the symbolic library [Sympy](https://www.sympy.org/en/index.html) to construct _**parametric quantum programs**_ easily.
-* Out-of-the-box automatic differentiability of quantum programs using [https://pytorch.org](https://pytorch.org)
+* [High-order generalized parameter shift rules](link to psr tutorial) for _**differentiating parametrized quantum operations**_.
-* `QuantumModel`s to make `QuantumCircuit`s differentiable and runnable on a variety of different
- backends like state vector simulators, tensor network emulators and real devices.
+* Out-of-the-box _**automatic differentiability**_ of quantum programs with [PyTorch](https://pytorch.org/) integration.
-Documentation can be found here: [https://pasqal-qadence.readthedocs-hosted.com/en/latest](https://pasqal-qadence.readthedocs-hosted.com/en/latest).
+* _**Efficient execution**_ on a variety of different purpose backends: from state vector simulators to tensor network emulators and real devices.
-## Remarks
-Quadence uses torch.float64 as the default datatype for tensors (torch.complex128 for complex tensors).
+In following are some rudimentary examples of Qadence possibilites in the digital, analog and digital-analog paradigms.
-## Examples
+## Sampling the canonical Bell state
-### Bell state
-
-Sample from the [Bell state](https://en.wikipedia.org/wiki/Bell_state) in one line.
+This example illustrates how to prepare a [Bell state](https://en.wikipedia.org/wiki/Bell_state) using digital gates and sampling from the outcome bitstring distribution:
```python exec="on" source="material-block" result="json"
import torch # markdown-exec: hide
torch.manual_seed(0) # markdown-exec: hide
from qadence import CNOT, H, chain, sample
-xs = sample(chain(H(0), CNOT(0,1)), n_shots=100)
-print(xs) # markdown-exec: hide
+# Preparing a Bell state by composing a Hadamard and CNOT gates in sequence.
+bell_state = chain(H(0), CNOT(0,1))
+
+# Sample with 100 shots.
+samples = sample(bell_state, n_shots=100)
+print(samples) # markdown-exec: hide
from qadence.divergences import js_divergence # markdown-exec: hide
from collections import Counter # markdown-exec: hide
-js = js_divergence(xs[0], Counter({"00":50, "11":50})) # markdown-exec: hide
+js = js_divergence(samples[0], Counter({"00":50, "11":50})) # markdown-exec: hide
assert js < 0.005 # markdown-exec: hide
```
+## Analog emulation of a perfect state transfer
-### Perfect state transfer
+This next example showcases the construction and sampling of a system that admits a perfect state transfer between the two edge qubits of a three qubit register laid out in a
+line. This relies on time-evolving a Hamiltonian for a custom defined qubit interation until $t=\frac{\pi}{\sqrt 2}$.
-We can construct a system that admits perfect state transfer between the two edge qubits in a
-line of qubits at time $t=\frac{\pi}{\sqrt 2}$.
```python exec="on" source="material-block" result="json"
-import torch
+from torch import pi
from qadence import X, Y, HamEvo, Register, product_state, sample, add
+# Define the qubit-qubit interaction term.
def interaction(i, j):
- return 0.5 * (X(i) @ X(j) + Y(i) @ Y(j))
+ return 0.5 * (X(i) @ X(j) + Y(i) @ Y(j)) # Compose gates in parallel and sum their contribution.
-# initial state with left-most qubit in the 1 state
+# Initial state with left-most qubit in the 1 state.
init_state = product_state("100")
-# register with qubits in a line
-reg = Register.line(n_qubits=3)
+# Define a register of 3 qubits laid out in a line.
+register = Register.line(n_qubits=3)
-# a line hamiltonian
-hamiltonian = add(interaction(*edge) for edge in reg.edges)
-# which is the same as:
+# Define an interaction Hamiltonian by summing interactions on indexed qubits.
# hamiltonian = interaction(0, 1) + interaction(1, 2)
+hamiltonian = add(interaction(*edge) for edge in register.edges)
-# define a hamiltonian evolution over t
-t = torch.pi/(2**0.5)
+# Define and time-evolve the Hamiltonian until t=pi/sqrt(2).
+t = pi/(2**0.5) # Dimensionless.
evolution = HamEvo(hamiltonian, t)
-samples = sample(reg, evolution, state=init_state, n_shots=1)
+# Sample with 100 shots.
+samples = sample(register, evolution, state=init_state, n_shots=100)
print(f"{samples = }") # markdown-exec: hide
from collections import Counter # markdown-exec: hide
-assert samples[0] == Counter({"001": 1}) # markdown-exec: hide
+assert samples[0] == Counter({"001": 100}) # markdown-exec: hide
```
+## Digital-analog example
-### Digital-analog emulation
+This final example deals with the construction and sampling of an Ising Hamiltonian that includes a distance-based interaction between qubits and a global analog block of rotations around the X-axis. Here, global has to be understood as applied to the whole register.
-Just as easily we can simulate an Ising hamiltonian that includes an interaction term based on the
-distance of two qubits. To learn more about digital-analog quantum computing see the
-[digital-analog section](/digital_analog_qc/analog-basics.md).
```python exec="on" source="material-block" result="json"
from torch import pi
from qadence import Register, AnalogRX, sample
-# global, analog RX block
+# Global analog RX block.
block = AnalogRX(pi)
-# two qubits far apart (practically non-interacting)
-reg = Register.from_coordinates([(0,0), (0,15)])
-samples = sample(reg, block)
+# Almost non-interacting qubits as too far apart.
+register = Register.from_coordinates([(0,0), (0,15)]) # Dimensionless.
+samples = sample(register, block)
print(f"distance = 15: {samples = }") # markdown-exec: hide
from collections import Counter # markdown-exec: hide
from qadence.divergences import js_divergence # markdown-exec: hide
js = js_divergence(samples[0], Counter({"11": 100})) # markdown-exec: hide
assert js < 0.01 # markdown-exec: hide
-# two qubits close together (interacting!)
-reg = Register.from_coordinates([(0,0), (0,5)])
-samples = sample(reg, AnalogRX(pi))
+# Interacting qubits as close together.
+register = Register.from_coordinates([(0,0), (0,5)])
+samples = sample(register, AnalogRX(pi))
print(f"distance = 5: {samples = }") # markdown-exec: hide
js = js_divergence(samples[0], Counter({"01":33, "10":33, "00":33, "11":1})) # markdown-exec: hide
assert js < 0.05 # markdown-exec: hide```
```
-
## Further Resources
-For a more comprehensive introduction and advanced topics, we suggest you to
-look at the following tutorials:
-* [Description of quantum state conventions.](tutorials/state_conventions.md)
-* [Basic tutorial](tutorials/getting_started.md) with a lot of detailed information
-* Building [digital-analog](digital_analog_qc/analog-basics.md) quantum programs with interacting qubits
-* [The sharp bits](tutorials/parameters.md) of creating parametric programs and observables
-* [Advanced features](advanced_tutorials) like the low-level backend interface
-* Building custom [`QuantumModel`](advanced_tutorials/custom-models.md)s
+For a more comprehensive introduction and advanced topics, please have a look at the following tutorials:
+
+* [Quantum state conventions](tutorials/state_conventions.md) used throughout **Qadence**.
+* [Basic tutorials](tutorials/getting_started.md) for first hands-on.
+* [Digital-analog basics](digital_analog_qc/analog-basics.md) to build quantum programs in the digital-analog paradigm.
+* [Parametric quantum circuits](tutorials/parameters.md) for the generation and manipulation of parametric programs.
+* [Advanced features](advanced_tutorials) about low-level backend interface and differentiablity.
+* [`QuantumModel`](advanced_tutorials/custom-models.md) for defining custom models.
## Installation guide
-Qadence can be install with `pip` as follows:
+Qadence can be install with `pip` from PyPI as follows:
```bash
-export TOKEN_USERNAME=MYUSERNAME
-export TOKEN_PASSWORD=THEPASSWORD
-
-pip install --extra-index-url "https://${TOKEN_USERNAME}:${TOKEN_PASSWORD}@gitlab.pasqal.com/api/v4/projects/190/packages/pypi/simple" qadence[pulser,visualization]
+pip install qadence
```
-where the token username and password can be generated on the
-[Gitlab UI](https://gitlab.pasqal.com/-/profile/personal_access_tokens). Remember to give registry read/write permissions to the generated token.
+The default backend for Qadence is [PyQTorch](https://github.com/pasqal-io/pyqtorch), a differentiable state vector simulator for digital-analog simulation. It is possible to install additional backends and the circuit visualization library using the following extras:
-The default backend for qadence is pyqtorch (a differentiable state vector simulator).
-You can install one or all of the following additional backends and the circuit visualization library using the following extras:
+* `braket`: the [Braket](https://github.com/amazon-braket/amazon-braket-sdk-python) backend.
+* `pulser`: the [Pulser](https://github.com/pasqal-io/Pulser) backend for composing, simulating and executing pulse sequences for neutral-atom quantum devices.
+* `visualization`: to display diagrammatically quantum circuits.
-* `braket`: install the Amazon Braket quantum backend
-* `emu-c`: install the Pasqal circuit tensor network emulator EMU-C
-* `pulser`: install the Pulser backend. Pulser is a framework for composing, simulating and executing pulse sequences for neutral-atom quantum devices.
-* `visualization`: install the library necessary to visualize quantum circuits.
+by running:
+
+```bash
+pip install qadence[braket, pulser, visualization]
+```
!!! warning
- In order to correctly install the "visualization" extra, you need to have `graphviz` installed
- in your system. This depends on the operating system you are using:
+ In order to correctly install the `visualization` extra, the `graphviz` package needs to be installed
+ in your system:
```bash
# on Ubuntu
diff --git a/docs/tutorials/backends.md b/docs/tutorials/backends.md
index 786044554..e6fd1f172 100644
--- a/docs/tutorials/backends.md
+++ b/docs/tutorials/backends.md
@@ -1,117 +1,100 @@
-Backends in Qadence are what make an abstract quantum circuit executable on different kinds of
-emulators and hardware **and** they make our circuits
-[differentiable](https://en.wikipedia.org/wiki/Automatic_differentiation). Under the hood they are
-what the `QuantumModel`s use.
+Backends allow execution of Qadence abstract quantum circuits. They could be chosen from a variety of simulators, emulators and hardware
+and can enable circuit [differentiability](https://en.wikipedia.org/wiki/Automatic_differentiation). The primary way to interact and configure
+a backcend is via the high-level API `QuantumModel`.
-In order to use the different backends you do not have to know anything about their implementation
-details. Qadence conveniently lets you specify the backend you want to run on in the `QuantumModel`.
-Some backends do not support all operations, for example the Braket backend cannot execute analog
-blocks, but Qadence will throw descriptive errors when you try to execute unsupported blocks.
+!!! note: "Not all backends are equivalent"
+ Not all backends support the same set of operations, especially while executing analog blocks.
+ Qadence will throw descriptive errors in such cases.
## Execution backends
-[_**PyQTorch**_](https://github.com/pasqal-io/PyQ): An efficient, large-scale emulator designed for
-quantum machine learning, seamlessly integrated with the popular PyTorch deep learning framework for automatic differentiability.
-Implementation details: [`PyQTorchBackend`][qadence.backends.pyqtorch.backend.Backend].
+[_**PyQTorch**_](https://github.com/pasqal-io/PyQ): An efficient, large-scale simulator designed for
+quantum machine learning, seamlessly integrated with the popular [PyTorch](https://pytorch.org/) deep learning framework for automatic differentiability.
+It also offers analog computing for time-independent pulses. See [`PyQTorchBackend`][qadence.backends.pyqtorch.backend.Backend].
-[_**Pulser**_](https://pulser.readthedocs.io/en/stable/): Library for pulse-level/analog control of
-neutral atom devices. Emulator via QuTiP.
+[_**Pulser**_](https://github.com/pasqal-io/Pulser): A Python library for pulse-level/analog control of
+neutral atom devices. Execution via [QuTiP](https://qutip.org/). See [`PulserBackend`][qadence.backends.pulser.backend.Backend].
-[_**Braket**_](https://github.com/aws/amazon-braket-sdk-python): A Python SDK for interacting with
+[_**Braket**_](https://github.com/aws/amazon-braket-sdk-python): A Python SDK for interacting with
quantum devices on Amazon Braket. Currently, only the devices with the digital interface of Amazon Braket
are supported and execution is performed using the local simulator. Execution on remote simulators and
-quantum processing units will be available soon.
+quantum processing units will be available soon. See [`BraketBackend`][qadence.backends.braket.backend.Backend]
-_**More**_: In the premium version of Qadence we provide even more backends such as a tensor network
-emulator. For more info write us at: [`info@pasqal.com`](mailto:info@pasqal.com).
+_**More**_: Proprietary Qadence extensions provide more high-performance backends based on tensor networks or differentiation engines.
+For more enquiries, please contact: [`info@pasqal.com`](mailto:info@pasqal.com).
-## Differentiation backends
+## Differentiation backend
-[`DifferentiableBackend`][qadence.backends.pytorch_wrapper.DifferentiableBackend] is the class
-that takes care of applying the different differentiation modes.
-In your scripts you only have to provide a `diff_mode` in the `QuantumModel` via
+The [`DifferentiableBackend`][qadence.backends.pytorch_wrapper.DifferentiableBackend] class enables different differentiation modes
+for the given backend. This can be chosen from two types:
-You can make any circuit differentiable using efficient and general parameter shift rules (PSRs).
-See [link](...) for more information on differentiability and PSR.
-```python
-QuantumModel(..., diff_mode="gpsr")
-```
+- Automatic differentiation (AD): available for PyTorch based backends (PyQTorch).
+- Parameter Shift Rules (PSR): available for all backends. See this [section](/advanced_tutorials/differentiability) for more information on differentiability and PSR.
+In practice, only a `diff_mode` should be provided in the `QuantumModel`. Please note that `diff_mode` defaults to `None`:
-??? note "Set up a circuit with feature parameters (defines the `circuit` function used below)."
- ```python exec="on" source="material-block" session="diff-backend"
- import sympy
- from qadence import Parameter, RX, RZ, CNOT, QuantumCircuit, chain
+```python exec="on" source="material-block" session="diff-backend"
+import sympy
+import torch
+from qadence import Parameter, RX, RZ, Z, CNOT, QuantumCircuit, QuantumModel, chain, BackendName, DiffMode
- def circuit(n_qubits: int):
- x = Parameter("x", trainable=False)
- y = Parameter("y", trainable=False)
- fm = chain(
- RX(0, 3 * x),
- RX(0, x),
- RZ(1, sympy.exp(y)),
- RX(0, 3.14),
- RZ(1, "theta")
- )
- ansatz = CNOT(0, 1)
- block = chain(fm, ansatz)
- return QuantumCircuit(2, block)
- ```
+x = Parameter("x", trainable=False)
+y = Parameter("y", trainable=False)
+fm = chain(
+ RX(0, 3 * x),
+ RX(0, x),
+ RZ(1, sympy.exp(y)),
+ RX(0, 3.14),
+ RZ(1, "theta")
+)
-!!! note "Make any circuit differentiable via PSR diff mode."
- ```python exec="on" source="material-block" result="json" session="diff-backend"
- import torch
- from qadence import QuantumModel, Z
+ansatz = CNOT(0, 1)
+block = chain(fm, ansatz)
- circuit = circuit(n_qubits=2)
- observable = Z(0)
+circuit = QuantumCircuit(2, block)
- # you can freely choose any backend with diff_mode="psr"
- # diff_mode="ad" will only work with natively differentiable backends.
- model = QuantumModel(circuit, observable, backend="pyqtorch", diff_mode="gpsr")
+observable = Z(0)
- # get some values for the feature parameters
- values = {"x": (x := torch.tensor([0.5], requires_grad=True)), "y": torch.tensor([0.1])}
+# DiffMode.GPSR is available for any backend.
+# DiffMode.AD is only available for natively differentiable backends.
+model = QuantumModel(circuit, observable, backend=BackendName.PYQTORCH, diff_mode=DiffMode.GPSR)
- # compute expectation
- e = model.expectation(values)
+# Get some values for the feature parameters.
+values = {"x": (x := torch.tensor([0.5], requires_grad=True)), "y": torch.tensor([0.1])}
- # differentiate it!
- g = torch.autograd.grad(e, x, torch.ones_like(e))
- print(f"{g = }") # markdown-exec: hide
- ```
+# Compute expectation.
+exp = model.expectation(values)
+# Differentiate the expectation wrt x.
+dexp_dx = torch.autograd.grad(exp, x, torch.ones_like(exp))
+print(f"{dexp_dx = }") # markdown-exec: hide
+```
-## Low-level `Backend` Interface
+## Low-level `backend_factory` interface
Every backend in `qadence` inherits from the abstract `Backend` class:
-[`Backend`](../backends/backend.md).
+[`Backend`](../backends/backend.md) and implement the following methods:
-All backends implement these methods:
+- [`run`][qadence.backend.Backend.run]: propagate the initial state according to the quantum circuit and return the final wavefunction object.
+- [`sample`][qadence.backend.Backend.sample]: sample from a circuit.
+- [`expectation`][qadence.backend.Backend.expectation]: computes the expectation of a circuit given an observable.
+- [`convert`][qadence.backend.Backend.convert]: convert the abstract `QuantumCircuit` object to its backend-native representation including a backend specific parameter embedding function.
-- [`run`][qadence.backend.Backend.run]: Propagate the initial state according to the quantum circuit and return the final wavefunction object.
-- [`sample`][qadence.backend.Backend.sample]: Sample from a circuit.
-- [`expectation`][qadence.backend.Backend.expectation]: Computes the expectation of a circuit given
- an observable.
-- [`convert`][qadence.backend.Backend.convert]: Convert the abstract `QuantumCircuit` object to
- its backend-native representation including a backend specific parameter embedding function.
+Backends are purely functional objects which take as input the values for the circuit
+parameters and return the desired output from a call to a method. In order to use a backend directly,
+*embedded* parameters must be supplied as they are returned by the backend specific embedding function.
-The quantum backends are purely functional objects which take as input the values of the circuit
-parameters and return the desired output. In order to use a backend directly, you need to supply
-*embedded* parameters as they are returned by the backend specific embedding function.
-
-To demonstrate how to use a backend directly we will construct a simple `QuantumCircuit` and run it
-on the Braket backend.
+Here is a simple demonstration of the use of the Braket backend to execute a circuit in non-differentiable mode:
```python exec="on" source="material-block" session="low-level-braket"
from qadence import QuantumCircuit, FeatureParameter, RX, RZ, CNOT, hea, chain
-# construct a featuremap
+# Construct a feature map.
x = FeatureParameter("x")
z = FeatureParameter("y")
fm = chain(RX(0, 3 * x), RZ(1, z), CNOT(0, 1))
-# circuit with hardware-efficient ansatz
+# Construct a circuit with an hardware-efficient ansatz.
circuit = QuantumCircuit(3, fm, hea(3,1))
```
@@ -121,23 +104,24 @@ backend.
```python exec="on" source="material-block" result="json" session="low-level-braket"
from qadence import backend_factory
-# use only Braket without differentiable backend by supplying `diff_mode=None`:
-backend = backend_factory("braket", diff_mode=None)
+# Use only Braket in non-differentiable mode:
+backend = backend_factory("braket")
# the `Converted` object
-# (contains a `ConvertedCircuit` wiht the original and native representation)
+# (contains a `ConvertedCircuit` with the original and native representation)
conv = backend.convert(circuit)
print(f"{conv.circuit.original = }")
print(f"{conv.circuit.native = }")
```
-Additionally `Converted` contains all fixed and variational parameters, as well as an embedding
-function which accepts feature parameters to construct a dictionary of *circuit native parameters*. These are needed since each backend uses a different representation of the circuit parameters under the hood:
+Additionally, `Converted` contains all fixed and variational parameters, as well as an embedding
+function which accepts feature parameters to construct a dictionary of *circuit native parameters*.
+These are needed as each backend uses a different representation of the circuit parameters:
```python exec="on" source="material-block" result="json" session="low-level-braket"
import torch
-# contains fixed parameters and variational (from the HEA)
+# Contains fixed parameters and variational (from the HEA)
conv.params
print("conv.params = {") # markdown-exec: hide
for k, v in conv.params.items(): print(f" {k}: {v}") # markdown-exec: hide
@@ -152,8 +136,9 @@ for k, v in embedded.items(): print(f" {k}: {v}") # markdown-exec: hide
print("}") # markdown-exec: hide
```
-Note that above the keys of the parameters have changed, because they now address the keys on the
-Braket device. A more readable embedding is the embedding of the PyQTorch backend:
+Note that above the parameters keys have changed as they now address the keys on the
+Braket device. A more readable embedding is provided by the PyQTorch backend:
+
```python exec="on" source="material-block" result="json" session="low-level-braket"
pyq_backend = backend_factory("pyqtorch", diff_mode="ad")
@@ -166,36 +151,36 @@ for k, v in embedded.items(): print(f" {k}: {v}") # markdown-exec: hide
print("}") # markdown-exec: hide
```
-With the embedded parameters we can call the methods we know from the `QuantumModel` like
-`backend.run`:
+With the embedded parameters, `QuantumModel` methods are accessible:
+
```python exec="on" source="material-block" result="json" session="low-level-braket"
embedded = conv.embedding_fn(conv.params, inputs)
samples = backend.run(conv.circuit, embedded)
print(f"{samples = }")
```
-### Even lower-level: Use the backend representation directly
+## Lower-level: the `Backend` representation
+
+If there is a requirement to work with a specific backend, it is possible to access _**directly the native circuit**_.
+For example, Braket noise features can be imported which are not exposed directly by Qadence.
-If you have to do things that are not currently supported by `qadence` but only by a specific backend
-itself, you can always _**work directly with the native circuit**_.
-For example, we can couple `qadence` directly with Braket noise features which are not exposed directly by Qadence.
```python exec="on" source="material-block" session="low-level-braket"
from braket.circuits import Noise
-# get the native Braket circuit with the given parameters
+# Get the native Braket circuit with the given parameters
inputs = {"x": torch.rand(1), "y":torch.rand(1)}
embedded = conv.embedding_fn(conv.params, inputs)
native = backend.assign_parameters(conv.circuit, embedded)
-# define a noise channel
+# Define a noise channel
noise = Noise.Depolarizing(probability=0.1)
-# add noise to every gate in the circuit
+# Add noise to every gate in the circuit
native.apply_gate_noise(noise)
```
-The density matrix simulator is needed in Braket to run this noisy circuit. Let's do the rest of the
-example using Braket directly.
+In order to run this noisy circuit, the density matrix simulator is needed in Braket:
+
```python exec="on" source="material-block" result="json" session="low-level-braket"
from braket.devices import LocalSimulator
diff --git a/docs/tutorials/getting_started.md b/docs/tutorials/getting_started.md
index 24ae3e2e2..297ece307 100644
--- a/docs/tutorials/getting_started.md
+++ b/docs/tutorials/getting_started.md
@@ -1,95 +1,112 @@
-Quantum programs in Qadence are constructed via a block-system, which makes it easily possible to
-compose small, *primitive* blocks to obtain larger, *composite* blocks. This approach is very
-different from how other frameworks (like Qiskit) construct circuits which follow an object-oriented
-approach.
+Quantum programs in Qadence are constructed via a block-system, with an emphasis on composability of
+*primitive* blocks to obtain larger, *composite* blocks. This functional approach is different from other frameworks
+which follow a more object-oriented way to construct circuits and express programs.
-## [`PrimitiveBlock`][qadence.blocks.primitive.PrimitiveBlock]
+??? note "Visualize blocks"
-A `PrimitiveBlock` is a basic operation such as a digital gate or an analog
-time-evolution block. This is the only concrete element of the block system
-and the program can always be decomposed into a list of `PrimitiveBlock`s.
+ There are two ways to display blocks in a Python interpreter: either as a tree in ASCII format using `print`
-Two examples of primitive blocks are the `X` and the `CNOT` gates:
+ ```python exec="on" source="material-block" html="1"
+ from qadence import X, Y, kron
+ print(kron(X(0), Y(1)))
+ ```
+
+ Or using the visualization package which opens an interactive window:
+
+ ```
+ from qadence import X, Y, kron
+ from visualisation import display
+
+ display(kron(X(0), Y(1)))
+
+ from qadence.draw import html_string # markdown-exec: hide
+ from qadence import chain # markdown-exec: hide
+ print(html_string(kron(X(0), Y(1))), size="2,2")) # markdown-exec: hide
+ ```
+
+## Primitive Blocks
+
+A [`PrimitiveBlock`][qadence.blocks.primitive.PrimitiveBlock] represents a digital or an analog time-evolution quantum operation applied to a qubit support.
+Programs can always be decomposed down into a sequence of `PrimitiveBlock` elements.
+
+Two canonical examples of digital primitive blocks are the parametrized `RX` and the `CNOT` gates:
```python exec="on" source="material-block" html="1"
from qadence import RX
-# a rotation gate on qubit 0
+# A rotation gate on qubit 0 with a fixed numerical parameter.
rx0 = RX(0, 0.5)
+
from qadence.draw import html_string # markdown-exec: hide
from qadence import chain # markdown-exec: hide
print(html_string(chain(rx0), size="2,2")) # markdown-exec: hide
```
+
```python exec="on" source="material-block" html="1"
from qadence import CNOT
-# a CNOT gate with control=0 and target=1
+# A CNOT gate with control on qubit 0 and target on qubit 1.
c01 = CNOT(0, 1)
from qadence.draw import html_string # markdown-exec: hide
from qadence import chain # markdown-exec: hide
print(html_string(chain(c01), size="2,2")) # markdown-exec: hide
```
-You can find a list of all instances of primitive blocks (also referred to as *operations*)
-[here](/qadence/operations.md).
+A list of all instances of primitive blocks (also referred to as *operations*) can be found [here](../qadence/operations.md).
+## Composite Blocks
-## [`CompositeBlock`][qadence.blocks.composite.CompositeBlock]
+Programs can be expressed by composing blocks to result in a larger [`CompositeBlock`][qadence.blocks.composite.CompositeBlock] using three fundamental operations:
+_chain_, _kron_, and _add_.
-Larger programs can be constructed from three operations:
-[`chain`][qadence.blocks.utils.chain],
-[`kron`][qadence.blocks.utils.kron], and
-[`add`][qadence.blocks.utils.add].
+- [**chain**][qadence.blocks.utils.chain] applies a set of blocks in sequence on the *same or overlapping qubit supports* and results in a `ChainBlock` type.
+It is akin to applying a matrix product of the sub-blocks with the `*` operator.
-[**`chain`**][qadence.blocks.utils.chain]ing blocks applies a set of sub-blocks in series, i.e. one
-after the other on the *same or different qubit support*. A `ChainBlock` is akin to applying a
-matrix product of the sub-blocks which is why it can also be used via the `*`-operator.
```python exec="on" source="material-block" html="1" session="i-xx"
from qadence import X, chain
-i = chain(X(0), X(0))
+# Chaining on the same qubit using a call to the function.
+chain_x = chain(X(0), X(0))
from qadence.draw import html_string # markdown-exec: hide
-print(html_string(i, size="2,2")) # markdown-exec: hide
+print(html_string(chain_x, size="2,2")) # markdown-exec: hide
```
```python exec="on" source="material-block" html="1" session="i-xx"
-xx = X(0) * X(1)
+# Chaining on different qubits using the operator overload.
+# Identical to the kron operation.
+chain_xx = X(0) * X(1)
from qadence.draw import html_string # markdown-exec: hide
-print(html_string(xx, size="2,2")) # markdown-exec: hide
+print(html_string(chain_xx, size="2,2")) # markdown-exec: hide
```
-??? note "Get the matrix of a block"
- You can always translate a block to its matrix representation. Note that the returned tensor
- contains a batch dimension because of parametric blocks.
- ```python exec="on" source="material-block" result="json" session="i-xx"
- print("X(0) * X(0)")
- print(i.tensor())
- print("\n") # markdown-exec: hide
- print("X(0) * X(1)")
- print(xx.tensor())
- ```
+- [**kron**][qadence.blocks.utils.kron] applies a set of blocks in parallel (simultaneously) on *disjoint qubit support* and results in a `KronBlock` type. This is akin to applying a tensor product of the sub-blocks with the `@` operator.
-In order to stack blocks (i.e. apply them simultaneously) you can use
-[**`kron`**][qadence.blocks.utils.kron]. A `KronBlock` applies a set of sub-blocks simultaneously on
-*different qubit support*. This is akin to applying a tensor product of the sub-blocks.
```python exec="on" source="material-block" html="1" session="i-xx"
from qadence import X, kron
-xx = kron(X(0), X(1))
-# equivalent to X(0) @ X(1)
+kron_xx = kron(X(0), X(1)) # Equivalent to X(0) @ X(1)
from qadence.draw import html_string # markdown-exec: hide
-print(html_string(xx, size="2,2")) # markdown-exec: hide
+print(html_string(kron_xx, size="2,2")) # markdown-exec: hide
```
-"But this is the same as `chain`ing!", you may say. And yes, for the digital case `kron` and `chain`
-have the same meaning apart from how they influence the plot of your block. However, Qadence also
-supports *analog* blocks, which need this concept of sequential/simultaneous blocks. To learn more
-about analog blocks check the [digital-analog](/digital_analog_qc/analog-basics) section.
-Finally, we have [**`add`**][qadence.blocks.utils.add]. This simply sums the corresponding matrix of
-each sub-block. `AddBlock`'s can also be used to construct Pauli operators.
+For the digital case, it should be noted that `kron` and `chain` are semantically equivalent up to the diagrammatic representation as `chain` implicitly fills blank wires with identities.
+However, Qadence also supports *analog* blocks, for which composing sequentially or in parallel becomes non-equivalent. More
+about analog blocks can be found in the [digital-analog](/digital_analog_qc/analog-basics) section.
-!!! warning
- Notice that `AddBlock`s can give rise to non-unitary blocks and thus might not be
- executed by all backends but only by certain simulators.
+- [**add**][qadence.blocks.utils.add] sums the corresponding matrix of
+each sub-block and results in a `AddBlock` type which can be used to construct Pauli operators.
+Please note that `AddBlock` can give rise to non-unitary computations that might not be supported by all backends.
+
+??? note "Get the matrix of a block"
+ It is always possible to retrieve the matrix representation of a block. Please note that the returned tensor
+ contains a batch dimension for the purposes of block parametrization.
+
+ ```python exec="on" source="material-block" result="json" session="i-xx"
+ print("X(0) * X(0)")
+ print(chain_x.tensor())
+ print("\n") # markdown-exec: hide
+ print("X(0) * X(1)")
+ print(chain_xx.tensor())
+ ```
```python exec="on" source="material-block" result="json"
from qadence import X, Z
@@ -98,153 +115,138 @@ xz = X(0) + Z(0)
print(xz.tensor())
```
-Finally, a slightly more complicated example.
+Finally, it is possible to tag blocks with human-readable names:
+
```python exec="on" source="material-block" html="1" session="getting_started"
from qadence import X, Y, CNOT, kron, chain, tag
-xy = chain(X(0), Y(1))
+xy = kron(X(0), Y(1))
tag(xy, "subblock")
composite_block = kron(xy, CNOT(3,4))
final_block = chain(composite_block, composite_block)
-# tag the block with a human-readable name
-tag(final_block, "my_block")
from qadence.draw import html_string # markdown-exec: hide
print(html_string(final_block, size="4,4")) # markdown-exec: hide
```
## Program execution
-### Quick, one-off execution
-To quickly run quantum operations and access wavefunctions, samples or expectation values of
-observables, one can use the convenience functions `run`, `sample` and `expectation`.
-More fine-grained control and better performance is provided via the `QuantumModel`.
-
-??? note "The quick and dirty way"
- Define a simple quantum program and perform some quantum operations on it:
- ```python exec="on" source="material-block" result="json" session="index"
- from qadence import chain, add, H, Z, run, sample, expectation
-
- n_qubits = 2
- block = chain(H(0), H(1))
-
- # compute wavefunction with the `pyqtorch` backend
- # check the documentation for other available backends!
- wf = run(block)
- print(f"{wf = }") # markdown-exec: hide
-
- # sample the resulting wavefunction with a given number of shots
- xs = sample(block, n_shots=1000)
- print(f"{xs = }") # markdown-exec: hide
-
- # compute an expectation based on an observable
- obs = add(Z(i) for i in range(n_qubits))
- ex = expectation(block, obs)
- print(f"{ex = }") # markdown-exec: hide
- ```
+### Fast block execution
-### Proper execution via `QuantumCircuit` and `QuantumModel`
+To quickly run quantum operations and access wavefunctions, samples or expectation values of
+observables, one can use the convenience functions `run`, `sample` and `expectation`. The following
+example shows an execution workflow with the natively available `PyQTorch` backend:
+
+```python exec="on" source="material-block" result="json" session="index"
+from qadence import chain, add, H, Z, run, sample, expectation
+
+n_qubits = 2
+block = chain(H(0), H(1))
+
+# Compute the wavefunction.
+# Please check the documentation for other available backends.
+wf = run(block)
+print(f"{wf = }") # markdown-exec: hide
+
+# Sample the resulting wavefunction with a given number of shots.
+xs = sample(block, n_shots=1000)
+print(f"{xs = }") # markdown-exec: hide
+
+# Compute an expectation based on an observable of Pauli-Z operators.
+obs = add(Z(i) for i in range(n_qubits))
+ex = expectation(block, obs)
+print(f"{ex = }") # markdown-exec: hide
+```
-Quantum programs in qadence are constructed in two steps:
+More fine-grained control and better performance is provided via the high-level `QuantumModel` abstraction.
-1. Define a `QuantumCircuit` which ties together a block and a register to a well-defined circuit.
-2. Define a `QuantumModel` which takes care of compiling and executing the circuit.
+### Execution via `QuantumCircuit` and `QuantumModel`
-#### 1. [`QuantumCircuit`][qadence.circuit.QuantumCircuit]s
+Quantum programs in Qadence are constructed in two steps:
-The `QuantumCircuit` is one of the central classes in Qadence. For example, to specify the `Register`
-to run your block on you use a `QuantumCircuit` (under the hood the functions above were already
-using `QuantumCircuits` with a `Register` that fits the qubit support of the given block).
+1. Build a [`QuantumCircuit`][qadence.circuit.QuantumCircuit] which ties together a composite block and a register.
+2. Define a [`QuantumModel`](/tutorials/quantumodels) which differentiates, compiles and executes the circuit.
-The `QuantumCircuit` ties a block together with a register.
+`QuantumCircuit` is a central class in Qadence and circuits are abstract
+objects from the actual hardware/simulator that they are expected to be executed on.
+They require to specify the `Register` of resources to execute your program on. Previous examples
+were already using `QuantumCircuit` with a `Register` that fits the qubit support for the given block.
```python exec="on" source="material-block" result="json"
from qadence import QuantumCircuit, Register, H, chain
-# NOTE: we run a block which supports two qubits
-# on a register with three qubits
-reg = Register(3)
-circ = QuantumCircuit(reg, chain(H(0), H(1)))
-print(circ) # markdown-exec: hide
+# NOTE: Run a block which supports two qubits
+# on a register of three qubits.
+register = Register(3)
+circuit = QuantumCircuit(register, chain(H(0), H(1)))
+print(circuit) # markdown-exec: hide
```
-!!! note "`Register`s"
- Registers can also be constructed e.g. from qubit coordinates to create arbitrary register
- layouts, but more on that in the [digital-analog](/digital_analog_qc/analog-basics.md) section.
-
-
-#### 2. [`QuantumModel`](/tutorials/quantumodels)s
+!!! note "Registers and qubit supports"
+ Registers can also be constructed from qubit coordinates to create arbitrary register
+ topologies. See details in the [digital-analog](/digital_analog_qc/analog-basics.md) section.
+ Qubit supports are subsets of the circuit register tied to blocks.
-`QuantumModel`s are another central class in Qadence's library. Blocks and circuits are completely abstract
-objects that have nothing to do with the actual hardware/simulator that they are running on. This is
-where the `QuantumModel` comes in. It contains a [`Backend`](/tutorials/backend.md) and a
-compiled version of your abstract circuit (constructed by the backend).
-The `QuantumModel` is also what makes our circuit *differentiable* (either via automatic
-differentiation, or on hardware via parameter shift rule).
+`QuantumModel` is another central class in Qadence. It specifies a [Backend](/tutorials/backend.md) for
+the differentiation, compilation and execution of the abstract circuit.
```python exec="on" source="material-block" result="json"
-from qadence import QuantumCircuit, QuantumModel, Register, H, chain
+from qadence import BackendName, DiffMode, QuantumCircuit, QuantumModel, Register, H, chain
reg = Register(3)
circ = QuantumCircuit(reg, chain(H(0), H(1)))
-model = QuantumModel(circ, backend="pyqtorch", diff_mode='ad')
+model = QuantumModel(circ, backend=BackendName.PYQTORCH, diff_mode=DiffMode.AD)
xs = model.sample(n_shots=100)
-print(f"{xs = }")
+print(f"{xs = }") # markdown-exec: hide
```
-For more details on how to use `QuantumModel`s, see [here](/tutorials/quantummodels).
-
+For more details on `QuantumModel`, see [here](/tutorials/quantummodels).
## State initialization
-!!! warning "moved here from another page; improve?"
- #### Quantum state preparation
+Qadence offers convenience routines for preparing initial quantum states.
+These routines are divided into two approaches:
- Qadence offers some convenience routines for preparing the initial quantum state.
- These routines are divided into two approaches:
- * generate the initial state as a dense matrix (routines with `_state` postfix).
- This only works for backends which support state vectors as inputs, currently
- only PyQ.
- * generate the initial state from a suitable quantum circuit (routines with
- `_block` postfix). This is available for every backend and it should be added
- in front of the desired quantum circuit to simulate.
+- As a dense matrix.
+- From a suitable quantum circuit. This is available for every backend and it should be added
+in front of the desired quantum circuit to simulate.
- Let's illustrate the usage of the state preparation routine. For more details,
- please refer to the [API reference](/qadence/index).
+Let's illustrate the usage of the state preparation routine. For more details,
+please refer to the [API reference](/qadence/index).
- ```python exec="on" source="material-block" result="json" session="seralize"
- from qadence import random_state, product_state, is_normalized, StateGeneratorType
+```python exec="on" source="material-block" result="json" session="seralize"
+from qadence import random_state, product_state, is_normalized, StateGeneratorType
- # random initial state
- # the default `type` is StateGeneratorType.HaarMeasureFast
- state = random_state(n_qubits=2, type=StateGeneratorType.RANDOM_ROTATIONS)
- print(f"Random initial state generated with rotations:\n {state.detach().numpy().flatten()}")
+# Random initial state.
+# the default `type` is StateGeneratorType.HaarMeasureFast
+state = random_state(n_qubits=2, type=StateGeneratorType.RANDOM_ROTATIONS)
+print(f"{Random initial state generated with rotations:\n {state.detach().numpy().flatten()}}") # markdown-exec: hide
- # check the normalization
- assert is_normalized(state)
-
- # product state from a given bitstring
- # remember that qadence follows the big endian convention
- state = product_state("01")
- print(f"Product state corresponding to bitstring '10':\n {state.detach().numpy().flatten()}")
- ```
+# Check the normalization.
+assert is_normalized(state)
+# Product state from a given bitstring.
+# NB: Qadence follows the big endian convention.
+state = product_state("01")
+print(f"{Product state corresponding to bitstring '10':\n {state.detach().numpy().flatten()}}") # markdown-exec: hide
+```
- Now we see how to generate the product state corresponding to the one above with
- a suitable quantum circuit.
- ```python
- from qadence import product_block, tag, QuantumCircuit
+Now we see how to generate the product state corresponding to the one above with
+a suitable quantum circuit.
- state_prep_b = product_block("10")
- display(state_prep_b)
+```python
+from qadence import product_block, tag, QuantumCircuit
- # let's now prepare a circuit
- state_prep_b = product_block("1000")
- tag(state_prep_b, "prep")
- qc_with_state_prep = QuantumCircuit(4, state_prep_b, fourier_b, hea_b)
+state_prep_b = product_block("10")
+display(state_prep_b)
- display(qc_with_state_prep)
- ```
+# let's now prepare a circuit
+state_prep_b = product_block("1000")
+tag(state_prep_b, "prep")
+qc_with_state_prep = QuantumCircuit(4, state_prep_b, fourier_b, hea_b)
+
+print(html_string(qc_with_state_prep), size="4,4")) # markdown-exec: hide
+```
diff --git a/docs/tutorials/hamiltonians.md b/docs/tutorials/hamiltonians.md
index ecae72503..0b3542303 100644
--- a/docs/tutorials/hamiltonians.md
+++ b/docs/tutorials/hamiltonians.md
@@ -1,10 +1,11 @@
# Constructing arbitrary Hamiltonians
-A big part of working with digital-analog quantum computing is handling large analog blocks, which represent a set of interacting qubits under some interaction Hamiltonian. In `qadence` we can use the [`hamiltonian_factory`](../qadence/constructors.md) function to create arbitrary Hamiltonian blocks to be used as generators of `HamEvo` or as observables to be measured.
+At the heart of digital-analog quantum computing is the description and execution of analog blocks, which represent a set of interacting qubits under some interaction Hamiltonian.
+For this purpose, Qadence relies on the [`hamiltonian_factory`](../qadence/constructors.md) function to create arbitrary Hamiltonian blocks to be used as generators of `HamEvo` or as observables to be measured.
## Arbitrary all-to-all Hamiltonians
-Arbitrary all-to-all interaction Hamiltonians can be easily created by passing the number of qubits in the first argument. The type of `interaction` can be chosen from the available ones in the [`Interaction`](../qadence/types.md) enum. Alternatively, the strings `"ZZ", "NN", "XY", "XYZ"` can also be used.
+Arbitrary all-to-all interaction Hamiltonians can be easily created by passing the number of qubits in the first argument. The type of `interaction` can be chosen from the available ones in the [`Interaction`](../qadence/types.md) enum type.
```python exec="on" source="material-block" result="json" session="hamiltonians"
from qadence import hamiltonian_factory
@@ -15,7 +16,7 @@ n_qubits = 3
hamilt = hamiltonian_factory(n_qubits, interaction = Interaction.ZZ)
-print(hamilt)
+print(hamilt) # markdown-exec: hide
```
Single-qubit terms can also be added by passing the respective operator directly to the `detuning` argument. For example, the total magnetization is commonly used as an observable to be measured:
@@ -25,7 +26,7 @@ total_mag = hamiltonian_factory(n_qubits, detuning = Z)
print(total_mag) # markdown-exec: hide
```
-For further customization, arbitrary coefficients can be passed as arrays to the `interaction_strength` and `detuning_strength` arguments.
+For further customization, arbitrary coefficients can be passed as arrays to the `interaction_strength` and `detuning_strength` arguments for the two-qubits and single-qubit terms respectively.
```python exec="on" source="material-block" result="json" session="hamiltonians"
n_qubits = 3
@@ -40,15 +41,17 @@ hamilt = hamiltonian_factory(
print(hamilt) # markdown-exec: hide
```
-To get random interaction coefficients between -1 and 1, you can ommit `interaction_strength` and `detuning_strength` and simply pass `random_strength = True`.
-Note that for passing interaction strengths as an array, you should order them in the same order obtained from the `edge` property of a Qadence [`Register`](register.md):
+!!! warning "Ordering interaction strengths matters"
-```python exec="on" source="material-block" result="json" session="hamiltonians"
-from qadence import Register
+ When passing interaction strengths as an array, the ordering must be indentical to the one
+ obtained from the `edge` property of a Qadence [`Register`](register.md):
-print(Register(n_qubits).edges)
-```
+ ```python exec="on" source="material-block" result="json" session="hamiltonians"
+ from qadence import Register
+
+ print(Register(n_qubits).edges)
+ ```
For one more example, let's create a transverse-field Ising model,
@@ -73,10 +76,15 @@ x_ham = hamiltonian_factory(n_qubits, detuning = X, detuning_strength = x_terms)
transverse_ising = zz_ham + x_ham
```
+!!! note "Random interaction coefficients"
+ Random interaction coefficients can be chosen between -1 and 1 by simply passing `random_strength = True` instead of `detuning_strength`
+ and `interaction_strength`.
+
-## Changing the Hamiltonian topology
+## Arbitrary Hamiltonian topologies
-We can also create arbitrary interaction topologies using the Qadence [`Register`](register.md). To do so, simply pass the register with the desired topology as the first argument.
+Arbitrary interaction topologies can be created using the Qadence [`Register`](register.md).
+Simply pass the register with the desired topology as the first argument to the `hamiltonian_factory`:
```python exec="on" source="material-block" result="json" session="hamiltonians"
from qadence import Register
@@ -87,7 +95,7 @@ square_hamilt = hamiltonian_factory(reg, interaction = Interaction.NN)
print(square_hamilt) # markdown-exec: hide
```
-If you wish to add specific coefficients to the Hamiltonian, you can either pass them as shown earlier, or add them to the register beforehand using the `"strength"` key.
+Custom Hamiltonian coefficients can also be added to the register beforehand using the `"strength"` key.
```python exec="on" source="material-block" result="json" session="hamiltonians"
@@ -100,11 +108,13 @@ square_hamilt = hamiltonian_factory(reg, interaction = Interaction.NN)
print(square_hamilt) # markdown-exec: hide
```
-Alternatively, if your register already has saved interaction or detuning strengths but you wish to override them in the Hamiltonian creation, you can use `force_update = True`.
+Alternatively, if the register already stores interaction or detuning strengths, it is possible to override them in the Hamiltonian creation by using `force_update = True`.
+
## Adding variational parameters
-Finally, we can also easily create fully parameterized Hamiltonians by passing a string to the strength arguments. Below we create a fully parametric neutral-atom Hamiltonian,
+Finally, fully parameterized Hamiltonians can be created by passing a string to the strength arguments:
+
```python exec="on" source="material-block" result="json" session="hamiltonians"
n_qubits = 3
diff --git a/docs/tutorials/ml_tools.md b/docs/tutorials/ml_tools.md
new file mode 100644
index 000000000..281d00680
--- /dev/null
+++ b/docs/tutorials/ml_tools.md
@@ -0,0 +1,154 @@
+`qadence` also offers a out-of-the-box training routine called `train_with_grad`
+for optimizing fully-differentiable models like `QNN`s and `QuantumModel`s containing either *trainable* and/or *non-trainable* parameters (i.e., inputs). Feel free to [refresh your memory about different parameter types](/tutorials/parameters).
+
+## ML tools Basics
+
+`train_with_grad` performs training, logging/printing loss metrics and storing intermediate checkpoints of models.
+
+As every other training routine commonly used in Machine Learning, it requires
+`model`, `data` and an `optimizer` as input arguments.
+However, in addition, it requires a `loss_fn` and a `TrainConfig`.
+A `loss_fn` is required to be a function which expects both a model and data and returns a tuple of (loss, metrics: dict), where `metrics` is a dict of scalars which can be customized too.
+
+```python exec="on" source="material-block" result="json"
+import torch
+from itertools import count
+cnt = count()
+criterion = torch.nn.MSELoss()
+
+def loss_fn(model: torch.nn.Module, data: torch.Tensor) -> tuple[torch.Tensor, dict]:
+ next(cnt)
+ x, y = data[0], data[1]
+ out = model(x)
+ loss = criterion(out, y)
+ return loss, {}
+
+```
+
+The `TrainConfig` [qadence.ml_tools.config] tells `train_with_grad` what batch_size should be used, how many epochs to train, in which intervals to print/log metrics and how often to store intermediate checkpoints.
+
+```python exec="on" source="material-block" result="json"
+from qadence.ml_tools import TrainConfig
+
+batch_size = 5
+n_epochs = 100
+
+config = TrainConfig(
+ folder="some_path/",
+ max_iter=n_epochs,
+ checkpoint_every=100,
+ write_every=100,
+ batch_size=batch_size,
+)
+
+```
+## Fitting a funtion with a QNN using ml_tools
+
+Let's look at a complete example of how to use `train_with_grad` now.
+
+```python exec="on" source="material-block" result="json"
+from pathlib import Path
+import torch
+from itertools import count
+from qadence.constructors import total_magnetization, hea, feature_map
+from qadence import chain, Parameter, QuantumCircuit
+from qadence.models import QNN
+from qadence.ml_tools import train_with_grad, TrainConfig
+import matplotlib.pyplot as plt
+
+n_qubits = 2
+fm = feature_map(n_qubits)
+ansatz = hea(n_qubits=n_qubits, depth=3)
+observable = total_magnetization(n_qubits)
+circuit = QuantumCircuit(n_qubits, fm, ansatz)
+
+model = QNN(circuit, observable, backend="pyqtorch", diff_mode="ad")
+batch_size = 1
+input_values = {"phi": torch.rand(batch_size, requires_grad=True)}
+pred = model(input_values)
+
+cnt = count()
+criterion = torch.nn.MSELoss()
+optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
+
+def loss_fn(model: torch.nn.Module, data: torch.Tensor) -> tuple[torch.Tensor, dict]:
+ next(cnt)
+ x, y = data[0], data[1]
+ out = model(x)
+ loss = criterion(out, y)
+ return loss, {}
+
+tmp_path = Path("/tmp")
+
+n_epochs = 5
+
+config = TrainConfig(
+ folder=tmp_path,
+ max_iter=n_epochs,
+ checkpoint_every=100,
+ write_every=100,
+ batch_size=batch_size,
+)
+
+batch_size = 25
+
+x = torch.linspace(0, 1, batch_size).reshape(-1, 1)
+y = torch.sin(x)
+
+train_with_grad(model, (x, y), optimizer, config, loss_fn=loss_fn)
+
+plt.plot(y.numpy())
+plt.plot(model(input_values).detach().numpy())
+
+```
+
+For users who want to use the low-level API of `qadence`, here is the example from above
+written without `train_with_grad`.
+
+## Fitting a function - Low-level API
+
+```python exec="on" source="material-block" result="json"
+from pathlib import Path
+import torch
+from itertools import count
+from qadence.constructors import total_magnetization, hea, feature_map
+from qadence import chain, Parameter, QuantumCircuit
+from qadence.models import QNN
+from qadence.ml_tools import train_with_grad, TrainConfig
+
+n_qubits = 2
+fm = feature_map(n_qubits)
+ansatz = hea(n_qubits=n_qubits, depth=3)
+observable = total_magnetization(n_qubits)
+circuit = QuantumCircuit(n_qubits, fm, ansatz)
+
+model = QNN(circuit, observable, backend="pyqtorch", diff_mode="ad")
+batch_size = 1
+input_values = {"phi": torch.rand(batch_size, requires_grad=True)}
+pred = model(input_values)
+
+criterion = torch.nn.MSELoss()
+optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
+n_epochs=50
+cnt = count()
+
+tmp_path = Path("/tmp")
+
+config = TrainConfig(
+ folder=tmp_path,
+ max_iter=n_epochs,
+ checkpoint_every=100,
+ write_every=100,
+ batch_size=batch_size,
+)
+
+x = torch.linspace(0, 1, batch_size).reshape(-1, 1)
+y = torch.sin(x)
+
+for i in range(n_epochs):
+ out = model(x)
+ loss = criterion(out, y)
+ loss.backward()
+ optimizer.step()
+
+```
diff --git a/docs/tutorials/overlap.md b/docs/tutorials/overlap.md
index c4aabf6dd..f07657931 100644
--- a/docs/tutorials/overlap.md
+++ b/docs/tutorials/overlap.md
@@ -1,14 +1,14 @@
-`qadence` offers some convenience functions for computing the overlap between the
+Qadence offers convenience functions for computing the overlap between the
wavefunctions generated by two quantum circuits. We define the overlap between
the wavefunction generated by the circuits $U$ and $W$ as:
$$
-S = |\langle \psi_U | \psi_W \rangle|^2 \;\; \textrm{where} \; \psi_U = U|\psi_0\rangle
+S = |\langle \psi_U | \psi_W \rangle|^2 \quad \textrm{where} \quad \psi_U = U|\psi_0\rangle
$$
-Let's jump right in and see how to compute the overlap between two very simple parametric circuits
-consisting of a single `RX` rotation on different qubits. We expect the overlap to be
-non-zero only when the rotation angle is different from $\pi$ for both rotations:
+Here is an example on how to compute the overlap between two very simple parametric circuits
+consisting of a single `RX` rotation on different qubits. The overlap is expected to be
+non-zero only when the rotation angle is different from $\pi \quad \textrm{mod}\; 2\pi$ for both rotations:
```python exec="on" source="material-block" result="json" session="overlap"
import torch
@@ -16,7 +16,7 @@ import numpy as np
from qadence import Overlap, OverlapMethod, QuantumCircuit, H, RX, X, FeatureParameter, hea
-# let's create two quantum circuits
+# Create two quantum circuits
# with a single qubit rotation on two random qubits
n_qubits = 4
qubits = np.random.choice(list(range(n_qubits)), n_qubits, replace=True)
@@ -27,29 +27,28 @@ circuit_bra = QuantumCircuit(n_qubits, RX(qubits[0], phi))
psi = FeatureParameter("psi")
circuit_ket = QuantumCircuit(n_qubits, RX(qubits[1], psi))
-# values for the feature parameters
+# Values for the feature parameters
values_bra = {"phi": torch.Tensor([torch.pi / 2, torch.pi])}
values_ket = {"psi": torch.Tensor([torch.pi / 2, torch.pi])}
-# calculate overlap by assigning values to the given bra and ket circuits
+# Calculate overlap by assigning values to the given bra and ket circuits
ovrlp = Overlap(circuit_bra, circuit_ket)
ovrlp = ovrlp(bra_param_values=values_bra, ket_param_values=values_ket)
print("Overlap with exact method:\n", ovrlp)
```
-The `Overlap` class above inherits from `QuantumModel` and its forward method
-computes the overlap given input parameter values. By default,
+The `Overlap` class above inherits from `QuantumModel` and is executed through its inherited forward method
+for the given input parameter values. By default,
the overlap is computed exactly by performing the dot product of the wavefunction propagated
-from the bra and ket circuits.
+from bra and ket circuits.
-However, one can use the `OverlapMethod` enumeration
-to choose which kind of overlap to compute via the `overlap_method` argument of the
-overlap constructor class. Currently, one can choose from:
+However, it is possible to choose a different method from the `OverlapMethod` enumeration
+to be passed via the `overlap_method` argument in the `Overlap` initializer.
+Currently, one can choose from:
-* `EXACT`: exact computation using the wavefunction matrix representation. Does not work with
-on real devices since it assumes access to the full qubit system wavefunction.
-* `COMPUTE_UNCOMPUTE`: exact or sampling-based computation using brak $U$ and ket $W^{\dagger}$ unitaries.
+* `EXACT`: exact computation using the wavefunction matrix representation. Does not work with real devices since it assumes access to the complete qubit system wavefunction.
+* `COMPUTE_UNCOMPUTE`: exact or sampling-based computation using bra $U$ and ket $W^{\dagger}$ unitaries.
* `SWAP_TEST`: exact or sampling-based computation using the SWAP test method.
* `HADAMARD_TEST`: exact or sampling-based computation using the Hadamard test method.
* `JENSEN_SHANNON`: compute the overlap using the Jensen-Shannon divergence of the two
@@ -64,12 +63,12 @@ for performing shot-based calculations.
it cannot be used as output of a quantum model if gradients are required.
```python exec="on" source="material-block" result="json" session="overlap"
-# calculate overlap with SWAP test
+# Calculate overlap with SWAP test
ovrlp = Overlap(circuit_bra, circuit_ket, method=OverlapMethod.SWAP_TEST)
ovrlp_ha = ovrlp(values_bra, values_ket)
print("Overlap with SWAP test:\n", ovrlp_ha)
-# calculate overlap with SWAP test
+# Calculate overlap with SWAP test
# using a finite number of shots
ovrlp = Overlap(circuit_bra, circuit_ket, method=OverlapMethod.SWAP_TEST)
ovrlp_ha = ovrlp(values_bra, values_ket, n_shots=10_000)
diff --git a/docs/tutorials/parameters.md b/docs/tutorials/parameters.md
index 2a71f2937..1b561f23e 100644
--- a/docs/tutorials/parameters.md
+++ b/docs/tutorials/parameters.md
@@ -1,6 +1,98 @@
-```python exec="on" html="1"
+There are three kinds of parameters in `qadence`:
+[_**Fixed Parameter**_]: A constant with a fixed, non-trainable value (e.g. pi/2).
+[_**Variational Parameter**_]: A trainable parameter which can be be optimized.
+[_**Feature Parameter**_]: A non-trainable parameter which can be used to encode classical data into a quantum state.
+
+## Parametrized Blocks
+### Fixed Parameters
+To pass a fixed parameter to a gate, we can simply use either python numeric types by themselves or wrapped in
+a torch.tensor.
+```python exec="on" source="material-block" result="json"
+from torch import pi
+from qadence import RX, run
+
+# Let's use a torch type.
+block = RX(0, pi)
+wf = run(block)
+print(wf)
+
+# Lets pass a simple float.
+print(run(RX(0, 1.)))
+```
+
+### Variational Parameters
+To parametrize a block by an angle `theta`, you can pass either a string or an instance of `VariationalParameter` instead of a numeric type to the gate constructor:
+
+```python exec="on" source="material-block" result="json"
+from qadence import RX, run, VariationalParameter
+
+block = RX(0, "theta")
+# This is equivalent to:
+block = RX(0, VariationalParameter("theta"))
+
+wf = run(block)
+print(wf)
+```
+In the first case in the above example, `theta` is automatically inferred as a `VariationalParameter` (i.e., a trainable one), hence we do not have to pass a value for `theta` to the `run` method since its stored within the underlying model!
+
+### Feature Parameters
+
+However, for `FeatureParameter`s (i.e, inputs), we always have to provide a value. And, in contrast to `VariationalParameter`s, we can also provide a batch of values.
+
+```python exec="on" source="material-block" result="json"
+from torch import tensor
+from qadence import RX, run, FeatureParameter
+
+block = RX(0, FeatureParameter("phi"))
+
+wf = run(block, values={"phi": tensor([1., 2.])})
+print(wf)
+```
+
+Now, we see that `run` returns a batch of states, one for every provided angle.
+In the above case, the angle of the `RX` gate coincides with the value of the particular `FeatureParameter`.
+
+### Multiparameter Expressions
+However, an angle can itself also be a function of `Parameter`- types (fixed, trainable and non-trainable).
+We can pass any sympy expression `expr: sympy.Basic` consisting of a combination of free symbols (`sympy` types) and qadence `Parameter`s to a block. This also includes, e.g., trigonometric functions!
+
+```python exec="on" source="material-block" result="json"
+from torch import tensor
+from qadence import RX, Parameter, run, FeatureParameter
+from sympy import sin
+
+theta, phi = Parameter("theta"), FeatureParameter("phi")
+block = RX(0, sin(theta+phi))
+
+# Remember, to run the block, only the FeatureParameters have to be provided:
+values = {"phi": tensor([1.0, 2.0])}
+wf = run(block, values=values)
+print(wf)
+```
+
+### Re-using Parameters
+
+Parameters are uniquely defined by their name, so you can repeat a parameter in a composite block to
+assign the same parameter to different blocks.
+```python exec="on" source="material-block" result="json"
import torch
+from qadence import RX, RY, run, chain, kron
+
+block = chain(
+ kron(RX(0, "phi"), RY(1, "theta")),
+ kron(RX(0, "phi"), RY(1, "theta")),
+)
+
+wf = run(block)
+print(wf)
+```
+
+## Parametrized Circuits
+
+Now, let's have a look at a variational ansatz in `qadence`.
+
+```python exec="on" html="1"
import sympy
from qadence import RX, RY, RZ, CNOT, Z, run, chain, kron, FeatureParameter, VariationalParameter
@@ -39,99 +131,41 @@ from qadence.draw import html_string # markdown-exec: hide
print(html_string(block)) # markdown-exec: hide
```
+## Parametrized QuantumModels
-## Parametrized blocks
-
-To parametrize a block simply by an angle `x` you can pass a string instead of
-a fixed float to the gate constructor:
-
-```python exec="on" source="material-block" result="json"
-import torch
-from qadence import RX, run
-
-# fixed rotation
-# block = RX(0, 2.0)
-
-# parametrised rotation
-block = RX(0, "x")
-
-wf = run(block, values={"x": torch.tensor([1.0, 2.0])})
-print(wf)
-```
-Above you can see that `run` returns a batch of states, one for every provided angle.
-You can provide any sympy expression `expr: sympy.Basic` to a block, e.g. also one with multiple
-free symbols.
-```python exec="on" source="material-block" result="json"
-import torch
-from qadence import RX, Parameter, run
-
-x, y = Parameter("x"), Parameter("y")
-block = RX(0, x+y)
-
-# to run the block, both parameters have to be given
-values = {"x": torch.tensor([1.0, 2.0]), "y": torch.tensor([2.0, 1.0])}
-wf = run(block, values=values)
-print(wf)
-```
-
-Parameters are uniquely defined by their name, so you can repeat a parameter in a composite block to
-assign the same parameter to different blocks.
-```python exec="on" source="material-block" result="json"
-import torch
-from qadence import RX, RY, run, chain, kron
-
-block = chain(
- kron(RX(0, "phi"), RY(1, "theta")),
- kron(RX(0, "phi"), RY(1, "theta")),
-)
-
-values = {"phi": torch.rand(3), "theta": torch.tensor(3)}
-wf = run(block, values=values)
-print(wf)
-```
-
-## Parametrized models
-
-In quantum models we distinguish between two kinds of parameters:
-
-* _**Feature**_ parameters are used for data input and encode data into the quantum state.
+Recap:
+* _**Feature**_ parameters are used for data input and encode data into a quantum state.
* _**Variational**_ parameters are trainable parameters in a variational ansatz.
-
-As a reminder, in `qadence` a [`QuantumModel`][qadence.models.quantum_model.QuantumModel] takes an
+* [`QuantumModel`][qadence.models.quantum_model.QuantumModel] takes an
abstract quantum circuit and makes it differentiable with respect to variational and feature
parameters.
+* Both `VariationalParameter`s and `FeatureParameter`s are uniquely identified by their name.
-Again, both variational and feature parameters are uniquely identified by their name.
```python exec="on" source="material-block" session="parametrized-models"
-from qadence import VariationalParameter, FeatureParameter, Parameter
-
-p1 = VariationalParameter("theta")
-p2 = FeatureParameter("phi")
-
-p1_dup = VariationalParameter("theta")
-p2_dup = FeatureParameter("phi")
+from qadence import FeatureParameter, Parameter, VariationalParameter
-assert p1 == p1_dup
-assert p2 == p2_dup
-
-# feature parameters are non-trainable parameters - meaning
-# they can be specified via input data. The FeatureParameter
-# is therefore exactly the same as a non-trainable parameter
+# Feature parameters are non-trainable parameters.
+# Their primary use is input data encoding.
fp = FeatureParameter("x")
assert fp == Parameter("x", trainable=False)
-# variational parameters are trainable parameters
+# Variational parameters are trainable parameters.
+# Their primary use is for optimization.
vp = VariationalParameter("y")
assert vp == Parameter("y", trainable=True)
```
-Let's see them first in a quantum circuit.
+Let's construct a parametric quantum circuit.
+
```python exec="on" source="material-block" result="json" session="parametrized-models"
from qadence import QuantumCircuit, RX, RY, chain, kron
+theta = VariationalParameter("theta")
+phi = FeatureParameter("phi")
+
block = chain(
- kron(RX(0, p1), RY(1, p1)),
- kron(RX(0, p2), RY(1, p2)),
+ kron(RX(0, theta), RY(1, theta)),
+ kron(RX(0, phi), RY(1, phi)),
)
circuit = QuantumCircuit(2, block)
@@ -139,22 +173,25 @@ circuit = QuantumCircuit(2, block)
print("Unique parameters in the circuit: ", circuit.unique_parameters)
```
-In the circuit above, we define 4 parameters but only 2 unique names. Therefore, the number of
-variational parameters picked up by the optimizer in the resulting quantum model will be just 1. The
-`QuantumModel` class provides some convenience methods to deal with parameters.
+In the circuit above, four parameters are defined but only two unique names. Therefore, there will be only one
+variational parameter to be optimized.
+
+The `QuantumModel` class also provides convenience methods to manipulate parameters.
```python exec="on" source="material-block" result="json" session="parametrized-models"
-from qadence import QuantumModel
+from qadence import QuantumModel, BackendName, DiffMode
-model = QuantumModel(circuit, backend="pyqtorch", diff_mode="ad")
+model = QuantumModel(circuit, backend=BackendName.PYQTORCH, diff_mode=DiffMode.AD)
print(f"Number of variational parameters: {model.num_vparams}")
print(f"Current values of the variational parameters: {model.vparams}")
```
-!!! note "Only provide feature parameters to the quantum model!"
- In order to `run` the variational circuit we have to _**provide only feature parameters**_, because
- the variational parameters are stored in the model itself.
+!!! note "Only provide feature parameter values to the quantum model"
+ In order to `run` the variational circuit _**only feature parameter values**_ have to be provided.
+ Variational parameters are stored in the model itself. If multiple feature parameters are present,
+ values must be provided in batches of same length.
+
```python exec="on" source="material-block" result="json" session="parametrized-models"
import torch
@@ -163,11 +200,11 @@ print(f"Current values of the variational parameters: {model.vparams}")
print(wf)
```
-## Usage with standard constructors
+## Standard constructors
-The unique parameter identification explained above is important when using built-in `qadence` block
-constructors in the `qadence.constructors` such as feature maps and hardware
-efficient ansatze. Let's see it in practice:
+The unique parameter identification is relevant when using built-in Qadence block
+constructors in the `qadence.constructors` module such as feature maps and hardware
+efficient ansatze (HEA).
```python exec="on" source="material-block" result="json" session="parametrized-constructors"
from qadence import QuantumCircuit, hea
@@ -177,18 +214,15 @@ depth = 2
hea1 = hea(n_qubits=n_qubits, depth=depth)
circuit = QuantumCircuit(n_qubits, hea1)
-n_params_one_hea = circuit.num_unique_parameters
-print(f"Unique parameters with a single HEA: {n_params_one_hea}")
+print(f"Unique parameters with a single HEA: {circuit.num_unique_parameters}")
```
```python exec="on" html="1" session="parametrized-constructors"
from qadence.draw import html_string
print(html_string(circuit))
```
-Let's now add another HEA defined in the same way as above and create a circuit
-stacking the two HEAs. As you can see below, the number of unique parameters
-(and thus what gets optimized in the variational procedure) is the same since
-the parameters are defined under the hood with the same names.
+A new circuit can be created by adding another identical HEA. As expected, the number of unique parameters
+is the same.
```python exec="on" source="material-block" result="json" session="parametrized-constructors"
hea2 = hea(n_qubits=n_qubits, depth=depth)
@@ -202,10 +236,9 @@ from qadence.draw import html_string # markdown-exec: hide
print(html_string(circuit)) # markdown-exec: hide
```
-!!! warning "Avoid non-unique names!"
- The above is likely not the expected behavior when stacking two variational circuits
- together since one usually wants all the parameters to be optimized. To ensure
- this, assign a different parameter prefix for each HEA as follows.
+!!! warning "Avoid non-unique names by prefixing"
+ A parameter prefix for each HEA can be passed as follows:
+
```python exec="on" source="material-block" result="json" session="parametrized-constructors"
hea1 = hea(n_qubits=n_qubits, depth=depth, param_prefix="p1")
hea2 = hea(n_qubits=n_qubits, depth=depth, param_prefix="p2")
@@ -219,12 +252,12 @@ print(html_string(circuit)) # markdown-exec: hide
print(html_string(circuit)) # markdown-exec: hide
```
+The `hea` function will be further explored in the [QML Constructors tutorial](qml_constructors.md).
## Parametric observables
-In `qadence` one can define quantum observables with some (classical) optimizable parameters. This
-can be very useful for improving the convergence of some QML calculations, particularly in the
-context of differentiable quantum circuits. Let's see how to define a parametrized observable:
+In Qadence, one can define quantum observables with classical optimizable parameters to
+improve the convergence of QML calculations. This is particularly useful for differentiable quantum circuits.
```python exec="on" source="material-block" session="parametrized-constructors"
from qadence import VariationalParameter, Z, add, tag
@@ -233,8 +266,9 @@ s = VariationalParameter("s")
observable = add(s * Z(i) for i in range(n_qubits))
```
-Create a quantum model with the parametric observable and check that the variational parameters of
-the observable are among the ones of the model
+Now, a quantum model can be created with the parametric observable.
+The observable variational parameters are included among the model ones.
+
```python exec="on" source="material-block" result="json" session="parametrized-constructors"
from qadence import QuantumModel, QuantumCircuit
@@ -243,8 +277,9 @@ model = QuantumModel(circuit, observable=observable, backend="pyqtorch", diff_mo
print(model.vparams)
```
-We can perform one optimization step and check that the model parameters have
-been updated including the observable coefficients
+One optimization step (forward and backeward pass) can be performed and variational parameters
+have been updated accordingly:
+
```python exec="on" source="material-block" result="json" session="parametrized-constructors"
import torch
@@ -263,13 +298,14 @@ print(model.vparams)
## Non-unitary circuits
-`qadence` allows to write arbitrary blocks which might also lead to non-unitary
-quantum circuits. For example, let's define a non-unitary block as a sum on
-Pauli operators with complex coefficients.
+Qadence allows to compose with possibly non-unitary blocks.
+Here is an exampl of a non-unitary block as a sum of Pauli operators with complex coefficients.
Backends which support the execution on non-unitary circuits can execute the
-circuit below. *Currently, only PyQTorch backend fully supports execution on
-non-unitary circuits.*
+circuit below.
+
+!!! warning "Currently, only the PyQTorch backend fully supports execution with non-unitary circuits."
+
```python exec="on" source="material-block" html="1" session="non-unitary"
from qadence import QuantumModel, QuantumCircuit, Z, X
c1 = 2.0
diff --git a/docs/tutorials/qml_constructors.md b/docs/tutorials/qml_constructors.md
new file mode 100644
index 000000000..411c64d65
--- /dev/null
+++ b/docs/tutorials/qml_constructors.md
@@ -0,0 +1,105 @@
+# QML Constructors
+
+Besides the [arbitrary Hamiltonian constructors](hamiltonians.md), Qadence also provides a complete set of program constructors useful for digital-analog quantum machine learning programs.
+
+## Feature-Maps
+
+A few feature maps are directly available for feature loading,
+
+```python exec="on" source="material-block" result="json" session="fms"
+from qadence import feature_map
+
+n_qubits = 3
+
+fm = feature_map(n_qubits, fm_type="fourier")
+print(f"{fm = }")
+
+fm = feature_map(n_qubits, fm_type="chebyshev")
+print(f"{fm = }")
+
+fm = feature_map(n_qubits, fm_type="tower")
+print(f"{fm = }")
+```
+
+## Hardware-Efficient Ansatz
+
+### Digital HEA
+
+Ansatze blocks for quantum machine-learning are typically built following the Hardware-Efficient Ansatz formalism (HEA). Both fully digital and digital-analog HEAs can easily be built with the `hea` function. By default, the digital version is returned:
+
+```python exec="on" source="material-block" html="1" session="ansatz"
+from qadence import hea
+from qadence.draw import display
+
+n_qubits = 3
+depth = 2
+
+ansatz = hea(n_qubits, depth)
+from qadence.draw import html_string # markdown-exec: hide
+print(html_string(ansatz, size="2,2")) # markdown-exec: hide
+```
+
+As seen above, the rotation layers are automatically parameterized, and the prefix `"theta"` can be changed with the `param_prefix` argument.
+
+Furthermore, both the single-qubit rotations and the two-qubit entangler can be customized with the `operations` and `entangler` argument. The operations can be passed as a list of single-qubit rotations, while the entangler should be either `CNOT`, `CZ`, `CRX`, `CRY`, `CRZ` or `CPHASE`.
+
+```python exec="on" source="material-block" html="1" session="ansatz"
+from qadence import RX, RY, CPHASE
+
+ansatz = hea(
+ n_qubits = n_qubits,
+ depth = depth,
+ param_prefix = "phi",
+ operations = [RX, RY, RX],
+ entangler = CPHASE
+ )
+from qadence.draw import html_string # markdown-exec: hide
+print(html_string(ansatz, size="2,2")) # markdown-exec: hide
+```
+
+### Digital-Analog HEA
+
+Having a truly *hardware-efficient* ansatz means that the entangling operation can be chosen according to each device's native interactions. Besides digital operations, in Qadence it is also possible to build digital-analog HEAs with the entanglement produced by the natural evolution of a set of interacting qubits, as is natural in neutral atom devices. As with other digital-analog functions, this can be controlled with the `strategy` argument which can be chosen from the [`Strategy`](../qadence/types.md) enum type. Currently, only `Strategy.DIGITAL` and `Strategy.SDAQC` are available. By default, calling `strategy = Strategy.SDAQC` will use a global entangling Hamiltonian with Ising-like NN interactions and constant interaction strength inside a `HamEvo` operation,
+
+```python exec="on" source="material-block" html="1" session="ansatz"
+from qadence import Strategy
+
+ansatz = hea(
+ n_qubits = n_qubits,
+ depth = depth,
+ strategy = Strategy.SDAQC
+ )
+from qadence.draw import html_string # markdown-exec: hide
+print(html_string(ansatz, size="2,2")) # markdown-exec: hide
+```
+
+Note that, by default, only the time-parameter is automatically parameterized when building a digital-analog HEA. However, as described in the [Hamiltonians tutorial](hamiltonians.md), arbitrary interaction Hamiltonians can be easily built with the `hamiltonian_factory` function, with both customized or fully parameterized interactions, and these can be directly passed as the `entangler` for a customizable digital-analog HEA.
+
+```python exec="on" source="material-block" html="1" session="ansatz"
+from qadence import hamiltonian_factory, Interaction, N, Register, hea
+
+# Build a parameterized neutral-atom Hamiltonian following a honeycomb_lattice:
+register = Register.honeycomb_lattice(1, 1)
+
+entangler = hamiltonian_factory(
+ register,
+ interaction = Interaction.NN,
+ detuning = N,
+ interaction_strength = "e",
+ detuning_strength = "n"
+)
+
+# Build a fully parameterized Digital-Analog HEA:
+n_qubits = register.n_qubits
+depth = 2
+
+ansatz = hea(
+ n_qubits = register.n_qubits,
+ depth = depth,
+ operations = [RX, RY, RX],
+ entangler = entangler,
+ strategy = Strategy.SDAQC
+ )
+from qadence.draw import html_string # markdown-exec: hide
+print(html_string(ansatz, size="2,2")) # markdown-exec: hide
+```
diff --git a/docs/tutorials/quantummodels.md b/docs/tutorials/quantummodels.md
index 65b6b2fb4..7b9b4d817 100644
--- a/docs/tutorials/quantummodels.md
+++ b/docs/tutorials/quantummodels.md
@@ -1,49 +1,50 @@
-Quantum programs are executed via [`QuantumModel`][qadence.models.quantum_model.QuantumModel]s.
-They serve three purposes:
+A quantum program can be expressed and executed using the [`QuantumModel`][qadence.models.quantum_model.QuantumModel] type.
+They serve three primary purposes:
-_**Execution**_: They define on which backend your program is using (i.e. which simulator or
-which device), they compile your circuit to the native backend representation.
+_**Execution**_: by defining which backend the program is expected to be executed on. Qadence supports circuit compilation to the native backend representation.
-_**Parameter handling**_: They conveniently handle the two types of parameters that qadence supports
-(*feature* and *variational* parameters) and make sure they are embedded correctly in the given
-backend. Details on parameters can be found in [this section](parameters.md).
+_**Parameter handling**_: by conveniently handling and embedding the two parameter types that Qadence supports:
+*feature* and *variational* (see more details in [this section](parameters.md)).
-_**Differentiability**_: They make your program differentiable by defining what we call a
-*differentiable backend*. There are currently two differentiable backends: the autodiff backend
-which works with PyTorch-based simulators, and the parameter shift rule (PSR) based backend which
-can make any program differentiable (even on hardware).
+_**Differentiability**_: by enabling a *differentiable backend* that supports two differentiable modes: automated differentiation (AD) and parameter shift rule (PSR).
+The former is used to differentiate non-gate parameters and enabled for PyTorch-based simulators only. The latter is used to differentiate gate parameters and is enabled for all backends.
!!! note "Backends"
- Quantum models can execute on a number of different backends like simulators, or real hardware.
- Commonly used backends are: The [*PyQTorch*](https://github.com/pasqal-io/PyQ) backend which
- implements a state vector simulator, or the [*Pulser*](https://pulser.readthedocs.io/en/stable/)
+ Quantum models can execute on a number of different purpose backends: simulators, emulators or real hardware.
+ By default, Qadence executes on the [*PyQTorch*](https://github.com/pasqal-io/PyQ) backend which
+ implements a state vector simulator. Other choices include the [*Pulser*](https://pulser.readthedocs.io/en/stable/)
backend (pulse sequences on programmable neutral atom arrays). For more information see
[backend tutorial](backends.md).
The base `QuantumModel` exposes the following methods:
-* `QuantumModel.run()`: To extract the wavefunction after propagating the quantum
- circuit. This works only for certain backends
-* `QuantumModel.sample()`: Sample bitstring out of the quantum state generated by
- the input circuit. This is available for all backends.
-* `QuantumModel.expectaction()`: Compute the expectation value of an observable
+* `QuantumModel.run()`: To extract the wavefunction after circuit execution. Not supported by all backends.
+* `QuantumModel.sample()`: Sample bitstring from the resulting quantum state after circuit execution. Supported by all backends.
+* `QuantumModel.expectaction()`: Compute the expectation value of an observable.
Every `QuantumModel` is an instance of a
-[`torch.nn.Module`](https://pytorch.org/docs/stable/generated/torch.nn.Module.html) which means that
-its `expectation` method is _**differentiable**_.
+[`torch.nn.Module`](https://pytorch.org/docs/stable/generated/torch.nn.Module.html) that enables differentiability for
+its `expectation` method.
Upon construction of the model a compiled version of the abstract `QuantumCircuit` is
created:
+
```python exec="on" source="material-block" result="json" session="quantum-model"
-from qadence import QuantumCircuit, QuantumModel, RX, Z, chain
+from qadence import QuantumCircuit, QuantumModel, RX, Z, chain, Parameter, BackendName
+
+# Construct a parametrized abstract circuit.
+# At this point we cannot run anything yet.
+
+x = Parameter("x")
-# construct abstract circuit
-# at this point we cannot run anything yet!
n_qubits = 2
-block = chain(RX(0, "x"), RX(1, "x"))
+block = chain(RX(0, x), RX(1, x))
circuit = QuantumCircuit(n_qubits, block)
observable = Z(0)
+# Construct a QuantumModel which will compile
+# the abstract circuit to targetted backend.
+model = QuantumModel(circuit, observable, backend=BackendName.PYQTORCH)
# now we construct a QuantumModel which will compile
# the abstract circuit to the backend we specify
@@ -57,10 +58,12 @@ from pyqtorch.modules import QuantumCircuit as PyQCircuit
assert isinstance(model._circuit.native, PyQCircuit)
```
-Now we can compute the wavefunction, sample, or compute the expectation:
+Now, the wavefunction, sample, or expectation value are computable:
+
```python exec="on" source="material-block" result="json" session="quantum-model"
import torch
+# Set a batch of parameter values.
values = {"x": torch.rand(3)}
wf = model.run(values)
@@ -82,10 +85,6 @@ print(ex)
### Quantum Neural Network (QNN)
-The `QNN` is a subclass of the `QuantumModel` geared towards quantum machine learning. See the [ML
+The `QNN` is a subclass of the `QuantumModel` geared towards quantum machine learning and parameter optimisation. See the [ML
Tools](/tutorials/ml_tools.md) section or the [`QNN`][qadence.models.QNN] for more detailed
-information.
-
-!!! note "Parametrized Models"
- For more information on parametrizing `QuantumModel`s refer to the [parametric program
- tutorial](/tutorials/parameters.md#parametrized-models).
+information and the [parametric program tutorial](/tutorials/parameters.md#parametrized-models) for parametrization.
diff --git a/docs/tutorials/register.md b/docs/tutorials/register.md
index c0d8a03fd..8aa11ad80 100644
--- a/docs/tutorials/register.md
+++ b/docs/tutorials/register.md
@@ -1,3 +1,9 @@
+Quantum programs ideally work by specifying the layout of a register of resources as a lattice.
+In Qadence, a [`Register`][qadence.register.Register] of interacting qubits can be constructed for arbitrary topologies.
+
+Commonly used register topologies are available and illustrated in the plot below.
+
+
```python exec="on" html="1"
import numpy as np
import matplotlib.pyplot as plt
@@ -45,15 +51,10 @@ from docs import docsutils # markdown-exec: hide
print(docsutils.fig_to_html(fig)) # markdown-exec: hide
```
-To construct programs that work with interacting qubit systems the
-[`Register`][qadence.register.Register] lets you construct arbitrary topologies of qubit registers.
+## Building adn drawing registers
-Qadence provides a few commonly used register lattices, such as `"line"` or `"rectangular_lattice"`.
-The available topologies are shown in the plot above.
+In following are few examples of built-in topologies accessible:
-## Building registers
-
-As an example, lets construct a honeycomb lattice and draw it:
```python exec="on" source="material-block" html="1"
from qadence import Register
@@ -67,8 +68,8 @@ fig.set_size_inches(3, 3) # markdown-exec: hide
print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide
```
-You can also construct arbitrarily shaped registers by manually providing coordinates.
-Note that there are no edges defined in `Register`s that are constructed via `from_coordinates`.
+Arbitrarily shaped registers can be constructed by providing coordinates.
+_N.B._: `Register` constructed via the `from_coordinates` do not define edges in the connecticity graph.
```python exec="on" source="material-block" html="1"
import numpy as np
@@ -88,22 +89,20 @@ from docs import docsutils # markdown-exec: hide
print(docsutils.fig_to_html(fig)) # markdown-exec: hide
```
-!!! warning "Qubit coordinate units"
- The coordinates of qubits in `qadence` are *dimensionless*, e.g. for the Pulser backend they are
- converted to $\mu m$.
+!!! warning "Units for qubit coordinates"
+ Qubits coordinates in Qadence are *dimensionless* but converted to the required unit when executed on a backend.
+ For instance, [Pulser](https://github.com/pasqal-io/Pulser) uses _\mu m_.
-## Usage
+## Detailed Usage
-In the digital computing paradigm, register topology is often disregarded in
-simulations and an all-to-all qubit connectivity is assumed. This is of course not the case when
-running on real devices. In the [digital-analog](/digital_analog_qc/index.md) computing paradigm,
-we have to specify how qubits interact either by taking into account the distances between qubits,
-or by manually defining edges in the register graph.
+Register topology is often disregarded in simulations where an all-to-all qubit connectivity is assumed.
+When running on real devices that enable the [digital-analog](/digital_analog_qc/index.md) computing paradigm,
+qubit interaction must be specified either by taking into account the distances between qubits,
+or by defining edges in the register connectivity graph.
### Abstract graphs
-We can ignore the register coordinates and only deal with the edges that are present in the
-`Register.edges`. For instance, this is the case in the [perfect state
+It is possible to access the abstract graph nodes and edges to work with if needed as in the [perfect state
transfer](/#perfect-state-transfer) example.
```python exec="on" source="material-block" result="json" session="reg-usage"
@@ -114,16 +113,15 @@ print(f"{reg.nodes=}")
print(f"{reg.edges=}")
```
-### Graphs with coordinates
+### Concrete graphs with coordinates
+
+It is possible to customize qubit interaction through the [`add_interaction`][qadence.transpile.emulate.add_interaction] method.
+In that case, `Register.coords` are accessible:
-If interactions are based on the distance of the individual qubits in the register then instead of
-the edges, we deal with `Register.coords` like in
-[`add_interaction`][qadence.transpile.emulate.add_interaction].
```python exec="on" source="material-block" result="json" session="reg-usage"
print(f"{reg.coords=}")
```
-You might have already seen the [simplest example](/#digital-analog-emulation) that makes
-use of register coordinates. See the [digital-analog section](/digital_analog_qc/analog-basics)
-for more details.
+Register coordinates are used in a [previous example](/#digital-analog-emulation).
+More details about their usage in the digital-analog paradigm can be found in this [section](/digital_analog_qc/analog-basics).
diff --git a/docs/tutorials/state_conventions.md b/docs/tutorials/state_conventions.md
index 57027b14b..326470575 100644
--- a/docs/tutorials/state_conventions.md
+++ b/docs/tutorials/state_conventions.md
@@ -1,22 +1,18 @@
# State Conventions
-Here we describe the state conventions used in `qadence` and give a few practical examples.
+Here is an overview of the state conventions used in Qadence together with practical examples.
## Qubit register order
-Qubit registers in quantum computing are often indexed in increasing or decreasing order. In `qadence` we use an increasing order. For example, for a register of 4 qubits we have:
-
-$$q_0 \otimes q_1 \otimes q_2 \otimes q_3$$
-
-Or alternatively in bra-ket notation,
+Qubit registers in quantum computing are often indexed in increasing or decreasing order from left to right. In Qadence, the convention is qubit indexation in increasing order. For example, a register of four qubits in bra-ket notation reads:
$$|q_0, q_1, q_2, q_3\rangle$$
-Furthermore, when displaying a quantum circuit, the qubits are ordered from top to bottom.
+Furthermore, when displaying a quantum circuit, qubits are ordered from top to bottom.
## Basis state order
-Basis state ordering refers to how basis states are ordered when considering the conversion from bra-ket notation to the standard linear algebra basis. In `qadence` the basis states are ordered in the following manner:
+Basis state ordering refers to how basis states are ordered when considering the conversion from bra-ket notation to the standard linear algebra basis. In Qadence, basis states are ordered in the following manner:
$$
\begin{align}
@@ -29,18 +25,18 @@ $$
## Endianness
-Endianness refers to the convention of how binary information is stored in a memory register. Tyically, in classical computers, it refers to the storage of *bytes*. However, in quantum computing information is mostly described in terms of single bits, or qubits. The most commonly used conventions are:
+Endianness refers to the storage convention for binary information (in *bytes*) in a classical memory register. In quantum computing, information is either stored in bits or in qubits. The most commonly used conventions are:
-- A **big-endian** system stores the **most significant bit** of a word at the smallest memory address.
-- A **little-endian** system stores the **least significant bit** of a word at the smallest memory address.
+- A **big-endian** system stores the **most significant bit** of a binary word at the smallest memory address.
+- A **little-endian** system stores the **least significant bit** of a binary word at the smallest memory address.
-Given the register convention described for `qadence`, as an example, the integer $2$ written in binary as $10$ can be encoded in a qubit register in both big-endian as $|10\rangle$ or little-endian as $|01\rangle$.
+Given the register convention in Qadence, the integer $2$ written in binary big-endian as $10$ can be encoded in a qubit register in both big-endian as $|10\rangle$ or little-endian as $|01\rangle$.
-In general, the default convention for `qadence` is **big-endian**.
+The convention for Qadence is **big-endian**.
## In practice
-In practical scenarios, the conventions regarding *register order*, *basis state order* and *endianness* are very much connected, and the same results can be obtained by fixing or varying any of them. In `qadence`, we assume that qubit ordering and basis state ordering is fixed, and allow an `endianness` argument that can be passed to control the expected result. We now describe a few examples:
+In practical scenarios, conventions regarding *register order*, *basis state order* and *endianness* are very much intertwined, and identical results can be obtained by fixing or varying any of them. In Qadence, we assume that qubit ordering and basis state ordering is fixed, and allow an `endianness` argument that can be passed to control the expected result. Here are a few examples:
### Quantum states
@@ -48,31 +44,39 @@ A simple and direct way to exemplify the endianness convention is the following:
```python exec="on" source="material-block" result="json" session="end-0"
import qadence as qd
+from qadence import Endianness
+
+# The state |10>, the 3rd basis state.
+state_big = qd.product_state("10", endianness = Endianness.BIG) # or just "Big"
-state_big = qd.product_state("10", endianness = qd.Endianness.BIG) # or just "Big"
-state_little = qd.product_state("10", endianness = qd.Endianness.LITTLE) # or just "Little"
+# The state |01>, the 2nd basis state.
+state_little = qd.product_state("10", endianness = Endianness.LITTLE) # or just "Little"
-print(state_big) # The state |10>, the 3rd basis state.
-print(state_little) # The state |01>, the 2nd basis state.
+print(state_big) # markdown-exec: hide
+print(state_little) # markdown-exec: hide
```
-Here we took a bit word written as a Python string and used it to create the respective basis state following both conventions. However, note that we would actually get the same results by saying that we fixed the endianness convention as big-endian, thus creating the state $|10\rangle$ in both cases, but changed the basis state ordering. We could also make a similar argument for fixing both endianness and basis state ordering and simply changing the qubit index order. This is simply an illustration of how these concepts are connected.
+Here, a bit word expressed as a Python string is used to create the respective basis state following both conventions. However, note that the same results can be obtained by fixing the endianness convention as big-endian (thus creating the state $|10\rangle$ in both cases), and changing the basis state ordering. A similar argument holds for fixing both endianness and basis state ordering and simply changing the qubit index order.
-Another example where endianness will come directly into play is when *measuring* a register. A big or little endian measurement will choose the first or the last qubit, respectively, as the most significant bit. Let's see this in an example:
+Another example where endianness directly comes into play is when *measuring* a register. A big or little endian measurement will choose the first or the last qubit, respectively, as the most significant bit. Let's see this in an example:
```python exec="on" source="material-block" result="json" session="end-0"
+from qadence import I, H
+
# Create superposition state: |00> + |01> (normalized)
-block = qd.I(0) @ qd.H(1) # Identity on qubit 0, Hadamard on qubit 1
+block = I(0) @ H(1) # Identity on qubit 0, Hadamard on qubit 1
# Generate bitword samples following both conventions
-result_big = qd.sample(block, endianness = qd.Endianness.BIG)
-result_little = qd.sample(block, endianness = qd.Endianness.LITTLE)
+# Samples "00" and "01"
+result_big = qd.sample(block, endianness = Endianness.BIG)
+# Samples "00" and "10"
+result_little = qd.sample(block, endianness = Endianness.LITTLE)
-print(result_big) # Samples "00" and "01"
-print(result_little) # Samples "00" and "10"
+print(result_big) # markdown-exec: hide
+print(result_little) # markdown-exec: hide
```
-In `qadence` we can also invert endianness of many objects with the same `invert_endianness` function:
+In Qadence we can also invert endianness of many objects with the same `invert_endianness` function:
```python exec="on" source="material-block" result="json" session="end-0"
# Equivalent to sampling in little-endian.
@@ -84,7 +88,7 @@ print(qd.invert_endianness(state_big))
### Quantum operations
-When looking at quantum operations in matrix form, our usage of the term *endianness* slightly deviates from its absolute definition. To exemplify, we maybe consider the CNOT operation with `control = 0` and `target = 1`. This operation is often described with two different matrices:
+When looking at quantum operations in matrix form, the usage of the term *endianness* slightly deviates from its absolute definition. To exemplify, we may consider the CNOT operation with `control = 0` and `target = 1`. This operation is often described with two different matrices:
$$
\text{CNOT(0, 1)} =
@@ -106,27 +110,26 @@ $$
\end{bmatrix}
$$
-The difference between these two matrices can be easily explained either by considering a different ordering of the qubit indices, or a different ordering of the basis states. In `qadence`, we can get both through the endianness argument:
+The difference between these two matrices can be easily explained either by considering a different ordering of the qubit indices, or a different ordering of the basis states. In Qadence, both can be retrieved through the endianness argument:
```python exec="on" source="material-block" result="json" session="end-0"
-matrix_big = qd.block_to_tensor(qd.CNOT(0, 1), endianness = "Big")
+matrix_big = qd.block_to_tensor(qd.CNOT(0, 1), endianness=Endianness.BIG)
print(matrix_big.detach())
print("") # markdown-exec: hide
-matrix_big = qd.block_to_tensor(qd.CNOT(0, 1), endianness = "Little")
-print(matrix_big.detach())
+matrix_little = qd.block_to_tensor(qd.CNOT(0, 1), endianness=Endianness.LITTLE)
+print(matrix_little.detach())
```
-While the usage of the term here may not be fully accurate, it helps with keeping a consistent interface, and it still relates to the same general idea of qubit index ordering or which qubit is considered the most significant.
-
## Backends
-An important part of having clear state conventions is that we need to make sure our results are consistent accross different computational backends, which may have their own conventions that we need to take into account. In `qadence` we take care of this automatically, such that by calling a certain operation for different backends we expect a result that is equivalent in qubit ordering.
+An important part of having clear state conventions is that we need to make sure our results are consistent accross different computational backends, which may have their own conventions that we need to take into account. In Qadence, we take care of this automatically, such that by calling a certain operation for different backends we expect a result that is equivalent in qubit ordering.
```python exec="on" source="material-block" result="json" session="end-0"
import warnings # markdown-exec: hide
warnings.filterwarnings("ignore") # markdown-exec: hide
import qadence as qd
+from qadence import BackendName
import torch
# RX(pi/4) on qubit 1
@@ -134,12 +137,12 @@ n_qubits = 2
op = qd.RX(1, torch.pi/4)
print("Same sampling order:")
-print(qd.sample(n_qubits, op, endianness = "Big", backend = qd.BackendName.PYQTORCH))
-print(qd.sample(n_qubits, op, endianness = "Big" ,backend = qd.BackendName.BRAKET))
-print(qd.sample(n_qubits, op, endianness = "Big", backend = qd.BackendName.PULSER))
+print(qd.sample(n_qubits, op, endianness=Endianness.BIG, backend=BackendName.PYQTORCH))
+print(qd.sample(n_qubits, op, endianness=Endianness.BIG, backend=BackendName.BRAKET))
+print(qd.sample(n_qubits, op, endianness=Endianness.BIG, backend=BackendName.PULSER))
print("") # markdown-exec: hide
print("Same wavefunction order:")
-print(qd.run(n_qubits, op, endianness = "Big", backend = qd.BackendName.PYQTORCH))
-print(qd.run(n_qubits, op, endianness = "Big" ,backend = qd.BackendName.BRAKET))
-print(qd.run(n_qubits, op, endianness = "Big", backend = qd.BackendName.PULSER))
+print(qd.run(n_qubits, op, endianness=Endianness.BIG, backend=BackendName.PYQTORCH))
+print(qd.run(n_qubits, op, endianness=Endianness.BIG, backend=BackendName.BRAKET))
+print(qd.run(n_qubits, op, endianness=Endianness.BIG, backend=BackendName.PULSER))
```
diff --git a/docs/tutorials/state_init.md b/docs/tutorials/state_init.md
new file mode 100644
index 000000000..0604b8ff7
--- /dev/null
+++ b/docs/tutorials/state_init.md
@@ -0,0 +1,121 @@
+# State initialization
+
+Several standard quantum states can be quickly initialized in `qadence`, both in statevector form as well as in block form.
+
+## Statevector initialization
+
+Creating uniform, all-zero or all-one:
+
+```python exec="on" source="material-block" result="json" session="states"
+from qadence import uniform_state, zero_state, one_state
+
+n_qubits = 3
+batch_size = 2
+
+print(uniform_state(n_qubits, batch_size))
+print(zero_state(n_qubits, batch_size))
+print(one_state(n_qubits, batch_size))
+```
+
+Creating product states:
+
+```python exec="on" source="material-block" result="json" session="states"
+from qadence import product_state, rand_product_state
+
+# From a bitsring "100"
+print(product_state("100", batch_size))
+
+# Or a random product state
+print(rand_product_state(n_qubits, batch_size))
+```
+
+Creating a GHZ state:
+
+```python exec="on" source="material-block" result="json" session="states"
+from qadence import ghz_state
+
+print(ghz_state(n_qubits, batch_size))
+```
+
+Creating a random state uniformly sampled from a Haar measure:
+
+```python exec="on" source="material-block" result="json" session="states"
+from qadence import random_state
+
+print(random_state(n_qubits, batch_size))
+```
+
+Custom initial states can then be passed to `run`, `sample` and `expectation` by passing the `state` argument
+
+```python exec="on" source="material-block" result="json" session="states"
+from qadence import random_state, product_state, CNOT, run
+
+init_state = product_state("10")
+final_state = run(CNOT(0, 1), state = init_state)
+print(final_state)
+```
+
+## Block initialization
+
+Not all backends support custom statevector initialization, however there are also utility functions to initialize the respective blocks:
+
+```python exec="on" source="material-block" result="json" session="states"
+from qadence import uniform_block, one_block
+
+n_qubits = 3
+
+print(uniform_block(n_qubits))
+print(one_block(n_qubits))
+```
+
+Similarly, for product states:
+
+```python exec="on" source="material-block" result="json" session="states"
+from qadence import product_block, rand_product_block
+
+print(product_block("100"))
+print(rand_product_block(n_qubits))
+```
+
+And GHZ states:
+
+```python exec="on" source="material-block" result="json" session="states"
+from qadence import ghz_block
+
+print(ghz_block(n_qubits))
+```
+
+Initial state blocks can simply be chained at the start of a given circuit.
+
+## Utility functions
+
+Some statevector utility functions are also available. We can easily create the probability mass function of a given statevector using `torch.distributions.Categorical`
+
+```python exec="on" source="material-block" result="json" session="states"
+from qadence import random_state, pmf
+
+n_qubits = 3
+
+state = random_state(n_qubits)
+distribution = pmf(state)
+print(distribution) # markdown-exec: hide
+```
+
+We can also check if a state is normalized:
+
+```python exec="on" source="material-block" result="json" session="states"
+from qadence import random_state, is_normalized
+
+state = random_state(n_qubits)
+print(is_normalized(state))
+```
+
+Or normalize a state:
+
+```python exec="on" source="material-block" result="json" session="states"
+import torch
+from qadence import normalize, is_normalized
+
+state = torch.tensor([[1, 1, 1, 1]], dtype = torch.cdouble)
+print(normalize(state))
+```
diff --git a/mkdocs.yml b/mkdocs.yml
index 16cc9a193..f631ba7cc 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -5,18 +5,21 @@ repo_name: "qadence"
nav:
- Qadence:
- - index.md
+ - Qadence: index.md
- Getting Started: tutorials/getting_started.md
- Quantum Models: tutorials/quantummodels.md
- Parametric Programs: tutorials/parameters.md
- Quantum Registers: tutorials/register.md
+ - State Initialization: tutorials/state_init.md
- Arbitrary Hamiltonians: tutorials/hamiltonians.md
+ - QML Constructors: tutorials/qml_constructors.md
- Wavefunction Overlaps: tutorials/overlap.md
- Program Constructors, States & Serialization: tutorials/serializ_and_prep.md
- Backends: tutorials/backends.md
- State Conventions: tutorials/state_conventions.md
+ - Quantum Machine Learning: tutorials/ml_tools.md
- - Digital-Analog:
+ - Digital-Analog Quantum Computing:
- digital_analog_qc/daqc-basics.md
- Digital-Analog Emulation:
- Basics: digital_analog_qc/analog-basics.md
@@ -128,8 +131,6 @@ plugins:
merge_init_into_class: true
docstring_section_style: spacy
-- mkdocs-jupyter:
- theme: light
- markdown-exec
# To get nice tabs
diff --git a/pyproject.toml b/pyproject.toml
index ea9004e57..461a8b472 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,7 +39,8 @@ dependencies = [
"jsonschema",
"nevergrad",
"scipy<1.11",
- "pyqtorch==0.5.0"
+ "pyqtorch==0.5.0",
+ "matplotlib"
]
[tool.hatch.metadata]
@@ -55,7 +56,6 @@ visualization = [
# "latex2svg @ git+https://github.com/Moonbase59/latex2svg.git#egg=latex2svg",
# "scour",
]
-scipy = ["scipy<1.11"]
all = [
"pulser>=0.12.0",
"amazon-braket-sdk",
@@ -74,15 +74,11 @@ dependencies = [
"pytest-cov",
"pytest-mypy",
"pytest-xdist",
- "nbconvert",
"ipykernel",
- "jupyter_contrib_nbextensions",
"pre-commit",
"black",
"isort",
"ruff",
- "notebook<7.0",
- "dill",
]
features = ["all"]
@@ -120,12 +116,8 @@ dependencies = [
"mkdocstrings",
"mkdocstrings-python",
"mkdocs-section-index==0.3.6",
- "mkdocs-jupyter",
"mkdocs-exclude",
"markdown-exec",
- "notebook<7",
- "jupyter_contrib_nbextensions",
- "dill",
]
features = ["pulser", "braket", "visualization"]
diff --git a/qadence/blocks/matrix.py b/qadence/blocks/matrix.py
index d44ca987a..e2f8b9763 100644
--- a/qadence/blocks/matrix.py
+++ b/qadence/blocks/matrix.py
@@ -71,7 +71,7 @@ def __init__(self, matrix: torch.Tensor | np.ndarray, qubit_support: tuple[int,
if not self.is_square(matrix):
raise TypeError("Please provide a square matrix.")
if not self.is_hermitian(matrix):
- logger.warning("Provided matrix is not hermetian.")
+ logger.warning("Provided matrix is not hermitian.")
if not self.is_unitary(matrix):
logger.warning("Provided matrix is not unitary.")
self.matrix = matrix.clone()
diff --git a/qadence/ml_tools/config.py b/qadence/ml_tools/config.py
index 439ead1fe..dfbb71199 100644
--- a/qadence/ml_tools/config.py
+++ b/qadence/ml_tools/config.py
@@ -38,6 +38,8 @@ class TrainConfig:
"""A boolean function which evaluates a given validation metric is satisfied"""
trainstop_criterion: Optional[Callable] = None
"""A boolean function which evaluates a given training stopping metric is satisfied"""
+ batch_size: int = 1
+ """The batch_size to use when passing a list/tuple of torch.Tensors."""
def __post_init__(self) -> None:
if self.folder:
diff --git a/qadence/ml_tools/data.py b/qadence/ml_tools/data.py
index 08fc2c954..17d9739ba 100644
--- a/qadence/ml_tools/data.py
+++ b/qadence/ml_tools/data.py
@@ -3,7 +3,7 @@
from dataclasses import dataclass
import torch
-from torch.utils.data import DataLoader
+from torch.utils.data import DataLoader, TensorDataset
@dataclass
@@ -25,3 +25,8 @@ def __iter__(self) -> DictDataLoader:
def __next__(self) -> dict[str, torch.Tensor]:
return {key: next(it) for key, it in self.iters.items()}
+
+
+def to_dataloader(x: torch.Tensor, y: torch.Tensor, batch_size: int = 1) -> DataLoader:
+ """Convert two torch tensors x and y to a Dataloader."""
+ return DataLoader(TensorDataset(x, y), batch_size=batch_size)
diff --git a/qadence/ml_tools/train_grad.py b/qadence/ml_tools/train_grad.py
index 9e8aa6c07..e17edb8c2 100644
--- a/qadence/ml_tools/train_grad.py
+++ b/qadence/ml_tools/train_grad.py
@@ -3,6 +3,7 @@
from typing import Callable
from rich.progress import BarColumn, Progress, TaskProgressColumn, TextColumn, TimeRemainingColumn
+from torch import Tensor
from torch.nn import Module
from torch.optim import Optimizer
from torch.utils.data import DataLoader
@@ -20,7 +21,7 @@
def train(
model: Module,
- dataloader: DictDataLoader | DataLoader | None,
+ dataloader: DictDataLoader | DataLoader | list[Tensor] | tuple[Tensor, Tensor] | None,
optimizer: Optimizer,
config: TrainConfig,
loss_fn: Callable,
@@ -57,6 +58,54 @@ def train(
called every `config.write_every` iterations. The function must have
the signature `write_tensorboard(writer, loss, metrics, iteration)`
(see the example below).
+
+ Example:
+ ```python exec="on" source="material-block"
+ from pathlib import Path
+ import torch
+ from itertools import count
+ from qadence.constructors import total_magnetization, hea, feature_map
+ from qadence import chain, Parameter, QuantumCircuit
+ from qadence.models import QNN
+ from qadence.ml_tools import train_with_grad, TrainConfig
+
+ n_qubits = 2
+ fm = feature_map(n_qubits)
+ ansatz = hea(n_qubits=n_qubits, depth=3)
+ observable = total_magnetization(n_qubits)
+ circuit = QuantumCircuit(n_qubits, fm, ansatz)
+
+ model = QNN(circuit, observable, backend="pyqtorch", diff_mode="ad")
+ batch_size = 1
+ input_values = {"phi": torch.rand(batch_size, requires_grad=True)}
+ pred = model(input_values)
+
+ ## lets prepare the train routine
+
+ cnt = count()
+ criterion = torch.nn.MSELoss()
+ optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
+
+ def loss_fn(model: torch.nn.Module, data: torch.Tensor) -> tuple[torch.Tensor, dict]:
+ next(cnt)
+ x, y = data[0], data[1]
+ out = model(x)
+ loss = criterion(out, y)
+ return loss, {}
+ tmp_path = Path("/tmp")
+ n_epochs = 5
+ config = TrainConfig(
+ folder=tmp_path,
+ max_iter=n_epochs,
+ checkpoint_every=100,
+ write_every=100,
+ batch_size=batch_size,
+ )
+ batch_size = 25
+ x = torch.linspace(0, 1, batch_size).reshape(-1, 1)
+ y = torch.sin(x)
+ train_with_grad(model, (x, y), optimizer, config, loss_fn=loss_fn)
+ ```
"""
assert loss_fn is not None, "Provide a valid loss function"
@@ -79,6 +128,12 @@ def train(
TaskProgressColumn(),
TimeRemainingColumn(elapsed_when_finished=True),
)
+ if isinstance(dataloader, (list, tuple)):
+ from qadence.ml_tools.data import to_dataloader
+
+ assert len(dataloader) == 2, "Please provide exactly two torch tensors."
+ x, y = dataloader
+ dataloader = to_dataloader(x=x, y=y, batch_size=config.batch_size)
with progress:
dl_iter = iter(dataloader) if isinstance(dataloader, DictDataLoader) else None
diff --git a/qadence/operations.py b/qadence/operations.py
index d9a04cc97..8caaebb5b 100644
--- a/qadence/operations.py
+++ b/qadence/operations.py
@@ -1171,6 +1171,8 @@ def AnalogRot(
ConstantAnalogRotation
"""
q = _cast(QubitSupport, qubit_support)
+ if isinstance(duration, str):
+ duration = Parameter(duration)
alpha = duration * sympy.sqrt(omega**2 + delta**2) / 1000 # type: ignore [operator]
ps = ParamMap(alpha=alpha, duration=duration, omega=omega, delta=delta, phase=phase)
diff --git a/qadence/states.py b/qadence/states.py
index 89982a307..b7ee2eb34 100644
--- a/qadence/states.py
+++ b/qadence/states.py
@@ -310,7 +310,7 @@ def random_state(
n_qubits = 2
- # The default is StateGeneratorType.HAARMEASUREFAST
+ # The default is StateGeneratorType.HAAR_MEASURE_FAST
state = random_state(n_qubits=n_qubits)
print(state)
@@ -322,7 +322,7 @@ def random_state(
if type == StateGeneratorType.HAAR_MEASURE_FAST:
state = concat(tuple(_rand_haar_fast(n_qubits) for _ in range(batch_size)), dim=0)
- elif type == StateGeneratorType.HAAR_MEASURE_FAST:
+ elif type == StateGeneratorType.HAAR_MEASURE_SLOW:
state = concat(tuple(_rand_haar_slow(n_qubits) for _ in range(batch_size)), dim=0)
elif type == StateGeneratorType.RANDOM_ROTATIONS:
state = _run_state(_abstract_random_state(n_qubits, batch_size), backend) # type: ignore
diff --git a/readthedocs.yml b/readthedocs.yml
index f68f10e4d..718c036c6 100644
--- a/readthedocs.yml
+++ b/readthedocs.yml
@@ -1,15 +1,12 @@
version: 2
+
build:
os: "ubuntu-22.04"
tools:
- python: "3.10"
- apt_packages:
- - graphviz
- commands:
- - pip install hatch
- - hatch -v run docs:build
- - mkdir _readthedocs/
- - mv site _readthedocs/html
+ python: "mambaforge-22.9"
+
+conda:
+ environment: docs/environment.yml
mkdocs:
- configuration: mkdocs.yml
+ configuration: mkdocs.yml
diff --git a/tests/backends/pulser_basic/test_entanglement.py b/tests/backends/pulser_basic/test_entanglement.py
index 189548a0a..d2a68a457 100644
--- a/tests/backends/pulser_basic/test_entanglement.py
+++ b/tests/backends/pulser_basic/test_entanglement.py
@@ -11,7 +11,7 @@
from qadence.backends.pulser import Device
from qadence.blocks import AbstractBlock, chain
from qadence.divergences import js_divergence
-from qadence.operations import RY, AnalogRot, entangle, wait
+from qadence.operations import RY, entangle
from qadence.register import Register
@@ -24,35 +24,6 @@
Register(2),
Counter({"00": 250, "11": 250}),
),
- # Four qubits GHZ state
- (
- chain(
- AnalogRot(duration=100, omega=5 * torch.pi, delta=0, phase=0),
- wait(2300),
- AnalogRot(duration=300, omega=5 * torch.pi, delta=0, phase=0),
- ),
- Register.square(qubits_side=2),
- Counter(
- {
- "1111": 145,
- "1110": 15,
- "1101": 15,
- "1100": 15,
- "1011": 15,
- "1010": 15,
- "1001": 15,
- "1000": 15,
- "0111": 15,
- "0110": 15,
- "0101": 15,
- "0100": 15,
- "0011": 15,
- "0010": 15,
- "0001": 15,
- "0000": 145,
- }
- ),
- ),
],
)
def test_entanglement(blocks: AbstractBlock, register: Register, goal: Counter) -> None:
diff --git a/tests/backends/pulser_basic/test_quantum_pulser.py b/tests/backends/pulser_basic/test_quantum_pulser.py
index ca4a09061..aa6ed5cc1 100644
--- a/tests/backends/pulser_basic/test_quantum_pulser.py
+++ b/tests/backends/pulser_basic/test_quantum_pulser.py
@@ -1,28 +1,18 @@
from __future__ import annotations
-from collections import Counter
-
import pytest
import torch
-from metrics import JS_ACCEPTANCE
from qadence import (
RX,
- RY,
- AnalogRot,
BackendName,
FeatureParameter,
QuantumCircuit,
- Register,
VariationalParameter,
backend_factory,
- chain,
- entangle,
kron,
total_magnetization,
)
-from qadence.backends.pulser import Device
-from qadence.divergences import js_divergence
@pytest.fixture
@@ -35,53 +25,6 @@ def batched_circuit() -> QuantumCircuit:
return QuantumCircuit(n_qubits, block)
-@pytest.mark.parametrize(
- "circuit,goal",
- [
- (
- QuantumCircuit(
- Register(2), chain(entangle(383, qubit_support=(0, 1)), RY(0, 3 * torch.pi / 2))
- ),
- Counter({"00": 250, "11": 250}),
- ),
- (
- QuantumCircuit(
- Register.square(qubits_side=2),
- chain(
- entangle(2488),
- AnalogRot(duration=300, omega=5 * torch.pi, delta=0, phase=0),
- ),
- ),
- Counter(
- {
- "1111": 145,
- "1110": 15,
- "1101": 15,
- "1100": 15,
- "1011": 15,
- "1010": 15,
- "1001": 15,
- "1000": 15,
- "0111": 15,
- "0110": 15,
- "0101": 15,
- "0100": 15,
- "0011": 15,
- "0010": 15,
- "0001": 15,
- "0000": 145,
- }
- ),
- ),
- ],
-)
-def test_pulser_sequence_sample(circuit: QuantumCircuit, goal: Counter) -> None:
- config = {"device_type": Device.REALISTIC}
- backend = backend_factory(backend=BackendName.PULSER, diff_mode=None, configuration=config)
- sample = backend.sample(backend.circuit(circuit), {}, n_shots=500)[0]
- assert js_divergence(sample, goal) < JS_ACCEPTANCE
-
-
def test_expectation_batched(batched_circuit: QuantumCircuit) -> None:
batch_size = 3
values = {"phi": torch.tensor([torch.pi / 5, torch.pi / 4, torch.pi / 3])}
diff --git a/tests/ml_tools/test_train.py b/tests/ml_tools/test_train.py
index ed98c9455..8ac8c37c5 100644
--- a/tests/ml_tools/test_train.py
+++ b/tests/ml_tools/test_train.py
@@ -158,3 +158,36 @@ def loss_fn(model: torch.nn.Module, data: torch.Tensor) -> tuple[torch.Tensor, d
model, optimizer = train_with_grad(model, data, optimizer, config, loss_fn=loss_fn)
x = torch.rand(1)
assert torch.allclose(torch.sin(x), model(x), rtol=1e-1, atol=1e-1)
+
+
+@pytest.mark.flaky(max_runs=10)
+def test_train_tensor_tuple(tmp_path: Path, Basic: torch.nn.Module) -> None:
+ model = Basic
+ batch_size = 25
+ x = torch.linspace(0, 1, batch_size).reshape(-1, 1)
+ y = torch.sin(x)
+
+ cnt = count()
+ criterion = torch.nn.MSELoss()
+ optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
+
+ def loss_fn(model: torch.nn.Module, data: torch.Tensor) -> tuple[torch.Tensor, dict]:
+ next(cnt)
+ x, y = data[0], data[1]
+ out = model(x)
+ loss = criterion(out, y)
+ return loss, {}
+
+ n_epochs = 100
+ config = TrainConfig(
+ folder=tmp_path,
+ max_iter=n_epochs,
+ checkpoint_every=100,
+ write_every=100,
+ batch_size=batch_size,
+ )
+ train_with_grad(model, (x, y), optimizer, config, loss_fn=loss_fn)
+ assert next(cnt) == n_epochs
+
+ x = torch.rand(5, 1)
+ assert torch.allclose(torch.sin(x), model(x), rtol=1e-1, atol=1e-1)