aadya-m1
aadya-m1 forecasts when the next period is likely to start as a full probability distribution, not a single date. Open weights. It runs on your own machine, and your cycle data never has to leave it.
Aadya (आद्या) is Sanskrit for “the first” or “the beginning”, a fitting name for a model that forecasts the start of a cycle, whose day one is the first day of bleeding.
Need something tiny? aadya-m1-mini (0.84M parameters, 3.4 MB) is built for phones and low-power devices, with the same API and level accuracy on the public real data we have. See which model to use.
Paper | Live demo | Contribute on GitHub
Which model should I use?
| aadya-m1 | aadya-m1-mini | |
|---|---|---|
| Parameters | 256M | 0.84M |
| Weights | about 1 GB | 3.4 MB |
| Best for | servers, desktops, research, fine-tuning, the larger starting point | phones, browsers, low-power devices |
| Accuracy on 364 public real users (mean CRPS, lower is better) | 1.930 | 1.932 (level) |
| Live demo | aadya-m1-try | aadya-m1-mini-try |
Same API, same inputs, same outputs. On the public real data we have, the two are level, so choosing the small one costs nothing in accuracy there; choose aadya-m1 when you want the extra capacity to fine-tune or to study larger-scale behaviour.
Highlights
- #1 by mean error on every public benchmark we ran: AadyaBench simulated users, and the Creighton and Marquette real-data cohorts (364 real users). It is clearly ahead of the LSTM, Poisson-skip, rolling-median and 28-day methods, and slightly but clearly ahead of the classical Bayes reference on the pooled real users (about 1% lower error).
- 18% lower error than the usual tracker method (a rolling median of recent cycles) on 364 public real users, and 21% lower on simulated users.
- Honest uncertainty. It gives a probability for each possible cycle length (1 to 120 days), so an app can say "most likely these days, about 8 in 10 chance" instead of showing one brittle date.
- Optional ovulation-test input (experimental). It narrows the 80% window from about 12 to about 7 days in a 41-participant study.
- Private by design. Runs locally, learns from confirmed periods on the device, no accounts, no network calls.
Model details
| Type | Probabilistic model for next-cycle length |
| Parameters | ~256M |
| Input | Past cycle lengths (days), days since the last period, optional age group, optional ovulation-test day |
| Output | A probability for each cycle length from 1 to 120 days |
| Training data | No real person's records were used |
| License | Apache-2.0 |
Benchmarks
AadyaBench, mean error (CRPS, lower is better). Evaluation uses fixed seeds and public data only.

| Method | Simulated users (539) | Creighton, real (251, cross-fitted) | Marquette, real (113) |
|---|---|---|---|
| aadya-m1* | 2.925 | 2.091 | 1.572 |
| Classical Bayes reference | 3.026 | 2.119 | 1.580 |
| LSTM (same training data) | 2.965 | 2.139 | 1.639 |
| Poisson-with-skips model | 3.109 | 2.491 | 1.969 |
| Rolling median (usual tracker method) | 3.702 | 2.536 | 1.950 |
| 28-day constant | 3.712 | 2.717 | 2.145 |
| Rank of aadya-m1 | 1 | 1 | 1 |

* On the real users the lead over the classical Bayes reference is small, about 1% of the error: clear on Creighton (251 users, cross-fitted) and on the pooled 364 users, within noise on Marquette alone (113 users). On the simulated users it is also clear. Every margin to the other methods is clear. Simulated users are a stand-in for real people.
Larger simulated board (5,400 users, same protocol): aadya-m1 2.889, LSTM 2.926, Bayes reference 2.988, rolling median 3.625. aadya-m1 is first, and the lead over the Bayes reference (0.099 lower) is concentrated in irregular cycles; on regular, drifting and follicular-plus-luteal users it is level with it or slightly behind.


With the optional ovulation-test input (41 participants, 110 cycles; mean error):
| Mean error | |
|---|---|
| aadya-m1 with ovulation-test input | 1.70 |
| aadya-m1 without it | 2.43 |
| Rolling median (usual tracker method) | 3.49 |

The numbers behind every chart are in benchmarks/scores.json, and example outputs are in samples/.
Evaluation data: simulated users from AadyaBench; Marquette FedCycleData; Creighton Model cycle lengths (Stanford and Najmabadi, Hive, CC BY-NC 4.0); the mcPHASES dataset (PhysioNet) for the ovulation-test study.
Example outputs


Intended use
- Apps and tools that show when a period is likely to start, with honest uncertainty
- Research and education on probabilistic forecasting of cycle timing
Out of scope: diagnosis, contraception or conception planning, "safe days", treatment decisions, and anything that sends a person's data to a server. This is not a medical device.
Limitations
- Results come from simulated users plus two public cohorts (364 real users in total). The lead over the classical reference on real data is small, about 1% of the error, and on an unseen simulated generator the model is slightly behind the reference and an LSTM (it stays clearly ahead of simple heuristics).
- Less accurate for very irregular cycles, postpartum or perimenopause phases, hormonal contraception, and very short histories. With no history at all it matches a classical population prior.
- Forgotten period logs hurt every method: for aadya-m1 the error rises from 3.0 to 9.5 when 20% of periods go unlogged. Apps should ask "did you forget to log a period?" when a cycle looks too long.
- The ovulation-test input has been tested on one small study only.
Inference
pip install -r requirements.txt # numpy, scipy, torch, safetensors, huggingface_hub
from huggingface_hub import snapshot_download
import sys
path = snapshot_download("manasdutta04/aadya-m1")
sys.path.insert(0, path)
from aadya_m1 import AadyaM1
model = AadyaM1.from_pretrained(path)
# Past cycle lengths in days (oldest first), days since the last period started
pmf = model.forecast([28, 30, 27, 29], days_since_last_period=0, age_group="25-34")
print(model.window(pmf)) # 80% window, for example (26, 33)
# Update daily as time passes without a period
pmf = model.forecast([28, 30, 27, 29], days_since_last_period=31, age_group="25-34")
# Optional, experimental: cycle day on which an ovulation test showed the surge had passed
pmf = model.forecast([24, 36, 29], days_since_last_period=17, ovulation_test_day=17)
# Learn from the real outcome, on this device only
model.observe_outcome(29)
pmf[k - 1] is the probability that the next cycle is k days long. Show the whole distribution or a window, never a single date presented as certain. Personalisation state (export_state) stays on the device; never upload it.
License
Apache-2.0. See LICENSE.
Contributing
Ideas, bug reports and forecasting methods are welcome at github.com/manasdutta04/aadya.
Citation
If you use aadya-m1, please cite the preprint (DOI 10.5281/zenodo.23236800):
@misc{dutta2026aadyam1,
title = {aadya-m1: An Open, On-Device Probabilistic Model for Calibrated Next-Period Forecasting},
author = {Dutta, Manas},
year = {2026},
doi = {10.5281/zenodo.23236800},
url = {https://doi.org/10.5281/zenodo.23236800},
note = {Preprint}
}