Skip to content

Commit

Permalink
Merge branch 'master' into qiboml_models_updates
Browse files Browse the repository at this point in the history
  • Loading branch information
BrunoLiegiBastonLiegi committed Sep 12, 2024
2 parents a5986df + 218f298 commit 9ac3d06
Show file tree
Hide file tree
Showing 28 changed files with 1,781 additions and 214 deletions.
6 changes: 3 additions & 3 deletions doc/source/api-reference/qibo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1213,9 +1213,9 @@ Matrix Hamiltonian
^^^^^^^^^^^^^^^^^^

The first implementation of Hamiltonians uses the full matrix representation
of the Hamiltonian operator in the computational basis. This matrix has size
``(2 ** nqubits, 2 ** nqubits)`` and therefore its construction is feasible
only when number of qubits is small.
of the Hamiltonian operator in the computational basis.
For :math:`n` qubits, this matrix has size :math:`2^{n} \times 2^{n}`.
Therefore, its construction is feasible only when :math:`n` is small.

Alternatively, the user can construct this Hamiltonian using a sparse matrices.
Sparse matrices from the
Expand Down
43 changes: 43 additions & 0 deletions doc/source/code-examples/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,46 @@ For example
q2: ──────o──|──|────o──|──|──H─U1─U1────────|─|─
q3: ─────────o──|───────o──|────o──|──H─U1───|─x─
q4: ────────────o──────────o───────o────o──H─x───

How to visualize a circuit with style?
--------------------------------------

Qibo is able to draw a circuit using ``matplotlib`` library by calling the function ``plot_circuit``. It also have built-in styles ready to use
and also it is possible to apply custom styles to the circuit. The function is able to cluster the gates to reduce the circuit depth.
The built-in styles are: ``garnacha``, ``fardelejo``, ``quantumspain``, ``color-blind``, ``cachirulo`` or custom dictionary.

For example, we can draw the QFT circuit for 5-qubits:

.. testcode::

import matplotlib.pyplot as plt
import qibo
from qibo import gates, models
from qibo.models import QFT

# new plot function based on matplotlib
from qibo.ui import plot_circuit

# create a 5-qubits QFT circuit
c = QFT(5)
c.add(gates.M(qubit) for qubit in range(2))

# print circuit with default options (default black & white style, scale factor of 0.6 and clustered gates)
plot_circuit(c);

# print the circuit with built-int style "garnacha", clustering gates and a custom scale factor
# built-in styles: "garnacha", "fardelejo", "quantumspain", "color-blind", "cachirulo" or custom dictionary
plot_circuit(c, scale = 0.8, cluster_gates = True, style="garnacha");

# plot the Qibo circuit with a custom style
custom_style = {
"facecolor" : "#6497bf",
"edgecolor" : "#01016f",
"linecolor" : "#01016f",
"textcolor" : "#01016f",
"fillcolor" : "#ffb9b9",
"gatecolor" : "#d8031c",
"controlcolor" : "#360000"
}

plot_circuit(c, scale = 0.8, cluster_gates = True, style=custom_style);
470 changes: 470 additions & 0 deletions examples/circuit-draw-mpl/qibo-draw-circuit-matplotlib.ipynb

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions src/qibo/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
QIBO_NATIVE_BACKENDS = ("numpy", "tensorflow", "pytorch", "qulacs")


class MissingBackend(ValueError):
"""Impossible to locate backend provider package."""


class MetaBackend:
"""Meta-backend class which takes care of loading the qibo backends."""

Expand Down Expand Up @@ -89,7 +93,7 @@ def __new__(cls):
try:
cls._instance = construct_backend(**kwargs)
break
except (ModuleNotFoundError, ImportError):
except (ImportError, MissingBackend):
pass

if cls._instance is None: # pragma: no cover
Expand Down Expand Up @@ -228,7 +232,7 @@ def construct_backend(backend, **kwargs) -> Backend:
if provider not in e.msg:
raise e
raise_error(
ValueError,
MissingBackend,
f"The '{backend}' backends' provider is not available. Check that a Python "
f"package named '{provider}' is installed, and it is exposing valid Qibo "
"backends.",
Expand Down
2 changes: 1 addition & 1 deletion src/qibo/backends/clifford.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def execute_circuit_repeated(self, circuit, nshots: int = 1000, initial_state=No
samples = self.np.vstack(samples)

for meas in circuit.measurements:
meas.result.register_samples(samples[:, meas.target_qubits], self)
meas.result.register_samples(samples[:, meas.target_qubits])

result = Clifford(
self.zero_state(circuit.nqubits),
Expand Down
49 changes: 26 additions & 23 deletions src/qibo/derivative.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,43 @@ def parameter_shift(
nshots=None,
):
"""In this method the parameter shift rule (PSR) is implemented.
Given a circuit U and an observable H, the PSR allows to calculate the derivative
of the expected value of H on the final state with respect to a variational
Given a circuit :math:`U` and an observable :math:`H`, the PSR allows to calculate the derivative
of the expected value of :math:`H` on the final state with respect to a variational
parameter of the circuit.
There is also the possibility of setting a scale factor. It is useful when a
circuit's parameter is obtained by combination of a variational
parameter and an external object, such as a training variable in a Quantum
Machine Learning problem. For example, performing a re-uploading strategy
to embed some data into a circuit, we apply to the quantum state rotations
whose angles are in the form: theta' = theta * x, where theta is a variational
parameter and x an input variable. The PSR allows to calculate the derivative
with respect of theta' but, if we want to optimize a system with respect its
variational parameters we need to "free" this procedure from the x depencency.
If the `scale_factor` is not provided, it is set equal to one and doesn't
whose angles are in the form :math:`\\theta^{\\prime} = x \\, \\theta`,
where :math:`\\theta` is a variational parameter, and :math:`x` an input variable.
The PSR allows to calculate the derivative with respect to :math:`\\theta^{\\prime}`.
However, if we want to optimize a system with respect to its
variational parameters, we need to "free" this procedure from the :math:`x` depencency.
If the ``scale_factor`` is not provided, it is set equal to one and doesn't
affect the calculation.
If the PSR is needed to be executed on a real quantum device, it is important
to set `nshots` to some integer value. This enables the execution on the
to set ``nshots`` to some integer value. This enables the execution on the
hardware by calling the proper methods.
Args:
circuit (:class:`qibo.models.circuit.Circuit`): custom quantum circuit.
hamiltonian (:class:`qibo.hamiltonians.Hamiltonian`): target observable.
if you want to execute on hardware, a symbolic hamiltonian must be
provided as follows (example with Pauli Z and ``nqubits=1``):
provided as follows (example with Pauli-:math:`Z` and :math:`n = 1`):
``SymbolicHamiltonian(np.prod([ Z(i) for i in range(1) ]))``.
parameter_index (int): the index which identifies the target parameter
in the ``circuit.get_parameters()`` list.
initial_state (ndarray, optional): initial state on which the circuit
acts. Default is ``None``.
scale_factor (float, optional): parameter scale factor. Default is ``1``.
acts. If ``None``, defaults to the zero state :math:`\\ket{\\mathbf{0}}`.
Defaults to ``None``.
scale_factor (float, optional): parameter scale factor. Defaults to :math:`1`.
nshots (int, optional): number of shots if derivative is evaluated on
hardware. If ``None``, the simulation mode is executed.
Default is ``None``.
Defaults to ``None``.
Returns:
(float): Value of the derivative of the expectation value of the hamiltonian
float: Value of the derivative of the expectation value of the hamiltonian
with respect to the target variational parameter.
Example:
Expand Down Expand Up @@ -167,27 +169,28 @@ def finite_differences(
step_size=1e-7,
):
"""
Calculate derivative of the expectation value of `hamiltonian` on the
final state obtained by executing `circuit` on `initial_state` with
respect to the variational parameter identified by `parameter_index`
Calculate derivative of the expectation value of ``hamiltonian`` on the
final state obtained by executing ``circuit`` on ``initial_state`` with
respect to the variational parameter identified by ``parameter_index``
in the circuit's parameters list. This method can be used only in
exact simulation mode.
Args:
circuit (:class:`qibo.models.circuit.Circuit`): custom quantum circuit.
hamiltonian (:class:`qibo.hamiltonians.Hamiltonian`): target observable.
if you want to execute on hardware, a symbolic hamiltonian must be
provided as follows (example with Pauli Z and ``nqubits=1``):
To execute on hardware, a symbolic hamiltonian must be
provided as follows (example with Pauli-:math:`Z` and :math:`n = 1`):
``SymbolicHamiltonian(np.prod([ Z(i) for i in range(1) ]))``.
parameter_index (int): the index which identifies the target parameter
in the ``circuit.get_parameters()`` list.
in the :meth:`qibo.models.Circuit.get_parameters` list.
initial_state (ndarray, optional): initial state on which the circuit
acts. Default is ``None``.
step_size (float): step size used to evaluate the finite difference
(default 1e-7).
acts. If ``None``, defaults to the zero state :math:`\\ket{\\mathbf{0}}`.
Defaults to ``None``.
step_size (float, optional): step size used to evaluate the finite difference.
Defaults to :math:`10^{-7}`.
Returns:
(float): Value of the derivative of the expectation value of the hamiltonian
float: Value of the derivative of the expectation value of the hamiltonian
with respect to the target variational parameter.
"""

Expand Down
15 changes: 14 additions & 1 deletion src/qibo/gates/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,18 @@
"_target_qubits",
"_control_qubits",
]
REQUIRED_FIELDS_INIT_KWARGS = ["theta", "phi", "lam", "phi0", "phi1"]
REQUIRED_FIELDS_INIT_KWARGS = [
"theta",
"phi",
"lam",
"phi0",
"phi1",
"register_name",
"collapse",
"basis",
"p0",
"p1",
]


class Gate:
Expand Down Expand Up @@ -107,6 +118,8 @@ def from_dict(raw: dict):
raise ValueError(f"Unknown gate {raw['_class']}")

gate = cls(*raw["init_args"], **raw["init_kwargs"])
if raw["_class"] == "M" and raw["measurement_result"]["samples"] is not None:
gate.result.register_samples(raw["measurement_result"]["samples"])
try:
return gate.controlled_by(*raw["_control_qubits"])
except RuntimeError as e:
Expand Down
71 changes: 37 additions & 34 deletions src/qibo/gates/measurements.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from typing import Dict, Optional, Tuple
from typing import Dict, Optional, Tuple, Union

from qibo import gates
from qibo.config import raise_error
Expand All @@ -23,11 +23,13 @@ class M(Gate):
performed. Can be used only for single shot measurements.
If ``True`` the collapsed state vector is returned. If ``False``
the measurement result is returned.
basis (:class:`qibo.gates.Gate`, list): Basis to measure.
Can be a qibo gate or a callable that accepts a qubit,
for example: ``lambda q: gates.RX(q, 0.2)``
or a list of these, if a different basis will be used for each
measurement qubit.
basis (:class:`qibo.gates.Gate`, str, list): Basis to measure.
Can be either:
- a qibo gate
- the string representing the gate
- a callable that accepts a qubit, for example: ``lambda q: gates.RX(q, 0.2)``
- a list of the above, if a different basis will be used for each
measurement qubit.
Default is Z.
p0 (dict): Optional bitflip probability map. Can be:
A dictionary that maps each measured qubit to the probability
Expand All @@ -46,7 +48,7 @@ def __init__(
*q,
register_name: Optional[str] = None,
collapse: bool = False,
basis: Gate = Z,
basis: Union[Gate, str] = Z,
p0: Optional["ProbsType"] = None,
p1: Optional["ProbsType"] = None,
):
Expand All @@ -61,15 +63,24 @@ def __init__(
# relevant for experiments only
self.pulses = None
# saving basis for __repr__ ans save to file
to_gate = lambda x: getattr(gates, x) if isinstance(x, str) else x
if not isinstance(basis, list):
self.basis_gates = len(q) * [basis]
self.basis_gates = len(q) * [to_gate(basis)]
basis = len(self.target_qubits) * [basis]
elif len(basis) != len(self.target_qubits):
raise_error(
ValueError,
f"Given basis list has length {len(basis)} while "
f"we are measuring {len(self.target_qubits)} qubits.",
)
else:
self.basis_gates = basis
self.basis_gates = [to_gate(g) for g in basis]

self.init_args = q
self.init_kwargs = {
"register_name": register_name,
"collapse": collapse,
"basis": [g.__name__ for g in self.basis_gates],
"p0": p0,
"p1": p1,
}
Expand All @@ -88,20 +99,25 @@ def __init__(

# list of gates that will be added to the circuit before the
# measurement, in order to rotate to the given basis
if not isinstance(basis, list):
basis = len(self.target_qubits) * [basis]
elif len(basis) != len(self.target_qubits):
raise_error(
ValueError,
f"Given basis list has length {len(basis)} while "
f"we are measuring {len(self.target_qubits)} qubits.",
)
self.basis = []
for qubit, basis_cls in zip(self.target_qubits, basis):
for qubit, basis_cls in zip(self.target_qubits, self.basis_gates):
gate = basis_cls(qubit).basis_rotation()
if gate is not None:
self.basis.append(gate)

@property
def raw(self) -> dict:
"""Serialize to dictionary.
The values used in the serialization should be compatible with a
JSON dump (or any other one supporting a minimal set of scalar
types). Though the specific implementation is up to the specific
gate.
"""
encoded_simple = super().raw
encoded_simple.update({"measurement_result": self.result.raw})
return encoded_simple

@staticmethod
def _get_bitflip_tuple(
qubits: Tuple[int, ...], probs: "ProbsType"
Expand Down Expand Up @@ -178,7 +194,7 @@ def apply(self, backend, state, nqubits):
qubits = sorted(self.target_qubits)
# measure and get result
probs = backend.calculate_probabilities(state, qubits, nqubits)
shot = self.result.add_shot(probs)
shot = self.result.add_shot(probs, backend=backend)
# collapse state
return backend.collapse_state(state, qubits, shot, nqubits)

Expand All @@ -190,7 +206,7 @@ def apply_density_matrix(self, backend, state, nqubits):
qubits = sorted(self.target_qubits)
# measure and get result
probs = backend.calculate_probabilities_density_matrix(state, qubits, nqubits)
shot = self.result.add_shot(probs)
shot = self.result.add_shot(probs, backend=backend)
# collapse state
return backend.collapse_density_matrix(state, qubits, shot, nqubits)

Expand All @@ -204,25 +220,12 @@ def apply_clifford(self, backend, state, nqubits):
self.result.add_shot_from_sample(sample[0])
return state

def to_json(self):
"""Serializes the measurement gate to json."""
encoding = json.loads(super().to_json())
encoding.pop("_control_qubits")
encoding.update({"basis": [g.__name__ for g in self.basis_gates]})
return json.dumps(encoding)

@classmethod
def load(cls, payload):
"""Constructs a measurement gate starting from a json serialized
one."""
args = json.loads(payload)
# drop general serialization data, unused in this specialized loader
for key in ("name", "init_args", "_class"):
args.pop(key)
qubits = args.pop("_target_qubits")
args["basis"] = [getattr(gates, g) for g in args["basis"]]
args.update(args.pop("init_kwargs"))
return cls(*qubits, **args)
return cls.from_dict(args)

# Overload on_qubits to copy also gate.result, controlled by can be removed for measurements
def on_qubits(self, qubit_map) -> "Gate":
Expand Down
Loading

0 comments on commit 9ac3d06

Please sign in to comment.