Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements the structure of a general qiboml model #20

Open
wants to merge 66 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
420ac26
feat: sketched the general structure of a qml model
BrunoLiegiBastonLiegi May 14, 2024
7e828ff
fix: removed cache files
BrunoLiegiBastonLiegi May 14, 2024
0e8656b
feat: implemented some sample encoding, decoding and ansatz layers
BrunoLiegiBastonLiegi May 16, 2024
2c79d18
fix: small modifications to layers
BrunoLiegiBastonLiegi Jun 3, 2024
e015465
feat: added phase encoding and more examples + drafted torch interface
BrunoLiegiBastonLiegi Jun 3, 2024
12c53e8
Update src/qiboml/models/abstract.py
BrunoLiegiBastonLiegi Jun 5, 2024
aa38656
Update src/qiboml/models/abstract.py
BrunoLiegiBastonLiegi Jun 5, 2024
06d4a16
feat: drafted a torch module factory
BrunoLiegiBastonLiegi Jun 12, 2024
a6ac851
feat: minor refinements to the torch factory
BrunoLiegiBastonLiegi Jun 13, 2024
3c09554
feat: drafted keras factory
BrunoLiegiBastonLiegi Jun 13, 2024
82a7079
feat: added pytorch tf tutorials
BrunoLiegiBastonLiegi Jun 13, 2024
ddec82a
fix: removed cache files
BrunoLiegiBastonLiegi Jun 13, 2024
10f533b
fix: minor fixes to keras interface
BrunoLiegiBastonLiegi Jun 14, 2024
76a2b2b
feat: implemented keras Model
BrunoLiegiBastonLiegi Jun 20, 2024
86969bd
fix: finally made keras interface working
BrunoLiegiBastonLiegi Jul 7, 2024
e10ffe6
feat: implemented QuantumModel for torch as well
BrunoLiegiBastonLiegi Jul 7, 2024
5ab8583
test: started implementing tests
BrunoLiegiBastonLiegi Jul 12, 2024
6aeaa1b
fix: various fixes + further tests
BrunoLiegiBastonLiegi Jul 16, 2024
3f166e3
fix: various fixes
BrunoLiegiBastonLiegi Jul 17, 2024
8fff5b8
build: added test dependencies
BrunoLiegiBastonLiegi Jul 17, 2024
c27cfdf
build: changed poetry rules
BrunoLiegiBastonLiegi Jul 17, 2024
542f64a
test: added backend and frontend fixtures
BrunoLiegiBastonLiegi Jul 17, 2024
c9e48b8
build: lock update
BrunoLiegiBastonLiegi Jul 17, 2024
d69b130
fix: disabled pylint check
BrunoLiegiBastonLiegi Jul 17, 2024
2e0c1bd
fix: some fixes
BrunoLiegiBastonLiegi Jul 17, 2024
42c0a1b
fix: small fix to expectation layer
BrunoLiegiBastonLiegi Jul 17, 2024
87ea094
fix: fixed exp layer tests and commented amp encoding
BrunoLiegiBastonLiegi Jul 17, 2024
474ad93
fix: small fixes to test_models.py
BrunoLiegiBastonLiegi Jul 17, 2024
ce108f9
test: testing expecation from samples as well
BrunoLiegiBastonLiegi Jul 18, 2024
2469037
fix: test fix
BrunoLiegiBastonLiegi Jul 22, 2024
7b6b3a2
fix: added qubit_map to expactation_from_samples + casting to double
BrunoLiegiBastonLiegi Jul 22, 2024
2a2d881
build: lock update
BrunoLiegiBastonLiegi Jul 22, 2024
1c049d1
fix: skipping tf test with windows
BrunoLiegiBastonLiegi Jul 22, 2024
9bffae7
fix: skipping tf test with windows
BrunoLiegiBastonLiegi Jul 22, 2024
13f6a31
fix: import sys
BrunoLiegiBastonLiegi Jul 22, 2024
c78f189
fix: commented tf import
BrunoLiegiBastonLiegi Jul 22, 2024
2ac5168
build: made tf optional
BrunoLiegiBastonLiegi Jul 22, 2024
6213bfe
build: added deploy workflow
BrunoLiegiBastonLiegi Jul 23, 2024
3dacd8d
build: changed python dependency
BrunoLiegiBastonLiegi Jul 23, 2024
7b6b7af
test: added runtest setup
BrunoLiegiBastonLiegi Jul 29, 2024
0313bb2
fix: various fixes to keras interface + testing both keras and torch …
BrunoLiegiBastonLiegi Jul 29, 2024
ffc37ea
fix: pylint disable import-error
BrunoLiegiBastonLiegi Jul 30, 2024
6916600
fix: removed some leftovers + moved tf import inside functions
BrunoLiegiBastonLiegi Jul 30, 2024
c0120ff
test: added tests for errors and other things
BrunoLiegiBastonLiegi Jul 30, 2024
8b8db4d
fix: added pytest_configure
BrunoLiegiBastonLiegi Jul 31, 2024
af8001e
fix: updated workflow
BrunoLiegiBastonLiegi Jul 31, 2024
71286eb
build: pylint update
BrunoLiegiBastonLiegi Jul 31, 2024
59aadf7
build: updated pytest packages and removed pytest-env
BrunoLiegiBastonLiegi Jul 31, 2024
26944a6
build: merge main, replaced numpy backend with jax backend for testing
BrunoLiegiBastonLiegi Aug 1, 2024
e9f3e32
fix: using qibo's pytorch and small fix to jax backend
BrunoLiegiBastonLiegi Aug 1, 2024
3e2a0e5
fix: rename issparse in pytorch backend
BrunoLiegiBastonLiegi Aug 1, 2024
bf116f0
fix: moved backend import in conftest
BrunoLiegiBastonLiegi Aug 1, 2024
eb39ca8
fix: removed keras and pytorch interface import from __init__.py
BrunoLiegiBastonLiegi Aug 3, 2024
2726853
fix: removed super().__post_init__ call
BrunoLiegiBastonLiegi Aug 3, 2024
ef91fc2
fix: added super().__init__() to interface __post_init__
BrunoLiegiBastonLiegi Aug 3, 2024
a610cf4
fix: fixed seed of random_clifford in test_decoding
BrunoLiegiBastonLiegi Aug 3, 2024
606cbb9
fix: added analytic argument exp layer in test_models_decoding.py
BrunoLiegiBastonLiegi Aug 3, 2024
a44942e
fix: trying to mitigate numerical instability
BrunoLiegiBastonLiegi Aug 3, 2024
97ef336
fix: fix to test exp layer
BrunoLiegiBastonLiegi Aug 6, 2024
e516fb5
fix: small improvements to coverage
BrunoLiegiBastonLiegi Aug 6, 2024
87d0f02
feat: added the qiboml.ndarray dtype and fixed various type hints
BrunoLiegiBastonLiegi Aug 23, 2024
bd7d1a0
fix: pylint disable for keras import
BrunoLiegiBastonLiegi Aug 23, 2024
f1f43fd
fix: using the parameters property when needed
BrunoLiegiBastonLiegi Aug 28, 2024
6820b25
fix: removed cache files
BrunoLiegiBastonLiegi Aug 28, 2024
c0fd516
fix: replacing c() with backend.execute
BrunoLiegiBastonLiegi Aug 28, 2024
51037fd
build: lock update
BrunoLiegiBastonLiegi Sep 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
989 changes: 436 additions & 553 deletions poetry.lock

Large diffs are not rendered by default.

22 changes: 14 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,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'" }
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 = "^2.2.0"
torch = { version = "^2.3.1", optional = true }
qibo = {git="https://github.com/qiboteam/qibo", branch="qiboml_models_updates"}
BrunoLiegiBastonLiegi marked this conversation as resolved.
Show resolved Hide resolved
jax = "^0.4.25"
jaxlib = "^0.4.25"
qibo = "^0.2.8"

[tool.poetry.group.dev]
optional = true
Expand All @@ -29,11 +29,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:
pass

try:
from torch import Tensor as pt_tensor

from qiboml.models import pytorch

ndarray = Union[ndarray, pt_tensor]
except ImportError:
pass
Comment on lines +10 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should never import from a level directly above yours, and the package __init__.py is directly above everything.

I'd suggest you to move this definition in a standalone module (though I still a bit undecided about which would be the best course of action for this...).

2 changes: 1 addition & 1 deletion src/qiboml/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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"]
Expand Down
2 changes: 1 addition & 1 deletion src/qiboml/backends/jax.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def cast(self, x, dtype=None, copy=False):
dtype = self.dtype
if isinstance(x, self.tensor_types):
return x.astype(dtype)
elif self.issparse(x):
elif self.is_sparse(x):
return x.astype(dtype)
return self.np.array(x, dtype=dtype, copy=copy)

Expand Down
2 changes: 1 addition & 1 deletion src/qiboml/backends/pytorch.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def issparse(self, x):
if isinstance(x, self.np.Tensor):
return x.is_sparse

return super().issparse(x)
return super().is_sparse(x)

def to_numpy(self, x):
if isinstance(x, list):
Expand Down
45 changes: 45 additions & 0 deletions src/qiboml/models/abstract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Defines the general structure of a qiboml module"""

from abc import ABC, abstractmethod
from dataclasses import dataclass

from qibo import Circuit
from qibo.backends import Backend
from qibo.config import raise_error
from qibo.gates import abstract

from qiboml import ndarray
from qiboml.backends import JaxBackend


@dataclass
class QuantumCircuitLayer(ABC):

nqubits: int
qubits: list[int] = None
_circuit: Circuit = None
backend: Backend = JaxBackend()

def __post_init__(self) -> None:
if self.qubits is None:
self.qubits = list(range(self.nqubits))
Comment on lines +24 to +25
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this, I believe it would be more elegant to use a ._qubits attribute, and expose a .qubits property.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem I see here, though, is that then you will need to construct the layer as:

QuantumCircuitLayer(nqubits=5, _qubits=[0,1,2])

if you want to specify the qubits. I am not sure if there's any workaround with dataclasses.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly enough, you can do it with Pydantic:

class Ciao(BaseModel):
    name_: str = Field(alias="name")
                                                                                                                                                                                      
    @property
    def name(self):
        print(f"Ciao, {self.name_}")
        return self.name_
                                                                                                                                                                                      
c = Ciao(name="come va?")
c.name

(though you can not use ._qubit, since fields are supposed to be public, and they can not contain leading underscores)

self._circuit = Circuit(self.nqubits)

@abstractmethod
def forward(self, x): # pragma: no cover
pass

def __call__(self, x):
return self.forward(x)

@property
def parameters(self) -> ndarray:
return self.backend.cast(self.circuit.get_parameters())

@parameters.setter
def parameters(self, params: ndarray):
self._circuit.set_parameters(self.backend.cast(params.ravel()))

@property
def circuit(self) -> Circuit:
return self._circuit
30 changes: 19 additions & 11 deletions src/qiboml/models/ansatze.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
from dataclasses import dataclass

import numpy as np
from qibo import Circuit, gates

from qiboml.models.abstract import QuantumCircuitLayer


@dataclass
class ReuploadingLayer(QuantumCircuitLayer):

def __post_init__(self):
super().__post_init__()
for q in self.qubits:
self.circuit.add(gates.RY(q, theta=0.0))
self.circuit.add(gates.RZ(q, theta=0.0))
for i, q in enumerate(self.qubits[:-2]):
self.circuit.add(gates.CNOT(q0=q, q1=self.qubits[i + 1]))
self.circuit.add(gates.CNOT(q0=self.qubits[-1], q1=self.qubits[0]))
BrunoLiegiBastonLiegi marked this conversation as resolved.
Show resolved Hide resolved

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 forward(self, x: Circuit) -> Circuit:
return x + self.circuit
BrunoLiegiBastonLiegi marked this conversation as resolved.
Show resolved Hide resolved
128 changes: 128 additions & 0 deletions src/qiboml/models/encoding_decoding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""Some standard encoding and decoding layers"""

from dataclasses import dataclass
from typing import Union

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

from qiboml import ndarray
from qiboml.models.abstract import QuantumCircuitLayer


@dataclass
class QuantumEncodingLayer(QuantumCircuitLayer):
pass


@dataclass
class BinaryEncodingLayer(QuantumEncodingLayer):

def forward(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


@dataclass
class PhaseEncodingLayer(QuantumEncodingLayer):

def __post_init__(self):
super().__post_init__()
for q in self.qubits:
self.circuit.add(gates.RZ(q, theta=0.0))

def forward(self, x: ndarray) -> Circuit:
self.parameters = x
return self.circuit


@dataclass
class QuantumDecodingLayer(QuantumCircuitLayer):

nshots: int = 1000

def __post_init__(self):
super().__post_init__()
self.circuit.add(gates.M(*self.qubits))

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


class ProbabilitiesLayer(QuantumDecodingLayer):

def __post_init__(self):
super().__post_init__()

def forward(self, x: Circuit) -> ndarray:
return super().forward(x).probabilities(self.qubits).reshape(1, -1)

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


class SamplesLayer(QuantumDecodingLayer):

def forward(self, x: Circuit) -> ndarray:
return self.backend.cast(super().forward(x).samples(), dtype=np.float64)

@property
def output_shape(self):
return (self.nshots, len(self.qubits))


class StateLayer(QuantumDecodingLayer):

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

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


@dataclass
class ExpectationLayer(QuantumDecodingLayer):

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 forward(self, x: Circuit) -> ndarray:
if self.analytic:
return self.observable.expectation(
super().forward(x).state(),
).reshape(1, 1)
else:
return self.backend.cast(
self.observable.expectation_from_samples(
super().forward(x).frequencies(),
qubit_map=self.qubits,
).reshape(1, 1),
dtype=np.float64,
)

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