Documentation Guide#

OpenSTEF welcomes improvements to documentation! Whether you’re fixing typos, clarifying explanations, or adding tutorials, your contributions help make forecasting more accessible to everyone.

Getting started#

Documentation structure#

OpenSTEF documentation is organized following the Diátaxis framework:

  • Tutorials (examples/tutorials/): Learning-oriented guides for beginners

  • How-to guides (user_guide/): Problem-oriented practical guides

  • Reference (api/): Information-oriented technical reference

  • Explanation (user_guide/intro/): Understanding-oriented background material

The documentation lives in several places:

docs/source/          # Main documentation source
├── api/              # API reference (auto-generated)
├── user_guide/       # User guides and tutorials
├── project/          # Project information
├── contribute/       # Contributing guides
└── examples/         # Example gallery

examples/             # Example scripts and tutorials
├── examples/         # Gallery examples
└── tutorials/        # Tutorials

packages/*/src/       # Inline code documentation (docstrings)

Building the documentation#

To build the documentation locally:

# Build the documentation
poe docs

# Build and serve with live reload (recommended for editing)
poe docs --serve

# Clean previous builds
poe docs-clean

The built documentation will be available at docs/build/html/index.html.

Note

Building documentation requires additional dependencies that are included in the development environment. Make sure you’ve run uv sync --group dev first.

How the documentation pipeline works#

A poe docs invocation chains several stages. Knowing where each one lives helps when something goes wrong or when you want to extend the build.

Tutorial and benchmark sync#

The examples/tutorials/ and examples/benchmarks/ directories are the canonical home for runnable notebooks. Before Sphinx starts, docs.sync_sources copies them into docs/source/tutorials/ and docs/source/benchmarks/ so Sphinx can pick them up. The script also embeds a few tutorials into the user-guide nav (for example, examples/tutorials/feature_engineering.py is also synced to docs/source/user_guide/guides/feature_engineering_tutorial.py). The sync runs automatically during every build via conf.py’s setup() hook.

Notebook rendering with myst-nb#

Tutorial .py files are paired with .ipynb companions via jupytext. During the build, myst-nb executes the notebooks and embeds the outputs into the rendered pages. Execution results are cached under docs/build/.jupyter_cache, so unchanged notebooks are not re-run on subsequent builds. Benchmarks under benchmarks/*/* are explicitly excluded from execution (they are too expensive to run on every build) and appear with the outputs that were saved when the notebook was last run by hand.

Autosummary stub caching#

API reference pages are generated from package source code by sphinx.ext.autosummary. Running it across hundreds of modules is slow, so the custom docs.autosummary_cache extension tracks .py source mtimes against a stamp file (docs/build/.autosummary_stamp) and skips stub generation when nothing has changed. Delete the stamp file or any packages/*/src source file to force a refresh.

Concept and guide figures#

Static figures used by concept and guide pages live in docs/source/_figures/. These are stand-alone scripts (generate_concept_figures.py, generate_guide_figures.py) that train real models on the Liander dataset and write SVG/GIF outputs into docs/source/images/. They are not run by the normal docs build; the _figures/ directory is excluded via exclude_patterns in conf.py. Regenerate by hand when you change the figures:

uv run python docs/source/_figures/generate_concept_figures.py
uv run python docs/source/_figures/generate_guide_figures.py

Each generated SVG/GIF needs a sibling .license sidecar for REUSE compliance; copy from an existing file when adding a new figure.

CI deployment#

The Deploy Documentation workflow in .github/workflows/docs.yaml builds the docs on every push to main and publishes them to the root of the gh-pages branch. CI uses the same poe docs invocation as local development, so a build that is clean locally should also be clean in CI.

Writing docstrings#

OpenSTEF uses Google-style docstrings for all code documentation. This style is clear, readable, and well-supported by Sphinx.

Basic docstring structure#

def forecast_energy(data: pd.DataFrame, horizon: int = 24) -> pd.DataFrame:
    """Generate energy forecasts for the specified horizon.

    This function creates forecasts using the configured model and feature
    engineering pipeline. It handles missing data and provides uncertainty
    estimates for each prediction.

    Args:
        data: Historical energy consumption data with datetime index.
            Must include columns: ['load', 'temperature', 'wind_speed'].
        horizon: Number of hours to forecast ahead. Must be positive.

    Returns:
        DataFrame with forecasted values and uncertainty bounds:
            - 'forecast': Point predictions
            - 'forecast_lower': Lower confidence bound (5th percentile)
            - 'forecast_upper': Upper confidence bound (95th percentile)

    Raises:
        ValueError: If data is empty or missing required columns.
        TypeError: If horizon is not a positive integer.

    Example:
        Basic usage with sample data:

        >>> import pandas as pd
        >>> data = pd.DataFrame({
        ...     'load': [100, 120, 110],
        ...     'temperature': [20, 22, 21],
        ...     'wind_speed': [5, 7, 6]
        ... }, index=pd.date_range('2025-01-01', periods=3, freq='h'))
        >>> forecast = forecast_energy(data, horizon=6)
        >>> forecast.shape
        (6, 3)

    Note:
        The model automatically handles daylight saving time transitions and
        public holidays for improved accuracy.

    See Also:
        evaluate_forecast: Evaluate forecast accuracy against ground truth.
        prepare_features: Prepare input data for forecasting.
    """

Docstring sections#

Use these sections in your docstrings (order matters):

  1. Summary line: One-line description of what the function does

  2. Extended description: More detailed explanation (optional)

  3. Args: Function parameters and their types/descriptions

  4. Returns: Description of return value(s)

  5. Raises: Exceptions that may be raised

  6. Example: Code examples showing how to use the function

  7. Note: Additional important information

  8. Invariants: Contract guarantees and requirements (for classes and interfaces)

  9. See Also: References to related functions/classes

Type hints and docstrings#

Always use type hints in function signatures. The docstring should complement, not repeat, the type information:

# Good: Type hints in signature, description in docstring
def train_model(data: TimeSeriesDataset, config: ModelConfig) -> ForecastModel:
    """Train a forecasting model on the provided dataset.

    Args:
        data: Training dataset with features and targets.
        config: Model configuration including hyperparameters.

    Returns:
        Trained model ready for forecasting.
    """

# Avoid: Repeating type information in docstring
def train_model(data: TimeSeriesDataset, config: ModelConfig) -> ForecastModel:
    """Train a forecasting model on the provided dataset.

    Args:
        data (TimeSeriesDataset): Training dataset with features and targets.
        config (ModelConfig): Model configuration including hyperparameters.

    Returns:
        ForecastModel: Trained model ready for forecasting.
    """

Invariants section#

For classes and interfaces, use an Invariants section to document the contract guarantees and requirements that implementers and users must follow:

class ForecastModel(ABC):
    """Base class for all forecasting models.

    Provides a standardized interface for training and prediction across
    different forecasting algorithms and approaches.

    Invariants:
        - fit() must be called before predict() for stateful models
        - predict() should handle all horizons specified in configuration
        - Output format must be consistent with ForecastDataset structure
        - Model state must remain unchanged during prediction calls

    Example:
        Basic model implementation:

        >>> class SimpleModel(ForecastModel):
        ...     def fit(self, data):
        ...         self._fitted = True
        ...     def predict(self, data):
        ...         return generate_forecasts(data)
    """

The Invariants section should document:

  • Pre-conditions: What must be true before calling methods

  • Post-conditions: What the implementation guarantees after execution

  • State requirements: How object state should be managed

  • Interface contracts: Consistent behavior expectations across implementations

This helps both implementers understand what they must provide and users understand what they can rely on.

Note

The Invariants section is an OpenSTEF-specific extension to Google-style docstrings. Use it for classes and interfaces where contract guarantees are important for correct implementation and usage.

Examples in docstrings#

Include practical examples in your docstrings using the Example section. These examples are automatically tested with poe doctests.

Writing good examples#

def calculate_mae(y_true: np.ndarray, y_pred: np.ndarray) -> float:
    """Calculate Mean Absolute Error between predictions and ground truth.

    Example:
        Basic usage:

        >>> import numpy as np
        >>> y_true = np.array([1, 2, 3, 4, 5])
        >>> y_pred = np.array([1.1, 2.2, 2.9, 3.8, 5.2])
        >>> mae = calculate_mae(y_true, y_pred)
        >>> round(mae, 2)
        0.16

        With perfect predictions:

        >>> perfect_pred = np.array([1, 2, 3, 4, 5])
        >>> calculate_mae(y_true, perfect_pred)
        0.0
    """

Example guidelines#

  • Keep examples simple but realistic

  • Use ``>>>`` prompts for interactive examples

  • Show expected output when it’s not obvious

  • Test edge cases (empty inputs, perfect predictions, etc.)

  • Use ``round()`` for floating-point outputs to avoid precision issues

  • Import required modules within the example if needed

Writing narrative documentation#

For user guides, tutorials, and explanatory content, use reStructuredText (.rst) files.

reStructuredText basics#

Section headers
===============

Subsection headers
------------------

*Italic text* and **bold text**

``Code snippets`` and :func:`function references`

.. code-block:: python

    # Code blocks with syntax highlighting
    import openstef
    model = openstef.create_model()

.. note::

    Informational notes for readers.

.. warning::

    Important warnings about potential issues.

Cross-references#

Link to other parts of the documentation:

# Link to other documents
See the :doc:`user_guide/installation` guide.

# Link to specific functions/classes
Use :func:`openstef.models.forecast` for predictions.

# Link to sections within documents
Refer to :ref:`development_setup` for setup instructions.

Contributing to examples and tutorials#

Examples and tutorials are crucial for user onboarding. When adding new examples:

  1. Choose the right location:

    • examples/examples/ - Short, focused examples

    • examples/tutorials/ - Multi-step tutorials

  2. Follow naming conventions:

    • Use descriptive filenames: basic_forecasting.py, advanced_transforms.py

    • Start with a docstring explaining the example’s purpose

  3. Structure your example:

    """
    Basic Energy Forecasting
    ========================
    
    This example demonstrates how to create simple energy forecasts using OpenSTEF.
    We'll load sample data, train a model, and generate predictions.
    """
    
    import pandas as pd
    import openstef
    
    # Load sample data
    data = openstef.load_sample_data()
    
    # ... rest of example
    
  4. Include explanations: Use comments and markdown cells to explain each step

  5. Test your examples: Run poe doctests to ensure all examples work

Documentation style guide#

Writing style#

  • Be clear and concise - Avoid jargon, explain technical terms

  • Use active voice - “The model predicts” rather than “Predictions are made”

  • Write for your audience - Tutorials for beginners, reference for experts

  • Include context - Explain why something is useful, not just how to do it

Code style in documentation#

  • Use realistic examples - Avoid foo, bar; use domain-relevant names

  • Show complete examples - Include imports and setup code

  • Highlight important parts - Use comments to draw attention to key concepts

  • Test all code - Ensure examples actually work

All code examples in documentation should follow our Style Guide.

Building and testing documentation#

Before submitting documentation changes:

# Check that documentation builds without errors
poe docs

# Test all code examples in docstrings
poe doctests

# Run full quality checks (includes documentation)
poe all --check

Common issues#

  • Import errors: Make sure all imports in examples are available

  • Outdated examples: Keep examples current with API changes

  • Broken links: Verify that all cross-references work

  • Missing docstrings: All public functions need documentation

Getting help#

If you need assistance:

  • 💬 Slack: Join the LF Energy Slack workspace (#openstef channel)

  • 🐛 Issues: Check GitHub Issues or create a new one

  • 📧 Email: Contact us at openstef@lfenergy.org

  • 🤝 Community meetings: Join our four-weekly co-coding sessions

For more information, see our Support page.

Working with tutorial notebooks#

Tutorials live in examples/tutorials/ as paired Jupytext files: a .py (percent format) source of truth and a .ipynb companion kept in sync.

Key rules#

  • Edit the .py file, not the .ipynb; the script is the single source of truth.

  • Never commit notebook outputs. The .ipynb on main must be output-free.

  • Notebooks are rendered into the docs via myst-nb with cached execution (nb_execution_mode = "cache").

Workflow#

# After editing a .py tutorial:
poe notebooks          # Sync .py → .ipynb

# Before committing:
poe notebooks-clear    # Strip any outputs from .ipynb
poe notebooks-check    # Verify sync + no outputs (runs in CI)

Creating a new tutorial#

# Create the .py file in percent format, then pair it:
jupytext --set-formats "ipynb,py:percent" examples/tutorials/my_tutorial.py

# Add a toctree entry in docs/source/examples.rst
# Optionally tag the first SPDX cell with "remove-cell" (see existing tutorials)

Additional documentation resources#

If you need help with documentation specifically: