views-baseline 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. views_baseline/__init__.py +0 -0
  2. views_baseline/evaluation/__init__.py +0 -0
  3. views_baseline/evaluation/closeness.py +152 -0
  4. views_baseline/infrastructure/__init__.py +0 -0
  5. views_baseline/infrastructure/exceptions.py +10 -0
  6. views_baseline/infrastructure/reproducibility_gate.py +112 -0
  7. views_baseline/manager/__init__.py +0 -0
  8. views_baseline/manager/baseline_manager.py +152 -0
  9. views_baseline/model/__init__.py +1 -0
  10. views_baseline/model/catalog.py +120 -0
  11. views_baseline/model/defaults.py +9 -0
  12. views_baseline/model/distributions/__init__.py +64 -0
  13. views_baseline/model/distributions/families.py +182 -0
  14. views_baseline/model/distributions/registry.py +39 -0
  15. views_baseline/model/distributions/transforms.py +61 -0
  16. views_baseline/model/frames/__init__.py +0 -0
  17. views_baseline/model/frames/input.py +246 -0
  18. views_baseline/model/frames/output.py +144 -0
  19. views_baseline/model/frames/pooling.py +148 -0
  20. views_baseline/model/grid.py +90 -0
  21. views_baseline/model/models/__init__.py +0 -0
  22. views_baseline/model/models/distributional/__init__.py +13 -0
  23. views_baseline/model/models/distributional/conflictology.py +74 -0
  24. views_baseline/model/models/distributional/mixture.py +104 -0
  25. views_baseline/model/models/distributional/parametric.py +95 -0
  26. views_baseline/model/models/distributional/parametric_hurdle.py +115 -0
  27. views_baseline/model/models/point/__init__.py +5 -0
  28. views_baseline/model/models/point/average.py +78 -0
  29. views_baseline/model/models/point/locf.py +75 -0
  30. views_baseline/model/models/point/zero.py +63 -0
  31. views_baseline/model/protocol.py +44 -0
  32. views_baseline/model/spatial.py +72 -0
  33. views_baseline-1.0.0.dist-info/METADATA +207 -0
  34. views_baseline-1.0.0.dist-info/RECORD +36 -0
  35. views_baseline-1.0.0.dist-info/WHEEL +4 -0
  36. views_baseline-1.0.0.dist-info/licenses/LICENSE +21 -0
File without changes
File without changes
@@ -0,0 +1,152 @@
1
+ """Closeness-to-conflictology measurement harness (ADR-022 / epic #33 S3).
2
+
3
+ Operationalises "I couldn't tell which model the 1000 draws came from" as a
4
+ distributional two-sample comparison between two ``dict[str, PredictionFrame]``:
5
+
6
+ - per (cell, target): 1-Wasserstein, energy distance, and a 1-NN **C2ST** accuracy
7
+ (0.5 = indistinguishable);
8
+ - a **same-model null** (conflictology two seeds) as the finite-N noise floor;
9
+ - **stratification** by activity regime (zero / low / high), target, and level;
10
+ - an **equivalence** verdict against a pre-registered margin ``delta`` (not a p-value).
11
+
12
+ This module is *not* in the model path — it only consumes PredictionFrames.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import numpy as np
18
+ from scipy.stats import energy_distance, wasserstein_distance
19
+
20
+ # Activity strata by the reference (conflictology) per-cell mean, in raw counts.
21
+ _LOW_HIGH_CUT = 1.0 # mean < this (and not all-zero) = "low"; >= this = "high"
22
+
23
+
24
+ def c2st_1nn(a: np.ndarray, b: np.ndarray, rng: np.random.Generator | None = None) -> float:
25
+ """Leave-one-out 1-NN classifier two-sample accuracy in 1-D.
26
+
27
+ Pools the two balanced samples, labels them, and for each point asks whether its
28
+ nearest neighbour shares its label. ``0.5`` ⇒ indistinguishable, ``1.0`` ⇒
29
+ perfectly separable. Pure numpy, O(n log n). Ties (e.g. constant/degenerate
30
+ cells) are broken by a random permutation before the stable sort, so identical
31
+ samples score ~0.5 rather than spuriously ~1.0.
32
+ """
33
+ rng = np.random.default_rng(0) if rng is None else rng
34
+ a = np.asarray(a, dtype=np.float64).ravel()
35
+ b = np.asarray(b, dtype=np.float64).ravel()
36
+ x = np.concatenate([a, b])
37
+ y = np.concatenate([np.zeros(len(a)), np.ones(len(b))])
38
+ n = len(x)
39
+ if n < 2:
40
+ return 0.5
41
+ perm = rng.permutation(n) # randomise tie order
42
+ x, y = x[perm], y[perm]
43
+ order = np.argsort(x, kind="mergesort") # stable → ties keep the random order
44
+ xs, ys = x[order], y[order]
45
+ dl = np.empty(n)
46
+ dl[0] = np.inf
47
+ dl[1:] = xs[1:] - xs[:-1]
48
+ dr = np.empty(n)
49
+ dr[-1] = np.inf
50
+ dr[:-1] = xs[1:] - xs[:-1]
51
+ idx = np.arange(n)
52
+ nn = np.where(dl <= dr, idx - 1, idx + 1)
53
+ return float((ys[nn] == ys).mean())
54
+
55
+
56
+ def _cell_map(pf) -> dict:
57
+ """Map (unit, time) -> row index for a PredictionFrame."""
58
+ time = np.asarray(pf.index.time)
59
+ unit = np.asarray(pf.index.unit)
60
+ return {(int(u), int(t)): i for i, (u, t) in enumerate(zip(unit, time))}
61
+
62
+
63
+ def _stratum(ref_samples: np.ndarray) -> str:
64
+ m = float(np.mean(ref_samples))
65
+ if m == 0.0:
66
+ return "zero"
67
+ return "low" if m < _LOW_HIGH_CUT else "high"
68
+
69
+
70
+ def compare_models(model: dict, reference: dict, seed: int = 0) -> dict:
71
+ """Per-cell distances between ``model`` and ``reference`` (e.g. conflictology).
72
+
73
+ Both are ``dict[str, PredictionFrame]`` with ``.values`` shape ``(N, S)``. Cells
74
+ are aligned by ``(unit, time)``; the stratum is assigned from the *reference*
75
+ per-cell mean. ``seed`` makes the C2ST tie-breaking reproducible. Returns
76
+ ``{target: {"wasserstein","energy","c2st","stratum","level"}}`` with numpy arrays
77
+ over the common cells.
78
+ """
79
+ rng = np.random.default_rng(seed)
80
+ out = {}
81
+ for target, ref_pf in reference.items():
82
+ mod_pf = model[target]
83
+ ref_map, mod_map = _cell_map(ref_pf), _cell_map(mod_pf)
84
+ common = [k for k in ref_map if k in mod_map]
85
+ w = np.empty(len(common))
86
+ e = np.empty(len(common))
87
+ c = np.empty(len(common))
88
+ strat = np.empty(len(common), dtype=object)
89
+ for j, key in enumerate(common):
90
+ ra = np.asarray(ref_pf.values[ref_map[key]], dtype=np.float64).ravel()
91
+ ma = np.asarray(mod_pf.values[mod_map[key]], dtype=np.float64).ravel()
92
+ w[j] = wasserstein_distance(ma, ra)
93
+ e[j] = energy_distance(ma, ra)
94
+ c[j] = c2st_1nn(ma, ra, rng)
95
+ strat[j] = _stratum(ra)
96
+ out[target] = {
97
+ "wasserstein": w,
98
+ "energy": e,
99
+ "c2st": c,
100
+ "stratum": strat,
101
+ "level": ref_pf.index.level.name,
102
+ }
103
+ return out
104
+
105
+
106
+ def summarize(metrics: dict) -> dict:
107
+ """Stratified summary: per (target, stratum) → median / p90 / p99 of each metric."""
108
+ rows = {}
109
+ for target, m in metrics.items():
110
+ for stratum in ("zero", "low", "high", "all"):
111
+ mask = np.ones(len(m["c2st"]), bool) if stratum == "all" else (m["stratum"] == stratum)
112
+ if not mask.any():
113
+ continue
114
+ rows[(target, stratum)] = {
115
+ "n": int(mask.sum()),
116
+ "wasserstein": _pcts(m["wasserstein"][mask]),
117
+ "energy": _pcts(m["energy"][mask]),
118
+ "c2st": _pcts(m["c2st"][mask]),
119
+ }
120
+ return rows
121
+
122
+
123
+ def _pcts(a: np.ndarray) -> dict:
124
+ return {
125
+ "median": float(np.median(a)),
126
+ "p90": float(np.percentile(a, 90)),
127
+ "p99": float(np.percentile(a, 99)),
128
+ }
129
+
130
+
131
+ def equivalence(candidate: dict, null: dict, delta: dict) -> dict:
132
+ """Per (target, stratum) equivalence verdict.
133
+
134
+ A candidate is *equivalent to conflictology* in a stratum when its median C2ST is
135
+ within ``0.5 + delta["c2st"]`` AND its median Wasserstein is within the same-model
136
+ null median plus ``delta["wasserstein"]``.
137
+ """
138
+ verdict = {}
139
+ for key, cand in candidate.items():
140
+ null_row = null.get(key)
141
+ c2st_ok = cand["c2st"]["median"] <= 0.5 + delta["c2st"]
142
+ w_ref = null_row["wasserstein"]["median"] if null_row else 0.0
143
+ w_ok = cand["wasserstein"]["median"] <= w_ref + delta["wasserstein"]
144
+ verdict[key] = {
145
+ "equivalent": bool(c2st_ok and w_ok),
146
+ "c2st_median": cand["c2st"]["median"],
147
+ "c2st_ok": bool(c2st_ok),
148
+ "wasserstein_median": cand["wasserstein"]["median"],
149
+ "wasserstein_null_median": w_ref,
150
+ "wasserstein_ok": bool(w_ok),
151
+ }
152
+ return verdict
File without changes
@@ -0,0 +1,10 @@
1
+ class ReproducibilityError(Exception):
2
+ """Base class for all reproducibility gate failures."""
3
+
4
+ pass
5
+
6
+
7
+ class MissingHyperparameterError(ReproducibilityError):
8
+ """Raised when a mandatory hyperparameter is missing from the config."""
9
+
10
+ pass
@@ -0,0 +1,112 @@
1
+ import logging
2
+
3
+ from .exceptions import MissingHyperparameterError
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ class ReproducibilityGate:
9
+ """
10
+ Canonical hyperparameter contract for views-baseline models.
11
+
12
+ Defines which configuration keys are required by all baseline models
13
+ (CORE_GENOME) and which are required per algorithm (ALGORITHM_GENOMES).
14
+ The audit_manifest() method enforces these contracts at runtime.
15
+
16
+ This class is importable by downstream packages (e.g. views-models)
17
+ so they can validate their config_hyperparameters.py files statically.
18
+ """
19
+
20
+ class Config:
21
+ """Gates related to configuration and hyperparameter integrity."""
22
+
23
+ # Core keys required by ALL baseline models regardless of algorithm.
24
+ CORE_GENOME = ["steps", "time_steps", "prediction_format"]
25
+
26
+ # Algorithm-specific keys (audited only when the algorithm matches).
27
+ ALGORITHM_GENOMES = {
28
+ "ZeroModel": [],
29
+ "LocfModel": [],
30
+ "AverageModel": ["window_months"],
31
+ "ConflictologyModel": ["window_months", "n_samples", "seed"],
32
+ "MixtureBaseline": ["window_months", "lambda_mix", "n_samples", "seed"],
33
+ # Parametric climatology (ADR-022): `family` and `transform` join `seed` as
34
+ # required, audited reproducibility keys — no magic defaults (ADR-021).
35
+ "ParametricConflictology": [
36
+ "window_months", "n_samples", "seed", "family", "transform",
37
+ ],
38
+ "ParametricHurdleConflictology": [
39
+ "window_months", "n_samples", "seed", "family", "transform",
40
+ ],
41
+ }
42
+
43
+ @staticmethod
44
+ def audit_manifest(config: dict) -> None:
45
+ """
46
+ Verify that all mandatory hyperparameters are present and non-None.
47
+
48
+ Checks in order:
49
+ 1. All CORE_GENOME keys are present.
50
+ 2. The algorithm is registered in ALGORITHM_GENOMES.
51
+ 3. All algorithm-specific keys are present.
52
+ 4. No required key has a value of None.
53
+
54
+ Raises MissingHyperparameterError on any violation.
55
+ """
56
+ # 1. Audit Core Genome
57
+ missing_core = [
58
+ k
59
+ for k in ReproducibilityGate.Config.CORE_GENOME
60
+ if k not in config
61
+ ]
62
+ if missing_core:
63
+ msg = (
64
+ "REPRODUCIBILITY CONTRACT VIOLATED: "
65
+ f"Missing core parameters: {missing_core}"
66
+ )
67
+ logger.error(msg)
68
+ raise MissingHyperparameterError(msg)
69
+
70
+ # 2. Identify algorithm and check it is registered
71
+ if "algorithm" not in config:
72
+ msg = (
73
+ "REPRODUCIBILITY CONTRACT VIOLATED: "
74
+ "Missing required key: 'algorithm'"
75
+ )
76
+ logger.error(msg)
77
+ raise MissingHyperparameterError(msg)
78
+
79
+ algo = config["algorithm"]
80
+ algo_genomes = ReproducibilityGate.Config.ALGORITHM_GENOMES
81
+ if algo not in algo_genomes:
82
+ available = list(algo_genomes.keys())
83
+ msg = (
84
+ "REPRODUCIBILITY CONTRACT VIOLATED: "
85
+ f"Unknown algorithm '{algo}'. "
86
+ f"Available: {available}"
87
+ )
88
+ logger.error(msg)
89
+ raise MissingHyperparameterError(msg)
90
+
91
+ # 3. Audit algorithm-specific keys
92
+ algo_keys = algo_genomes[algo]
93
+ missing_algo = [k for k in algo_keys if k not in config]
94
+ if missing_algo:
95
+ msg = (
96
+ "REPRODUCIBILITY CONTRACT VIOLATED: "
97
+ f"Algorithm '{algo}' requires missing parameters: {missing_algo}"
98
+ )
99
+ logger.error(msg)
100
+ raise MissingHyperparameterError(msg)
101
+
102
+ # 4. Reject None values for all required keys
103
+ all_required = list(ReproducibilityGate.Config.CORE_GENOME) + algo_keys
104
+ explicit_nones = [k for k in all_required if config.get(k) is None]
105
+ if explicit_nones:
106
+ msg = (
107
+ "REPRODUCIBILITY CONTRACT VIOLATED: "
108
+ f"Mandatory parameters set to None: {explicit_nones}. "
109
+ "Implicit defaults are forbidden."
110
+ )
111
+ logger.error(msg)
112
+ raise MissingHyperparameterError(msg)
File without changes
@@ -0,0 +1,152 @@
1
+ import logging
2
+ import pickle
3
+
4
+ from views_pipeline_core.files.utils import generate_model_file_name, read_dataframe
5
+ from views_pipeline_core.managers.model import ForecastingModelManager, ModelPathManager
6
+ from views_pipeline_core.modules.dataloaders.datafactory_contract import (
7
+ DATA_FORMAT_DATAFRAME,
8
+ DATA_FORMAT_FEATURE_FRAME,
9
+ )
10
+ from views_pipeline_core.modules.dataloaders.frame_cache import load_frame_cache
11
+
12
+ from views_baseline.infrastructure.reproducibility_gate import ReproducibilityGate
13
+ from views_baseline.model.catalog import BaselineModelCatalog
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class BaselineForecastingModelManager(ForecastingModelManager):
19
+ """
20
+ Baseline Forecasting Model Manager
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ model_path: ModelPathManager,
26
+ wandb_notifications: bool = False,
27
+ use_prediction_store: bool = False,
28
+ ) -> None:
29
+ """
30
+ Initialize Baseline Forecasting Model Manager.
31
+ """
32
+ super().__init__(model_path, wandb_notifications, use_prediction_store)
33
+
34
+ logger.info("Initializing BaselineModelManager")
35
+
36
+ def _train_model_artifact(self):
37
+ """
38
+ Fit the baseline model and save it as a pickle artifact.
39
+
40
+ Although baselines are stateless and deterministic, the downstream
41
+ ensemble manager requires an artifact file to exist so it can
42
+ resolve timestamps via get_latest_model_artifact_path().
43
+ """
44
+ self.model, _ = self._setup_model_and_data()
45
+ path_artifacts = self._model_path.artifacts
46
+ run_type = self.config["run_type"]
47
+ model_filename = generate_model_file_name(run_type, file_extension=".pkl")
48
+ with open(path_artifacts / model_filename, "wb") as f:
49
+ pickle.dump(self.model, f)
50
+ logger.info(f"Saved baseline artifact: {model_filename}")
51
+ return self.model
52
+
53
+ def _load_source(self):
54
+ """Load the fit/predict input for the current partition, dispatching on declared format.
55
+
56
+ Frame-native seam (issue #64). A model that declares ``data_format: feature_frame``
57
+ is served the pipeline-core FeatureFrame directory cache directly, so the
58
+ ``FeatureFrame`` flows straight into ``fit``/``predict`` with no pandas in the
59
+ manager — the models have consumed either shape since ADR-019/epic #47. Every other
60
+ model keeps the byte-identical pandas-parquet path (``read_dataframe``).
61
+
62
+ Dispatch is on the declared ``_data_format`` (set by the base manager during data
63
+ fetching; absent → ``dataframe``, i.e. legacy behavior). The frame branch resolves
64
+ the cache through the LOUD ``_get_cached_frame_path()`` getter, which raises if the
65
+ frame cache was never populated. There is deliberately NO silent
66
+ ``getattr(..., None)`` fallback from the frame path back to pandas: a frames-declared
67
+ config that has lost its cache must fail loud, never silently degrade to a pandas run
68
+ (pipeline-core register C-214).
69
+ """
70
+ if getattr(self, "_data_format", DATA_FORMAT_DATAFRAME) == DATA_FORMAT_FEATURE_FRAME:
71
+ return load_frame_cache(self._get_cached_frame_path())
72
+ return read_dataframe(self._get_cached_data_path())
73
+
74
+ def _setup_model_and_data(self):
75
+ """
76
+ Instantiate the baseline model via the catalog, load data, fit, and return both.
77
+ """
78
+ ReproducibilityGate.Config.audit_manifest(self.config)
79
+ loa = self.config["level"]
80
+ partition_dict = self._data_loader.partition_dict
81
+ catalog = BaselineModelCatalog(
82
+ config=self.config, partition_dict=partition_dict, loa=loa
83
+ )
84
+ model = catalog.get_model(self.config["algorithm"])
85
+ logger.info(f"Model type is {self.config['algorithm']}")
86
+ df_source = self._load_source()
87
+ model.fit(df_source)
88
+ return model, df_source
89
+
90
+ def _generate_predictions(self, model, df, eval_type):
91
+ """
92
+ Generate predictions for all sequence numbers in an evaluation.
93
+
94
+ All models return Dict[str, PredictionFrame], so this accumulates
95
+ into Dict[str, list[PredictionFrame]] across sequence numbers.
96
+ """
97
+ sequence_numbers = self._resolve_evaluation_sequence_number(eval_type)
98
+ output_length = self.config["time_steps"]
99
+
100
+ predictions = {}
101
+ for seq_num in range(sequence_numbers):
102
+ pf_dict = model.predict(
103
+ df=df, sequence_number=seq_num, output_length=output_length
104
+ )
105
+ for target, pf in pf_dict.items():
106
+ predictions.setdefault(target, []).append(pf)
107
+ return predictions
108
+
109
+ def _evaluate_model_artifact(self, eval_type: str, artifact_name: str = None):
110
+ """
111
+ Evaluate trained model artifact.
112
+ """
113
+ logger.info("Evaluating baseline model artifact")
114
+ if artifact_name:
115
+ path_artifact = self._model_path.artifacts / artifact_name
116
+ else:
117
+ path_artifact = self._model_path.get_latest_model_artifact_path(
118
+ run_type=self.config["run_type"]
119
+ )
120
+ self._config_manager.add_config({"timestamp": path_artifact.stem[-15:]})
121
+ self.model, df_source = self._setup_model_and_data()
122
+ logger.info(f"Generating predictions for {eval_type} evaluation")
123
+ return self._generate_predictions(self.model, df_source, eval_type)
124
+
125
+ def _forecast_model_artifact(self, artifact_name: str = None):
126
+ """
127
+ Generate forecasts using trained model artifact.
128
+ """
129
+ logger.info("Generating forecasts")
130
+ if artifact_name:
131
+ path_artifact = self._model_path.artifacts / artifact_name
132
+ else:
133
+ path_artifact = self._model_path.get_latest_model_artifact_path(
134
+ run_type=self.config["run_type"]
135
+ )
136
+ self._config_manager.add_config({"timestamp": path_artifact.stem[-15:]})
137
+ self.model, df_source = self._setup_model_and_data()
138
+ output_length = self.config["time_steps"]
139
+
140
+ return self.model.predict(
141
+ df=df_source, sequence_number=0, output_length=output_length
142
+ )
143
+
144
+ def _evaluate_sweep(self, eval_type: str, model):
145
+ """
146
+ Evaluate a baseline model during a WandB sweep iteration.
147
+
148
+ The model has already been fitted by _train_model_artifact().
149
+ We load the data and generate predictions using it.
150
+ """
151
+ df_source = self._load_source()
152
+ return self._generate_predictions(model, df_source, eval_type)
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,120 @@
1
+ from views_baseline.infrastructure.reproducibility_gate import ReproducibilityGate
2
+ from views_baseline.model.models.distributional import (
3
+ ConflictologyModel,
4
+ MixtureBaseline,
5
+ ParametricConflictology,
6
+ ParametricHurdleConflictology,
7
+ )
8
+ from views_baseline.model.models.point import AverageModel, LocfModel, ZeroModel
9
+
10
+
11
+ class BaselineModelCatalog:
12
+ # Single source of truth lives in ReproducibilityGate.Config.ALGORITHM_GENOMES.
13
+ # This alias keeps the catalog's get_model() validation unchanged.
14
+ MODEL_GENOMES = ReproducibilityGate.Config.ALGORITHM_GENOMES
15
+
16
+ def __init__(self, config: dict, partition_dict: dict, loa: str):
17
+ """
18
+ Catalog of available baseline models.
19
+ """
20
+ self.config = config
21
+ self.partition_dict = partition_dict
22
+ self.loa = loa
23
+
24
+ self.models = {
25
+ "ZeroModel": self._get_zero_model,
26
+ "LocfModel": self._get_locf_model,
27
+ "AverageModel": self._get_average_model,
28
+ "ConflictologyModel": self._get_conflictology_model,
29
+ "MixtureBaseline": self._get_mixture_model,
30
+ "ParametricConflictology": self._get_parametric_conflictology,
31
+ "ParametricHurdleConflictology": self._get_parametric_hurdle,
32
+ }
33
+
34
+ def get_model(self, model_name: str):
35
+ """
36
+ Returns an initialized model instance.
37
+ Validates that required config keys for the model are present.
38
+ """
39
+ if model_name not in self.models:
40
+ raise ValueError(
41
+ f"Model '{model_name}' is not in the catalog. "
42
+ f"Available: {self.list_models()}"
43
+ )
44
+ missing = [k for k in self.MODEL_GENOMES[model_name] if k not in self.config]
45
+ if missing:
46
+ raise ValueError(
47
+ f"Model '{model_name}' requires config keys {missing} "
48
+ f"but they are missing"
49
+ )
50
+ return self.models[model_name]()
51
+
52
+ def list_models(self):
53
+ return list(self.models.keys())
54
+
55
+ def _get_zero_model(self):
56
+ return ZeroModel(
57
+ targets=self.config["targets"], partition_dict=self.partition_dict, loa=self.loa
58
+ )
59
+
60
+ def _get_locf_model(self):
61
+ return LocfModel(
62
+ targets=self.config["targets"], partition_dict=self.partition_dict, loa=self.loa
63
+ )
64
+
65
+ def _get_average_model(self):
66
+ return AverageModel(
67
+ targets=self.config["targets"],
68
+ window_months=self.config["window_months"],
69
+ partition_dict=self.partition_dict,
70
+ loa=self.loa,
71
+ )
72
+
73
+ def _get_conflictology_model(self):
74
+ return ConflictologyModel(
75
+ targets=self.config["targets"],
76
+ window_months=self.config["window_months"],
77
+ partition_dict=self.partition_dict,
78
+ loa=self.loa,
79
+ n_samples=self.config["n_samples"],
80
+ seed=self.config["seed"], # required + audited genome key (ADR-021); no silent default
81
+ )
82
+
83
+ def _get_mixture_model(self):
84
+ return MixtureBaseline(
85
+ targets=self.config["targets"],
86
+ window_months=self.config["window_months"],
87
+ lambda_mix=self.config["lambda_mix"],
88
+ n_samples=self.config["n_samples"],
89
+ partition_dict=self.partition_dict,
90
+ loa=self.loa,
91
+ seed=self.config["seed"], # required + audited genome key (ADR-021); no silent default
92
+ )
93
+
94
+ def _get_parametric_conflictology(self):
95
+ # family/transform/seed are required, audited genome keys (ADR-021/ADR-022); the
96
+ # constructor fails loud on an unsupported family or an illegal family×transform.
97
+ return ParametricConflictology(
98
+ targets=self.config["targets"],
99
+ window_months=self.config["window_months"],
100
+ partition_dict=self.partition_dict,
101
+ loa=self.loa,
102
+ n_samples=self.config["n_samples"],
103
+ family=self.config["family"],
104
+ transform=self.config["transform"],
105
+ seed=self.config["seed"],
106
+ )
107
+
108
+ def _get_parametric_hurdle(self):
109
+ # family/transform/seed are required, audited genome keys (ADR-021/ADR-022); the
110
+ # constructor fails loud on a non-continuous family or an illegal family×transform.
111
+ return ParametricHurdleConflictology(
112
+ targets=self.config["targets"],
113
+ window_months=self.config["window_months"],
114
+ partition_dict=self.partition_dict,
115
+ loa=self.loa,
116
+ n_samples=self.config["n_samples"],
117
+ family=self.config["family"],
118
+ transform=self.config["transform"],
119
+ seed=self.config["seed"],
120
+ )
@@ -0,0 +1,9 @@
1
+ """Package-level default sentinels for baseline models.
2
+
3
+ Zero-Magic / Explicit-Defaults (ADR-021): the single source of truth for the
4
+ default RNG seed. It is a **sentinel for direct/test construction only** —
5
+ production configs MUST declare ``seed`` (a required, audited genome key). The
6
+ catalog reads ``config["seed"]`` strictly and never falls back to this constant.
7
+ """
8
+
9
+ DEFAULT_SEED = 42
@@ -0,0 +1,64 @@
1
+ """Distribution family + transform registries for the parametric climatology models.
2
+
3
+ A **strategy registry** (ADR-022) split across three cohesive leaves:
4
+
5
+ * :mod:`transforms` — `TRANSFORMS`, `clamp_log`/`clamp_floor`, `EMIT_*` (pure log math).
6
+ * :mod:`families` — per-family `fit_`/`sample_` (method-of-moments; point-mass fallback).
7
+ * :mod:`registry` — `fit_family`/`sample_family` dispatch + `validate_family_transform`.
8
+
9
+ This package re-exports the stable public surface so importers depend on
10
+ ``views_baseline.model.distributions`` and are insulated from the internal split.
11
+ Pure package: numpy + scipy only; no pandas, no views-frames, no model imports.
12
+ """
13
+
14
+ from views_baseline.model.distributions.families import (
15
+ CONTINUOUS_FAMILIES,
16
+ FAMILIES,
17
+ NATIVE_ZERO_FAMILIES,
18
+ fit_gamma,
19
+ fit_gumbel,
20
+ fit_lognormal,
21
+ fit_nb,
22
+ fit_zinb,
23
+ sample_gamma,
24
+ sample_gumbel,
25
+ sample_lognormal,
26
+ sample_nb,
27
+ sample_zinb,
28
+ )
29
+ from views_baseline.model.distributions.registry import (
30
+ fit_family,
31
+ sample_family,
32
+ validate_family_transform,
33
+ )
34
+ from views_baseline.model.distributions.transforms import (
35
+ EMIT_FLOOR,
36
+ EMIT_LOG_CEIL,
37
+ TRANSFORMS,
38
+ clamp_floor,
39
+ clamp_log,
40
+ )
41
+
42
+ __all__ = [
43
+ "CONTINUOUS_FAMILIES",
44
+ "EMIT_FLOOR",
45
+ "EMIT_LOG_CEIL",
46
+ "FAMILIES",
47
+ "NATIVE_ZERO_FAMILIES",
48
+ "TRANSFORMS",
49
+ "clamp_floor",
50
+ "clamp_log",
51
+ "fit_family",
52
+ "fit_gamma",
53
+ "fit_gumbel",
54
+ "fit_lognormal",
55
+ "fit_nb",
56
+ "fit_zinb",
57
+ "sample_family",
58
+ "sample_gamma",
59
+ "sample_gumbel",
60
+ "sample_lognormal",
61
+ "sample_nb",
62
+ "sample_zinb",
63
+ "validate_family_transform",
64
+ ]