Skip to content

Commit 22a8e45

Browse files
authored
add readthedocs (#28)
1 parent 666fc21 commit 22a8e45

25 files changed

Lines changed: 1163 additions & 8 deletions

.github/workflows/test.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,36 @@ jobs:
7373
run: build_tools/run_examples.sh
7474
shell: bash
7575

76+
docs:
77+
name: Build docs
78+
needs: code-quality
79+
runs-on: ubuntu-latest
80+
steps:
81+
- uses: actions/checkout@v7
82+
83+
- name: Set up Python
84+
uses: actions/setup-python@v7
85+
with:
86+
python-version: "3.12"
87+
88+
# nbsphinx shells out to the pandoc binary for the notebooks'
89+
# markdown cells. The "pandoc" PyPI package does not provide it.
90+
- name: Install pandoc
91+
run: sudo apt-get update && sudo apt-get install -y pandoc
92+
93+
- name: Install dependencies
94+
run: |
95+
python -m pip install --upgrade pip
96+
python -m pip install ".[docs]"
97+
98+
- name: Show dependencies
99+
run: python -m pip list
100+
101+
# -W turns warnings into errors, so a docstring with broken RST fails
102+
# here instead of rendering wrong on the published site.
103+
- name: Build docs
104+
run: python -m sphinx -b html docs docs/_build/html -W --keep-going
105+
76106
pytest-nosoftdeps:
77107
name: no-softdeps
78108
needs: code-quality

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,6 @@ dmypy.json
103103
checkpoints/
104104
lightning_logs/
105105
.DS_Store
106+
107+
# sphinx build output
108+
docs/_build/

.readthedocs.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Read the Docs build configuration.
2+
# https://docs.readthedocs.com/platform/stable/config-file/v2.html
3+
version: 2
4+
5+
build:
6+
os: ubuntu-24.04
7+
tools:
8+
python: "3.13"
9+
apt_packages:
10+
# nbsphinx shells out to the pandoc binary to render the notebooks'
11+
# markdown cells; the "pandoc" PyPI package is only a wrapper around it.
12+
- pandoc
13+
14+
sphinx:
15+
configuration: docs/conf.py
16+
17+
python:
18+
install:
19+
- method: uv
20+
command: pip
21+
path: .
22+
extras:
23+
- docs

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# PyQit
22

33
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
4-
[![Status: Active Development](https://img.shields.io/badge/status-active_development-orange.svg)]()
4+
![Status: Active Development](https://img.shields.io/badge/status-active_development-orange.svg)
55
[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-261230.svg)](https://github.com/astral-sh/ruff)
66
[![Build Status](https://img.shields.io/github/actions/workflow/status/phoeenniixx/pyQit/test.yml)](https://github.com/phoeenniixx/pyqit/actions)
77

@@ -11,7 +11,7 @@
1111
1212
**Version `0.1.0b1`. The API is unstable and still changing.**
1313

14-
### Key Features
14+
## Key Features
1515

1616
* **Lightweight & Modular:** PyQit runs natively on **PennyLane** and **NumPy**.
1717
> **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.

docs/Makefile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Minimal makefile for Sphinx documentation.
2+
# Run "make html" from this directory, then open _build/html/index.html.
3+
4+
SPHINXOPTS ?=
5+
SPHINXBUILD ?= python -m sphinx
6+
SOURCEDIR = .
7+
BUILDDIR = _build
8+
9+
help:
10+
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
11+
12+
.PHONY: help Makefile
13+
14+
# Catch-all: route anything else to sphinx-build's -M mode.
15+
# Gives html, clean, linkcheck and the rest with no target per command.
16+
%: Makefile
17+
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

docs/api/ansatzes.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
========
2+
Ansatzes
3+
========
4+
5+
.. currentmodule:: pyqit.ansatzes
6+
7+
An ansatz is the trainable part of the circuit. A model builds one from the
8+
class you hand it, using the model's own qubit count.
9+
10+
.. code-block:: python
11+
12+
from pyqit.ansatzes import SELAnsatz
13+
from pyqit.models import VQCClassifier
14+
15+
model = VQCClassifier(n_qubits=4, n_layers=3, ansatz=SELAnsatz)
16+
17+
:class:`SELAnsatz` wraps PennyLane's strongly entangling layers. Depth comes from
18+
the model's ``n_layers``. More layers buy expressivity and cost you gradient
19+
variance, which is what the barren-plateau check measures.
20+
21+
To add your own, subclass :class:`BaseAnsatz` and give it an ``object_type`` tag
22+
of ``"ansatz"``. Implement ``get_test_params()`` and the suite enrolls it
23+
automatically. See :doc:`the contributing guide </contributing>`.
24+
25+
Related
26+
=======
27+
28+
A :doc:`model <models>` builds the ansatz you give it. Depth is where gradients
29+
go to die, so pair this with :doc:`diagnostics` and the
30+
:doc:`barren-plateau tutorial </tutorials/barren_plateau>` before reaching for
31+
more layers.
32+
33+
.. autosummary::
34+
:nosignatures:
35+
36+
BaseAnsatz
37+
SELAnsatz
38+
39+
.. autoclass:: BaseAnsatz
40+
.. autoclass:: SELAnsatz

docs/api/callbacks.rst

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
=========
2+
Callbacks
3+
=========
4+
5+
.. currentmodule:: pyqit.core.callbacks
6+
7+
A pyqit callback implements up to three hooks, ``on_fit_start``,
8+
``on_epoch_end`` and ``on_fit_end``, each taking one :class:`LoopState`. Write it
9+
once and both backends honour it.
10+
11+
.. code-block:: python
12+
13+
import pyqit
14+
from pyqit.core import EarlyStopping, ModelCheckpoint
15+
16+
trainer = pyqit.Trainer(
17+
max_epochs=100,
18+
loss_fn="cross_entropy",
19+
callbacks=[
20+
EarlyStopping(monitor="val_loss", patience=3),
21+
ModelCheckpoint(dirpath="ckpts", save_best=True, save_last=True),
22+
],
23+
)
24+
history = trainer.fit(model, dm)
25+
26+
.. code-block:: text
27+
28+
[EarlyStopping] Stopped at epoch 18 - val_loss did not improve for 3 epoch(s)
29+
[Checkpoint] Restored best weights from epoch 15 (val_loss: 0.3721)
30+
31+
Why Lightning callbacks are rejected
32+
====================================
33+
34+
They are typed against Lightning's hooks, so the PennyLane loop could only
35+
ignore them. An ignored :class:`EarlyStopping` hands back a fully trained model
36+
without saying so, and that failure is invisible. Rejecting them at the door is
37+
the louder option.
38+
39+
On the torch backend a shim reads Lightning's ``callback_metrics`` into the same
40+
metric names and forwards ``state.stop`` onto ``trainer.should_stop``, so the
41+
same callback object works on both sides.
42+
43+
Checkpointing
44+
=============
45+
46+
:class:`ModelCheckpoint` owns checkpointing on both backends, and Lightning's
47+
own is switched off so a run is never written twice. Only the file format
48+
differs, ``.ckpt`` holding a ``state_dict`` on torch and ``.npz`` on pennylane.
49+
The array keys match ``model.weights`` either way.
50+
51+
Three files can be written independently. ``save_best`` uses the stem from
52+
``filename``, ``save_last`` uses ``last``, and ``every_n_epochs`` uses
53+
``epoch<n>``, numbered from zero to match ``best_epoch``. The best file is
54+
written once after training. Set ``save_on_improve=True`` to write on every
55+
improvement instead, at the cost of extra I/O.
56+
57+
``restore_best`` defaults to whatever ``save_best`` is, not to ``True``, so
58+
``save_best=False, save_last=True`` will not quietly hand you back the best
59+
model when you asked for the last one.
60+
61+
Nothing here resumes a run. These files hold weights only, with no optimizer
62+
state and no epoch counter.
63+
64+
Writing your own
65+
================
66+
67+
.. code-block:: python
68+
69+
from pyqit.core import BaseCallback
70+
71+
class StopWhenConverged(BaseCallback):
72+
def on_epoch_end(self, state):
73+
if state.metrics["train_loss"] < 0.01:
74+
state.stop = True
75+
76+
``state`` carries the model, datamodule, history, reporter, epoch index and this
77+
epoch's metrics. ``state.stop`` is the one field a callback may write.
78+
79+
Related
80+
=======
81+
82+
:doc:`trainer` takes the ``callbacks`` list and assembles it. The
83+
:doc:`callbacks tutorial </tutorials/callbacks>` runs both built-in callbacks
84+
together and reloads the checkpoint afterwards.
85+
86+
.. autosummary::
87+
:nosignatures:
88+
89+
BaseCallback
90+
LoopState
91+
HistoryCallback
92+
EarlyStopping
93+
ModelCheckpoint
94+
95+
.. autoclass:: BaseCallback
96+
.. autoclass:: LoopState
97+
.. autoclass:: HistoryCallback
98+
.. autoclass:: EarlyStopping
99+
.. autoclass:: ModelCheckpoint

docs/api/config.rst

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
=============
2+
Configuration
3+
=============
4+
5+
.. currentmodule:: pyqit
6+
7+
Backend selection is global, not per object. :func:`set_backend` writes to a
8+
context variable, and every object reads it once in its own ``__init__`` and
9+
caches the answer.
10+
11+
.. code-block:: python
12+
13+
import pyqit
14+
from pyqit.models import VQCClassifier
15+
16+
pyqit.set_backend("torch") # first
17+
model = VQCClassifier(n_qubits=4) # then
18+
19+
Order matters and getting it wrong fails quietly. Setting the backend after you
20+
build a model leaves that model on the old one.
21+
22+
:func:`set_backend` raises :class:`ImportError` when you ask for ``"torch"``
23+
without torch installed. Every torch import in the package sits behind either
24+
this call or a runtime type check, so the one guard covers them all and the
25+
error names its own cause instead of surfacing later as a bare
26+
``ModuleNotFoundError``.
27+
28+
What the backend changes
29+
========================
30+
31+
Three things fork on it. The QNode is wrapped in a ``qml.qnn.TorchLayer`` or
32+
stored as plain ``pnp`` arrays. Training runs through Lightning or through a
33+
PennyLane optimizer loop. Loaders come from ``torch.utils.data`` or from an
34+
internal NumPy loader.
35+
36+
Seeding
37+
=======
38+
39+
:func:`set_seed` seeds NumPy, which covers PennyLane too because
40+
``pennylane.numpy.random`` delegates to it, and seeds torch when it is
41+
installed. :meth:`Trainer.fit <pyqit.core.Trainer.fit>` calls it before anything
42+
stochastic runs.
43+
44+
Weights are drawn at construction, so reproducing them means seeding first:
45+
46+
.. code-block:: python
47+
48+
pyqit.set_seed(42)
49+
model = VQCClassifier(n_qubits=4)
50+
51+
``Trainer(seed=...)`` alone covers training and diagnostics, not
52+
initialisation. Note that this mutates global RNG state, the same contract as
53+
Lightning's ``seed_everything``.
54+
55+
Related
56+
=======
57+
58+
:doc:`models` and :doc:`trainer` both read the backend at construction, which is
59+
why the order on this page matters. :doc:`datamodule` picks its loader from the
60+
same setting.
61+
62+
.. autosummary::
63+
:nosignatures:
64+
65+
set_backend
66+
get_backend
67+
set_seed
68+
69+
.. autofunction:: set_backend
70+
.. autofunction:: get_backend
71+
.. autofunction:: set_seed

docs/api/datamodule.rst

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
==========
2+
DataModule
3+
==========
4+
5+
.. currentmodule:: pyqit
6+
7+
:class:`DataModule` holds the data and does nothing with it until ``setup()``
8+
runs. :meth:`Trainer.fit <pyqit.core.Trainer.fit>` and
9+
:meth:`Trainer.predict <pyqit.core.Trainer.predict>` call ``setup()`` for you,
10+
which is why properties like ``X_train`` raise before then.
11+
12+
.. code-block:: python
13+
14+
import pyqit
15+
from sklearn.datasets import make_moons
16+
17+
X, y = make_moons(n_samples=200, noise=0.1, random_state=0)
18+
dm = pyqit.DataModule(X, y, normalize="minmax", batch_size=16)
19+
20+
What setup does, in order
21+
=========================
22+
23+
#. Split into train, val and test.
24+
#. Normalize. This step is stateful, so the normalizer fits on train only and
25+
then applies to val and test. ``minmax``, ``zscore``, ``l1`` and ``l2`` are
26+
built in.
27+
#. Prescale for the circuit. This step is stateless and the model's embedding
28+
drives it, not the user.
29+
#. Apply any ``transform``.
30+
31+
Prescaling explains why feature shaping is not your job. ``AngleEmbedding`` pads
32+
or truncates to ``n_qubits`` and multiplies by pi. ``AmplitudeEmbedding`` pads to
33+
``2 ** n_qubits`` and L2-normalizes. The model class picks the embedding, so the
34+
model class decides the shape.
35+
36+
Repeated setup
37+
==============
38+
39+
``setup()`` returns early if it already ran, unless you pass ``force=True``. Two
40+
things sidestep that early return. ``batch_size`` applies every time, because it
41+
only affects loader construction and never the split or the fitted normalizer,
42+
so ``Trainer(batch_size=...)`` can override a datamodule you set up by hand.
43+
``encoder`` and ``n_qubits`` are overwritten only when actually supplied, so they
44+
survive a later ``setup(force=True)`` that omits them.
45+
46+
Building from tables
47+
====================
48+
49+
.. code-block:: python
50+
51+
dm = pyqit.DataModule.from_dataframe(df, label_col="target", normalize="zscore")
52+
dm = pyqit.DataModule.from_csv("data.csv", label_col="target")
53+
54+
Related
55+
=======
56+
57+
:doc:`embeddings` explains the prescaling step and why the model decides input
58+
shaping. :doc:`trainer` calls ``setup()`` for you. :doc:`pipeline` rebuilds a
59+
datamodule between sequential stages. Every
60+
:doc:`tutorial </tutorials/index>` starts by building one.
61+
62+
.. autosummary::
63+
:nosignatures:
64+
65+
DataModule
66+
67+
.. autoclass:: DataModule

0 commit comments

Comments
 (0)