Requirements: Python 3.10+, uv, Git, a Rust toolchain, and cargo-about.
git clone https://github.com/Nixtla/synforecast.git
cd synforecast
cargo install cargo-about --version 0.9.1 --locked --features cli
uv sync --all-groups
uv run pre-commit install # optional: run repository checks on every commituv sync builds and installs the Rust extension. After changing Rust code,
rebuild it in the development environment with:
uv run maturin develop --releaseVerify the setup:
uv run pytest tests/ --no-cov -m "not stats"All generators inherit from BaseGenerator (synforecast/base.py), which provides the common parameters (min_length, max_length, freq, engine, alias, seed, start_datetime, column names), timestamp generation, pattern injection, and the generate() method. A generator only declares its parameters as Pydantic fields and implements generate_single_series().
Create synforecast/generators/my_generator.py:
"""White noise time series generator."""
import numpy as np
from pydantic import Field
from synforecast.base import BaseGenerator
class WhiteNoiseGenerator(BaseGenerator):
"""Generate white noise time series.
Args:
mean (float): Mean of the noise (default: 0.0).
std (float): Standard deviation of the noise (default: 1.0).
"""
mean: float = Field(default=0.0, description="Mean of the noise")
std: float = Field(default=1.0, gt=0, description="Standard deviation")
def generate_single_series(self, length: int) -> np.ndarray:
return self.rng.normal(self.mean, self.std, length)Conventions:
- Constructors are keyword-only:
WhiteNoiseGenerator(min_length=100, max_length=150, freq="D", std=2.0). Do not define__init__; declare parameters as PydanticFieldattributes with validation constraints (gt,ge, ...) orfield_validators. freqis a pandas offset alias ('D','h','5min','MS', ...) or an integer; the base class validates it.- Always draw randomness from
self.rng, never from the global NumPy state, so output stays seed-deterministic. - Use
self._sample_innovations(size, scale)for noise terms so the generator honorsinnovation_distribution.
Add the class to synforecast/generators/__init__.py (import plus __all__).
Create tests/test_my_generator.py. Use the shared helpers in tests/helpers.py and the engine fixture (from tests/conftest.py), which parameterizes tests over the pandas and polars engines. Mark statistical property tests with @pytest.mark.stats:
import pytest
from synforecast.generators import WhiteNoiseGenerator
from tests.helpers import assert_long_format, assert_mean, assert_std
def test_long_format(engine: str) -> None:
gen = WhiteNoiseGenerator(
min_length=50, max_length=100, freq="D", seed=42, engine=engine
)
df = gen.generate(n_series=3)
assert_long_format(df, n_series=3, min_length=50, max_length=100)
@pytest.mark.stats
def test_moments() -> None:
gen = WhiteNoiseGenerator(
min_length=5000, max_length=5000, freq="D", mean=5.0, std=2.0, seed=42
)
values = gen.generate(n_series=1)["y"].to_numpy()
assert_mean(values, expected=5.0, std=2.0)
assert_std(values, expected=2.0)Statistical assertions in tests/helpers.py (assert_mean, assert_std, assert_acf, assert_distribution) use z-bounds derived from the estimator's sampling error; seed every generator so they are deterministic.
For the MAR and augmentation kernels, tests/README.md maps algorithms to their independent correctness checks and reproduction commands.
Also cover parameter validation (invalid inputs raise), seed determinism, and integer freq where relevant.
Create nbs/docs/generators/<category>/my_generator.ipynb: a focused,
executable guide that instantiates the generator with keyword arguments,
explains its behavior, and visualizes representative output. Run
uv sync --group docs --group integration-docs, then run
uv run --no-sync make execute_docs to refresh committed notebook outputs.
# Quick run of one file (skip coverage)
uv run pytest tests/test_my_generator.py --no-cov
# Skip the slower statistical property tests
uv run pytest tests/ --no-cov -m "not stats"
# Full suite with coverage
uv run pytest tests/ --cov=synforecast --cov-report=term-missingRuff handles linting and formatting (88-character lines). Pre-commit runs both Ruff hooks and regenerates the bundled Rust license notices when Rust dependency or license configuration files change. If the generated notice changes, stage it and run pre-commit again. Static typing is being tightened incrementally; mypy is not yet a release gate for the whole package.
uv run pre-commit run --all-files
cargo fmt --manifest-path rust/Cargo.toml --check
cargo clippy --manifest-path rust/Cargo.toml --all-targets --all-features -- -D warnings- Create a feature branch.
- Ensure tests pass and pre-commit is clean.
- Update GENERATORS.md and the generator count in README.md when adding a generator, and add an entry to CHANGELOG.md.
- Open a pull request with a clear description and a reference to any related issue.
Report bugs and request features via GitHub Issues. Planned work is listed in ROADMAP.md; update it when an item ships or when a pull request changes what is planned.
Publishing a GitHub Release triggers
.github/workflows/python-publish.yml.
The tag must exactly match the package version, such as v0.1.0 for
version = "0.1.0". The workflow verifies that the tagged commit is on the
protected main branch and that its CI, lint, and documentation checks
succeeded. It then builds and smoke-tests CPython 3.10–3.14 wheels for Linux
x86-64 and ARM64, Windows x86-64, and macOS Intel and Apple Silicon, plus the
source distribution. Successful artifacts are published automatically to PyPI
through Trusted Publishing. Publishing the release also triggers the production
documentation deployment.
To make a release:
- Create a release-preparation branch from the latest
main. Update the version inpyproject.toml,rust/Cargo.toml, andCITATION.cff, rename the## Unreleasedheading inCHANGELOG.mdto## X.Y.Z - YYYY-MM-DD, and refreshuv.lockandrust/Cargo.lock(see below).tests/test_release_metadata.pyfails if any of these versions disagree. - Open a pull request titled
Prepare vX.Y.Z releaseand merge it after its required checks pass. - On GitHub, choose Releases → Draft a new release. Create the matching
vX.Y.Ztag targetingmain, generate or edit the release notes, and publish the release.
After updating the version fields, refresh only the synforecast entry in
each lockfile:
uv lock
cargo update -p synforecast --manifest-path rust/Cargo.tomlThen check that the lockfiles match the manifests:
uv lock --check
cargo metadata --locked --manifest-path rust/Cargo.toml > /dev/nullThe Rust license notices list the crate's own version, so regenerate them too
(otherwise the rust-licenses pre-commit hook fails in lint):
make rust_licensesThe diff should touch only pyproject.toml, rust/Cargo.toml,
CITATION.cff, CHANGELOG.md, uv.lock, rust/Cargo.lock, and
synforecast/THIRD_PARTY_RUST.md, with one version line changed in each
lockfile and in the license notices.