A time series forecasting package for conflict prediction within the VIEWS (Violence and Impacts Early-Warning System) ecosystem. Built on Darts and PyTorch Lightning, it provides deep learning models, domain-specific loss functions, and reproducibility infrastructure tuned for zero-inflated, heavy-tailed conflict fatality data.
- 🚀 Production-Ready Integration: Seamlessly integrates with the VIEWS pipeline ecosystem via the Genomic Firewall and DNA manifest validation
- ⚡ Zero-Inflated Data Handling: Specialized loss functions (SpotlightLoss family) and scalers (AsinhTransform chains) purpose-built for conflict fatality distributions
- 🧠 10 Model Architectures: TFT, N-BEATS, N-HiTS, TiDE, TCN, BlockRNN, Transformer, NLinear, DLinear, TSMixer
- 📊 Static Covariate Fingerprints: Per-entity conflict statistics (µ, σ, max, trend, sparsity) — implemented in
transformers/static_covariates.pybut not currently wired into the dataset; seereports/technical_risk_register.mdC-36 - 🔗 Chained Scalers: Arrow-syntax pipelines (
AsinhTransform->MaxAbsScaler) for multi-stage feature normalization - 🛡️ Fortress Architecture: ADR-governed reproducibility, NaN detection, gradient health monitoring, and training stability callbacks throughout
- 🧮 GPU Acceleration: Optimized for single- and multi-GPU training via PyTorch Lightning
git clone https://github.com/views-platform/views-r2darts2.git
cd views-r2darts2
pip install -e .Requires darts==0.40.0 and pandas 1.x — the newest versions the VIEWS platform can host (its viewser / views-transformation-library dependencies pin pandas<2, views-datafactory caps xarray<2026 and zarr<3). views-pipeline-core>=3.0.0,<4.0.0 is an optional extra: pip install -e ".[manager]". It is imported lazily by DartsForecastingModelManager and by the four ViewsDataset persistence methods (save_predstore, save_appwrite, from_predstore_latest, from_appwrite_latest); everything else works without it. For GPU support, install the appropriate PyTorch version for your CUDA setup first. See the PyTorch installation guide.
| Model | Static Covariates | Description | Ideal For |
|---|---|---|---|
| TFT (Temporal Fusion Transformer) | ✅ VSN+GRN gating | Hybrid LSTM + multi-head attention. Variable Selection Networks gate static covariate influence per time step. | Interpretable multivariate forecasting; best when entity identity strongly conditions the forecast. |
| TSMixer | ✅ Concatenation | Alternating time-mixing and feature-mixing MLP blocks. Static covariates are concatenated at each block (no gating). | Large-scale multivariate; fast training. Requires AsinhTransform->MaxAbsScaler on static cov stats due to blunt concat. |
| TiDE | ✅ Concatenation | MLP encoder-decoder for long-horizon forecasting. Efficient and scalable. | Resource-constrained environments, long input horizons. |
| BlockRNN | ✅ Concatenation | Stacked RNN/LSTM/GRU with optional static covariate injection. | Sequential dependency modeling; autoregressive forecasting. |
| NLinear | ✅ Concatenation | Lightweight linear model with optional trend/seasonality decomposition. | Baseline modeling; rapid prototyping. |
| DLinear | ✅ Concatenation | Decomposition-Linear — separate linear layers for trend and seasonal components. | Trend/seasonality separation; fast inference. |
| N-HiTS | ❌ Not consumed | Hierarchical interpolation with per-stack pooling→FC→theta pipelines. Flag accepted but not passed to the Darts constructor — the entity-id static covariate is attached but ignored by the model. | Multi-scale temporal pattern extraction; long-term forecasting. |
| Transformer | ❌ Not consumed | Self-attention encoder with positional encoding. Flag accepted but silently ignored — no use_static_covariates is passed to the Darts constructor. |
Long-range temporal dependency modeling. |
| N-BEATS | ❌ Not supported | Fully-connected stacks with basis expansions for trend/seasonality. Static covariates are architecturally incompatible. Do not configure use_static_covariates=True. |
Interpretable decomposition; univariate/multivariate forecasting. |
| TCN (Temporal Convolutional Network) | ❌ Not consumed | Dilated causal convolutions with residual connections. | High-frequency series; long-range dependencies. |
Important: Models marked ❌ will silently drop static covariate fingerprints even if
use_static_covariates=Trueis set in the config. This is a Darts library constraint, not a bug in this package. On 0.2.x no fingerprint is computed for any model (see Static Covariate Fingerprints below); only the entity id is attached, and for N-HiTS and Transformer even that is never seen by the model weights.
Proper data scaling is critical for neural network training. For conflict data — zero-inflated, heavily right-skewed, spanning four orders of magnitude — the choice of transform directly determines whether models converge.
| Scaler | Formula | Best For | Notes |
|---|---|---|---|
| AsinhTransform | Zero-inflated counts, data with negatives | ⭐ Recommended for all fatality data. Handles zeros and extreme outliers. Near-linear below 1. | |
| MaxAbsScaler | Cross-entity normalization after element-wise transform | Maps to |
|
| StandardScaler | Roughly normal data | Zero-mean, unit-variance. Poor choice for zero-inflated distributions. | |
| MinMaxScaler | Bounded data (0–1, 0–100) | Best for V-Dem indices and WDI percentages. | |
| SqrtTransform | Moderate skew, count data | Gentler compression than asinh; undefined for negatives. | |
| LogTransform | Strictly positive skewed data | Undefined for negatives. Prefer AsinhTransform for conflict data. | |
| RobustScaler | Median + IQR | Outlier-heavy data | Resistant to extreme values. |
| QuantileNormal | Maps to |
Any distribution | Forces Gaussian marginal. |
| QuantileUniform | Maps to |
Any distribution | Forces uniform marginal. |
| YeoJohnsonTransform | Power transform | Mixed positive/negative | Makes data more Gaussian-like. |
Log(1+x) AsinhTransform
x = -50 ❌ undefined ✓ asinh(-50) = -4.61
x = 0 0.00 0.00
x = 1 0.69 0.88 (non-zero threshold in asinh space: 0.88 ≈ 1 death)
x = 100 4.62 5.30
x = 10000 9.21 9.90
The non_zero_threshold in SpotlightLoss is set to 0.88 because asinh(1) ≈ 0.88 — this exactly corresponds to the boundary of "at least 1 battle death" in raw space.
Use the -> operator to compose transforms sequentially. This is the production standard for all conflict and covariate features:
# Target: suppress zeros + bound output
"target_scaler": "AsinhTransform"
# Features: element-wise compression then cross-entity normalization
"feature_scaler_map": {
"AsinhTransform->MaxAbsScaler": [
"lr_splag_1_ged_sb", "lr_ged_ns", "lr_ged_os",
"lr_acled_sb", "lr_wdi_ny_gdp_mktp_kd",
# ... all conflict and macro features
],
}
# Static covariate statistics — INERT on 0.2.x: forwarded to a forecaster parameter its own
# docstring marks unused; nothing computes the fingerprint (register C-36)
"static_covariate_stats": {"transform": "AsinhTransform->MaxAbsScaler"}Why MaxAbsScaler after AsinhTransform for features?
AsinhTransform is element-wise: it compresses Syria's ged_sb ≈ 5000 and Chad's ged_sb ≈ 3 independently. After asinh, Syria is at ~8.5 and Chad at ~1.8 — a 4.7× gap persists. MaxAbsScaler maps the entire feature column to [-1, 1] across all 180+ countries, collapsing cross-entity scale while preserving ordinal rank. Without this, concatenation-based models (TSMixer, TiDE) inject raw magnitude bias at every block.
Forward / inverse chain direction:
Forward: X → Scaler₁.fit_transform(X) → Scaler₂.fit_transform(X') → X_scaled
Inverse: X_scaled → Scaler₂.inverse_transform → Scaler₁.inverse_transform → X_original
Designed, not wired (register C-36). The fingerprint below is implemented in
views_r2darts2/transformers/static_covariates.pyand fully tested, but nothing in the package calls it on 0.2.x —ViewsDataset.to_darts_timeseriesattaches only the entity id as a static covariate. The table describes what the module computes, not what models currently receive.
For models that consume static covariates (TFT, TSMixer, TiDE, BlockRNN, NLinear, DLinear), five per-entity statistics are computed from the training partition only (via stat_time_range) and would be injected as TimeSeries.static_covariates metadata:
| Statistic | Meaning | Transform Recommendation |
|---|---|---|
target_mu |
Mean conflict level | AsinhTransform->MaxAbsScaler |
target_sigma |
Volatility / spread | AsinhTransform->MaxAbsScaler |
target_max |
Peak value (spike extremity) | AsinhTransform->MaxAbsScaler |
target_trend |
OLS slope over training window | AsinhTransform->MaxAbsScaler |
target_sparsity |
Fraction of zero months | No transform (already in [0,1]) |
If the module is wired, stat_time_range must be the training window to prevent test-period leakage:
from views_r2darts2.transformers.static_covariates import compute_static_covariates, StaticCovariateConfig
stats = compute_static_covariates(
time=time_arr, entity=entity_arr, values=values_2d,
target_columns=["ged_sb"], column_order=column_names,
stat_time_range=(training_start_month_id, training_end_month_id),
config=StaticCovariateConfig(transform="AsinhTransform->MaxAbsScaler"),
)| Data Source | Feature Type | Recommended Chain | Rationale |
|---|---|---|---|
| UCDP / ACLED | Fatality counts | AsinhTransform->MaxAbsScaler |
Zero-inflated; cross-entity normalization required |
| WDI | GDP, population, aid flows | AsinhTransform->MaxAbsScaler |
Spans many orders of magnitude; can be negative (net migration) |
| WDI | Percentages (_zs suffix) |
AsinhTransform->MaxAbsScaler |
Handles near-zero values; consistent with other features |
| V-Dem | Democracy indices (0–1 bounded) | AsinhTransform->MaxAbsScaler or MinMaxScaler |
Already bounded; either works |
| Static cov stats | µ, σ, max, trend | AsinhTransform->MaxAbsScaler |
Required for concatenation-based models |
| Static cov stats | Sparsity | None (raw) | Already in [0,1] |
All loss functions target zero-inflated conflict data: ~90% zeros, ~10% events spanning four orders of magnitude. The loss function family has evolved substantially — the table below shows the current production-recommended functions and the full catalog.
| Loss Function | Status | Base Cell Loss | Key Mechanism | Use When |
|---|---|---|---|---|
| SpotlightLossLogcosh | ⭐ Production | log_cosh | Event-gated shape + event-only level + dead-cell anchor (three components) | Default for all models in production |
| SpotlightLoss | ⭐ Production | Barron(α=1.5) | Same three-component structure on a heavier-tailed base; single gene non_zero_threshold; spectral term hard-disabled (_STFT = False) |
When log_cosh gradient is too aggressive on large errors |
| PrismLoss | Research | MSE (= MSLE in log space) | KL-DRO + compound weights, no DC/AC decomp, no level anchor | MSLE-aligned optimization without RevIN |
| SpotlightFocalLoss | Research | log_cosh | Focal weighting by difficulty (1−exp(−|e|))^γ, no DRO |
Models without RevIN; exploration |
| SentinelLoss | Research | Generalised Charbonnier | Power-law magnitude weights + SiLU symmetry + temporal gradient | Alternative robust base when Barron α needs tuning |
| WeightedPenaltyHuberLoss | Legacy | Huber | FP/FN multiplicative penalties | Simple baselines; not recommended for production |
| WeightedHuberLoss | Legacy | Huber | Non-zero reweighting | Simple baselines |
| TimeAwareWeightedHuberLoss | Legacy | Huber | Temporal decay + event weights | Time-sensitive ablations |
| TweedieLoss | Legacy | Tweedie (p≈1.5) | Compound Poisson-Gamma | Count data without asinh transform |
| AsymmetricQuantileLoss | Legacy | Quantile | Asymmetric τ-penalty | When underestimation cost >> overestimation |
| ZeroInflatedLoss | Legacy | Huber (two-part) | Explicit binary + count split | Explicit zero-inflation modeling |
| SpikeFocalLoss | Legacy | MSE | Focal on absolute magnitude | Predates KL-DRO; superseded by Spotlight family |
| ShrinkageLoss | Legacy | Shrinkage | Suppresses easy samples via sigmoid gate | Exploratory; not validated for conflict |
The production loss for all current VIEWS models. Operates entirely in asinh space; the target scaler must be AsinhTransform.
Note (2026-09-10): both production classes —
views_r2darts2/math/spotlight_loss_logcosh.pyandviews_r2darts2/math/spotlight_loss.py— are three-component losses with a single gene,non_zero_threshold(LOSS_GENOMES): shape — base cell loss on demeaned per-cell errors, gated by an event mask, DRO-weighted; level — T-scaled loss on the event-only mean gap; anchor — loss on the dead-cell sum. Neither acceptsdelta;SpotlightLosscarries a spectral branch behind a hard-coded_STFT = False. A configureddeltais silently dropped by the genome filter. The five-component design (DC/AC decomposition, compound weights, KL-DRO, level anchor, spectral) described inviews_r2darts2/math/README.mdis the Gen-5 design lineage; that file's configuration block still advertisesdeltaand is stale (register C-15).
Configuration (SpotlightLossLogcosh):
"loss_function": "SpotlightLossLogcosh",
"non_zero_threshold": 0.88, # asinh(1) ≈ 0.88 (= 1 battle death in raw space) — the only geneThe loss function development followed a clear progression as each failure mode was identified and addressed:
| Generation | Loss | Problem It Solved | Limitation Discovered |
|---|---|---|---|
| Gen 1 | WeightedPenaltyHuberLoss |
Basic non-zero reweighting | Huber is symmetric; no handling of RevIN bias; manually tuned FP/FN parameters |
| Gen 2 | TweedieLoss, SpikeFocalLoss |
Compound Poisson structure; focal weighting | No DRO; focal exponent γ is a fragile hyperparameter |
| Gen 3 | PrismLoss |
KL-DRO replaces χ²-DRO; compound weighting is parameter-free | No DC/AC decomposition; RevIN bias accumulates; no level anchor |
| Gen 4 | SpotlightFocalLoss |
Focal mechanism adapted for regression, no class-specific logic | No DRO; still requires γ tuning |
| Gen 5 | SpotlightLossLogcosh |
DC/AC decomp + compound + KL-DRO + level anchor (+ spectral, since removed); parameter-free weighting | log_cosh gradient can clip large errors aggressively |
| Gen 5b | SpotlightLoss |
Barron(α=1.5) base cell loss — heavier tail than log_cosh | Same three-component structure as current Logcosh; spectral branch disabled |
N-HiTS and TSMixer are multi-stack architectures where residuals are passed between stacks. Layer width ordering is critical and non-obvious.
Both N-HiTS and TSMixer use a residual stacking pipeline:
Stack 0 (coarse) → absorbs easy patterns (trend, long cycles)
↓ residual
Stack 1 (mid) → absorbs medium-frequency patterns
↓ residual
Stack 2 (fine) → must absorb ALL remaining residuals, including spike patterns
The fine stack always has the hardest job. Under SpotlightLoss (DRO weighting), high-conflict country residuals (e.g., Sudan) dominate gradients. If the fine stack has minimal capacity, it cannot model these spikes, producing erratic theta coefficients that cause:
- Explosion on high-conflict countries (Sudan, Syria): fine stack theta blows up
- Flatline on peaceful countries: coarse stack weights drift toward dominant loss signal, collapsing peaceful predictions to near-zero
Correct configuration:
# WRONG — coarse gets most capacity, fine gets least
"layer_widths": [256, 128, 64] # ← Sudan explosion + flatline
# CORRECT — fine stack gets most capacity for residual absorption
"layer_widths": [64, 128, 256] # ← stable, fine stack can handle spikesN-HiTS pools the input sequence before each stack's FC block. The pooling_kernel_sizes and n_freq_downsample must be aligned:
pool_k = 4 → input compressed to ceil(36/4) = 9 time steps → FC input dim = 9
n_freq = 4 → theta output has 4 frequency coefficients, interpolated to 36 steps
If pool_k=4 but n_freq=3, there are 9 FC inputs but only 3 theta points — implicit upsampling by 9→36 via 3 basis functions creates a 3:1 gap. This forces the interpolation to guess intermediate values, introducing artificial smoothing that conflicts with spike reconstruction.
Correct alignment:
"pooling_kernel_sizes": [[4, 2, 1]], # coarse: 9 steps, mid: 18 steps, fine: 36 steps
"n_freq_downsample": [[4, 2, 1]], # 4 theta / 2 theta / 1 theta, interpolated to 36The fine stack (pool_k=1, n_freq=1) sees all 36 time steps and produces 36 theta coefficients — effectively identity interpolation. This is correct: the fine stack should not impose any temporal compression on spike signals.
Also use max_pool_1d=True in the coarse stack to preserve spike maxima during pooling (average pooling dilutes spike information that should be routed to the coarse trend stack, not the fine detail stack).
This repository adheres to the Fortress Architecture: strict engineering and mathematical standards designed to guarantee scientific integrity and reproducibility in conflict forecasting.
The repository is governed by:
- Architectural Decision Records (ADRs): Sequential, authoritative records of every major design choice.
- Class Intent Contracts (CICs): Explicit declarations of purpose and responsibility for every critical class.
- Reproducibility Manifest: The mandatory DNA genome that every experiment must declare before execution.
- Technical Risk Register: Every known, unfixed concern, tiered and triggered (ADR-014); and the ADR-vs-code disagreements D-01..D-07 (each filed as a GitHub issue, #39–#45; D-05 ruled and closed via #40).
bash docs/validate_docs.shchecks the documentation set against itself and against the code tree — everyviews_r2darts2/…pyortests/…pypath a live doc (this README included) names must exist. It is run by hand, not by CI (register C-55).
Every training run is monitored by mandatory Fortress callbacks configured in ModelCatalog (8 of the 14 attached are shown; the others are TrainingStepPatchCallback, ValMetricsCallback, InputBatchMonitorCallback, LossComponentCallback, RichLossDiagnosticsCallback, LossGradientDiagnosticsCallbackV2, plus Lightning's EarlyStopping, LearningRateMonitor and ModelCheckpoint):
| Callback | Purpose |
|---|---|
NaNDetectionCallback |
Halts training immediately on NaN in loss or weights |
GradientHealthCallback |
Monitors gradient norm; warns on explosion/vanishing |
WeightNormCallback |
Tracks parameter norm evolution across epochs |
LossStabilityCallback |
Detects loss spikes and plateau regimes |
RevINMonitorCallback |
Monitors RevIN affine parameters for drift |
PredictionSanityCallback |
Validates output shape and value range each epoch |
YHatBarCallback |
Tracks per-series mean predictions (ŷ bar) against targets |
EpochTimingCallback |
Logs wall-clock time per epoch for performance tracking |
Most core classes follow the 1-Class-1-File standard; three homogeneous families (dataset/converters.py, dataset/subclasses.py, transformers/static_covariates.py) do not — register D-04 / issue #44.
from views_r2darts2.transformers.scaler_selector import ScalerSelector
scaler = ScalerSelector.get_scaler("AsinhTransform")
pipeline = ScalerSelector.instantiate_darts_scaler("AsinhTransform->MaxAbsScaler") # Darts Pipeline; a single name returns a Darts Scalerfrom views_r2darts2.transformers.feature_scaler_manager import FeatureScalerManager
manager = FeatureScalerManager(
feature_scaler_map={"AsinhTransform->MaxAbsScaler": ["lr_ged_sb", "lr_ged_ns"]},
default_scaler=None,
)from views_r2darts2.catalogs.model_catalog import ModelCatalog
from views_r2darts2.catalogs.loss_catalog import LossCatalog
from views_r2darts2.catalogs.optimizer_catalog import OptimizerCatalog
from views_r2darts2.catalogs.scheduler_catalog import SchedulerCatalog
# Catalogs validate the DNA manifest on initialization
loss_fn = LossCatalog(config).get_loss()
model = ModelCatalog(config).get_model("NHiTSModel")from views_r2darts2 import ViewsDataset
# Zarr-backed, lazy; accepts parquet, DataFrame, FeatureFrame, PredictionFrame or Zarr
with ViewsDataset(source="cm_features.parquet", targets=["ged_sb"]) as ds:
# scalers are fitted on the training window and owned by the dataset
targets, past_cov = ds.fit_scalers(
target_scaler="AsinhTransform",
feature_scaler_map={"AsinhTransform->MaxAbsScaler": ["lr_ged_sb"]},
time_ids=range(training_start_id, training_end_id + 1), # fit on the training window only
return_series=True,
)
ts_list = ds.to_darts_timeseries()The dataset is the single source of truth for all data operations — see docs/CICs/views_dataset.md.
For level-specific validation use ViewsDataset.for_loa("cm", source=...).
from views_r2darts2.infrastructure.reproducibility_gate import ReproducibilityGate
ReproducibilityGate.Config.audit_manifest(config)
ReproducibilityGate.Data.lock_entropy(config["random_state"]) # seeds torch/numpy/random; not full GPU determinism (C-24)
ReproducibilityGate.Temporal.audit_continuity(partition)Minimal validated configuration for a new model:
def get_hp_config():
return {
# Forecast horizon
"steps": [*range(1, 37)],
"input_chunk_length": 36,
"output_chunk_length": 36,
"output_chunk_shift": 0,
# Scaling — production standard
"target_scaler": "AsinhTransform",
"feature_scaler": None,
"feature_scaler_map": {
"AsinhTransform": [
# All conflict counts, macro indicators, and lagged features
],
},
"static_covariate_stats": {"transform": "AsinhTransform->MaxAbsScaler"}, # inert on 0.2.x (C-36)
# Loss — production standard
"loss_function": "SpotlightLossLogcosh",
"non_zero_threshold": 0.88, # asinh(1): boundary of 1 battle death
# Optimizer
"optimizer_cls": "AdamW",
"lr": 0.0005,
"weight_decay": 0.0002,
"gradient_clip_val": 3,
"optimizer_kwargs": {
"betas": (0.9, 0.95), "lr": 0.0005, "weight_decay": 0.0002},
# Scheduler
"lr_scheduler_cls": "ReduceLROnPlateau",
"lr_scheduler_factor": 0.5,
"lr_scheduler_patience": 12,
"lr_scheduler_min_lr": 1e-6,
"lr_scheduler_kwargs": {
"mode": "min", "factor": 0.5, "patience": 12,
"min_lr": 1e-6, "cooldown": 3,
"threshold": 0.01, "threshold_mode": "rel",
},
# Training
"batch_size": 128,
"n_epochs": 300,
"early_stopping_patience": 35,
"early_stopping_min_delta": 0.001,
"force_reset": True,
# Normalization
"use_reversible_instance_norm": True,
"use_cyclic_encoders": False,
"use_static_covariates": True, # Set False for N-BEATS, N-HiTS, Transformer
# Reproducibility
"random_state": 67,
"time_steps": 36, # not read by views_r2darts2; consumed (if at all) by views-pipeline-core (C-57)
"rolling_origin_stride": 1, # not read by views_r2darts2 (C-57)
"prediction_format": "dataframe",
# Prediction
"likelihood": None,
"num_samples": 1,
"mc_dropout": False,
"n_jobs": -1, # not read by views_r2darts2 (C-57)
}- Darts: unit8co/darts — Time series forecasting library
- VIEWS: viewsforecasting.org — Violence Early-Warning System
