# How to Ensure Quantile Monotonicity with TimesFM: Non-Crossing Forecasts Explained

> Ensure quantile monotonicity with TimesFM forecasts by enabling fix_quantile_crossing=True. Learn how this post-processing feature prevents non-crossing forecasts for PyTorch and JAX.

- Repository: [Google Research/timesfm](https://github.com/google-research/timesfm)
- Tags: how-to-guide
- Published: 2026-04-02

---

**Enable `fix_quantile_crossing=True` in `ForecastConfig` to automatically enforce that lower quantile forecasts never exceed higher quantile forecasts, using built-in post-processing for both PyTorch and JAX backends.**

TimesFM (Time Series Foundation Model) from `google-research/timesfm` generates probabilistic forecasts through multiple quantiles, but raw model outputs may violate monotonicity constraints where lower percentiles exceed higher ones. This guide explains how to ensure quantile monotonicity with TimesFM using the library's built-in correction mechanisms that adjust crossing quantiles after decoding.

## Understanding the Quantile Crossing Problem

When TimesFM generates forecasts for percentiles (e.g., 10%, 20%, ..., 90%), statistical theory requires that lower quantiles never exceed higher quantiles. Specifically, the forecast for the 10th percentile must be less than or equal to the 50th percentile (median), which must be less than or equal to the 90th percentile.

However, the model's raw outputs can produce **crossing quantiles** where, for example, the 20th percentile prediction temporarily exceeds the median. This violates the non-decreasing property of quantile functions and produces invalid uncertainty bounds that break probabilistic interpretations.

## Enabling Monotonicity Correction in ForecastConfig

TimesFM provides a boolean configuration flag to automatically fix crossing quantiles. According to the source in [`src/timesfm/configs.py`](https://github.com/google-research/timesfm/blob/main/src/timesfm/configs.py) (lines 47-60), the `ForecastConfig` class exposes `fix_quantile_crossing`, which triggers a post-processing step after the forward pass.

To ensure quantile monotonicity, instantiate your configuration with the flag enabled:

```python
from timesfm import ForecastConfig

config = ForecastConfig(
    quantiles=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9],
    fix_quantile_crossing=True  # Enable monotonicity enforcement

)

```

When this flag is active, the model applies backend-specific correction logic immediately after generating raw forecasts, before returning results to the user.

## Backend-Specific Implementation Details

TimesFM implements quantile monotonicity enforcement differently depending on whether you use the PyTorch or Flax/JAX backend. Both approaches ensure that lower quantiles never exceed the median and upper quantiles never fall below it.

### PyTorch Implementation (`_compiled_decode`)

In [`src/timesfm/timesfm_2p5/timesfm_2p5_torch.py`](https://github.com/google-research/timesfm/blob/main/src/timesfm/timesfm_2p5/timesfm_2p5_torch.py) (lines 60-73), the PyTorch backend uses explicit loops with `torch.where` operations to clamp crossing values. The implementation processes the lower tail (indices 1-4, representing 10-40%) and upper tail (indices 6-9, representing 60-90%) separately:

```python
if fc.fix_quantile_crossing:
  # Lower quantiles (10–40 %) must be ≤ median (50 %).

  for i in [4, 3, 2, 1]:
    full_forecast[:, :, i] = torch.where(
        full_forecast[:, :, i] < full_forecast[:, :, i + 1],
        full_forecast[:, :, i],               # keep the lower value

        full_forecast[:, :, i + 1],           # otherwise raise it to the next higher quantile

    )
  # Upper quantiles (60–90 %) must be ≥ median.

  for i in [6, 7, 8, 9]:
    full_forecast[:, :, i] = torch.where(
        full_forecast[:, :, i] > full_forecast[:, :, i - 1],
        full_forecast[:, :, i],
        full_forecast[:, :, i - 1],
    )

```

The algorithm walks inward from the extremes toward the median index (5), ensuring the lower half forms a non-decreasing sequence and the upper half forms a non-increasing sequence relative to the median.

### Flax/JAX Implementation (`_fix_quantile_crossing_fn`)

The Flax backend in [`src/timesfm/timesfm_2p5/timesfm_2p5_flax.py`](https://github.com/google-research/timesfm/blob/main/src/timesfm/timesfm_2p5/timesfm_2p5_flax.py) (lines 329-353) uses JAX's scan operations for efficient vectorized processing. The `_fix_quantile_crossing_fn` function propagates constraints through the quantile dimension using functional programming patterns:

```python
def _fix_quantile_crossing_fn(full_forecast):
  # Propagate the minimum value from median outward for lower quantiles.

  lower_quantiles = _scan_along_axis(
      lambda carry, x: (w := jnp.minimum(carry, x), w),
      init=full_forecast[..., 5],
      xs=full_forecast[..., 1:5],
      axis=-1,
      reverse=True,
  )[1]

  # Propagate the maximum value from median outward for upper quantiles.

  upper_quantiles = _scan_along_axis(
      lambda carry, x: (w := jnp.maximum(carry, x), w),
      init=full_forecast[..., 5],
      xs=full_forecast[..., 6:10],
      axis=-1,
      reverse=False,
  )[1]

  # Re‑assemble the corrected forecast tensor.

  return jnp.concatenate(
      [
          full_forecast[..., :1],   # backcast placeholder

          lower_quantiles,
          full_forecast[..., 5:6],  # median

          upper_quantiles,
      ],
      axis=-1,
  )

```

This approach uses `_scan_along_axis` to sweep from the median outward, applying `jnp.minimum` to lower quantiles (moving backward through indices 4,3,2,1) and `jnp.maximum` to upper quantiles (moving forward through indices 6,7,8,9), ensuring monotonicity without explicit Python loops.

## Complete Usage Example

The [`timesfm-forecasting/scripts/forecast_csv.py`](https://github.com/google-research/timesfm/blob/main/timesfm-forecasting/scripts/forecast_csv.py) script demonstrates the practical workflow. After loading your TimesFM checkpoint, enable the correction before forecasting:

```python
from timesfm import TimesFm, ForecastConfig

# Initialize model (PyTorch or Flax)

model = TimesFm.load_from_checkpoint(checkpoint_path)

# Configure with quantile monotonicity enforcement

config = ForecastConfig(
    context_len=512,
    horizon_len=96,
    quantiles=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9],
    fix_quantile_crossing=True
)

# Generate forecasts - quantiles are now guaranteed non-crossing

forecasts = model.forecast(
    inputs=time_series_data,
    config=config
)

```

## Summary

- **Quantile crossing** occurs when TimesFM's raw predictions violate the requirement that lower percentiles must not exceed higher percentiles, producing invalid prediction intervals.
- **Enable `fix_quantile_crossing=True`** in `ForecastConfig` (defined in [`src/timesfm/configs.py`](https://github.com/google-research/timesfm/blob/main/src/timesfm/configs.py)) to activate automatic correction during the decoding phase.
- **PyTorch backend** applies iterative `torch.where` operations in [`src/timesfm/timesfm_2p5/timesfm_2p5_torch.py`](https://github.com/google-research/timesfm/blob/main/src/timesfm/timesfm_2p5/timesfm_2p5_torch.py) to clamp lower quantiles upward and upper quantiles downward toward the median.
- **Flax/JAX backend** uses functional scans in [`src/timesfm/timesfm_2p5/timesfm_2p5_flax.py`](https://github.com/google-research/timesfm/blob/main/src/timesfm/timesfm_2p5/timesfm_2p5_flax.py) to propagate minimum and maximum constraints from the median outward through the quantile dimension.
- The correction requires no manual intervention beyond the configuration flag and runs automatically as part of the `_compiled_decode` or `_fix_quantile_crossing_fn` execution.

## Frequently Asked Questions

### What is quantile crossing and why does it matter?

Quantile crossing occurs when a forecast for a lower percentile (e.g., 10%) exceeds a forecast for a higher percentile (e.g., 90%), violating the mathematical definition of quantiles. This produces logically inconsistent uncertainty bounds where prediction intervals have negative width, making the forecasts statistically invalid and unreliable for risk assessment or decision-making under uncertainty.

### Does enabling `fix_quantile_crossing` affect model calibration?

The monotonicity correction adjusts only the ordering of quantile forecasts to ensure $Q_{\tau_1} \leq Q_{\tau_2}$ for $\tau_1 < \tau_2$, without retraining model parameters or altering the central tendency. While this guarantees valid prediction intervals, it may slightly shift empirical coverage if raw outputs were severely crossed. However, crossed quantiles are inherently uncalibrated, so the correction improves interpretability and statistical validity.

### Which TimesFM backends support quantile monotonicity correction?

Both the **PyTorch** implementation ([`timesfm_2p5_torch.py`](https://github.com/google-research/timesfm/blob/main/timesfm_2p5_torch.py)) and the **Flax/JAX** implementation ([`timesfm_2p5_flax.py`](https://github.com/google-research/timesfm/blob/main/timesfm_2p5_flax.py)) in the `google-research/timesfm` repository support `fix_quantile_crossing`. The flag works identically in both frameworks, applying framework-specific post-processing automatically based on which backend your model instance uses.

### How can I verify that quantile monotonicity has been enforced?

After obtaining forecasts, verify monotonicity by checking that each quantile slice is less than or equal to the next along the quantile dimension. For PyTorch tensors, use `torch.all(forecasts[..., :-1] <= forecasts[..., 1:])`. For JAX arrays, use `jnp.all(jnp.diff(forecasts, axis=-1) >= 0)`. When `fix_quantile_crossing=True`, these assertions will return `True`, confirming the post-processing step successfully reordered any crossed quantiles.