Skip to content

Commit a5a7fc6

Browse files
elephaintmarcopeix
andauthored
[FEAT] Simulation paths (#1483)
Co-authored-by: marcopeix <marco@nixtla.io>
1 parent bcdf6cb commit a5a7fc6

6 files changed

Lines changed: 2840 additions & 8 deletions

File tree

docs/mintlify/docs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
"pages": [
5252
"docs/tutorials/uncertainty_quantification.html",
5353
"docs/tutorials/longhorizon_probabilistic.html",
54-
"docs/tutorials/conformal_prediction.html"
54+
"docs/tutorials/conformal_prediction.html",
55+
"docs/tutorials/simulation.html"
5556
]
5657
},
5758
{

nbs/docs/tutorials/simulation.ipynb

Lines changed: 1093 additions & 0 deletions
Large diffs are not rendered by default.

neuralforecast/common/_base_model.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2098,6 +2098,122 @@ def predict(
20982098

20992099
return fcsts
21002100

2101+
def simulate(
2102+
self,
2103+
dataset,
2104+
n_paths=500,
2105+
random_seed=None,
2106+
quantiles=None,
2107+
method="gaussian_copula",
2108+
**data_module_kwargs,
2109+
):
2110+
"""Simulate sample paths with temporal correlation.
2111+
2112+
Calls ``predict()`` to obtain quantile forecasts, then draws correlated
2113+
sample paths using the specified simulation method.
2114+
2115+
Works with any loss that supports quantile output (``DistributionLoss``,
2116+
``MQLoss``, etc.).
2117+
2118+
Args:
2119+
dataset (TimeSeriesDataset): NeuralForecast's ``TimeSeriesDataset``.
2120+
n_paths (int): Number of sample paths to generate.
2121+
random_seed (int, optional): Random seed for reproducibility.
2122+
quantiles (list of float, optional): Quantile grid for marginals.
2123+
Only used for ``DistributionLoss`` and mixture losses.
2124+
Defaults to ``[0.01, 0.02, ..., 0.99]``.
2125+
For ``MQLoss``/``HuberMQLoss``, the model's trained quantiles
2126+
are used automatically.
2127+
method (str): Simulation method. Default: ``"gaussian_copula"``.
2128+
**data_module_kwargs: Extra arguments for ``TimeSeriesDataModule``.
2129+
2130+
Returns:
2131+
samples (np.ndarray): Array of shape ``[n_series, n_paths, H]``.
2132+
"""
2133+
from neuralforecast.utils import (
2134+
DEFAULT_QUANTILE_GRID,
2135+
VALID_SIMULATION_METHODS,
2136+
)
2137+
2138+
if method not in VALID_SIMULATION_METHODS:
2139+
raise ValueError(
2140+
f"Unknown simulation method '{method}'. "
2141+
f"Valid methods: {sorted(VALID_SIMULATION_METHODS)}"
2142+
)
2143+
if quantiles is None:
2144+
quantiles = DEFAULT_QUANTILE_GRID
2145+
2146+
# Determine quantile grid based on loss type
2147+
if self.loss.is_distribution_output:
2148+
# DistributionLoss/mixture: can produce arbitrary quantiles
2149+
predict_quantiles = quantiles
2150+
quantile_positions = np.array(quantiles)
2151+
elif isinstance(self.loss, MULTIQUANTILE_LOSSES):
2152+
# MQLoss/HuberMQLoss: uses its trained quantiles
2153+
predict_quantiles = None # use model's built-in quantiles
2154+
quantile_positions = self.loss.quantiles.cpu().numpy()
2155+
elif isinstance(self.loss, (losses.IQLoss, losses.HuberIQLoss)):
2156+
# IQLoss: one forward pass per quantile, then take quantile over results
2157+
predict_quantiles = None # handled below
2158+
quantile_positions = np.array(quantiles)
2159+
else:
2160+
raise ValueError(
2161+
f"Simulation requires a loss with quantile output "
2162+
f"(DistributionLoss, MQLoss, IQLoss, etc.). "
2163+
f"Model uses {type(self.loss).__name__}."
2164+
)
2165+
2166+
# Get quantile forecasts via existing predict infrastructure
2167+
if isinstance(self.loss, (losses.IQLoss, losses.HuberIQLoss)):
2168+
# IQLoss: multiple forward passes, one per quantile in a grid
2169+
iq_grid = [0.01, 0.05, 0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9, 0.95, 0.99]
2170+
iq_fcsts = []
2171+
for q in iq_grid:
2172+
f = self.predict(
2173+
dataset=dataset,
2174+
random_seed=random_seed,
2175+
quantiles=[q],
2176+
**data_module_kwargs,
2177+
)
2178+
iq_fcsts.append(f)
2179+
# Each f is (n_series * H, 1); stack along last axis
2180+
iq_all = np.concatenate(iq_fcsts, axis=-1) # (n_series * H, len(iq_grid))
2181+
# Interpolate to the requested quantile positions
2182+
fcsts = np.quantile(iq_all, quantiles, axis=-1).T # (n_series * H, n_quantiles)
2183+
n_quantiles = len(quantile_positions)
2184+
quantile_fcsts = fcsts[:, :n_quantiles]
2185+
else:
2186+
fcsts = self.predict(
2187+
dataset=dataset,
2188+
random_seed=random_seed,
2189+
quantiles=predict_quantiles,
2190+
**data_module_kwargs,
2191+
)
2192+
# fcsts: flattened array, shape (n_series * H, n_outputs)
2193+
n_quantiles = len(quantile_positions)
2194+
if self.loss.is_distribution_output:
2195+
# col 0 = mean, cols 1..Q = quantiles
2196+
quantile_fcsts = fcsts[:, 1 : 1 + n_quantiles]
2197+
else:
2198+
# MQLoss: all columns are quantiles
2199+
quantile_fcsts = fcsts[:, :n_quantiles]
2200+
2201+
h = self.horizon_backup
2202+
n_series = quantile_fcsts.shape[0] // h
2203+
quantile_values = quantile_fcsts.reshape(n_series, h, n_quantiles)
2204+
2205+
# Generate sample paths using shared helper
2206+
from neuralforecast.utils import sample_from_quantiles
2207+
2208+
return sample_from_quantiles(
2209+
quantile_positions=quantile_positions,
2210+
quantile_values=quantile_values,
2211+
dataset=dataset,
2212+
n_paths=n_paths,
2213+
seed=random_seed,
2214+
method=method,
2215+
) # (n_series, n_paths, H)
2216+
21012217
def decompose(
21022218
self,
21032219
dataset,

0 commit comments

Comments
 (0)