Metadata-Version: 2.4
Name: views_r2darts2
Version: 0.2.2
Summary: Neural network based forecasting models for the VIEWS platform
Author: Dylan Pinheiro
Author-email: dylpin@prio.org
Requires-Python: >=3.11,<3.15
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Provides-Extra: manager
Requires-Dist: darts (==0.46.1)
Requires-Dist: dask (>=2026.7.1)
Requires-Dist: pyarrow (>=14.0.0)
Requires-Dist: scikit-learn (>=1.6.0,<1.10.0)
Requires-Dist: views-frames (>=0.1.0)
Requires-Dist: views-pipeline-core (>=3.0.0,<4.0.0) ; extra == "manager"
Requires-Dist: wandb (>=0.28.2)
Requires-Dist: xarray (>=2026.7.0)
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://github.com/user-attachments/assets/4cd8129b-9ad6-4fa3-a4ca-8288b0ab610f" alt="r2darts2 Banner" width="85%">
</p>
<p align="center">
  <img src="https://img.shields.io/badge/python-3.11%2B-blue.svg" alt="Python Version" />
  &nbsp;&nbsp;
  <img src="https://img.shields.io/badge/darts-0.46.1-green.svg" alt="Darts Version" />
  &nbsp;&nbsp;
  <img src="https://img.shields.io/badge/pytorch-2.x-orange.svg" alt="PyTorch" />
  &nbsp;&nbsp;
  <img src="https://img.shields.io/badge/license-MIT-lightgrey.svg" alt="License" />
</p>

<p align="center">
  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.
</p>

---

## Key Features

- 🚀 **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.py` but not currently wired into the dataset; see `reports/technical_risk_register.md` C-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

---

## 📦 Installation

```bash
git clone https://github.com/views-platform/views-r2darts2.git
cd views-r2darts2
pip install -e .
```

Requires `darts==0.46.1`. `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](https://pytorch.org/get-started/locally/).

---

## 🧠 Supported Models

| 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=True` is 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.

---

## 📊 Scalers

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.

### Available Scalers

| Scaler | Formula | Best For | Notes |
|--------|---------|----------|-------|
| **AsinhTransform** | $y = \text{asinh}(x)$ | Zero-inflated counts, data with negatives | ⭐ **Recommended for all fatality data**. Handles zeros and extreme outliers. Near-linear below 1. |
| **MaxAbsScaler** | $y = x / \max(\|x\|)$ | Cross-entity normalization after element-wise transform | Maps to $[-1,1]$. Preserves ordinal rank. Required after AsinhTransform for static covariate injection in concatenation-based models. |
| **StandardScaler** | $y = (x - \mu)/\sigma$ | Roughly normal data | Zero-mean, unit-variance. Poor choice for zero-inflated distributions. |
| **MinMaxScaler** | $y = (x - x_{\min})/(x_{\max} - x_{\min})$ | Bounded data (0–1, 0–100) | Best for V-Dem indices and WDI percentages. |
| **SqrtTransform** | $y = \sqrt{x}$ | Moderate skew, count data | Gentler compression than asinh; undefined for negatives. |
| **LogTransform** | $y = \log(1 + x)$ | 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 $\mathcal{N}(0,1)$ | Any distribution | Forces Gaussian marginal. |
| **QuantileUniform** | Maps to $U(0,1)$ | Any distribution | Forces uniform marginal. |
| **YeoJohnsonTransform** | Power transform | Mixed positive/negative | Makes data more Gaussian-like. |

### AsinhTransform vs LogTransform

```
            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.

### 🔗 Chained Scalers

Use the `->` operator to compose transforms sequentially. This is the **production standard** for all conflict and covariate features:

```python
# 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
```

### Static Covariate Fingerprints

> **Designed, not wired (register C-36).** The fingerprint below is implemented in `views_r2darts2/transformers/static_covariates.py` and fully tested, but nothing in the package calls it on 0.2.x — `ViewsDataset.to_darts_timeseries` attaches 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:
```python
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"),
)
```

### Recommended Scaler by Data Source

| 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] |

---

## ⚡ Loss Functions

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 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 |

### SpotlightLossLogcosh — Architecture Deep Dive

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.py` and `views_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 accepts `delta`; `SpotlightLoss` carries a spectral branch behind a hard-coded `_STFT = False`. A configured `delta` is silently dropped by the genome filter. The five-component design (DC/AC decomposition, compound weights, KL-DRO, level anchor, spectral) described in `views_r2darts2/math/README.md` is the Gen-5 *design lineage*; that file's configuration block still advertises `delta` and is stale (register C-15).

**Configuration (SpotlightLossLogcosh):**
```python
"loss_function": "SpotlightLossLogcosh",
"non_zero_threshold": 0.88,  # asinh(1) ≈ 0.88 (= 1 battle death in raw space) — the only gene
```

### Loss Function Evolution

The 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 |

---

## 🏗️ Multi-Stack Model Configuration

N-HiTS and TSMixer are multi-stack architectures where residuals are passed between stacks. **Layer width ordering is critical and non-obvious.**

### The Layer Widths Trap

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:**
```python
# 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 spikes
```

### N-HiTS Pooling / Frequency Alignment

N-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:**
```python
"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 36
```

The 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).

---

## 🛡️ Fortress Architecture & Governance

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)](docs/ADRs/README.md)**: Sequential, authoritative records of every major design choice.
- **[Class Intent Contracts (CICs)](docs/CICs/README.md)**: Explicit declarations of purpose and responsibility for every critical class.
- **[Reproducibility Manifest](docs/standards/REPRODUCIBILITY_MANIFEST.md)**: The mandatory DNA genome that every experiment must declare before execution.
- **[Technical Risk Register](reports/technical_risk_register.md)**: Every known, unfixed concern, tiered and triggered (ADR-014); and the open ADR-vs-code disagreements D-01..D-07 (each also filed as a GitHub issue, #39–#45).
- `bash docs/validate_docs.sh` checks the documentation set against itself *and* against the code tree — every `views_r2darts2/…py` or `tests/…py` path a live doc (this README included) names must exist. It is run by hand, not by CI (register C-55).

### Training Stability Callbacks

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 |

---

## 🔧 API Reference

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.

### ScalerSelector
```python
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 Scaler
```

### FeatureScalerManager
```python
from views_r2darts2.transformers.feature_scaler_manager import FeatureScalerManager

manager = FeatureScalerManager(
    feature_scaler_map={"AsinhTransform->MaxAbsScaler": ["lr_ged_sb", "lr_ged_ns"]},
    default_scaler=None,
)
```

### Catalogs (Genomic Firewall)
```python
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")
```

### ViewsDataset
```python
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=...)`.

### ReproducibilityGate
```python
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)
```

---

## 📐 Production Configuration Template

Minimal validated configuration for a new model:

```python
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)
    }
```

---

## 📚 References

- **Darts**: [unit8co/darts](https://github.com/unit8co/darts) — Time series forecasting library
- **VIEWS**: [viewsforecasting.org](https://viewsforecasting.org/) — Violence Early-Warning System

