Skip to content

Commit

Permalink
Merge pull request #20 from qiboteam/model
Browse files Browse the repository at this point in the history
Implements the structure of a general `qiboml` model
  • Loading branch information
BrunoLiegiBastonLiegi authored Nov 4, 2024
2 parents 3cd0a66 + 4bb6a5c commit 807c89b
Show file tree
Hide file tree
Showing 21 changed files with 1,528 additions and 363 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# A single CI script with github workflow
name: Build wheels and deploy

on:
workflow_dispatch:
push:
merge_group:
release:
types:
- published

jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [ 3.9, '3.10', '3.11', '3.12']
uses: qiboteam/workflows/.github/workflows/deploy-pip-poetry.yml@v1
with:
os: ${{ matrix.os }}
python-version: ${{ matrix.python-version }}
publish: ${{ github.event_name == 'release' && github.event.action == 'published' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10' }}
poetry-extras: "--with tests"
secrets: inherit
9 changes: 6 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ on:
workflow_dispatch:
push:
merge_group:
pull_request:
types: [labeled, opened] # opened is required to allow external contributors

jobs:
build:
if: contains(github.event.pull_request.labels.*.name, 'run-workflow') || github.event_name == 'push'
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.9, "3.10", "3.11"]
uses: qiboteam/workflows/.github/workflows/rules-poetry.yml@main
python-version: [3.9, "3.10", "3.11", "3.12"]
uses: qiboteam/workflows/.github/workflows/rules-poetry.yml@v1
with:
os: ${{ matrix.os }}
python-version: ${{ matrix.python-version }}
doctests: false
poetry-extras: --all-extras
poetry-extras: "--with tests"
secrets: inherit
544 changes: 222 additions & 322 deletions poetry.lock

Large diffs are not rendered by default.

25 changes: 16 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ packages = [{ include = "qiboml", from = "src" }]
[tool.poetry.dependencies]
python = ">=3.9,<3.13"
numpy = "^1.26.4"
numba = "^0.59.0"
tensorflow = { version = "^2.16.1", markers = "sys_platform == 'linux' or sys_platform == 'darwin'" }
torch = "^2.2.0"
keras = { version = "^3.0.0", optional = true }
tensorflow = { version = "^2.16.1", markers = "sys_platform == 'linux' or sys_platform == 'darwin'", optional = true }
# TODO: the marker is a temporary solution due to the lack of the tensorflow-io 0.32.0's wheels for Windows, this package is one of
# the tensorflow requirements
torch = { version = "^2.3.1", optional = true }
qibo = {git="https://github.com/qiboteam/qibo"}
jax = "^0.4.25"
jaxlib = "^0.4.25"
# TODO: this is temporary and has to be updated with Qibo
qibo = { git = "https://github.com/qiboteam/qibo.git" }

[tool.poetry.group.docs]
optional = true
Expand All @@ -41,11 +42,17 @@ optional = true
ipython = "^7.34"
pdbpp = "^0.10.3"

[tool.poetry.group.tests]
optional = true

[tool.poetry.group.tests.dependencies]
torch = "^2.3.1"
tensorflow = { version = "^2.16.1", markers = "sys_platform == 'linux'" }
pytest = "^7.2.1"
pylint = "3.1.0"
pytest-cov = "4.0.0"

[tool.poetry.group.benchmark.dependencies]
pytest = "^7.1.2"
pylint = "^2.17"
pytest-cov = "^3.0.0"
pytest-env = "^0.8.1"
pytest-benchmark = { version = "^4.0.0", extras = ["histogram"] }

[tool.poe.tasks]
Expand Down
23 changes: 23 additions & 0 deletions src/qiboml/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
import importlib.metadata as im
from typing import Union

import numpy.typing as npt

from qiboml.backends.__init__ import MetaBackend

__version__ = im.version(__package__)

ndarray = npt.NDArray

try:
from tensorflow import Tensor as tf_tensor

from qiboml.models import keras

ndarray = Union[ndarray, tf_tensor]
except ImportError: # pragma: no cover
pass

try:
from torch import Tensor as pt_tensor

from qiboml.models import pytorch

ndarray = Union[ndarray, pt_tensor]
except ImportError: # pragma: no cover
pass
6 changes: 4 additions & 2 deletions src/qiboml/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from typing import Union

from qibo.backends.pytorch import PyTorchBackend # qiboml pytorch is not updated
from qibo.config import raise_error

from qiboml.backends.jax import JaxBackend
from qiboml.backends.pytorch import PyTorchBackend
from qiboml.backends.tensorflow import TensorflowBackend

PLATFORMS = ["tensorflow", "pytorch", "jax"]
AVAILABLE_PLATFORMS = ["tensorflow"] # temporary: to remove once pytorch and tensorflow are migrated and jax is fully working
AVAILABLE_PLATFORMS = [
"tensorflow"
] # temporary: to remove once pytorch and tensorflow are migrated and jax is fully working
QibomlBackend = Union[TensorflowBackend, PyTorchBackend, JaxBackend]


Expand Down
8 changes: 8 additions & 0 deletions src/qiboml/backends/jax.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def __init__(self):

self.np = jnp
self.tensor_types = (jnp.ndarray, numpy.ndarray)
self.matrices.np = jnp

def set_precision(self, precision):
if precision != self.precision:
Expand All @@ -45,6 +46,13 @@ def cast(self, x, dtype=None, copy=False):
return x.astype(dtype)
return self.np.array(x, dtype=dtype, copy=copy)

def to_numpy(self, x):

if isinstance(x, list) or isinstance(x, tuple):
return self.numpy.asarray([self.to_numpy(i) for i in x])

return self.numpy.asarray(x)

# TODO: using numpy's rng for now. Shall we use Jax's?
def set_seed(self, seed):
self.numpy.random.seed(seed)
Expand Down
26 changes: 15 additions & 11 deletions src/qiboml/models/ansatze.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import random

import numpy as np
from qibo import Circuit, gates


def reuploading_circuit(nqubits, nlayers):
c = Circuit(nqubits)
for _ in range(nlayers):
for q in range(nqubits):
c.add(gates.RY(q, 0))
c.add(gates.RZ(q, 0))
for q in range(0, nqubits - 1, 1):
c.add(gates.CNOT(q0=q, q1=q + 1))
c.add(gates.CNOT(q0=nqubits - 1, q1=0))
c.add(gates.M(*range(nqubits)))
return c
def ReuploadingCircuit(nqubits: int, qubits: list[int] = None) -> Circuit:
if qubits is None:
qubits = list(range(nqubits))

circuit = Circuit(nqubits)
for q in qubits:
circuit.add(gates.RY(q, theta=random.random() * np.pi, trainable=True))
circuit.add(gates.RZ(q, theta=random.random() * np.pi, trainable=True))
for i, q in enumerate(qubits[:-2]):
circuit.add(gates.CNOT(q0=q, q1=qubits[i + 1]))
circuit.add(gates.CNOT(q0=qubits[-1], q1=qubits[0]))
return circuit
117 changes: 117 additions & 0 deletions src/qiboml/models/decoding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from dataclasses import dataclass
from typing import Union

from qibo import Circuit, gates
from qibo.backends import Backend, _check_backend
from qibo.config import raise_error
from qibo.hamiltonians import Hamiltonian

from qiboml import ndarray


@dataclass
class QuantumDecoding:

nqubits: int
qubits: list[int] = None
nshots: int = 1000
analytic: bool = True
backend: Backend = None
_circuit: Circuit = None

def __post_init__(self):
if self.qubits is None:
self.qubits = list(range(self.nqubits))
self._circuit = Circuit(self.nqubits)
self.backend = _check_backend(self.backend)
self._circuit.add(gates.M(*self.qubits))

def __call__(self, x: Circuit) -> "CircuitResult":
return self.backend.execute_circuit(x + self._circuit, nshots=self.nshots)

@property
def circuit(
self,
):
return self._circuit

def set_backend(self, backend):
self.backend = backend

@property
def output_shape(self):
raise_error(NotImplementedError)


@dataclass
class Probabilities(QuantumDecoding):

def __call__(self, x: Circuit) -> ndarray:
return super().__call__(x).probabilities()

@property
def output_shape(self):
return (1, 2**self.nqubits)


@dataclass
class Expectation(QuantumDecoding):

observable: Union[ndarray, Hamiltonian] = None
analytic: bool = False

def __post_init__(self):
if self.observable is None:
raise_error(
RuntimeError,
"Please provide an observable for expectation value calculation.",
)
super().__post_init__()

def __call__(self, x: Circuit) -> ndarray:
if self.analytic:
return self.observable.expectation(
super().__call__(x).state(),
).reshape(1, 1)
else:
return self.observable.expectation_from_samples(
super().__call__(x).frequencies(),
qubit_map=self.qubits,
).reshape(1, 1)

@property
def output_shape(self):
return (1, 1)

def set_backend(self, backend):
super().set_backend(backend)
self.observable.backend = backend


@dataclass
class State(QuantumDecoding):

def __call__(self, x: Circuit) -> ndarray:
state = super().__call__(x).state()
return self.backend.np.vstack(
(self.backend.np.real(state), self.backend.np.imag(state))
).reshape(self.output_shape)

@property
def output_shape(self):
return (2, 1, 2**self.nqubits)


@dataclass
class Samples(QuantumDecoding):

def __post_init__(self):
super().__post_init__()
self.analytic = False

def forward(self, x: Circuit) -> ndarray:
return self.backend.cast(super().__call__(x).samples(), self.backend.precision)

@property
def output_shape(self):
return (self.nshots, len(self.qubits))
68 changes: 68 additions & 0 deletions src/qiboml/models/encoding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass

import numpy as np
from qibo import Circuit, gates
from qibo.config import raise_error

from qiboml import ndarray


@dataclass
class QuantumEncoding(ABC):

nqubits: int
qubits: list[int] = None
_circuit: Circuit = None

def __post_init__(
self,
):
if self.qubits is None:
self.qubits = list(range(self.nqubits))
self._circuit = Circuit(self.nqubits)

@abstractmethod
def __call__(self, x: ndarray) -> Circuit:
pass

@property
def circuit(
self,
):
return self._circuit


@dataclass
class PhaseEncoding(QuantumEncoding):

def __post_init__(
self,
):
super().__post_init__()
for q in self.qubits:
self._circuit.add(gates.RY(q, theta=0.0, trainable=False))

def _set_phases(self, x: ndarray):
for gate, phase in zip(self._circuit.parametrized_gates, x.ravel()):
gate.parameters = phase

def __call__(self, x: ndarray) -> Circuit:
self._set_phases(x)
return self._circuit


@dataclass
class BinaryEncoding(QuantumEncoding):

def __call__(self, x: ndarray) -> Circuit:
if x.shape[-1] != len(self.qubits):
raise_error(
RuntimeError,
f"Invalid input dimension {x.shape[-1]}, but the allocated qubits are {self.qubits}.",
)
circuit = self.circuit.copy()
ones = np.flatnonzero(x.ravel() == 1)
for bit in ones:
circuit.add(gates.X(self.qubits[bit]))
return circuit
Loading

0 comments on commit 807c89b

Please sign in to comment.