In 1918, Srinivasa Ramanujan wrote down a family of trigonometric sums to study the divisors of integers. A century later they turn out to be the perfect instrument for a factory floor: filters that hear one period and nothing else. RPR listens to a woven grid the way a piano tuner listens to a string — not asking does this look different? but does this still repeat the way it should? When a thread pulls, a pitch drifts, or a roller stamps its wear into the sheet, exactly one channel of the periodicity spectrum goes quiet. An agent then follows that silence — through millimeters and hertz, process logs and maintenance records — to the machine part that caused it.
| “Does this look different?” | “Does this still repeat the way it should?” |
|---|---|
| Embedding-based detectors compare a patch to known-good patches in feature space. A thin scratch across a regular grid barely moves an embedding — the pattern is broken, and the detector shrugs. | Ramanujan sums give filters with a rare property: the period-q filter responds only to genuinely period-q structure — not its harmonics, not its neighbors. Break the repeat, and exactly the right channel drops. |
%%{init: {"theme": "base", "themeVariables": {
"primaryColor": "#F0EEE6", "primaryTextColor": "#191919", "primaryBorderColor": "#B0AEA5",
"lineColor": "#D97757", "fontFamily": "Georgia, serif", "fontSize": "15px"}}}%%
flowchart LR
A[texture image<br><i>camera on the line</i>] --> B[Ramanujan filter bank<br><i>c_q filters, rows + cols<br>HCN period set, 2x faster</i>]
B --> C[periodicity signature<br><i>per 32x32 patch,<br>vs memory bank of good</i>]
C --> D[anomaly heatmap<br><i>cosine NN residual</i>]
C --> E[disrupted period q<br><i>divisor-family attribution</i>]
E --> F[physical mapping<br><i>q x pixel pitch → mm<br>belt speed ÷ mm → Hz</i>]
F --> G[Claude-powered agent<br><i>process logs, incidents,<br>maintenance records</i>]
G --> H[ranked root causes<br><i>every claim cites a number</i>]
style D fill:#D97757,color:#ffffff,stroke:#D97757
style E fill:#D97757,color:#ffffff,stroke:#D97757
style H fill:#191919,color:#FAF9F5,stroke:#191919
The three demo textures (MVTec-AD directory layout, generated by data/make_demo_data.py):
grid · 8 px pitch · M-101 |
mesh · 12 px pitch · M-102 |
weave · 24 px weft · M-103 |
|---|---|---|
![]() |
![]() |
![]() |
The synthetic gate had to pass before anything else was written: an injected scratch and hole on a period-8 grid must spike ~100× above clean regions, and the reported disrupted period must be q = 8 — the texture’s true pitch — on the top patch and 9 of the top-10.
Injected scratch + hole on a period-8 grid: the heatmap lights both defects; the spectrum names the broken period.
Clean vs defect separation, and the highly-composite-number speedup: 8 candidate periods instead of 32, ~2× faster.
- Clean → both channels stay quiet. Scratch → both fire, but only RPR names
q = 8. - Stain → invisible to the baseline (
0.0002, clean level) while RPR flags it at0.036— ~180× its clean score. - Pitch shift → texture that still looks right but repeats at the wrong pitch: RPR lights the entire band.
The plot embeddings can’t draw: the pitch-shifted patch’s q = 8 channel drops 80% against its nearest clean reference.
q = 8 px → × 0.5 mm/px = 4.0 mm repeat → ÷ into 200 mm/s = 50.0 Hz
50.0 Hz = gear mesh (2.0 Hz roller × 25 teeth)
The agent (Claude claude-opus-4-8, tool loop, schema-constrained JSON) investigates with four plant-data tools:
▸ get_process_parameters("M-101") temperature_C: 82.1 belt_speed_mm_s: 200.0
▸ convert_period_to_physical_frequency(8) spatial_period_mm: 4.0 temporal_frequency_hz: 50.0
▸ search_incident_history_by_frequency(50.0) INC-001 · 50.0 Hz · deviation 0.0% · same machine
▸ get_maintenance_log("M-101") 2026-06-28 · drive gear · non-OEM spare installed
Top root cause — confidence 0.95 Drive gear tooth wear → tension flutter at gear-mesh frequency
- 8 px × 0.5 mm/px = 4.0 mm; at 200 mm/s → 50.0 Hz
- Incident
INC-001matched at 0.0% deviation- 50.0 Hz = the machine’s gear-mesh frequency (2.0 Hz × 25 teeth)
- Maintenance 2026-06-28: non-OEM drive gear installed
Recommended action: replace drive gear, re-tension belt.
Every hypothesis must cite (a) a specific parameter reading, (b) a matched incident with its % frequency deviation, or (c) an explicit no strong match — no generic “inspect the machine” filler. Without an ANTHROPIC_API_KEY, a deterministic matcher runs the same tools, so the demo never breaks.
pip install -r requirements.txt
python data/make_demo_data.py # generate the demo dataset
PYTHONPATH=. python tests/test_math_core.py # unit tests (Ramanujan identities)
PYTHONPATH=. python tests/test_synthetic.py # Phase-1 gate: synthetic validation
PYTHONPATH=. streamlit run frontend/app.py # dashboard
# optional API server: PYTHONPATH=. uvicorn backend.api:appSet ANTHROPIC_API_KEY (see .env.example) to run the root-cause agent on Claude; leave it unset for the offline deterministic path.
Ramanujan sums — exact-period selectivity
c_q(n) = Σ exp(2πikn/q) over the k ∈ [1,q] coprime with q. The magic property: c_q contains only the frequencies k/q with gcd(k,q)=1 — the frequencies that belong to period q and to no smaller period. A comb filter at period 8 also fires for period-4 and period-2 signals; a Ramanujan filter at q=8 fires only for genuinely period-8 structure. (rpr/ramanujan_sums.py, tested against the identities c_q(0)=φ(q), c_q(n)=μ(q) for gcd(n,q)=1, integrality, periodicity.)
Ramanujan Filter Bank — and the normalization that actually matters
For each candidate period q we tile c_q(n) into an FIR filter, convolve, and square — “period-q energy” at each position (Vaidyanathan & Ramamurthy 2014). One detail cost us a debugging session: with plain unit-norm filters the channel energy is (energy in q's harmonic family) / φ(q), which hands low-φ channels a gain boost — a 25%-duty grid of period 8 then measures equal energy at q=4 and q=8. We normalize filters by √φ(q) (“equal per-harmonic gain”) so channel energies are comparable across q and the true fundamental wins. (rpr/filter_bank.py)
Candidate periods from highly composite numbers — 8 periods instead of 32, 2× faster
Instead of sweeping q = 1..32 we take the highly composite numbers ≤ 32 (1, 2, 4, 6, 12, 24) and close under divisors: {1, 2, 3, 4, 6, 8, 12, 24} — 8 candidate periods instead of 32, ~2× faster scoring with identical detection on the validation suite. HCNs have the richest divisor lattices, covering the periods that occur in engineered textures while skipping large primes. (rpr/period_selection.py)
Patch signatures, memory bank, and calibrated thresholds
Each RFB filter runs along rows and columns of the whole image; per-patch mean energies (via integral images) give a 2×|Q| signature per 32×32 patch. Signatures from good images form a memory bank; a test patch’s anomaly score is its cosine distance to the nearest bank entry. The anomaly threshold is calibrated on clean data by leave-one-image-out scoring of the reference set. (rpr/periodicity_signature.py, rpr/anomaly_scoring.py, backend/pipeline.py)
Which period broke? — divisor-family attribution
Attribution runs on raw (un-normalized) energies — a defect that erases texture also injects broadband energy, which masks the loss on normalized signatures. And it aggregates over divisor families: in Ramanujan periodicity theory a period-Q signal decomposes exactly over the c_d subspaces for d | Q, so a disrupted period-8 grid loses energy at q=8 and q=4. Each candidate fundamental is scored by its family’s summed energy drop, requiring the fundamental’s own channel to drop above the bank’s noise floor and to carry ≥20% of the family loss — which disqualifies both bare harmonics and spurious superperiods whose channels gained energy from the defect. (rpr/anomaly_scoring.py)
- Periodic textures only. RPR is scoped to MVTec-AD-style periodic texture categories (grid, carpet, tile, wood, leather, mesh) and ships with a standard embedding baseline as the second channel for non-periodic categories. We do not claim RPR outperforms embedding baselines on non-periodic objects.
- Known misses, stated. 26/27 correct verdicts on the demo set; the two misses are thin scratches on the coarse weave, just under the calibrated threshold.
- Baseline channel. OpenCLIP ViT-B/32 when its weights are loadable; otherwise a multi-scale Gabor + local-statistics embedding (this build environment’s network policy blocked weight downloads). Results are labeled with whichever backend produced them.
- Demo plant data. Process logs, incidents, and maintenance records are mock data crafted for a self-consistent story; the agent tooling is real — swap
backend/tools.pyfor live sources. - Attribution of non-periodicity defects. Stains don’t disrupt any period — they’re caught through DC/low-frequency channels, and their reported “disrupted period” comes from a fallback path (largest energy injection).
rpr/ math core: ramanujan_sums, period_selection, filter_bank,
periodicity_signature, anomaly_scoring
backend/ pipeline, baseline (CLIP/Gabor), physical_mapping, tools,
root_cause_agent (Claude + deterministic), FastAPI api
frontend/ Streamlit dashboard
data/ demo dataset generator + mock process/incident/maintenance data
tests/ math-core unit tests + the synthetic validation gate
site/ the interactive project page (served at infinitule.github.io/RPR)
media/ repository banner · scripts/ figure + banner generation
docs/ video master prompt
Ramanujan Filter Bank after P. P. Vaidyanathan & P. Ramamurthy, “Ramanujan filter banks for estimation and tracking of periodicities,” IEEE ICASSP 2014. Root-cause analysis by a Claude-powered agent. Built at hackathon speed; validated before built on.




