From 81b2e0bc55595ead2794efc0e1ea5f50d0f2a4d9 Mon Sep 17 00:00:00 2001 From: Roland-djee <9250798+Roland-djee@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:23:51 +0100 Subject: [PATCH] Documentation review and improvements (#43) Co-authored-by: Joao Moutinho Co-authored-by: Mario Dagrada Co-authored-by: Vytautas Abramavicius --- README.md | 1 - docs/advanced_tutorials/custom-models.md | 4 +- docs/advanced_tutorials/differentiability.md | 4 +- docs/digital_analog_qc/pulser-basic.md | 189 +++++++------ docs/index.md | 51 ++-- docs/qml/qcl.md | 2 +- docs/tutorials/backends.md | 17 +- docs/tutorials/getting_started.md | 96 ++----- docs/tutorials/hamiltonians.md | 43 +-- docs/tutorials/ml_tools.md | 12 +- docs/tutorials/overlap.md | 13 +- docs/tutorials/parameters.md | 140 +++++----- docs/tutorials/qml_constructors.md | 105 -------- docs/tutorials/qml_tools.md | 254 ++++++++++++++++++ docs/tutorials/quantummodels.md | 50 ++-- docs/tutorials/register.md | 39 ++- docs/tutorials/serializ_and_prep.md | 46 +--- docs/tutorials/state_conventions.md | 91 ++++--- docs/tutorials/state_init.md | 116 ++++++-- mkdocs.yml | 7 +- qadence/blocks/block_to_tensor.py | 4 +- qadence/ml_tools/models.py | 6 +- qadence/ml_tools/train_grad.py | 6 +- qadence/models/qnn.py | 4 +- qadence/serialization.py | 16 +- .../pulser_basic/test_entanglement.py | 2 +- tests/backends/test_pulser_pyq_compat.py | 21 +- 27 files changed, 742 insertions(+), 597 deletions(-) delete mode 100644 docs/tutorials/qml_constructors.md create mode 100644 docs/tutorials/qml_tools.md diff --git a/README.md b/README.md index 159e2abe4..0e535b799 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,6 @@ differentiable state vector simulator). You can install one or all of the follo backends and the circuit visualization library using the following extras: * `braket`: install the Amazon Braket quantum backend -* `emu-c`: install the Pasqal circuit tensor network emulator EMU-C * `pulser`: install the [Pulser](https://github.com/pasqal-io/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. diff --git a/docs/advanced_tutorials/custom-models.md b/docs/advanced_tutorials/custom-models.md index c55609a67..f27e13158 100644 --- a/docs/advanced_tutorials/custom-models.md +++ b/docs/advanced_tutorials/custom-models.md @@ -46,7 +46,7 @@ class CustomQuantumModel(QuantumModel): The custom model can be used like any other `QuantumModel`: ```python exec="on" source="material-block" result="json" session="custom-model" from qadence import Parameter, RX, CNOT, QuantumCircuit -from qadence import chain, kron, total_magnetization +from qadence import chain, kron, hamiltonian_factory, Z from sympy import acos def quantum_circuit(n_qubits): @@ -64,7 +64,7 @@ def quantum_circuit(n_qubits): n_qubits = 4 batch_size = 10 circuit = quantum_circuit(n_qubits) -observable = total_magnetization(n_qubits) +observable = hamiltonian_factory(n_qubits, detuning=Z) # Total magnetization model = CustomQuantumModel(circuit, observable, backend="pyqtorch") diff --git a/docs/advanced_tutorials/differentiability.md b/docs/advanced_tutorials/differentiability.md index 5c3007041..bb6034fe6 100644 --- a/docs/advanced_tutorials/differentiability.md +++ b/docs/advanced_tutorials/differentiability.md @@ -66,7 +66,7 @@ In Qadence, the GPSR differentiation engine can be selected by passing `diff_mod ```python exec="on" source="material-block" session="differentiability" from qadence import (FeatureParameter, HamEvo, X, I, Z, - total_magnetization, QuantumCircuit, + hamiltonian_factory, QuantumCircuit, QuantumModel, BackendName, DiffMode) import torch @@ -83,7 +83,7 @@ block = HamEvo(generator, x) circuit = QuantumCircuit(n_qubits, block) # create total magnetization cost operator -obs = total_magnetization(n_qubits) +obs = hamiltonian_factory(n_qubits, detuning=Z) # create models with AD and GPSR differentiation engines model_ad = QuantumModel(circuit, obs, diff --git a/docs/digital_analog_qc/pulser-basic.md b/docs/digital_analog_qc/pulser-basic.md index 639d2cbc2..a51a65684 100644 --- a/docs/digital_analog_qc/pulser-basic.md +++ b/docs/digital_analog_qc/pulser-basic.md @@ -1,3 +1,7 @@ +!!! warning + This tutorial needs to be fixed. + + Qadence offers a direct interface with Pulser[^1], an open-source pulse-level interface written in Python and specifically designed for programming neutral atom quantum computers. Using directly Pulser requires deep knowledge on pulse-level programming and on how neutral atom devices work. Qadence abstracts out this complexity by using the familiar block-based interface for building pulse sequences in Pulser while leaving the possibility @@ -54,7 +58,7 @@ bell_state = chain( To convert the chain block into a pulse sequence, we define a `Register` with two qubits and combine it to create a circuit as usual. Then we construct a `QuantumModel` with a Pulser backend to convert it into a proper parametrized pulse sequence. Supplying the parameter values allows to sample from the pulse sequence result. -```python exec="on" source="material-block" session="pulser-basic" +```python exec="on" source="material-block" "html=1" session="pulser-basic" import torch import matplotlib.pyplot as plt from qadence import Register, QuantumCircuit, QuantumModel @@ -96,7 +100,10 @@ At variance with other backends, the Pulser one provides the concept of `Device` A `Device` instance encapsulate all the properties defining a real neutral atoms processor, including but not limited to the maximum laser amplitude for the pulses, the maximum distance between two qubits and the maximum duration of the pulse. -`qadence` offers a simplified interface with only two devices which can be found [here][qadence.backends.pulser.devices] +!!! warning + Fix link below. + +Qadence offers a simplified interface with only two devices which can be found [here](/backends.pulser.devices) * `IDEALIZED` (default): ideal device which should be used only for testing purposes. It does not have any limitation in what can be run with it. * `REALISTIC`: device specification very similar to a real neutral atom quantum processor. @@ -121,16 +128,17 @@ model = QuantumModel( configuration={"device_type": Device.REALISTIC} ) -# alternatively directly one of the devices available in Pulser -# can also be supplied in the same way -from pulser.devices import AnalogDevice - -model = QuantumModel( - circuit, - backend="pulser", - diff_mode="gpsr", - configuration={"device_type": AnalogDevice} -) +# FIXME: Specified device is not supported. +# # alternatively directly one of the devices available in Pulser +# # can also be supplied in the same way +# from pulser.devices import AnalogDevice + +# model = QuantumModel( +# circuit, +# backend="pulser", +# diff_mode="gpsr", +# configuration={"device_type": AnalogDevice} +# ) ``` ## Create your own gate @@ -156,32 +164,33 @@ obs = [zz, xy + yx] Now we define the `QuantumModel` and pass the observable list to it together with the constructed circuit. -```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" +# FIXME: protocol not defined +# from qadence import RX, AnalogRot -register = Register(2) -circuit = QuantumCircuit(register, protocol) -model = QuantumModel(circuit, backend="pulser", diff_mode='gpsr') +# register = Register(2) +# circuit = QuantumCircuit(register, protocol) +# model = QuantumModel(circuit, backend="pulser", diff_mode='gpsr') -params = { - "t": torch.tensor([383]), # ns - "y": torch.tensor([torch.pi / 2]), -} +# params = { +# "t": torch.tensor([383]), # ns +# "y": torch.tensor([torch.pi / 2]), +# } -sample = model.sample(params, n_shots=50)[0] +# sample = model.sample(params, n_shots=50)[0] -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 +# 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 ``` One can also easily access and manipulate the underlying pulse sequence. ```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 +# 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 ``` ## Large qubits registers @@ -191,48 +200,50 @@ 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 +# 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" html="1" session="pulser-basic" -protocol = chain( - entangle("t"), - AnalogRY(torch.pi / 2), -) +# from qadence import AnalogRY -register = Register.square(qubits_side=2) -circuit = QuantumCircuit(register, protocol) -model = QuantumModel(circuit, backend="pulser", diff_mode="gpsr") +# protocol = chain( +# entangle("t"), +# AnalogRY(torch.pi / 2), +# ) -# add modulation to the pulse sequence by modifying the -# backend configuration -model.backend.backend.config.with_modulation = True +# register = Register.square(qubits_side=2) +# circuit = QuantumCircuit(register, protocol) +# model = QuantumModel(circuit, backend="pulser", diff_mode="gpsr") -params = { - "x": torch.tensor([3*torch.pi/2]), # ns -} +# # add modulation to the pulse sequence by modifying the +# # backend configuration +# model.backend.backend.config.with_modulation = True -sample = model.sample(params, n_shots=500)[0] +# params = { +# "x": torch.tensor([3*torch.pi/2]), # ns +# } -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 +# 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 ``` Again, let's plot the corresponding pulse sequence. ```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 +# 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 @@ -247,40 +258,40 @@ version of a quantum neural network circuit with feature map and variational ansatz. ```python exec="on" source="material-block" html="1" session="pulser-basic" -from qadence import kron, fourier_feature_map -from qadence.operations import RX, RY, AnalogRX - -hea_one_layer = chain( - kron(RY(0, "th00"), RY(1, "th01")), - kron(RX(0, "th10"), RX(1, "th11")), - kron(RY(0, "th20"), RY(1, "th21")), - entangle("t", qubit_support=(0,1)), -) - -protocol = chain( - fourier_feature_map(1, param="x"), - hea_one_layer, - AnalogRX(torch.pi/4) -) - -register = Register(2) -circuit = QuantumCircuit(register, protocol) -model = QuantumModel(circuit, backend="pulser", diff_mode="gpsr") - -params = { - "x": torch.tensor([0.8]), # rad - "t": torch.tensor([900]), # ns - "th00": torch.rand(1), # rad - "th01": torch.rand(1), # rad - "th10": torch.rand(1), # rad - "th11": torch.rand(1), # rad - "th20": torch.rand(1), # rad - "th21": torch.rand(1), # rad -} - -model.assign_parameters(params).draw(draw_phase_area=True, show=True) -from docs import docsutils # markdown-exec: hide -print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide +# from qadence import kron, fourier_feature_map +# from qadence.operations import RX, RY, AnalogRX + +# hea_one_layer = chain( +# kron(RY(0, "th00"), RY(1, "th01")), +# kron(RX(0, "th10"), RX(1, "th11")), +# kron(RY(0, "th20"), RY(1, "th21")), +# entangle("t", qubit_support=(0,1)), +# ) + +# protocol = chain( +# fourier_feature_map(1, param="x"), +# hea_one_layer, +# AnalogRX(torch.pi/4) +# ) + +# register = Register(2) +# circuit = QuantumCircuit(register, protocol) +# model = QuantumModel(circuit, backend="pulser", diff_mode="gpsr") + +# params = { +# "x": torch.tensor([0.8]), # rad +# "t": torch.tensor([900]), # ns +# "th00": torch.rand(1), # rad +# "th01": torch.rand(1), # rad +# "th10": torch.rand(1), # rad +# "th11": torch.rand(1), # rad +# "th20": torch.rand(1), # rad +# "th21": torch.rand(1), # rad +# } + +# model.assign_parameters(params).draw(draw_phase_area=True, show=True) +# from docs import docsutils # markdown-exec: hide +# print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide ``` ## References diff --git a/docs/index.md b/docs/index.md index ed98b4173..d8c704176 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,16 +1,13 @@ **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. -[![pre-commit](https://github.com/pasqal-io/qadence/actions/workflows/lint.yml/badge.svg)](https://github.com/pasqal-io/qadence/actions/workflows/lint.yml) -[![tests](https://github.com/pasqal-io/qadence/actions/workflows/test_fast.yml/badge.svg)](https://github.com/pasqal-io/qadence/actions/workflows/test_fast.yml) - ## Feature highlights * A [block-based system](tutorials/getting_started.md) for composing _**complex digital-analog 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**_ +* A [simple interface](digital_analog_qc/analog-basics.md) to work with _**interacting neutral-atom qubit systems**_ using [arbitrary registers topologies](tutorials/register.md). * 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. @@ -21,26 +18,7 @@ programs**_ with tunable qubit interaction defined on _**arbitrary register topo * _**Efficient execution**_ on a variety of different purpose backends: from state vector simulators to tensor network emulators and real devices. -In following are some rudimentary examples of Qadence possibilites in the digital, analog and digital-analog paradigms. - - -## Citation - -If you use Qadence for a publication, we kindly ask you to cite our work using the bibtex citation: - -``` -@misc{qadence2023pasqal, - url = {https://github.com/pasqal-io/qadence}, - title = {Qadence: {A} {D}igital-analog quantum programming interface.}, - year = {2023} -} -``` - -The library name is from music terminology: Qadence allows to compose blocks into complex quantum programs in such -a seamless way that they flow like music. - -## Remarks -Quadence uses torch.float64 as the default datatype for tensors (torch.complex128 for complex tensors). +In following are some examples of Qadence possibilites in the digital, analog and digital-analog paradigms. ## Sampling the canonical Bell state @@ -56,7 +34,7 @@ bell_state = chain(H(0), CNOT(0,1)) # Sample with 100 shots. samples = sample(bell_state, n_shots=100) -print(samples) # markdown-exec: hide +print(f"samples = {samples}") # markdown-exec: hide from qadence.divergences import js_divergence # markdown-exec: hide from collections import Counter # markdown-exec: hide js = js_divergence(samples[0], Counter({"00":50, "11":50})) # markdown-exec: hide @@ -99,7 +77,7 @@ assert samples[0] == Counter({"001": 100}) # markdown-exec: hide ## Digital-analog example -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. +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 for qubits. ```python exec="on" source="material-block" result="json" from torch import pi @@ -125,20 +103,20 @@ js = js_divergence(samples[0], Counter({"01":33, "10":33, "00":33, "11":1})) # m assert js < 0.05 # markdown-exec: hide``` ``` -## Further Resources +## Further resources 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**. +* [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. +* [Advanced features](advanced_tutorials/differentiability.md) 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` from PyPI as follows: +Qadence can be installed from PyPI with `pip` as follows: ```bash pip install qadence @@ -170,4 +148,15 @@ pip install qadence[braket, pulser, visualization] # via conda conda install python-graphviz ``` ---- + +## Citation + +If you use Qadence for a publication, we kindly ask you to cite our work using the following BibTex entry: + +``` +@misc{qadence2023pasqal, + url = {https://github.com/pasqal-io/qadence}, + title = {Qadence: {A} {D}igital-analog quantum programming interface.}, + year = {2023} +} +``` diff --git a/docs/qml/qcl.md b/docs/qml/qcl.md index 1206c5acc..a06309fba 100644 --- a/docs/qml/qcl.md +++ b/docs/qml/qcl.md @@ -72,7 +72,7 @@ ansatz = qd.hea(n_qubits, depth=n_qubits, strategy=qd.Strategy.SDAQC) ansatz = qd.tag(ansatz, "ansatz") # total magnetization observable -observable = qd.total_magnetization(n_qubits) +observable = qd.hamiltonian_factory(n_qubits, detuning = qd.Z) circuit = qd.QuantumCircuit(n_qubits, feature_map, ansatz) model = qd.QNN(circuit, [observable]) diff --git a/docs/tutorials/backends.md b/docs/tutorials/backends.md index e6fd1f172..509dd4237 100644 --- a/docs/tutorials/backends.md +++ b/docs/tutorials/backends.md @@ -1,8 +1,8 @@ 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`. +a backend is via the high-level API `QuantumModel`. -!!! note: "Not all backends are equivalent" +!!! 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. @@ -33,7 +33,7 @@ for the given backend. This can be chosen from two types: In practice, only a `diff_mode` should be provided in the `QuantumModel`. Please note that `diff_mode` defaults to `None`: -```python exec="on" source="material-block" session="diff-backend" +```python exec="on" source="material-block" result="json" session="diff-backend" import sympy import torch from qadence import Parameter, RX, RZ, Z, CNOT, QuantumCircuit, QuantumModel, chain, BackendName, DiffMode @@ -72,7 +72,7 @@ print(f"{dexp_dx = }") # markdown-exec: hide ## Low-level `backend_factory` interface -Every backend in `qadence` inherits from the abstract `Backend` class: +Every backend in Qadence inherits from the abstract `Backend` class: [`Backend`](../backends/backend.md) and implement the following methods: - [`run`][qadence.backend.Backend.run]: propagate the initial state according to the quantum circuit and return the final wavefunction object. @@ -107,11 +107,11 @@ from qadence import backend_factory # Use only Braket in non-differentiable mode: backend = backend_factory("braket") -# the `Converted` object +# The `Converted` object # (contains a `ConvertedCircuit` with the original and native representation) conv = backend.convert(circuit) -print(f"{conv.circuit.original = }") -print(f"{conv.circuit.native = }") +print(f"{conv.circuit.original = }") # markdown-exec: hide +print(f"{conv.circuit.native = }") # markdown-exec: hide ``` Additionally, `Converted` contains all fixed and variational parameters, as well as an embedding @@ -140,7 +140,8 @@ Note that above the parameters keys have changed as they now address the keys on 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") +from qadence import BackendName, DiffMode +pyq_backend = backend_factory(backend=BackendName.PYQTORCH, diff_mode=DiffMode.AD) # the `Converted` object # (contains a `ConvertedCircuit` wiht the original and native representation) diff --git a/docs/tutorials/getting_started.md b/docs/tutorials/getting_started.md index 3939c49fc..bace4776d 100644 --- a/docs/tutorials/getting_started.md +++ b/docs/tutorials/getting_started.md @@ -2,29 +2,32 @@ Quantum programs in Qadence are constructed via a block-system, with an emphasis *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. -??? note "Visualize blocks" +??? note "How to visualize blocks" - There are two ways to display blocks in a Python interpreter: either as a tree in ASCII format using `print` + There are two ways to display blocks in a Python interpreter: either as a tree in ASCII format using `print`: - ```python exec="on" source="material-block" html="1" + ```python exec="on" source="material-block" result="json" from qadence import X, Y, kron - print(kron(X(0), Y(1))) + + kron_block = kron(X(0), Y(1)) + print(kron_block) ``` Or using the visualization package which opens an interactive window: - ``` + ```python exec="on" source="material-block" html="1" from qadence import X, Y, kron - from visualisation import display + #from visualization import display - display(kron(X(0), Y(1))) + kron_block = kron(X(0), Y(1)) + #display(kron_block) 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 + print(html_string(kron(X(0), Y(1))), size="2,2") # markdown-exec: hide ``` -## Primitive Blocks +## 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. @@ -35,21 +38,21 @@ Two canonical examples of digital primitive blocks are the parametrized `RX` and from qadence import RX # A rotation gate on qubit 0 with a fixed numerical parameter. -rx0 = RX(0, 0.5) +rx_gate = 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 +print(html_string(chain(rx_gate), size="2,2")) # markdown-exec: hide ``` ```python exec="on" source="material-block" html="1" from qadence import CNOT # A CNOT gate with control on qubit 0 and target on qubit 1. -c01 = CNOT(0, 1) +cnot_gate = 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 +print(html_string(chain(cnot_gate), size="2,2")) # markdown-exec: hide ``` A list of all instances of primitive blocks (also referred to as *operations*) can be found [here](../qadence/operations.md). @@ -97,15 +100,12 @@ each sub-block and results in a `AddBlock` type which can be used to construct P 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. + It is always possible to retrieve the matrix representation of a block by calling the `block.tensor()` method. + 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()) + print(f"X(0) * X(0) tensor = {chain_x.tensor()}") # markdown-exec: hide + print(f"X(0) @ X(1) tensor = {chain_xx.tensor()}") # markdown-exec: hide ``` ```python exec="on" source="material-block" result="json" @@ -130,9 +130,7 @@ from qadence.draw import html_string # markdown-exec: hide print(html_string(final_block, size="4,4")) # markdown-exec: hide ``` -## Program execution - -### Fast block execution +## Block 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`. The following @@ -161,12 +159,12 @@ print(f"{ex = }") # markdown-exec: hide More fine-grained control and better performance is provided via the high-level `QuantumModel` abstraction. -### Execution via `QuantumCircuit` and `QuantumModel` +## Execution via `QuantumCircuit` and `QuantumModel` Quantum programs in Qadence are constructed in two steps: 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. +2. Define a [`QuantumModel`](/tutorials/quantummodels) which differentiates, compiles and executes the circuit. `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. @@ -180,7 +178,7 @@ from qadence import QuantumCircuit, Register, H, chain # on a register of three qubits. register = Register(3) circuit = QuantumCircuit(register, chain(H(0), H(1))) -print(circuit) # markdown-exec: hide +print(f"circuit = {circuit}") # markdown-exec: hide ``` !!! note "Registers and qubit supports" @@ -204,49 +202,3 @@ print(f"{xs = }") # markdown-exec: hide ``` For more details on `QuantumModel`, see [here](/tutorials/quantummodels). - -## State initialization - -Qadence offers convenience routines for preparing initial quantum states. -These routines are divided into two approaches: - -- 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). - -```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()}}") # markdown-exec: hide - -# 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 - -state_prep_b = product_block("10") -display(state_prep_b) - -# 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 0b3542303..2695ed7e4 100644 --- a/docs/tutorials/hamiltonians.md +++ b/docs/tutorials/hamiltonians.md @@ -14,7 +14,7 @@ from qadence import Interaction n_qubits = 3 -hamilt = hamiltonian_factory(n_qubits, interaction = Interaction.ZZ) +hamilt = hamiltonian_factory(n_qubits, interaction=Interaction.ZZ) print(hamilt) # markdown-exec: hide ``` @@ -33,15 +33,14 @@ n_qubits = 3 hamilt = hamiltonian_factory( n_qubits, - interaction = Interaction.ZZ, - detuning = Z, - interaction_strength = [0.5, 0.2, 0.1], - detuning_strength = [0.1, 0.5, -0.3] - ) + interaction=Interaction.ZZ, + detuning=Z, + interaction_strength=[0.5, 0.2, 0.1], + detuning_strength=[0.1, 0.5, -0.3] +) print(hamilt) # markdown-exec: hide ``` - !!! warning "Ordering interaction strengths matters" When passing interaction strengths as an array, the ordering must be indentical to the one @@ -55,7 +54,7 @@ print(hamilt) # markdown-exec: hide For one more example, let's create a transverse-field Ising model, -```python exec="on" source="material-block" session="hamiltonians" +```python exec="on" source="material-block" result="json" session="hamiltonians" n_qubits = 4 n_edges = int(0.5 * n_qubits * (n_qubits - 1)) @@ -64,16 +63,18 @@ zz_terms = [2.0] * n_edges zz_ham = hamiltonian_factory( n_qubits, - interaction = Interaction.ZZ, - detuning = Z, - interaction_strength = zz_terms, - detuning_strength = z_terms - ) + interaction=Interaction.ZZ, + detuning=Z, + interaction_strength=zz_terms, + detuning_strength=z_terms +) x_terms = [-1.0] * n_qubits x_ham = hamiltonian_factory(n_qubits, detuning = X, detuning_strength = x_terms) transverse_ising = zz_ham + x_ham + +print(transverse_ising) # markdown-exec: hide ``` !!! note "Random interaction coefficients" @@ -89,9 +90,9 @@ Simply pass the register with the desired topology as the first argument to the ```python exec="on" source="material-block" result="json" session="hamiltonians" from qadence import Register -reg = Register.square(qubits_side = 2) +reg = Register.square(qubits_side=2) -square_hamilt = hamiltonian_factory(reg, interaction = Interaction.NN) +square_hamilt = hamiltonian_factory(reg, interaction=Interaction.NN) print(square_hamilt) # markdown-exec: hide ``` @@ -104,7 +105,7 @@ reg = Register.square(qubits_side = 2) for i, edge in enumerate(reg.edges): reg.edges[edge]["strength"] = (0.5 * i) ** 2 -square_hamilt = hamiltonian_factory(reg, interaction = Interaction.NN) +square_hamilt = hamiltonian_factory(reg, interaction=Interaction.NN) print(square_hamilt) # markdown-exec: hide ``` @@ -121,11 +122,11 @@ n_qubits = 3 nn_ham = hamiltonian_factory( n_qubits, - interaction = Interaction.NN, - detuning = N, - interaction_strength = "c", - detuning_strength = "d" - ) + interaction=Interaction.NN, + detuning=N, + interaction_strength="c", + detuning_strength="d" +) print(nn_ham) # markdown-exec: hide ``` diff --git a/docs/tutorials/ml_tools.md b/docs/tutorials/ml_tools.md index 281d00680..652c216dc 100644 --- a/docs/tutorials/ml_tools.md +++ b/docs/tutorials/ml_tools.md @@ -50,8 +50,8 @@ Let's look at a complete example of how to use `train_with_grad` now. 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.constructors import hamiltonian_factory, hea, feature_map +from qadence import chain, Parameter, QuantumCircuit, Z from qadence.models import QNN from qadence.ml_tools import train_with_grad, TrainConfig import matplotlib.pyplot as plt @@ -59,7 +59,7 @@ 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) +observable = hamiltonian_factory(n_qubits, detuning=Z) circuit = QuantumCircuit(n_qubits, fm, ansatz) model = QNN(circuit, observable, backend="pyqtorch", diff_mode="ad") @@ -111,15 +111,15 @@ written without `train_with_grad`. 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.constructors import hamiltonian_factory, hea, feature_map +from qadence import chain, Parameter, QuantumCircuit, Z 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) +observable = hamiltonian_factory(n_qubits, detuning=Z) circuit = QuantumCircuit(n_qubits, fm, ansatz) model = QNN(circuit, observable, backend="pyqtorch", diff_mode="ad") diff --git a/docs/tutorials/overlap.md b/docs/tutorials/overlap.md index f07657931..36c5a3d39 100644 --- a/docs/tutorials/overlap.md +++ b/docs/tutorials/overlap.md @@ -1,6 +1,5 @@ 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: +wavefunctions generated by two quantum circuits $U$ and $W$ as: $$ S = |\langle \psi_U | \psi_W \rangle|^2 \quad \textrm{where} \quad \psi_U = U|\psi_0\rangle @@ -8,7 +7,7 @@ $$ 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: +non-zero only when the rotation angle is different from $\pi \; \textrm{mod}\; 2\pi$ for both rotations: ```python exec="on" source="material-block" result="json" session="overlap" import torch @@ -35,7 +34,7 @@ values_ket = {"psi": torch.Tensor([torch.pi / 2, torch.pi])} 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) +print("Overlap with exact method:\n", ovrlp) # markdown-exec: hide ``` The `Overlap` class above inherits from `QuantumModel` and is executed through its inherited forward method @@ -56,7 +55,7 @@ probability distributions obtained by sampling the propagated circuits. This wil result than the other methods. All methods (except for the `EXACT` method) take an optional `n_shots` argument which can be used -for performing shot-based calculations. +to perform shot-based calculations. !!! warning If you select a finite number of shots, the overlap is not differentiable. Therefore, @@ -66,11 +65,11 @@ for performing shot-based calculations. # 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) +print("Overlap with SWAP test:\n", ovrlp_ha) # markdown-exec: hide # 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) -print("Overlap with SWAP test with finite number of shots:\n", ovrlp_ha) +print("Overlap with SWAP test with finite number of shots:\n", ovrlp_ha) # markdown-exec: hide ``` diff --git a/docs/tutorials/parameters.md b/docs/tutorials/parameters.md index 1b561f23e..ea8929626 100644 --- a/docs/tutorials/parameters.md +++ b/docs/tutorials/parameters.md @@ -1,13 +1,14 @@ +Qadence base `Parameter` type is a subtype of `sympy.Symbol`. There are three kinds of parameter subtypes used: -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. +- _**Fixed Parameter**_: A constant with a fixed, non-trainable value (_e.g._ $\dfrac{\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. + +## Fixed Parameters + +To pass a fixed parameter to a gate (or any parametrizable block), one can simply use either Python numeric types or wrapped in +a `torch.Tensor`. -## 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 @@ -15,14 +16,17 @@ from qadence import RX, run # Let's use a torch type. block = RX(0, pi) wf = run(block) -print(wf) +print(f"{wf = }") # markdown-exec: hide -# Lets pass a simple float. -print(run(RX(0, 1.))) +# Let's pass a simple float. +block = RX(0, 1.) +wf = run(block) +print(f"{wf = }") # markdown-exec: hide ``` -### 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: +## Variational Parameters + +To parametrize a block by an angle `theta`, either a Python `string` or an instance of `VariationalParameter` can be passed instead of a numeric type to the gate constructor: ```python exec="on" source="material-block" result="json" from qadence import RX, run, VariationalParameter @@ -32,13 +36,14 @@ block = RX(0, "theta") block = RX(0, VariationalParameter("theta")) wf = run(block) -print(wf) +print(f"{wf = }") # markdown-exec: hide ``` -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 +In the first case in the above example, `theta` is automatically inferred as a `VariationalParameter` (_i.e._ trainable). It is initialized to a random value for the purposes of execution. In the context of a `QuantumModel`, there is no need to pass a value for `theta` to the `run` method since it is stored within the underlying model parameter dictionary. + +## 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. +`FeatureParameter` types (_i.e._ inputs), always need to be provided with a value or a batch of values as a dictionary: ```python exec="on" source="material-block" result="json" from torch import tensor @@ -47,15 +52,16 @@ from qadence import RX, run, FeatureParameter block = RX(0, FeatureParameter("phi")) wf = run(block, values={"phi": tensor([1., 2.])}) -print(wf) +print(f"{wf = }") # markdown-exec: hide ``` -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`. +Now, `run` returns a batch of states, one for every provided angle which coincides with the value of the particular `FeatureParameter`. + +## Multiparameter Expressions -### 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! +However, an angle can itself be an expression `Parameter` types of any kind. +As such, any sympy expression `expr: sympy.Basic` consisting of a combination of free symbols (_i.e._ `sympy` types) and Qadence `Parameter` can +be passed to a block, including trigonometric functions. ```python exec="on" source="material-block" result="json" from torch import tensor @@ -65,16 +71,17 @@ 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: +# Remember, to run the block, only FeatureParameter values have to be provided: values = {"phi": tensor([1.0, 2.0])} wf = run(block, values=values) -print(wf) +print(f"{wf = }") # markdown-exec: hide ``` -### Re-using Parameters +## Parameters Redundancy + +Parameters are uniquely defined by their name and redundancy is allowed in composite blocks to +assign the same value to different blocks. -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 @@ -84,15 +91,15 @@ block = chain( kron(RX(0, "phi"), RY(1, "theta")), ) -wf = run(block) -print(wf) +wf = run(block) # Same random initialization for all instances of phi and theta. +print(f"{wf = }") # markdown-exec: hide ``` ## Parametrized Circuits -Now, let's have a look at a variational ansatz in `qadence`. +Now, let's have a look at the construction of a variational ansatz which composes `FeatureParameter` and `VariationalParameter` types: -```python exec="on" html="1" +```python exec="on" source="material-block" html="1" import sympy from qadence import RX, RY, RZ, CNOT, Z, run, chain, kron, FeatureParameter, VariationalParameter @@ -122,24 +129,22 @@ block = chain( ), chain(CNOT(0,1), CNOT(1,2)) ) -block.tag = "rotations" +block.tag = "Rotations" obs = 2*kron(*map(Z, range(3))) block = chain(block, obs) from qadence.draw import html_string # markdown-exec: hide -print(html_string(block)) # markdown-exec: hide +print(html_string(block, size="4,4")) # markdown-exec: hide ``` +Please note the different colors for the parametrization with different types. The default palette assigns light blue for `VariationalParameter`, light green for `FeatureParameter` and shaded red for observables. + ## Parametrized QuantumModels -Recap: -* _**Feature**_ parameters are used for data input and encode data into a quantum state. -* _**Variational**_ parameters are trainable parameters in a variational ansatz. -* [`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. +As a quick reminder: `FeatureParameter` are used for data input and data encoding into a quantum state. +`VariationalParameter` are trainable parameters in a variational ansatz. When used within a [`QuantumModel`][qadence.models.quantum_model.QuantumModel], an abstract quantum circuit is made differentiable with respect to both variational and feature +parameters which are uniquely identified by their name. ```python exec="on" source="material-block" session="parametrized-models" from qadence import FeatureParameter, Parameter, VariationalParameter @@ -169,8 +174,9 @@ block = chain( ) circuit = QuantumCircuit(2, block) +unique_params = circuit.unique_parameters -print("Unique parameters in the circuit: ", circuit.unique_parameters) +print(f"{unique_params = }") # markdown-exec: hide ``` In the circuit above, four parameters are defined but only two unique names. Therefore, there will be only one @@ -182,9 +188,11 @@ The `QuantumModel` class also provides convenience methods to manipulate paramet from qadence import QuantumModel, BackendName, DiffMode model = QuantumModel(circuit, backend=BackendName.PYQTORCH, diff_mode=DiffMode.AD) +num_vparams = model.num_vparams +vparams_values = model.vparams -print(f"Number of variational parameters: {model.num_vparams}") -print(f"Current values of the variational parameters: {model.vparams}") +print(f"{num_vparams = }") # markdown-exec: hide +print(f"{vparams_values = }") # markdown-exec: hide ``` !!! note "Only provide feature parameter values to the quantum model" @@ -197,7 +205,7 @@ print(f"Current values of the variational parameters: {model.vparams}") values = {"phi": torch.rand(3)} # theta does not appear here wf = model.run(values) - print(wf) + print(f"{wf = }") # markdown-exec: hide ``` ## Standard constructors @@ -214,7 +222,8 @@ depth = 2 hea1 = hea(n_qubits=n_qubits, depth=depth) circuit = QuantumCircuit(n_qubits, hea1) -print(f"Unique parameters with a single HEA: {circuit.num_unique_parameters}") +num_unique_parameters = circuit.num_unique_parameters +print(f"Unique parameters with a single HEA: {num_unique_parameters}") # markdown-exec: hide ``` ```python exec="on" html="1" session="parametrized-constructors" from qadence.draw import html_string @@ -228,8 +237,8 @@ is the same. hea2 = hea(n_qubits=n_qubits, depth=depth) circuit = QuantumCircuit(n_qubits, hea1, hea2) -n_params_two_heas = circuit.num_unique_parameters -print(f"Unique parameters with two stacked HEAs: {n_params_two_heas}") +num_unique_params_two_heas = circuit.num_unique_parameters +print(f"Unique parameters with two stacked HEAs: {num_unique_params_two_heas}") # markdown-exec: hide ``` ```python exec="on" html="1" session="parametrized-constructors" from qadence.draw import html_string # markdown-exec: hide @@ -245,14 +254,14 @@ print(html_string(circuit)) # markdown-exec: hide circuit = QuantumCircuit(n_qubits, hea1, hea2) n_params_two_heas = circuit.num_unique_parameters - print(f"Unique parameters with two stacked HEAs: {n_params_two_heas}") + print(f"Unique parameters with two stacked HEAs: {n_params_two_heas}") # markdown-exec: hide ``` ```python exec="on" html="1" session="parametrized-constructors" from qadence.draw import html_string # 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). +The `hea` function will be further explored in the [QML Constructors tutorial](qml_tools.md). ## Parametric observables @@ -273,12 +282,12 @@ The observable variational parameters are included among the model ones. from qadence import QuantumModel, QuantumCircuit circuit = QuantumCircuit(n_qubits, hea(n_qubits, depth)) -model = QuantumModel(circuit, observable=observable, backend="pyqtorch", diff_mode="ad") -print(model.vparams) +model = QuantumModel(circuit, observable=observable) +print(f"Variational parameters = {model.vparams}") # markdown-exec: hide ``` -One optimization step (forward and backeward pass) can be performed and variational parameters -have been updated accordingly: +One optimization step (forward and backward pass) can be performed using built-in `torch` functionalities. Variational parameters +can be checked to have been updated accordingly: ```python exec="on" source="material-block" result="json" session="parametrized-constructors" import torch @@ -286,36 +295,31 @@ import torch mse_loss = torch.nn.MSELoss() optimizer = torch.optim.Adam(model.parameters()) -# compute forward & backward pass +# Compute forward & backward pass optimizer.zero_grad() loss = mse_loss(model.expectation({}), torch.zeros(1)) loss.backward() -# update the parameters +# Update the parameters and check the parameters. optimizer.step() -print(model.vparams) +print(f"Variational parameters = {model.vparams}") # markdown-exec: hide ``` ## Non-unitary circuits -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. +Qadence allows to compose with non-unitary blocks. +Here is an example 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. +!!! warning "Currently, only the `PyQTorch` backend fully supports execution with non-unitary circuits." -!!! warning "Currently, only the PyQTorch backend fully supports execution with non-unitary circuits." - -```python exec="on" source="material-block" html="1" session="non-unitary" +```python exec="on" source="material-block" result="json" session="non-unitary" from qadence import QuantumModel, QuantumCircuit, Z, X c1 = 2.0 c2 = 2.0 + 2.0j block = c1 * Z(0) + c2 * X(1) + c1 * c2 * (Z(2) + X(3)) circuit = QuantumCircuit(4, block) -from qadence.draw import html_string # markdown-exec: hide -print(html_string(circuit)) # markdown-exec: hide -model = QuantumModel(circuit, backend='pyqtorch', diff_mode='ad') -print(model.run({})) +model = QuantumModel(circuit) # BackendName.PYQTORCH and DiffMode.AD by default. +print(f"wf = {model.run({})}") # markdown-exec: hide ``` diff --git a/docs/tutorials/qml_constructors.md b/docs/tutorials/qml_constructors.md deleted file mode 100644 index 411c64d65..000000000 --- a/docs/tutorials/qml_constructors.md +++ /dev/null @@ -1,105 +0,0 @@ -# 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/qml_tools.md b/docs/tutorials/qml_tools.md new file mode 100644 index 000000000..cb213051d --- /dev/null +++ b/docs/tutorials/qml_tools.md @@ -0,0 +1,254 @@ +# Quantum Machine Learning 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"Fourier = {fm}") # markdown-exec: hide + +fm = feature_map(n_qubits, fm_type="chebyshev") +print(f"Chebyshev {fm}") # markdown-exec: hide + +fm = feature_map(n_qubits, fm_type="tower") +print(f"Tower {fm}") # markdown-exec: hide +``` + +## Hardware-Efficient Ansatz + +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="4,4")) # 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="4,4")) # markdown-exec: hide +``` + +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, + depth=depth, + strategy=Strategy.SDAQC +) +from qadence.draw import html_string # markdown-exec: hide +print(html_string(ansatz, size="4,4")) # 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="4,4")) # markdown-exec: hide +``` +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). + +## Machine Learning Tools + +`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: ``), 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 hamiltonian_factory, hea, feature_map +from qadence import chain, Parameter, QuantumCircuit, Z +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 = hamiltonian_factory(n_qubits, detuning=Z) +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 hamiltonian_factory, hea, feature_map +from qadence import chain, Parameter, QuantumCircuit, Z +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 = hamiltonian_factory(n_qubits, detuning=Z) +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/quantummodels.md b/docs/tutorials/quantummodels.md index 7b9b4d817..42e3e8295 100644 --- a/docs/tutorials/quantummodels.md +++ b/docs/tutorials/quantummodels.md @@ -1,7 +1,5 @@ A quantum program can be expressed and executed using the [`QuantumModel`][qadence.models.quantum_model.QuantumModel] type. -They serve three primary purposes: - -_**Execution**_: by defining which backend the program is expected to be executed on. Qadence supports circuit compilation to the native backend representation. +It serves three primary purposes: _**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)). @@ -9,28 +7,30 @@ _**Parameter handling**_: by conveniently handling and embedding the two paramet _**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. +_**Execution**_: by defining which backend the program is expected to be executed on. Qadence supports circuit compilation to the native backend representation. + !!! note "Backends" 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/) + 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 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. +* `QuantumModel.sample()`: Sample a bitstring from the resulting quantum state after circuit execution. Supported by all backends. +* `QuantumModel.expectation()`: 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) that enables differentiability for its `expectation` method. -Upon construction of the model a compiled version of the abstract `QuantumCircuit` is +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, Parameter, BackendName +from qadence import QuantumCircuit, QuantumModel, RX, Z, chain, BackendName, Parameter # Construct a parametrized abstract circuit. # At this point we cannot run anything yet. @@ -44,47 +44,47 @@ observable = Z(0) # Construct a QuantumModel which will compile # the abstract circuit to targetted backend. +# By default, diff_mode=DiffMode.AD. model = QuantumModel(circuit, observable, backend=BackendName.PYQTORCH) -# now we construct a QuantumModel which will compile -# the abstract circuit to the backend we specify -model = QuantumModel(circuit, observable, backend="pyqtorch", diff_mode='ad') - -# the converted circuit is a private attribute and should not +# The converted circuit is a private attribute and should not # manually be tampered with, but we can at least verify its there +# by printing it. print(model._circuit.native) -from pyqtorch.modules import QuantumCircuit as PyQCircuit -assert isinstance(model._circuit.native, PyQCircuit) +from pyqtorch.modules import QuantumCircuit as PyQCircuit # markdown-exec: hide +assert isinstance(model._circuit.native, PyQCircuit) # markdown-exec: hide ``` -Now, the wavefunction, sample, or expectation value are computable: +Now, the wavefunction, sample, or expectation value are computable by passing a batch of values : ```python exec="on" source="material-block" result="json" session="quantum-model" import torch -# Set a batch of parameter values. +# Set a batch of random parameter values. values = {"x": torch.rand(3)} wf = model.run(values) -print(f"{wf=}") +print(f"{wf = }") # markdown-exec: hide xs = model.sample(values, n_shots=100) -print(f"{xs=}") +print(f"{xs = }") # markdown-exec: hide ex = model.expectation(values) -print(f"{ex=}") +print(f"{ex = }") # markdown-exec: hide ``` You can also measure multiple observables by passing a list of blocks. + ```python exec="on" source="material-block" result="json" session="quantum-model" -model = QuantumModel(circuit, [Z(0), Z(1)], backend="pyqtorch", diff_mode='ad') +# By default, backend=BackendName.PYQTORCH. +model = QuantumModel(circuit, [Z(0), Z(1)]) ex = model.expectation(values) -print(ex) +print(f"{ex = }") # markdown-exec: hide ``` ### Quantum Neural Network (QNN) 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 and the [parametric program tutorial](/tutorials/parameters.md#parametrized-models) for parametrization. +Tools](/tutorials/ml_tools) section or the [`QNN` API reference][qadence.models.QNN] for more detailed +information, and the [parametric program tutorial](parameters.md) for parameterization. diff --git a/docs/tutorials/register.md b/docs/tutorials/register.md index 8aa11ad80..9a1ddfee1 100644 --- a/docs/tutorials/register.md +++ b/docs/tutorials/register.md @@ -1,8 +1,6 @@ -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. - +In Qadence, quantum programs can be executed by specifying the layout of a register of resources as a lattice. +Built-in [`Register`][qadence.register.Register] types can be used or constructed for arbitrary topologies. +Common register topologies are available and illustrated in the plot below. ```python exec="on" html="1" import numpy as np @@ -51,9 +49,9 @@ from docs import docsutils # markdown-exec: hide print(docsutils.fig_to_html(fig)) # markdown-exec: hide ``` -## Building adn drawing registers +## Building and drawing registers -In following are few examples of built-in topologies accessible: +Built-in topologies are directly accessible in the `Register`: ```python exec="on" source="material-block" html="1" from qadence import Register @@ -69,7 +67,9 @@ print(docsutils.fig_to_html(plt.gcf())) # markdown-exec: hide ``` 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. + +!!! note "Registers defined from coordinates" + `Register` constructed via the `from_coordinates` method do not define edges in the connectivity graph. ```python exec="on" source="material-block" html="1" import numpy as np @@ -91,17 +91,15 @@ print(docsutils.fig_to_html(fig)) # markdown-exec: hide !!! 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_. + For instance, [Pulser](https://github.com/pasqal-io/Pulser) uses $\mu \textrm{m}$. -## Detailed Usage +## Connectivity graphs -Register topology is often disregarded in simulations where an all-to-all qubit connectivity is assumed. +Register topology is often asssumed in simulations to be an all-to-all qubit connectivity. 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, +qubit interaction must be specified either by specifying distances between qubits, or by defining edges in the register connectivity graph. -### Abstract graphs - 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. @@ -109,19 +107,16 @@ transfer](/#perfect-state-transfer) example. from qadence import Register reg = Register.rectangular_lattice(2,3) -print(f"{reg.nodes=}") -print(f"{reg.edges=}") +print(f"{reg.nodes = }") # markdown-exec: hide +print(f"{reg.edges = }") # markdown-exec: hide ``` -### 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: +In that case, `Register.coords` are accessible from the concrete graph: ```python exec="on" source="material-block" result="json" session="reg-usage" -print(f"{reg.coords=}") +print(f"{reg.coords = }") # markdown-exec: hide ``` -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). +More details about their usage in the digital-analog paradigm can be found in the [digital-analog basics](/digital_analog_qc/analog-basics) section. diff --git a/docs/tutorials/serializ_and_prep.md b/docs/tutorials/serializ_and_prep.md index de5d3a0cb..a0fe5600d 100644 --- a/docs/tutorials/serializ_and_prep.md +++ b/docs/tutorials/serializ_and_prep.md @@ -1,60 +1,42 @@ -!!! warning "Serialization" - either on a separate page or move to API? (I would prefer the latter I believe) - -```python exec="on" session="seralize" -from rich import print -from qadence.draw import html_string -display = lambda x: print(html_string(x)) -``` -Here you will learn about some convenience tools offered by Qadence for -constructing quantum programs, state preparation, and serialization of `qadence` objects. - - -## Serialize and deserialize quantum programs - -Qadence offers some convenience functions for serializing and deserializing any -quantum program. This can be very useful for storing quantum programs and -sending them over the network via an API. +Qadence offers convenience functions for serializing and deserializing any +quantum program. This is useful for storing quantum programs and +sending them for execution over the network via an API. !!! note - Qadence currently uses a custom JSON serialization format. Support for QASM - format for digital quantum programs will come soon! + Qadence currently uses a custom JSON serialization as interchange format. Support for QASM + format for digital quantum programs is currently under consideration. -Qadence serialization offers two sets of serialization functions which work with -all the main components of Qadence: * `serialize/deserialize`: serialize and deserialize a Qadence object into a dictionary * `save/load`: save and load a Qadence object to a file with one of the supported - formats. This is built on top of the `serialize`/`deserialize` routines. - Currently, these are `.json` and the PyTorch-compatible `.pt` format. + formats. Currently, these are `.json` and the PyTorch-compatible `.pt` format. Let's start with serialization into a dictionary. ```python exec="on" source="material-block" session="seralize_2" import torch -from qadence import QuantumCircuit, QuantumModel -from qadence import chain, total_magnetization, feature_map, hea -from qadence.serialization import serialize, deserialize +from qadence import QuantumCircuit, QuantumModel, DiffMode +from qadence import chain, hamiltonian_factory, feature_map, hea, Z from qadence.serialization import serialize, deserialize n_qubits = 4 my_block = chain(feature_map(n_qubits, param="x"), hea(n_qubits, depth=2)) -obs = total_magnetization(n_qubits) +obs = hamiltonian_factory(n_qubits, detuning=Z) -# use the block defined above to create a quantum circuit +# Use the block defined above to create a quantum circuit # serialize/deserialize it qc = QuantumCircuit(n_qubits, my_block) qc_dict = serialize(qc) qc_deserialized = deserialize(qc_dict) assert qc == qc_deserialized -# you can also let's wrap it in a QuantumModel -# and also serialize it -qm = QuantumModel(qc, obs, diff_mode='ad') +# Let's wrap it in a QuantumModel +# and serialize it +qm = QuantumModel(qc, obs, diff_mode=DiffMode.AD) qm_dict = serialize(qm) qm_deserialized = deserialize(qm_dict) -# check if the loaded QuantumModel returns the same expectation +# Check if the loaded QuantumModel returns the same expectation values = {"x": torch.rand(10)} assert torch.allclose(qm.expectation(values=values), qm_deserialized.expectation(values=values)) ``` diff --git a/docs/tutorials/state_conventions.md b/docs/tutorials/state_conventions.md index 326470575..5ccd8cfbb 100644 --- a/docs/tutorials/state_conventions.md +++ b/docs/tutorials/state_conventions.md @@ -34,61 +34,66 @@ Given the register convention in Qadence, the integer $2$ written in binary big- The convention for Qadence is **big-endian**. -## In practice +## Quantum states 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 +A simple and direct way to exemplify the endianness convention is using convenience functions for state preparation. -A simple and direct way to exemplify the endianness convention is the following: +!!! note "Bitstring convention as inputs" + When a bitstring is passed as input to a function for state preparation, it has to be understood in + **big-endian** convention. ```python exec="on" source="material-block" result="json" session="end-0" -import qadence as qd -from qadence import Endianness +from qadence import Endianness, product_state # The state |10>, the 3rd basis state. -state_big = qd.product_state("10", endianness = Endianness.BIG) # or just "Big" +state_big = product_state("10", endianness=Endianness.BIG) # or just "Big" # The state |01>, the 2nd basis state. -state_little = qd.product_state("10", endianness = Endianness.LITTLE) # or just "Little" +state_little = product_state("10", endianness=Endianness.LITTLE) # or just "Little" -print(state_big) # markdown-exec: hide -print(state_little) # markdown-exec: hide +print(f"State in big endian = {state_big}") # markdown-exec: hide +print(f"State in little endian = {state_little}") # markdown-exec: hide ``` -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. +Here, a bitword expressed as a Python string to encode the integer 2 in big-endian is used to create the respective basis state in 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 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: +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 +from qadence import I, H, sample # Create superposition state: |00> + |01> (normalized) block = I(0) @ H(1) # Identity on qubit 0, Hadamard on qubit 1 # Generate bitword samples following both conventions # Samples "00" and "01" -result_big = qd.sample(block, endianness = Endianness.BIG) +result_big = sample(block, endianness=Endianness.BIG) # Samples "00" and "10" -result_little = qd.sample(block, endianness = Endianness.LITTLE) +result_little = sample(block, endianness=Endianness.LITTLE) -print(result_big) # markdown-exec: hide -print(result_little) # markdown-exec: hide +print(f"Sample in big endian = {result_big}") # markdown-exec: hide +print(f"Sample in little endian = {result_little}") # markdown-exec: hide ``` -In Qadence we can also invert endianness of many objects with the same `invert_endianness` function: +In Qadence, endianness can be flipped for many relevant objects: ```python exec="on" source="material-block" result="json" session="end-0" +from qadence import invert_endianness + # Equivalent to sampling in little-endian. -print(qd.invert_endianness(result_big)) +flip_big_sample = invert_endianness(result_big) +print(f"Flipped sample = {flip_big_sample}") # markdown-exec: hide -# Equivalent to a state created in little-endian -print(qd.invert_endianness(state_big)) +# Equivalent to a state created in little-endian. +flip_big_state = invert_endianness(state_big) +print(f"Flipped state = {flip_big_state}") # markdown-exec: hide ``` -### Quantum operations +## Quantum operations -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: +When looking at the matricial form of quantum operations, the usage of the term *endianness* becomes slightly abusive. 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)} = @@ -110,39 +115,39 @@ $$ \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, both can be retrieved through the endianness argument: +The difference 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=Endianness.BIG) -print(matrix_big.detach()) -print("") # markdown-exec: hide -matrix_little = qd.block_to_tensor(qd.CNOT(0, 1), endianness=Endianness.LITTLE) -print(matrix_little.detach()) +from qadence import block_to_tensor, CNOT + +matrix_big = block_to_tensor(CNOT(0, 1), endianness=Endianness.BIG) +print("CNOT matrix in big endian =\n") # markdown-exec: hide +print(f"{matrix_big.detach()}\n") # markdown-exec: hide +matrix_little = block_to_tensor(CNOT(0, 1), endianness=Endianness.LITTLE) +print("CNOT matrix in little endian =\n") # markdown-exec: hide +print(f"{matrix_little.detach()}") # markdown-exec: hide ``` ## 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. In Qadence, this is taken care for automatically: by calling operations for different backends, the result is expected to be equivalent up to 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 +from qadence import BackendName, RX, run, sample import torch # RX(pi/4) on qubit 1 n_qubits = 2 -op = qd.RX(1, torch.pi/4) - -print("Same sampling order:") -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=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)) +op = RX(1, torch.pi/4) + +print("Same sampling order in big endian:\n") # markdown-exec: hide +print(f"On PyQTorch = {sample(n_qubits, op, endianness=Endianness.BIG, backend=BackendName.PYQTORCH)}") # markdown-exec: hide +print(f"On Braket = {sample(n_qubits, op, endianness=Endianness.BIG, backend=BackendName.BRAKET)}") # markdown-exec: hide +print(f"On Pulser = {sample(n_qubits, op, endianness=Endianness.BIG, backend=BackendName.PULSER)}\n") # markdown-exec: hide +print("Same wavefunction order:\n") # markdown-exec: hide +print(f"On PyQTorch = {run(n_qubits, op, endianness=Endianness.BIG, backend=BackendName.PYQTORCH)}") # markdown-exec: hide +print(f"On Braket = {run(n_qubits, op, endianness=Endianness.BIG, backend=BackendName.BRAKET)}") # markdown-exec: hide +print(f"On Pulser = {run(n_qubits, op, endianness=Endianness.BIG, backend=BackendName.PULSER)}") # markdown-exec: hide ``` diff --git a/docs/tutorials/state_init.md b/docs/tutorials/state_init.md index 0604b8ff7..3d2738b03 100644 --- a/docs/tutorials/state_init.md +++ b/docs/tutorials/state_init.md @@ -1,10 +1,60 @@ # State initialization -Several standard quantum states can be quickly initialized in `qadence`, both in statevector form as well as in block form. +Qadence offers convenience routines for preparing initial quantum states. +These routines are divided into two approaches: -## Statevector initialization +- 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. -Creating uniform, all-zero or all-one: +Let's illustrate the usage of the state preparation routine. + +```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("Random initial state generated with rotations:\n") # markdown-exec: hide +print(f"state = {state.detach().numpy().flatten()}\n") # markdown-exec: hide + +# 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("Product state corresponding to bitstring '01':\n") # markdown-exec: hide +print(f"state = {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 exec="on" source="material-block" html="1" +from qadence import product_block, tag, hea, QuantumCircuit +from qadence.draw import display + +state_prep_block = product_block("01") +display(state_prep_block) + +# Let's now prepare a circuit. +n_qubits = 4 + +state_prep_block = product_block("0001") +tag(state_prep_block, "Prep block") + +circuit_block = tag(hea(n_qubits, depth = 2), "Circuit block") + +qc_with_state_prep = QuantumCircuit(n_qubits, state_prep_block, circuit_block) +from qadence.draw import html_string # markdown-exec: hide +print(html_string(qc_with_state_prep), size="4,4") # markdown-exec: hide +``` +Several standard quantum states can be conveniently initialized in Qadence, both in statevector form as well as in block form as shown in following. + +## State vector initialization + +Qadence offers a number of constructor functions for state vector preparation. ```python exec="on" source="material-block" result="json" session="states" from qadence import uniform_state, zero_state, one_state @@ -12,21 +62,31 @@ 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)) +uniform_state = uniform_state(n_qubits, batch_size) +zero_state = zero_state(n_qubits, batch_size) +one_state = one_state(n_qubits, batch_size) +print("Uniform state = \n") # markdown-exec: hide +print(f"{uniform_state}") # markdown-exec: hide +print("Zero state = \n") # markdown-exec: hide +print(f"{zero_state}") # markdown-exec: hide +print("One state = \n") # markdown-exec: hide +print(f"{one_state}") # markdown-exec: hide ``` -Creating product states: +As already seen, product states can be easily created, even in batches: ```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)) +prod_state = product_state("100", batch_size) +print("Product state = \n") # markdown-exec: hide +print(f"{prod_state}\n") # markdown-exec: hide # Or a random product state -print(rand_product_state(n_qubits, batch_size)) +rand_state = rand_product_state(n_qubits, batch_size) +print("Random state = \n") # markdown-exec: hide +print(f"{rand_state}") # markdown-exec: hide ``` Creating a GHZ state: @@ -34,7 +94,10 @@ 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)) +ghz = ghz_state(n_qubits, batch_size) + +print("GHZ state = \n") # markdown-exec: hide +print(f"{ghz}") # markdown-exec: hide ``` Creating a random state uniformly sampled from a Haar measure: @@ -42,30 +105,37 @@ 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)) +rand_haar_state = random_state(n_qubits, batch_size) + +print("Random state from Haar = \n") # markdown-exec: hide +print(f"{rand_haar_state}") # markdown-exec: hide ``` -Custom initial states can then be passed to `run`, `sample` and `expectation` by passing the `state` argument +Custom initial states can then be passed to either `run`, `sample` and `expectation` through 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) +final_state = run(CNOT(0, 1), state=init_state) + +print(f"Final state = {final_state}") # markdown-exec: hide ``` ## Block initialization -Not all backends support custom statevector initialization, however there are also utility functions to initialize the respective blocks: +Not all backends support custom statevector initialization, however previous utility functions have their counterparts 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)) +uniform_block = uniform_block(n_qubits) +print(uniform_block) # markdown-exec: hide + +one_block = one_block(n_qubits) +print(one_block) # markdown-exec: hide ``` Similarly, for product states: @@ -73,8 +143,11 @@ 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)) +product_block = product_block("100") +print(product_block) # markdown-exec: hide + +rand_product_block = rand_product_block(n_qubits) +print(rand_product_block) # markdown-exec: hide ``` And GHZ states: @@ -82,14 +155,15 @@ And GHZ states: ```python exec="on" source="material-block" result="json" session="states" from qadence import ghz_block -print(ghz_block(n_qubits)) +ghz_block = ghz_block(n_qubits) +print(ghz_block) # markdown-exec: hide ``` 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` +Some state vector 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 diff --git a/mkdocs.yml b/mkdocs.yml index f631ba7cc..79ee8e47b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,14 +10,13 @@ nav: - Quantum Models: tutorials/quantummodels.md - Parametric Programs: tutorials/parameters.md - Quantum Registers: tutorials/register.md + - State Conventions: tutorials/state_conventions.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 + - Serialization: tutorials/serializ_and_prep.md - Backends: tutorials/backends.md - - State Conventions: tutorials/state_conventions.md - - Quantum Machine Learning: tutorials/ml_tools.md + - Tools for Classical and Quantum Machine Learning: tutorials/qml_tools.md - Digital-Analog Quantum Computing: - digital_analog_qc/daqc-basics.md diff --git a/qadence/blocks/block_to_tensor.py b/qadence/blocks/block_to_tensor.py index c36a18de5..2c98d96ba 100644 --- a/qadence/blocks/block_to_tensor.py +++ b/qadence/blocks/block_to_tensor.py @@ -315,13 +315,13 @@ def block_to_tensor( Examples: ```python exec="on" source="material-block" result="json" - from qadence import hea, total_magnetization, block_to_tensor + from qadence import hea, hamiltonian_factory, Z, block_to_tensor block = hea(2,2) print(block_to_tensor(block)) # In case you have a diagonal observable, you can use - obs = total_magnetization(2) + obs = hamiltonian_factory(2, detuning = Z) print(block_to_tensor(obs, tensor_type="SparseDiagonal")) ``` """ diff --git a/qadence/ml_tools/models.py b/qadence/ml_tools/models.py index 9823a80c6..5de9eed12 100644 --- a/qadence/ml_tools/models.py +++ b/qadence/ml_tools/models.py @@ -60,14 +60,14 @@ class TransformedModule(torch.nn.Module): from qadence.models import QNN, TransformedModule from qadence.circuit import QuantumCircuit from qadence.blocks import chain - from qadence.constructors import total_magnetization, hea - from qadence import Parameter, QuantumCircuit + from qadence.constructors import hamiltonian_factory, hea + from qadence import Parameter, QuantumCircuit, Z n_qubits = 2 phi = Parameter("phi", trainable=False) fm = chain(*[RY(i, phi) for i in range(n_qubits)]) ansatz = hea(n_qubits=n_qubits, depth=3) - observable = total_magnetization(n_qubits) + observable = hamiltonian_factory(n_qubits, detuning = Z) circuit = QuantumCircuit(n_qubits, fm, ansatz) model = QNN(circuit, observable, backend="pyqtorch", diff_mode="ad") diff --git a/qadence/ml_tools/train_grad.py b/qadence/ml_tools/train_grad.py index e17edb8c2..b47b870d4 100644 --- a/qadence/ml_tools/train_grad.py +++ b/qadence/ml_tools/train_grad.py @@ -64,15 +64,15 @@ def train( 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.constructors import hamiltonian_factory, hea, feature_map + from qadence import chain, Parameter, QuantumCircuit, Z 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) + observable = hamiltonian_factory(n_qubits, detuning = Z) circuit = QuantumCircuit(n_qubits, fm, ansatz) model = QNN(circuit, observable, backend="pyqtorch", diff_mode="ad") diff --git a/qadence/models/qnn.py b/qadence/models/qnn.py index 69296330b..1cd141585 100644 --- a/qadence/models/qnn.py +++ b/qadence/models/qnn.py @@ -20,14 +20,14 @@ class QNN(QuantumModel): ```python exec="on" source="material-block" result="json" import torch from qadence import QuantumCircuit, QNN - from qadence import hea, feature_map, total_magnetization + from qadence import hea, feature_map, hamiltonian_factory, Z # create the circuit n_qubits, depth = 2, 4 fm = feature_map(n_qubits) ansatz = hea(n_qubits=n_qubits, depth=depth) circuit = QuantumCircuit(n_qubits, fm, ansatz) - obs_base = total_magnetization(n_qubits) + obs_base = hamiltonian_factory(n_qubits, detuning = Z) # the QNN will yield two outputs obs = [2.0 * obs_base, 4.0 * obs_base] diff --git a/qadence/serialization.py b/qadence/serialization.py index b67ec5e51..2a53cdb54 100644 --- a/qadence/serialization.py +++ b/qadence/serialization.py @@ -104,7 +104,7 @@ def serialize(obj: SUPPORTED_TYPES, save_params: bool = False) -> dict: Examples: ```python exec="on" source="material-block" result="json" import torch - from qadence import serialize, deserialize, hea, total_magnetization + from qadence import serialize, deserialize, hea, hamiltonian_factory, Z from qadence import QuantumCircuit, QuantumModel n_qubits = 2 @@ -120,7 +120,7 @@ def serialize(obj: SUPPORTED_TYPES, save_params: bool = False) -> dict: assert qc == qc_deserialized ## Finally, let's wrap it in a QuantumModel - obs = total_magnetization(n_qubits) + obs = hamiltonian_factory(n_qubits, detuning = Z) qm = QuantumModel(qc, obs, backend='pyqtorch', diff_mode='ad') qm_dict = serialize(qm) @@ -165,7 +165,7 @@ def deserialize(d: dict, as_torch: bool = False) -> SUPPORTED_TYPES: Examples: ```python exec="on" source="material-block" result="json" import torch - from qadence import serialize, deserialize, hea, total_magnetization + from qadence import serialize, deserialize, hea, hamiltonian_factory, Z from qadence import QuantumCircuit, QuantumModel n_qubits = 2 @@ -181,7 +181,7 @@ def deserialize(d: dict, as_torch: bool = False) -> SUPPORTED_TYPES: assert qc == qc_deserialized ## Finally, let's wrap it in a QuantumModel - obs = total_magnetization(n_qubits) + obs = hamiltonian_factory(n_qubits, detuning = Z) qm = QuantumModel(qc, obs, backend='pyqtorch', diff_mode='ad') qm_dict = serialize(qm) @@ -263,7 +263,7 @@ def save( from pathlib import Path import os - from qadence import save, load, hea, total_magnetization + from qadence import save, load, hea, hamiltonian_factory, Z from qadence import QuantumCircuit, QuantumModel n_qubits = 2 @@ -275,7 +275,7 @@ def save( qc == loaded_qc os.remove('circ.json') ## Let's wrap it in a QuantumModel and store that - obs = total_magnetization(n_qubits) + obs = hamiltonian_factory(n_qubits, detuning = Z) qm = QuantumModel(qc, obs, backend='pyqtorch', diff_mode='ad') save(qm, folder= '.',file_name= 'quantum_model') qm_loaded = load('quantum_model.json') @@ -317,7 +317,7 @@ def load(file_path: str | Path, map_location: str = "cpu") -> SUPPORTED_TYPES: from pathlib import Path import os - from qadence import save, load, hea, total_magnetization + from qadence import save, load, hea, hamiltonian_factory, Z from qadence import QuantumCircuit, QuantumModel n_qubits = 2 @@ -329,7 +329,7 @@ def load(file_path: str | Path, map_location: str = "cpu") -> SUPPORTED_TYPES: qc == loaded_qc os.remove('circ.json') ## Let's wrap it in a QuantumModel and store that - obs = total_magnetization(n_qubits) + obs = hamiltonian_factory(n_qubits, detuning = Z) qm = QuantumModel(qc, obs, backend='pyqtorch', diff_mode='ad') save(qm, folder= '.',file_name= 'quantum_model') qm_loaded = load('quantum_model.json') diff --git a/tests/backends/pulser_basic/test_entanglement.py b/tests/backends/pulser_basic/test_entanglement.py index d2a68a457..d955a04fd 100644 --- a/tests/backends/pulser_basic/test_entanglement.py +++ b/tests/backends/pulser_basic/test_entanglement.py @@ -20,7 +20,7 @@ [ # Bell state ( - chain(entangle(383, qubit_support=(0, 1)), RY(0, 3 * torch.pi / 2)), + chain(entangle(1000, qubit_support=(0, 1)), RY(0, 3 * torch.pi / 2)), Register(2), Counter({"00": 250, "11": 250}), ), diff --git a/tests/backends/test_pulser_pyq_compat.py b/tests/backends/test_pulser_pyq_compat.py index 1311f14cd..de332ad65 100644 --- a/tests/backends/test_pulser_pyq_compat.py +++ b/tests/backends/test_pulser_pyq_compat.py @@ -12,7 +12,7 @@ from qadence.constructors import ising_hamiltonian, total_magnetization from qadence.divergences import js_divergence from qadence.models import QuantumModel -from qadence.operations import CNOT, RX, RY, AnalogRX, AnalogRY, H, X, Z, entangle, wait +from qadence.operations import CNOT, RX, RY, AnalogRX, AnalogRY, H, X, Z, entangle from qadence.parameters import FeatureParameter from qadence.types import DiffMode @@ -26,23 +26,8 @@ # Bell state generation ( QuantumCircuit(2, chain(H(0), CNOT(0, 1))), - QuantumCircuit(2, chain(entangle(383, qubit_support=(0, 1)), RY(0, 3 * torch.pi / 2))), - ), - # GHZ state 3-qubits - ( - QuantumCircuit(Register.line(3), chain(H(0), CNOT(0, 1), CNOT(1, 2))), - QuantumCircuit( - Register.line(3), - chain( - entangle(383, qubit_support=(0, 1, 2)), - RY(0, 3 * torch.pi / 2), - wait(660), - RY(2, 3 * torch.pi / 2), - ), - ), - ), - # NOTE: Although we can create an effective GHZ state with four qubits in Pulser, - # the final distribution is not compatible with the "pure" result from pyqtorch. + QuantumCircuit(2, chain(entangle(1000, qubit_support=(0, 1)), RY(0, 3 * torch.pi / 2))), + ) ], ) @pytest.mark.flaky(max_runs=5)