lct-activation is a small PyTorch research package for testing Linear
Canonical Transform layers inside real models.
The package keeps two goals in view:
- correctness on finite grids, via a dense reference kernel and explicit tests for special cases
- structured execution, via FFT and Bluestein / chirp-z paths instead of Python loops
The package now exposes one lead model-facing building block and one experimental activation:
LCTLinear, a structurednn.Linear-style layer that uses fast spectral mixing instead of a dense learned matrixLCTActivation, a genuinely nonlinear modReLU-style activation in the LCT domain for experiments
Both layers are also available as native MLX
modules for Apple silicon via the optional mlx extra (see
MLX backend below).
Real channels are packed into complex pairs, transformed by an LCT, mixed in the transform domain, and unpacked back to real tensors. The default LCTLinear initialization is identity-like, so it can slot into an MLP without blowing up activations on step one.
The current research paper is an interactive HTML instrument under
site/: it combines a determinant-one 3D phase-space
explainer, a guided prediction loop, the controlled NanoGPT evidence, and the
exploratory H100 learned-transform result. paper/report.md
is retained as the archival fixed-transform report that preceded the learned
symplectic implementation.
The finite-dimensional tradeoff is explicit:
normalization="unitary"favors energy preservation and stable optimizationnormalization="compositional"favors behavior that tracks matrix composition more closely
The current public release is
v0.1.0.
Add it to a uv project directly from the immutable Git tag:
uv add "lct-activation @ git+https://github.com/alok/linear_canonical_transform.git@v0.1.0"Install the optional MLX backend explicitly when needed:
uv add "lct-activation[mlx] @ git+https://github.com/alok/linear_canonical_transform.git@v0.1.0"To make the packaged commands available independently of a project:
uv tool install \
--from "git+https://github.com/alok/linear_canonical_transform.git@v0.1.0" \
lct-activationQuick smoke test after installing:
lct quickstartThe same self-contained smoke test can emit JSON:
lct quickstart --format jsonManual Python smoke test in a project that has added the package:
uv run python - <<'PY'
import torch
from lct_activation import LCTLinear
layer = LCTLinear(16, 16)
x = torch.randn(2, 16)
print(layer(x).shape)
PYOr run the packaged self-check:
lct doctorClone the repository and install its locked development environment:
git clone https://github.com/alok/linear_canonical_transform.git
cd linear_canonical_transform
uv sync --extra dev
uv run pytest -qTo test an unreleased branch or tag in another uv project:
uv add "lct-activation @ git+https://github.com/alok/linear_canonical_transform.git@<branch-or-tag>"Inside this repository, include the checked-in paper evidence artifacts:
uv run lct doctor --result-dir paper/results --require-resultsimport torch
from lct_activation import LCTActivation, LCTLinear, property_report
act = LCTActivation(
1024,
a=0.0,
b=1.0,
c=0.0,
dense_threshold=256,
)
x = torch.randn(8, 128, 1024)
y = act(x)
print(y.shape)
linear = LCTLinear(1024, 2048)
z = linear(torch.randn(8, 1024))
print(z.shape)
dense_equivalent = linear.to_linear()
energy_preserving = LCTLinear(1024, 1024, normalization="unitary")
matrix_like = LCTLinear(1024, 1024, normalization="compositional")
report = property_report(
16,
(0.8660254, 0.5, -0.5),
(0.8660254, -0.5, 0.5),
normalization="unitary",
discretization="spectral-frft",
)
print(report.first_unitarity_error, report.composition_error)Compatibility imports under the older repo name also work:
from linear_canonical_transform import LCTLinearThe optional MLX backend provides the same two layers as native
mlx.nn.Modules. On Apple silicon, uv sync --extra dev already pulls in
mlx; downstream projects can use the extra explicitly:
uv add "lct-activation[mlx]"import mlx.core as mx
from lct_activation.mlx import LCTActivation, LCTLinear
act = LCTActivation(1024)
y = act(mx.random.normal((8, 128, 1024)))
linear = LCTLinear(1024, 1024)
z = linear(mx.random.normal((8, 1024)))The MLX backend matches the PyTorch numerics branch by branch and is covered
by parity tests (tests/test_mlx_backend.py). Transform parameters (a, b, c)
are fixed at construction and compiled into precomputed per-length plans
(chirps, Bluestein tables, dense kernels), because MLX's lazy tracing cannot
branch on traced parameter values; the modReLU bias/gain/residual mix and the
spectral diagonal/bias remain trainable.
Runnable examples live in examples/:
uv run lct quickstart
uv run python examples/quickstart.py
uv run python examples/property_diagnostics.py
uv run python examples/mlx_quickstart.py # trains a tiny MLX LCT-MLPsrc/lct_activation/functional/lct.py: dense reference kernel,b ~= 0branch, Fourier/Laplace special cases, and the finite-dimensional symplectic solvesymplectic_dsrc/lct_activation/functional/chirpz.py: genericO(N log N)Bluestein / chirp-z pathsrc/lct_activation/layers.py:LCTLayer,LCTActivation, andLCTLinearsrc/lct_activation/properties.py: finite-grid diagnostics for determinant, unitarity, and composition errorssrc/lct_activation/doctor.py: install, smoke-test, and local evidence checks
Math notes for the discrete approximation live in docs/lct_math.md.
The public API surface is summarized in docs/api.md.
lct-activation is distributed under the Apache License, Version 2.0. See
LICENSE. Copyright 2026 Alok Singh.
The package includes a small diagnostic CLI for the tradeoff that matters most in this project: finite LCT kernels can be made very nearly unitary, but that projection changes how closely finite matrices compose like their continuum canonical parameters.
lct-check-properties \
--length 16 \
--first-angle-degrees 30 \
--second-angle-degrees -30 \
--normalization unitary \
--unitary-projectionThe same check is available through the umbrella command:
lct check-properties \
--length 16 \
--first-angle-degrees 30 \
--second-angle-degrees -30 \
--discretization spectral-frftThe output is JSON with determinant errors, unitarity errors, and composition error. To compare the unprojected dense kernel:
lct-check-properties \
--length 16 \
--first-angle-degrees 30 \
--second-angle-degrees -30 \
--normalization unitary \
--no-unitary-projectionFor finite fractional Fourier transforms where exact finite-grid composition is the priority, use the spectral FrFT discretization:
lct-check-properties \
--length 16 \
--first-angle-degrees 30 \
--second-angle-degrees -30 \
--discretization spectral-frftThat path constructs a fractional power of the unitary DFT from its four spectral projectors. It is less a sampled continuum integral kernel and more a finite-dimensional FrFT algebra: unitary and compositional up to floating-point error.
The spectral FrFT path is intentionally a diagnostics/research API in this
release. It is not wired into LCTLinear as a model-facing execution path until
benchmark evidence justifies that promotion.
Use assert-properties when you want a CI-friendly pass/fail check with
thresholds:
lct assert-properties \
--length 16 \
--first-angle-degrees 30 \
--second-angle-degrees -30The assertion command defaults to the spectral FrFT discretization and exits
nonzero if determinant, unitarity, or composition errors exceed the configured
thresholds. Use check-properties or sweep-properties when comparing
finite-discretization tradeoffs without treating the sampled-kernel path as a
failure.
To generate a compact tradeoff table across lengths and angles:
lct sweep-properties \
--length 8 16 32 \
--angle-pair 30 -30 \
--angle-pair 45 -45Use JSON when collecting paper artifacts:
lct sweep-properties \
--length 8 16 32 \
--angle-pair 30 -30 \
--format json \
--output paper/results/property_sweep.jsonSaved sweep JSON is understood by lct-summarize-results, including unitarity
and composition columns.
The same diagnostics are available from Python:
from lct_activation import composition_error, finite_lct_matrix, property_sweep, unitarity_error
params = (0.8660254, 0.5, -0.5)
matrix = finite_lct_matrix(16, params, normalization="unitary")
print(unitarity_error(matrix))
rows = property_sweep(
lengths=[8, 16],
angle_pairs_degrees=[(30.0, -30.0)],
)
print(rows[0].as_dict())The checked-in NanoGPT and backend artifacts under paper/results/ can be
summarized without manual jq commands:
lct-summarize-results --result-dir paper/resultsTo emit JSON for a notebook or plotting script:
lct-summarize-results --result-dir paper/results --format jsonThis repo includes a minimal NanoGPT integration under lct_activation.integrations:
src/lct_activation/integrations/nanogpt.pyprovides source-sliced loading for an explicitly supplied NanoGPT checkout, an upstream patch path forkarpathy/nanoGPT, and model builders for both layoutsscripts/bench_nanogpt.pybenchmarks baseline vs LCT activation throughput on random tokensscripts/train_nanogpt_lct.pyruns upstreamtrain.pyin-process after applying the LCT patch
Smoke-test against a local NanoGPT checkout:
uv run python scripts/bench_nanogpt.py \
--repo-dir /path/to/nanoGPT \
--repo-kind local \
--device cpu \
--steps 1 \
--warmup-steps 1 \
--batch-size 1 \
--seq-len 12 \
--n-layers 1 \
--n-heads 4 \
--embed-dim 64Benchmark a different checkout explicitly:
uv run python scripts/bench_nanogpt.py \
--repo-dir /path/to/nanoGPT \
--repo-kind upstream \
--output paper/results/bench_nanogpt_upstream.jsonMicrobenchmark the structured linear layer against nn.Linear:
lct-bench-linear \
--device cpu \
--batch-size 256 \
--in-features 1024 \
--out-features 1024 \
--output paper/results/bench_linear_cpu.jsonIn the checked-in measurements, the current implementation is slower than
nn.Linear for small 512-wide CPU layers and faster around 4096 features,
where the structured FFT path starts to dominate the dense matmul.
Benchmark all local Mac backends (torch CPU, torch MPS, MLX) in one run:
uv run python scripts/bench_mac_local.py \
--output paper/results/bench_mac_local.jsonRepresentative numbers from an Apple-silicon laptop (batch 8, seq 256,
forward pass, median of 30 steps; see
paper/results/bench_mac_local.json):
| dim | backend | nn.Linear |
LCTLinear |
GELU | LCTActivation |
|---|---|---|---|---|---|
| 1024 | torch MPS | 0.54 ms | 0.39 ms | 0.29 ms | 1.49 ms |
| 1024 | MLX | 0.49 ms | 0.49 ms | 0.18 ms | 0.74 ms |
| 4096 | torch MPS | 5.64 ms | 1.11 ms | 0.47 ms | 2.53 ms |
| 4096 | MLX | 5.40 ms | 1.96 ms | 0.27 ms | 2.59 ms |
In this sweep, the structured LCTLinear overtakes the dense matmul around
1024-2048 features and is about 5x faster at 4096 on MPS. The nonlinear
LCTActivation costs roughly 4-10 GELUs in the same measurements; MLX has the
lowest measured time for it up to ~2048 features, with torch MPS edging ahead
at 4096.
Methodology notes: both layers are benchmarked at their default transform
(a, b, c) = (0, 1, 0), i.e. the FFT fast path. The torch numbers include
per-call branch dispatch (several scalar GPU-to-CPU syncs per forward on
MPS), which the MLX backend compiles away into per-length plans during
warmup; that is an honest end-to-end cost of each implementation, not a
like-for-like kernel comparison. Forward+backward timings differentiate
with respect to both the input and the layer parameters on both frameworks.
Run the local NanoGPT ablation sweep:
lct-tune-nanogpt \
--device cpu \
--steps 20 \
--eval-iters 4 \
--batch-size 8 \
--seq-len 24 \
--n-layers 2 \
--n-heads 4 \
--embed-dim 64Sweep a few additional FRFT angles for the linear layer:
lct-tune-nanogpt \
--device cpu \
--steps 20 \
--eval-iters 4 \
--batch-size 8 \
--seq-len 24 \
--n-layers 2 \
--n-heads 4 \
--embed-dim 64 \
--presets baseline linear-fourier \
--linear-angle-degrees 15 30 45 60 75 \
--output paper/results/nanogpt_linear_angle_sweep.jsonRun the packaged branch in a remote Linux container on Modal:
modal run scripts/modal_linux_smoke.pyRun the CUDA benchmark/sweep on Modal:
modal run scripts/modal_gpu_sweep.pyRun upstream NanoGPT with the LCT patch:
lct-train-nanogpt \
--clone-if-missing \
--variant linear \
-- --batch_size=8Core verification used in this repo:
uv run pytest -q tests/test_lct_core.py tests/test_activation.py tests/test_special_cases.py
uv run pytest -q tests/test_lct_linear.py
uv run pytest -q tests/test_lct_properties.pyAfter building a wheel, smoke-test it outside the source project:
uv build
uv run python scripts/smoke_dist.pyBefore publishing, run the release verifier against the exact artifacts. It checks wheel and sdist metadata, the Apache license, public project URLs, local git origin, the isolated wheel smoke test, and whether the current version is still uploadable on PyPI:
uv run python scripts/verify_release.py --check-pypiBefore public release, use docs/release_checklist.md.
The GitHub Actions workflow in .github/workflows/ci.yml
runs tests, examples, property diagnostics, result summaries, package build, and
isolated wheel smoke and release-metadata checks on Python 3.10 and 3.12.