Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
119 changes: 84 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,93 @@
# PyQit

## Current Plan (sub to change any time >_<)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![Status: Active Development](https://img.shields.io/badge/status-active_development-orange.svg)]()
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Build Status](https://img.shields.io/github/actions/workflow/status/phoeenniixx/pyQit/test.yml)](https://github.com/phoeenniixx/pyqit/actions)

- ansatz -> `QuantumPipeline` wrapper that connects the ansatz and DL model (if any) -> `Trainer` (with/w/o `torch`)
- Data preprocessing techniques (no idea how to do that rn) - maybe using a `lightning` type of data module (thanks a lot `lightning`). But as `lightning` has a core dep of `torch`, I have to reinvent(?) it ig
- any ansatz could go with any DL backbone (ideally, not sure how much is feasible - tbd)
- `torch` is used with `lightning` as a imp soft-dep now!!
> **A high-level quantum machine learning framework built on PennyLane.**
> It aims to make quantum machine learning more accessible by reducing the steep learning curve, streamlining the boilerplate code required for training, and providing mathematically rigorous diagnostics.

*Will add better vignettes once i have my ideas consolidated in my mind*

> ***Have a look at a basic tutorial [here](https://github.com/phoeenniixx/pyQit/tree/main/doc/tutorials/vqc.ipynb) (This is how the flow would look like in future, still working on making data module and Trainer etc more "user friendly" and expressive)***
## The Philosophy

for some idea how it might look like:
```python
# no torch
pyqit.set_backend("pennylane")
qml_model= QMLmodel(...) # may use their own ansatz?
dm = DataModule(...)
trainer = Trainer(...)
trainer.fit(qml_model, dm)
trainer.predict(qml_model, dm_new, return_format = "numpy") # or "torch" for torch tensors if torch is backend,
# should i add pennylane tensors as well? good question!
Scaling Quantum Machine Learning (QML) research from toy models to enterprise hybrid pipelines is painful. Researchers spend hours rewriting training loops, debugging shape mismatches, and blindly waiting for models to train, only to realize their deep circuit hit a Barren Plateau at epoch 1.

**PyQit** abstracts away the infrastructure so you can focus on the science.

### Key Features
* **Lightweight & Modular:** PyQit runs natively on **PennyLane** and **NumPy**.
> **PyTorch and PyTorch Lightning are strictly optional soft dependencies.** If you don't need deep learning hybrid models or GPU orchestration, you don't have to install them.
* **Backend Agnostic**: Seamlessly switch between native `pennylane` (pure Autograd) and `torch` (Lightning engine) with a single parameter.
* **Automated Diagnostics**: Features a mathematical "Pre-Flight Check" that runs Monte Carlo gradient sampling to detect Barren Plateaus mathematically *before* you waste compute time.
* **Enterprise Data Orchestration**: A stateful `DataModule` handles classical normalization (`minmax`, `zscore`) safely and separately from stateless quantum embedding projections (`Amplitude`, `Angle`).


## Installation

PyQit is currently in active development and is installed directly from source via `pyproject.toml`.

```bash
# 1. Clone the repository
git clone [https://github.com/yourusername/pyqit.git](https://github.com/yourusername/pyqit.git)
cd pyqit

# 2. Base Installation (PennyLane native, NO PyTorch required)
pip install -e .

# 3. Optional Installs
# If you want PyTorch and PyTorch Lightning support:
pip install -e .[pytorch]

# If you want the full suite (PyTorch, Matplotlib for diagnostics, Rich for terminal tables):
pip install -e .[all_extras]

# For contributors (pytest, sphinx, ruff, etc.):
pip install -e .[dev]
```
### Using Pipeline

## Quickstart
Here is how you build a Variational Quantum Classifier (VQC) and run an automated Barren Plateau diagnostic in under 20 lines of code:

```python
pyqit.set_backend("torch")
dm = DataModule(...)
model_a = QMLmodel(**params)
model_b = QMLmodel(**params) # or DLModel for that matter
trainer = Trainer( max_epochs=10, learning_rate=0.01)
pipeline = QuantumPipeline(
[
PipelineStage(model_a, name="stage_1", trainable=trainable_a),
PipelineStage(model_b, name="stage_2", trainable=True),
],
mode="sequential",
)
pipeline.fit(datamodule=dm, trainers=trainer, fit_mode="sequential_greedy")
preds = pipeline.predict(X_new, batch_size=8)
from sklearn.datasets import make_moons
import numpy as np

import pyqit
from pyqit.ansatzes.sel import SELAnsatz
from pyqit.core.embeddings import AngleEmbedding
from pyqit.models.classification.vqc import VQCClassifier
from pyqit.data.datamodule import DataModule
from pyqit.core.trainer import Trainer

# 1. Prepare Data
X, y = make_moons(n_samples=200, noise=0.1)
# DataModule handle stateful normalization securely
dm = DataModule(X=X, y=y, normalize="minmax", batch_size=16)

# 2. Define Model
model = VQCClassifier(
n_qubits=4,
n_layers=3,
n_classes=2,
ansatz=SELAnsatz,
encoder=AngleEmbedding
)

# 3. Initialize Trainer with Barren Plateau Pre-Flight Check
trainer = Trainer(
max_epochs=10,
check_bp=True, # Automatically diagnoses gradient variance before training
bp_samples=100,
verbose=1
)

# 4. Fit
# Will print a diagnostic table evaluating your circuit against the McClean et al. baseline
history = trainer.fit(model, datamodule=dm)
```
You can also train just QMLmodel using `Trainer`
here `anyQMLmodel` and `DLmodel` can be implemented by the user themselves or use the implemented ones from the package
Then package would also have a complete model zoo.

> #### Have a look some tutorials [here](https://github.com/phoeenniixx/pyQit/tree/main/doc/tutorials/) for more info!

## Contributing
Contributions, issues, and feature requests are welcome! Feel free to check the [issues page](https://github.com/phoeenniixx/pyQit/issues). If you are building novel ansatzes, custom embeddings, or new diagnostic tools, please submit a PR.
6 changes: 6 additions & 0 deletions pyqit/ansatzes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""A module for quantum ansatzes."""

from pyqit.ansatzes.base import BaseAnsatz
from pyqit.ansatzes.sel import SELAnsatz

__all__ = ["BaseAnsatz", "SELAnsatz"]
43 changes: 43 additions & 0 deletions pyqit/ansatzes/sel.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,60 @@


class SELAnsatz(BaseAnsatz):
"""
Strongly Entangling Layers (SEL) ansatz.

This class implements a parameterized quantum circuit using PennyLane's
`StronglyEntanglingLayers` template. It applies single-qubit rotations
and entangling gates across the specified number of layers.

Parameters
----------
n_qubits : int
The number of qubits the ansatz acts upon.
n_layers : int, optional
The number of entangling layers in the circuit. Default is 2.
"""

def __init__(self, n_qubits: int, n_layers: int = 2):
super().__init__(n_qubits, n_layers)

def build_circuit(self, weights):
"""
Construct and apply the strongly entangling layers to the quantum circuit.

Parameters
----------
weights : dict
A dictionary containing the parameter tensors. Must include the
key `"weights"` with a tensor of shape `(n_layers, n_qubits, 3)`.

"""
w_tensor = weights["weights"]
qml.templates.StronglyEntanglingLayers(w_tensor, wires=range(self.n_qubits))

def get_weight_shapes(self) -> dict:
"""
Get the shapes of the trainable weights required by the ansatz.

Returns
-------
dict
A dictionary mapping the weight parameter name (`"weights"`) to
its expected shape tuple `(n_layers, n_qubits, 3)`.
"""
shape = (self.n_layers, self.n_qubits, 3)
return {"weights": shape}

@classmethod
def get_test_params(cls):
"""
Retrieve a set of default parameters for testing the ansatz.

Returns
-------
list of dict
A list containing a dictionary of valid initialization parameters
for the class.
"""
return [{"n_qubits": 3, "n_layers": 2}]
2 changes: 2 additions & 0 deletions pyqit/base/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
"""Base module for PyQit."""

from pyqit.base.base_object import _PyQitObject, all_objects
27 changes: 27 additions & 0 deletions pyqit/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""A module for core functionality."""

from pyqit.core.callbacks import HistoryCallback
from pyqit.core.config import get_backend, set_backend
from pyqit.core.embeddings import AmplitudeEmbedding, AngleEmbedding, IQPEmbedding
from pyqit.core.losses import cross_entropy_loss, hinge_loss, mse_loss
from pyqit.core.measurements import measure_expval_x, measure_expval_z, measure_probs
from pyqit.core.pipeline import PipelineStage, QuantumPipeline
from pyqit.core.trainer import Trainer

__all__ = [
"Trainer",
"cross_entropy_loss",
"mse_loss",
"hinge_loss",
"AmplitudeEmbedding",
"AngleEmbedding",
"IQPEmbedding",
"measure_probs",
"measure_expval_z",
"measure_expval_x",
"HistoryCallback",
"set_backend",
"get_backend",
"QuantumPipeline",
"PipelineStage",
]
5 changes: 5 additions & 0 deletions pyqit/core/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""A module for adapters."""

from pyqit.core.adapters.lightning import _LightningDataAdapter, _LightningModelAdapter

__all__ = ["_LightningModelAdapter", "_LightningDataAdapter"]
43 changes: 43 additions & 0 deletions pyqit/core/adapters/lightning.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ def __init__(self, *args, **kwargs):


class _LightningModelAdapter(LightningModule):
"""
Adapter to wrap a model into a PyTorch Lightning Module.

This class handles the training loop, validation loop, and optimizer
configuration for the underlying PyQIT model, integrating seamlessly
with the PyTorch Lightning Trainer.

Parameters
----------
pyqit_model : torch.nn.Module or callable
The core model to be wrapped. If it contains a `_qnodes` dictionary
with `torch.nn.Module` objects, they will be registered as submodules.
lr : float
The learning rate for the optimizer.
optimizer_name : str
The name of the optimizer to use. Supports "sgd"; defaults to "Adam"
for all other values.
loss_fn : callable
The loss function used for training and validation. It must accept
`preds` and `y` as arguments.
"""

def __init__(self, pyqit_model, lr, optimizer_name, loss_fn):
super().__init__()
self.pyqit_model = pyqit_model
Expand Down Expand Up @@ -70,6 +92,27 @@ def configure_optimizers(self):


class _LightningDataAdapter(LightningDataModule):
"""
Adapter to wrap a PyQIT data module into a PyTorch Lightning DataModule.

This class extracts the internal training, validation, and testing arrays
from the PyQIT data module and converts them into standard PyTorch
DataLoaders compatible with the PyTorch Lightning Trainer.

Parameters
----------
pyqit_dm : object
The internal PyQIT data module containing the raw data attributes
(`_X_train`, `_y_train`, etc.) and configuration parameters.
num_workers : int, default=0
The number of workers for data loading. (Note: Currently overridden
by `pyqit_dm.num_workers` in the loader configuration).
train_loader_kwargs : dict or None, optional
Additional keyword arguments to pass to the training DataLoader.
eval_loader_kwargs : dict or None, optional
Additional keyword arguments to pass to the evaluation DataLoader.
"""

def __init__(
self,
pyqit_dm,
Expand Down
5 changes: 5 additions & 0 deletions pyqit/core/callbacks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""A module for callbacks."""

from pyqit.core.callbacks.history import HistoryCallback

__all__ = ["HistoryCallback"]
2 changes: 2 additions & 0 deletions pyqit/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@


def set_backend(backend: str):
"""Set the quantum computing backend for the current context."""
backend = backend.lower()
if backend not in ["pennylane", "torch"]:
raise ValueError(f"Unsupported backend '{backend}'.")
Expand All @@ -19,6 +20,7 @@ def set_backend(backend: str):


def get_backend() -> str:
"""Get the current quantum computing backend for the context."""
if not _EXPLICITLY_SET.get():
logger.warning(
"No backend explicitly set for this context. Defaulting to 'pennylane'."
Expand Down
6 changes: 6 additions & 0 deletions pyqit/core/embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ def prescale_key(cls) -> str | None:


class AngleEmbedding(BaseEmbedding):
"""A wrapper for PennyLane's AngleEmbedding circuit."""

_tags = {
"embedding_type": "angle",
"differentiable": True,
Expand All @@ -67,6 +69,8 @@ def get_test_params(cls):


class AmplitudeEmbedding(BaseEmbedding):
"""A wrapper for PennyLane's AmplitudeEmbedding circuit."""

_tags = {
"embedding_type": "amplitude",
"differentiable": False,
Expand All @@ -93,6 +97,8 @@ def get_test_params(cls):


class IQPEmbedding(BaseEmbedding):
"""A wrapper for PennyLane's IQPEmbedding circuit."""

_tags = {
"embedding_type": "iqp",
"differentiable": False,
Expand Down
43 changes: 43 additions & 0 deletions pyqit/core/losses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,58 @@


def mse_loss(preds, targets):
"""Mean Squared Error loss function.
Parameters
----------
preds : array-like
The predicted values from the model.
targets : array-like
The ground truth target values. Expected to have the same shape
as `preds`.

Returns
-------
float or tensor
The computed mean squared error loss across the batch.
"""
return pnp.mean((preds - targets) ** 2)


def hinge_loss(preds, targets):
"""Hinge loss function for binary classification.
Parameters
----------
preds : array-like
The predicted raw scores (logits) from the model.
targets : array-like
The ground truth binary labels, expected to be encoded as 0 or 1.

Returns
-------
float or tensor
The computed mean hinge loss across the batch."""
y_signed = 2.0 * targets - 1.0
return pnp.mean(pnp.maximum(0, 1 - y_signed * preds))


def cross_entropy_loss(preds, targets):
"""Cross-entropy loss function for binary and multi-class classification.

Parameters
----------
preds : array-like
The predicted probabilities from the model. For binary classification,
this should be a 1D array or a 2D array of shape `(n_samples, 1)`.
For multi-class classification, this should be a 2D array of shape
`(n_samples, n_classes)`.
targets : array-like
The ground truth labels. For binary classification, values should be
0 or 1. For multi-class classification, values should be integer
class indices in the range `[0, n_classes - 1]`.
Returns
-------
float or tensor
The computed mean cross-entropy loss across the batch."""
probs = pnp.clip(preds, 1e-9, 1.0 - 1e-9)

if probs.ndim == 1 or (probs.ndim == 2 and probs.shape[1] == 1):
Expand Down
Loading
Loading