MYTHOSLIVE / models /moirai_model.py
3VVM's picture
Fix evaluation identities, model failures, causal execution and reproducibility
a9e6751 verified
Raw History Blame Contribute Delete
5.76 kB
import numpy as np
import pandas as pd
import threading
from models.base_model import BaseForecastModel, validate_history_and_horizon
from config import MOIRAI_CHECKPOINT, RANDOM_SEED
from features.feature_pipeline import validate_features
_RNG_LOCK = threading.Lock()
_MODULE_CACHE = {}
_MODULE_LOCK = threading.Lock()
def _step_from_history(history: pd.Series) -> pd.Timedelta:
if isinstance(history.index, pd.DatetimeIndex) and len(history.index) >= 2:
diffs = history.index.to_series().diff().dropna()
if not diffs.empty:
step = diffs.median()
if step > pd.Timedelta(0):
if not diffs.eq(step).all():
raise ValueError('Moirai needs a continuous history; irregular timestamps cannot be relabelled.')
return step
return pd.Timedelta(minutes=1)
class MoiraiModel(BaseForecastModel):
name = "Moirai"
def __init__(
self,
checkpoint: str = MOIRAI_CHECKPOINT,
context_length: int = 256,
num_samples: int = 20,
):
self.checkpoint = checkpoint
self.context_length = context_length
self.num_samples = num_samples
self._module = None
self.last_diagnostics = {}
def _get_module(self):
from uni2ts.model.moirai import MoiraiModule
if self._module is None:
with _MODULE_LOCK:
self._module = _MODULE_CACHE.get(self.checkpoint)
if self._module is None:
self._module = MoiraiModule.from_pretrained(self.checkpoint)
self._module.eval()
_MODULE_CACHE[self.checkpoint] = self._module
return self._module
def predict(
self, history: pd.Series, horizon: int = 1, features: pd.DataFrame = None
) -> list:
self.last_diagnostics = {}
validate_history_and_horizon(history, horizon, self.name)
try:
import torch
except ImportError as error:
raise ImportError('Moirai requires the optional foundation-model packages. Install requirements-foundation.txt.') from error
with _RNG_LOCK, torch.random.fork_rng(devices=[]), torch.inference_mode():
torch.manual_seed(RANDOM_SEED)
return self._predict(history, horizon, features)
def _predict(self, history, horizon, features):
values = np.asarray(history, dtype=np.float32)
if len(values) < 10:
raise ValueError("Moirai needs at least 10 candles of history.")
validate_history_and_horizon(values, horizon, self.name)
try:
from uni2ts.model.moirai import MoiraiForecast
from gluonts.dataset.pandas import PandasDataset
except ImportError as e:
raise ImportError(
f"Moirai needs the 'uni2ts' and 'gluonts' packages. Install with:\n pip install uni2ts torch\n(original error: {e})"
)
if features is not None:
validate_features(
features,
expected_len=len(values),
model_name="Moirai",
check_nan=False,
)
# Bound the context for the small checkpoint so a large dashboard
# window cannot cause an avoidable CPU-memory spike.
context_length = min(max(int(self.context_length), 32), 512)
context = values[-context_length:] if len(values) > context_length else values
step = _step_from_history(history)
end_ts = pd.Timestamp(history.index[-1]) if isinstance(history.index, pd.DatetimeIndex) else pd.Timestamp('2000-01-01')
if end_ts.tzinfo is not None:
end_ts = end_ts.tz_convert('UTC').tz_localize(None)
idx = pd.date_range(end=end_ts, periods=len(context), freq=step)
data = {"target": context}
feat_cols = []
if features is not None:
feat_context = features.iloc[-len(context) :].reset_index(drop=True)
validate_features(
feat_context, expected_len=len(context), model_name="Moirai"
)
for col in feat_context.columns:
data[col] = feat_context[col].to_numpy(dtype=np.float32)
feat_cols = list(feat_context.columns)
series_df = pd.DataFrame(data, index=idx)
dataset = PandasDataset(
series_df,
target="target",
past_feat_dynamic_real=feat_cols if feat_cols else None,
)
module = self._get_module()
forecaster = MoiraiForecast(
module=module,
prediction_length=horizon,
context_length=len(context),
patch_size="auto",
num_samples=self.num_samples,
target_dim=1,
feat_dynamic_real_dim=0,
past_feat_dynamic_real_dim=len(feat_cols),
)
predictor = forecaster.create_predictor(batch_size=1)
forecast = next(iter(predictor.predict(dataset)))
samples = np.asarray(forecast.samples, dtype=float)
if samples.ndim == 3 and samples.shape[-1] == 1:
samples = samples[..., 0]
if samples.ndim != 2 or samples.shape != (self.num_samples, horizon):
raise ValueError('Moirai returned an unexpected univariate sample/horizon shape.')
out = np.median(samples, axis=0).tolist()
if not all(np.isfinite(x) and x > 0 for x in out):
raise ValueError(
"Moirai produced a non-finite forecast (NaN/inf) for this window."
)
self.last_diagnostics = {'checkpoint': self.checkpoint, 'context_candles': len(context), 'point_statistic': 'sample median', 'seed': RANDOM_SEED, 'fallback': False, 'weights_loaded': True}
return out