Skip to content
Merged
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
44 changes: 44 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
name: Bug report
about: Report a problem so it can be fixed
title: ""
labels: bug
assignees: ""
---

## What happened

A clear description of the problem.

## How to reproduce

A minimal script that shows the problem. For example:

```python
import torch
from qlstm import QLSTM

layer = QLSTM(input_size=8, hidden_size=4, n_qubits=4)
out, _ = layer(torch.randn(6, 2, 8))
# ...what goes wrong
```

## What you expected

What should have happened instead.

## Error output

The full traceback or wrong result, if any.

```
paste here
```

## Environment

- `qlstm` version:
- Python version:
- PyTorch version:
- PennyLane version:
- Operating system:
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Question or usage help
url: https://github.com/TravisCao/Quantum-LSTM/issues
about: For a question rather than a bug, open an issue or email travisyjcao@gmail.com.
25 changes: 25 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
name: Feature request
about: Suggest a new capability or improvement
title: ""
labels: enhancement
assignees: ""
---

## Use case

The problem you want to solve or the workflow you want to support.

## Proposed feature

What you would like `qlstm` to do. If it maps to a `torch.nn.LSTM` feature
(for example bidirectionality or a projection size), name it so the interface
can stay compatible.

## Alternatives

Other ways you have considered solving this, if any.

## Additional context

Links, papers, or examples that help explain the request.
21 changes: 21 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## Summary

What this pull request changes, and why.

## Related issue

Closes #<!-- issue number, if any -->

## Type of change

- [ ] Bug fix
- [ ] New feature
- [ ] Documentation
- [ ] Refactor or maintenance

## Checklist

- [ ] Added or updated a test for the change
- [ ] `pytest -q` passes locally
- [ ] Kept the public API compatible with `torch.nn.LSTM` where they overlap, or explained why not
- [ ] Updated `README.md` and `CHANGELOG.md` if behaviour or the interface changed
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ All notable changes to this project are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- Stacked layers: `num_layers` on `QLSTM`/`LQLSTM` stacks recurrent layers, with
`h_n`/`c_n` shaped `(num_layers, batch, hidden_size)`, matching
`torch.nn.LSTM`.
- Inter-layer `dropout` on `QLSTM`/`LQLSTM`, applied to the output of every layer
except the last.
- `examples/benchmark_vs_classical.py`: a fair side-by-side against
`torch.nn.LSTM` on the same task.
- Community health files: `CONTRIBUTING.md`, issue templates, and a pull-request
template.
- Python 3.13 added to the packaging classifiers (already covered by CI).

## [0.1.0] - 2026-08-13

First packaged release. The quantum LSTM from the paper is now an installable,
Expand Down
61 changes: 61 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Contributing to qlstm

Thanks for your interest in `qlstm`. Bug reports, feature requests, and pull
requests are all welcome.

## Ways to help

- **Report a bug.** Open an issue with the bug-report template. A short script
that reproduces the problem is the fastest way to a fix.
- **Request a feature.** Open an issue with the feature-request template and
describe the use case.
- **Send a pull request.** Fix a bug, add a test, improve the docs, or add a
feature. For a large change, open an issue first so the design can be agreed
before you write the code.

## Development setup

Python 3.10 or newer is required. Install the package and its test dependencies
in editable mode:

```bash
git clone https://github.com/TravisCao/Quantum-LSTM.git
cd Quantum-LSTM
pip install -e ".[test]"
```

This installs PyTorch, PennyLane, and pytest.

## Run the tests

```bash
pytest -q
```

The suite must pass before a pull request can merge. Continuous integration runs
the same command on Python 3.10 through 3.13, so run it locally first.

## Pull request checklist

1. Add or update a test for the behaviour you change. New features need a test
that fails before the change and passes after it.
2. Keep the public API compatible with `torch.nn.LSTM` where the two overlap. A
change that breaks that compatibility needs a clear reason in the pull
request description.
3. Run `pytest -q` and confirm it is green.
4. Update `README.md` and `CHANGELOG.md` if you change behaviour, add a feature,
or change the public interface.
5. Match the style of the surrounding code: type hints on public functions,
docstrings on public classes and methods, and clear names.

## Scope

The installable `qlstm` package under [`qlstm/`](qlstm/) targets current PyTorch
and PennyLane. The original paper-reproduction code under [`src/`](src/) targets
the pinned versions in `requirement.txt` and is kept for reference; new library
work belongs in `qlstm/`.

## Questions

Open an [issue](https://github.com/TravisCao/Quantum-LSTM/issues) or contact
travisyjcao@gmail.com.
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ print(h_n.shape) # torch.Size([1, 2, 4])
```

A complete training loop on a small synthetic task is in
[`examples/quickstart.py`](examples/quickstart.py).
[`examples/quickstart.py`](examples/quickstart.py). A fair side-by-side against a
classical `torch.nn.LSTM` on the same task is in
[`examples/benchmark_vs_classical.py`](examples/benchmark_vs_classical.py).

## Two models: `QLSTM` and `LQLSTM`

Expand All @@ -71,6 +73,26 @@ from qlstm import QLSTM
layer = QLSTM(input_size=4, hidden_size=3, n_qubits=7, linear_enhanced=False)
```

## Stacking layers

Set `num_layers` to stack recurrent layers, exactly as in `torch.nn.LSTM`. Each
layer reads the hidden-state sequence of the layer below. `output` is the top
layer's sequence, and `h_n`/`c_n` gather the final state of every layer with
shape `(num_layers, batch, hidden_size)`. A non-zero `dropout` applies dropout to
the output of each layer except the last.

```python
import torch
from qlstm import LQLSTM

layer = LQLSTM(input_size=8, hidden_size=4, n_qubits=4, num_layers=2, dropout=0.1)

x = torch.randn(6, 2, 8)
output, (h_n, c_n) = layer(x)
print(output.shape) # torch.Size([6, 2, 4])
print(h_n.shape) # torch.Size([2, 2, 4])
```

## API

| Object | Purpose |
Expand All @@ -80,12 +102,15 @@ layer = QLSTM(input_size=4, hidden_size=3, n_qubits=7, linear_enhanced=False)
| `QLSTMCell` | One recurrent step, for custom loops. |
| `make_vqc` | Build the underlying variational circuit as a `torch` layer. |

Key constructor arguments (shared by `QLSTM`, `LQLSTM`, and `QLSTMCell`):
Key constructor arguments (shared by `QLSTM`, `LQLSTM`, and `QLSTMCell`, except
`num_layers` and `dropout`, which apply to the sequence layers only):

| Argument | Default | Meaning |
| --- | --- | --- |
| `n_qubits` | `4` | Wires per gate circuit. |
| `n_qlayers` | `1` | Depth of the entangling ansatz. |
| `num_layers` | `1` | Number of stacked recurrent layers (`QLSTM`/`LQLSTM`). |
| `dropout` | `0.0` | Dropout on the output of each layer except the last (`QLSTM`/`LQLSTM`). |
| `ansatz` | `"basic"` | `"basic"` ([`BasicEntanglerLayers`](https://docs.pennylane.ai/en/stable/code/api/pennylane.BasicEntanglerLayers.html)) or `"strong"` ([`StronglyEntanglingLayers`](https://docs.pennylane.ai/en/stable/code/api/pennylane.StronglyEntanglingLayers.html), more expressive). |
| `rotation` | `"Y"` | Angle-embedding axis (`"X"`, `"Y"`, or `"Z"`). |
| `input_activation` | `"arctan"` | Angle activation before embedding; bounds the encoded angles as in the paper. `"tanh"`, `None`, or any callable also work. |
Expand Down
90 changes: 90 additions & 0 deletions examples/benchmark_vs_classical.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Compare an LQLSTM with a classical ``torch.nn.LSTM`` on a small task.

The point is a fair, reproducible side-by-side, not a claim that the quantum
layer wins. It trains both models on the same data with the same head and
optimiser settings, then prints each model's trainable-parameter count and its
final training and held-out loss. The task is a genuine recurrent one: predict
the running sum of the first input feature over the sequence, which needs the
cell to accumulate state.

Run with::

python examples/benchmark_vs_classical.py

Quantum simulation is slow, so the sizes are deliberately small.
"""

from __future__ import annotations

import torch
import torch.nn as nn

from qlstm import LQLSTM


def make_data(n, seq_len, input_size, generator):
x = torch.randn(seq_len, n, input_size, generator=generator)
# Target: sum of feature 0 over time -> requires accumulation across steps.
y = x[:, :, 0].sum(dim=0, keepdim=True).transpose(0, 1) # (n, 1)
return x, y


def count_params(module):
return sum(p.numel() for p in module.parameters() if p.requires_grad)


def train(layer, head, x, y, xv, yv, steps, lr):
params = list(layer.parameters()) + list(head.parameters())
opt = torch.optim.Adam(params, lr=lr)
loss_fn = nn.MSELoss()
for _ in range(steps):
opt.zero_grad()
out, _ = layer(x)
loss = loss_fn(head(out[-1]), y)
loss.backward()
opt.step()
with torch.no_grad():
out, _ = layer(x)
train_loss = loss_fn(head(out[-1]), y).item()
outv, _ = layer(xv)
val_loss = loss_fn(head(outv[-1]), yv).item()
return train_loss, val_loss


def main():
torch.manual_seed(0)
gen = torch.Generator().manual_seed(0)

seq_len, input_size, hidden = 6, 2, 4
x, y = make_data(64, seq_len, input_size, gen)
xv, yv = make_data(64, seq_len, input_size, gen)
steps, lr = 80, 0.05

print(f"task: running-sum regression seq_len={seq_len} input={input_size} "
f"hidden={hidden} train/val=64/64 steps={steps}\n")

q_layer = LQLSTM(input_size=input_size, hidden_size=hidden, n_qubits=4)
q_head = nn.Linear(hidden, 1)
q_params = count_params(q_layer) + count_params(q_head)
q_train, q_val = train(q_layer, q_head, x, y, xv, yv, steps, lr)

c_layer = nn.LSTM(input_size=input_size, hidden_size=hidden)
c_head = nn.Linear(hidden, 1)
c_params = count_params(c_layer) + count_params(c_head)
c_train, c_val = train(c_layer, c_head, x, y, xv, yv, steps, lr)

print(f"{'model':<16}{'params':>10}{'train MSE':>14}{'val MSE':>12}")
print("-" * 52)
print(f"{'LQLSTM':<16}{q_params:>10}{q_train:>14.4f}{q_val:>12.4f}")
print(f"{'nn.LSTM':<16}{c_params:>10}{c_train:>14.4f}{c_val:>12.4f}")
print(
"\nBoth models train on this task and reach a comparable validation "
"error.\nThe linear-enhanced quantum layer carries extra classical "
"projection\nparameters, so it is not smaller than a classical LSTM of "
"this size.\nNumbers vary with seed and size; this is a fair-setup "
"demonstration,\nnot a performance claim."
)


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
dependencies = [
Expand Down
Loading
Loading