continuousintelligence 0.1.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.
- continuousintelligence/__init__.py +9 -0
- continuousintelligence/_data/NOTICE +16 -0
- continuousintelligence/_data/bike_sharing.csv +145 -0
- continuousintelligence/_example.py +20 -0
- continuousintelligence/_loader.py +140 -0
- continuousintelligence/_plot.py +158 -0
- continuousintelligence/_vendor/__init__.py +1 -0
- continuousintelligence/_vendor/mamba3/LICENSE +201 -0
- continuousintelligence/_vendor/mamba3/__init__.py +6 -0
- continuousintelligence/_vendor/mamba3/angle_dt.py +213 -0
- continuousintelligence/_vendor/mamba3/layernorm_gated.py +115 -0
- continuousintelligence/_vendor/mamba3/mamba3_mimo_fwd.py +483 -0
- continuousintelligence/_vendor/mamba3/mamba3_mimo_utils.py +112 -0
- continuousintelligence/_vendor/mamba3/utils.py +25 -0
- continuousintelligence/ci_proto/__init__.py +5 -0
- continuousintelligence/ci_proto/_calendar.py +153 -0
- continuousintelligence/ci_proto/_checkpoint.py +106 -0
- continuousintelligence/ci_proto/_cuda.py +135 -0
- continuousintelligence/ci_proto/_forecast.py +708 -0
- continuousintelligence/ci_proto/_groups.py +198 -0
- continuousintelligence/ci_proto/_mamba.py +190 -0
- continuousintelligence/ci_proto/_model.py +489 -0
- continuousintelligence-0.1.0.dist-info/METADATA +29 -0
- continuousintelligence-0.1.0.dist-info/RECORD +30 -0
- continuousintelligence-0.1.0.dist-info/WHEEL +5 -0
- continuousintelligence-0.1.0.dist-info/licenses/LICENCE +95 -0
- continuousintelligence-0.1.0.dist-info/licenses/NOTICE +33 -0
- continuousintelligence-0.1.0.dist-info/licenses/src/continuousintelligence/_data/NOTICE +16 -0
- continuousintelligence-0.1.0.dist-info/licenses/src/continuousintelligence/_vendor/mamba3/LICENSE +201 -0
- continuousintelligence-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Continuous Intelligence models."""
|
|
2
|
+
|
|
3
|
+
from . import ci_proto
|
|
4
|
+
from ._example import load_example
|
|
5
|
+
from ._loader import load_model
|
|
6
|
+
from ._plot import plot_forecast
|
|
7
|
+
from .ci_proto import Forecaster
|
|
8
|
+
|
|
9
|
+
__all__ = ["ci_proto", "Forecaster", "load_example", "load_model", "plot_forecast"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Bike Sharing dataset — hourly bicycle rentals in Washington, D.C.
|
|
2
|
+
|
|
3
|
+
Hadi Fanaee-T (2013). Bike Sharing [Dataset]. UCI Machine Learning Repository.
|
|
4
|
+
https://doi.org/10.24432/C5W894
|
|
5
|
+
Licensed under Creative Commons Attribution 4.0 International (CC BY 4.0):
|
|
6
|
+
https://creativecommons.org/licenses/by/4.0/
|
|
7
|
+
|
|
8
|
+
Source: https://archive.ics.uci.edu/static/public/275/bike+sharing+dataset.zip
|
|
9
|
+
Extract: hour.csv, total rentals (cnt), 9–13 June 2012 (120 hourly observations).
|
|
10
|
+
Changes: selected dates and columns, renamed cnt to rentals; no smoothing or imputation.
|
|
11
|
+
The following 24 rows contain forecast timestamps only; their values are empty.
|
|
12
|
+
|
|
13
|
+
Publication requested by the source dataset:
|
|
14
|
+
Fanaee-T, H. and Gama, J. (2013). Event labeling combining ensemble detectors and
|
|
15
|
+
background knowledge. Progress in Artificial Intelligence.
|
|
16
|
+
https://doi.org/10.1007/s13748-013-0040-3
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
timestamp,rentals,split
|
|
2
|
+
2012-06-09T00:00:00,206,history
|
|
3
|
+
2012-06-09T01:00:00,123,history
|
|
4
|
+
2012-06-09T02:00:00,89,history
|
|
5
|
+
2012-06-09T03:00:00,40,history
|
|
6
|
+
2012-06-09T04:00:00,8,history
|
|
7
|
+
2012-06-09T05:00:00,19,history
|
|
8
|
+
2012-06-09T06:00:00,80,history
|
|
9
|
+
2012-06-09T07:00:00,87,history
|
|
10
|
+
2012-06-09T08:00:00,244,history
|
|
11
|
+
2012-06-09T09:00:00,340,history
|
|
12
|
+
2012-06-09T10:00:00,466,history
|
|
13
|
+
2012-06-09T11:00:00,569,history
|
|
14
|
+
2012-06-09T12:00:00,531,history
|
|
15
|
+
2012-06-09T13:00:00,585,history
|
|
16
|
+
2012-06-09T14:00:00,593,history
|
|
17
|
+
2012-06-09T15:00:00,555,history
|
|
18
|
+
2012-06-09T16:00:00,534,history
|
|
19
|
+
2012-06-09T17:00:00,495,history
|
|
20
|
+
2012-06-09T18:00:00,451,history
|
|
21
|
+
2012-06-09T19:00:00,360,history
|
|
22
|
+
2012-06-09T20:00:00,363,history
|
|
23
|
+
2012-06-09T21:00:00,316,history
|
|
24
|
+
2012-06-09T22:00:00,214,history
|
|
25
|
+
2012-06-09T23:00:00,230,history
|
|
26
|
+
2012-06-10T00:00:00,158,history
|
|
27
|
+
2012-06-10T01:00:00,116,history
|
|
28
|
+
2012-06-10T02:00:00,73,history
|
|
29
|
+
2012-06-10T03:00:00,29,history
|
|
30
|
+
2012-06-10T04:00:00,10,history
|
|
31
|
+
2012-06-10T05:00:00,13,history
|
|
32
|
+
2012-06-10T06:00:00,30,history
|
|
33
|
+
2012-06-10T07:00:00,62,history
|
|
34
|
+
2012-06-10T08:00:00,181,history
|
|
35
|
+
2012-06-10T09:00:00,266,history
|
|
36
|
+
2012-06-10T10:00:00,413,history
|
|
37
|
+
2012-06-10T11:00:00,499,history
|
|
38
|
+
2012-06-10T12:00:00,502,history
|
|
39
|
+
2012-06-10T13:00:00,492,history
|
|
40
|
+
2012-06-10T14:00:00,500,history
|
|
41
|
+
2012-06-10T15:00:00,475,history
|
|
42
|
+
2012-06-10T16:00:00,508,history
|
|
43
|
+
2012-06-10T17:00:00,521,history
|
|
44
|
+
2012-06-10T18:00:00,490,history
|
|
45
|
+
2012-06-10T19:00:00,375,history
|
|
46
|
+
2012-06-10T20:00:00,363,history
|
|
47
|
+
2012-06-10T21:00:00,253,history
|
|
48
|
+
2012-06-10T22:00:00,159,history
|
|
49
|
+
2012-06-10T23:00:00,110,history
|
|
50
|
+
2012-06-11T00:00:00,41,history
|
|
51
|
+
2012-06-11T01:00:00,24,history
|
|
52
|
+
2012-06-11T02:00:00,17,history
|
|
53
|
+
2012-06-11T03:00:00,6,history
|
|
54
|
+
2012-06-11T04:00:00,7,history
|
|
55
|
+
2012-06-11T05:00:00,37,history
|
|
56
|
+
2012-06-11T06:00:00,144,history
|
|
57
|
+
2012-06-11T07:00:00,503,history
|
|
58
|
+
2012-06-11T08:00:00,651,history
|
|
59
|
+
2012-06-11T09:00:00,316,history
|
|
60
|
+
2012-06-11T10:00:00,147,history
|
|
61
|
+
2012-06-11T11:00:00,202,history
|
|
62
|
+
2012-06-11T12:00:00,261,history
|
|
63
|
+
2012-06-11T13:00:00,248,history
|
|
64
|
+
2012-06-11T14:00:00,259,history
|
|
65
|
+
2012-06-11T15:00:00,259,history
|
|
66
|
+
2012-06-11T16:00:00,427,history
|
|
67
|
+
2012-06-11T17:00:00,800,history
|
|
68
|
+
2012-06-11T18:00:00,831,history
|
|
69
|
+
2012-06-11T19:00:00,596,history
|
|
70
|
+
2012-06-11T20:00:00,394,history
|
|
71
|
+
2012-06-11T21:00:00,250,history
|
|
72
|
+
2012-06-11T22:00:00,164,history
|
|
73
|
+
2012-06-11T23:00:00,80,history
|
|
74
|
+
2012-06-12T00:00:00,28,history
|
|
75
|
+
2012-06-12T01:00:00,16,history
|
|
76
|
+
2012-06-12T02:00:00,6,history
|
|
77
|
+
2012-06-12T03:00:00,12,history
|
|
78
|
+
2012-06-12T04:00:00,4,history
|
|
79
|
+
2012-06-12T05:00:00,29,history
|
|
80
|
+
2012-06-12T06:00:00,136,history
|
|
81
|
+
2012-06-12T07:00:00,303,history
|
|
82
|
+
2012-06-12T08:00:00,613,history
|
|
83
|
+
2012-06-12T09:00:00,285,history
|
|
84
|
+
2012-06-12T10:00:00,125,history
|
|
85
|
+
2012-06-12T11:00:00,65,history
|
|
86
|
+
2012-06-12T12:00:00,95,history
|
|
87
|
+
2012-06-12T13:00:00,59,history
|
|
88
|
+
2012-06-12T14:00:00,60,history
|
|
89
|
+
2012-06-12T15:00:00,116,history
|
|
90
|
+
2012-06-12T16:00:00,239,history
|
|
91
|
+
2012-06-12T17:00:00,681,history
|
|
92
|
+
2012-06-12T18:00:00,653,history
|
|
93
|
+
2012-06-12T19:00:00,491,history
|
|
94
|
+
2012-06-12T20:00:00,386,history
|
|
95
|
+
2012-06-12T21:00:00,274,history
|
|
96
|
+
2012-06-12T22:00:00,207,history
|
|
97
|
+
2012-06-12T23:00:00,89,history
|
|
98
|
+
2012-06-13T00:00:00,34,history
|
|
99
|
+
2012-06-13T01:00:00,28,history
|
|
100
|
+
2012-06-13T02:00:00,4,history
|
|
101
|
+
2012-06-13T03:00:00,8,history
|
|
102
|
+
2012-06-13T04:00:00,10,history
|
|
103
|
+
2012-06-13T05:00:00,40,history
|
|
104
|
+
2012-06-13T06:00:00,194,history
|
|
105
|
+
2012-06-13T07:00:00,505,history
|
|
106
|
+
2012-06-13T08:00:00,713,history
|
|
107
|
+
2012-06-13T09:00:00,352,history
|
|
108
|
+
2012-06-13T10:00:00,198,history
|
|
109
|
+
2012-06-13T11:00:00,246,history
|
|
110
|
+
2012-06-13T12:00:00,334,history
|
|
111
|
+
2012-06-13T13:00:00,255,history
|
|
112
|
+
2012-06-13T14:00:00,320,history
|
|
113
|
+
2012-06-13T15:00:00,281,history
|
|
114
|
+
2012-06-13T16:00:00,392,history
|
|
115
|
+
2012-06-13T17:00:00,857,history
|
|
116
|
+
2012-06-13T18:00:00,744,history
|
|
117
|
+
2012-06-13T19:00:00,671,history
|
|
118
|
+
2012-06-13T20:00:00,448,history
|
|
119
|
+
2012-06-13T21:00:00,396,history
|
|
120
|
+
2012-06-13T22:00:00,238,history
|
|
121
|
+
2012-06-13T23:00:00,153,history
|
|
122
|
+
2012-06-14T00:00:00,,forecast
|
|
123
|
+
2012-06-14T01:00:00,,forecast
|
|
124
|
+
2012-06-14T02:00:00,,forecast
|
|
125
|
+
2012-06-14T03:00:00,,forecast
|
|
126
|
+
2012-06-14T04:00:00,,forecast
|
|
127
|
+
2012-06-14T05:00:00,,forecast
|
|
128
|
+
2012-06-14T06:00:00,,forecast
|
|
129
|
+
2012-06-14T07:00:00,,forecast
|
|
130
|
+
2012-06-14T08:00:00,,forecast
|
|
131
|
+
2012-06-14T09:00:00,,forecast
|
|
132
|
+
2012-06-14T10:00:00,,forecast
|
|
133
|
+
2012-06-14T11:00:00,,forecast
|
|
134
|
+
2012-06-14T12:00:00,,forecast
|
|
135
|
+
2012-06-14T13:00:00,,forecast
|
|
136
|
+
2012-06-14T14:00:00,,forecast
|
|
137
|
+
2012-06-14T15:00:00,,forecast
|
|
138
|
+
2012-06-14T16:00:00,,forecast
|
|
139
|
+
2012-06-14T17:00:00,,forecast
|
|
140
|
+
2012-06-14T18:00:00,,forecast
|
|
141
|
+
2012-06-14T19:00:00,,forecast
|
|
142
|
+
2012-06-14T20:00:00,,forecast
|
|
143
|
+
2012-06-14T21:00:00,,forecast
|
|
144
|
+
2012-06-14T22:00:00,,forecast
|
|
145
|
+
2012-06-14T23:00:00,,forecast
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""The bundled real-data forecasting example."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
from importlib.resources import files
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
from torch import Tensor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_example() -> Tensor:
|
|
11
|
+
"""Return the example's observed history as a CPU float32 tensor ``[1, 120]``.
|
|
12
|
+
|
|
13
|
+
Forecast rows are excluded. Each call returns a new tensor. Data attribution
|
|
14
|
+
and source information accompany the CSV in ``_data/NOTICE``.
|
|
15
|
+
"""
|
|
16
|
+
with files("continuousintelligence").joinpath("_data/bike_sharing.csv").open() as stream:
|
|
17
|
+
values = [
|
|
18
|
+
float(row["rentals"]) for row in csv.DictReader(stream) if row["split"] == "history"
|
|
19
|
+
]
|
|
20
|
+
return torch.tensor([values], dtype=torch.float32)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Local and Hugging Face model loading for Continuous Intelligence."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import re
|
|
6
|
+
from contextlib import nullcontext
|
|
7
|
+
from functools import cache
|
|
8
|
+
from pathlib import Path, PureWindowsPath
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
|
|
12
|
+
from .ci_proto import Forecaster
|
|
13
|
+
|
|
14
|
+
_COMMIT = re.compile(r"[0-9a-f]{40}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _device(name: str) -> torch.device:
|
|
18
|
+
if name == "auto":
|
|
19
|
+
name = "cuda" if torch.cuda.is_available() else "cpu"
|
|
20
|
+
if name not in ("cpu", "cuda"):
|
|
21
|
+
raise ValueError("device must be 'cpu', 'cuda', or 'auto'")
|
|
22
|
+
if name == "cuda" and not torch.cuda.is_available():
|
|
23
|
+
raise RuntimeError("CUDA was requested but is not available")
|
|
24
|
+
return torch.device(name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _files(
|
|
28
|
+
source: str | Path, tta_config: str | Path | None, revision: str | None
|
|
29
|
+
) -> tuple[Path, Path, Path | None]:
|
|
30
|
+
path = Path(source).expanduser()
|
|
31
|
+
if path.exists():
|
|
32
|
+
if revision is not None:
|
|
33
|
+
raise ValueError("revision applies only to Hugging Face models")
|
|
34
|
+
else:
|
|
35
|
+
if (
|
|
36
|
+
isinstance(source, Path)
|
|
37
|
+
or str(source).startswith(("/", "./", "../", "~", ".\\", "..\\"))
|
|
38
|
+
or "\\" in str(source)
|
|
39
|
+
or PureWindowsPath(source).drive
|
|
40
|
+
or str(source).count("/") != 1
|
|
41
|
+
or path.parent.is_dir()
|
|
42
|
+
or path.suffix in (".ckpt", ".pt", ".pth")
|
|
43
|
+
or tta_config is not None
|
|
44
|
+
):
|
|
45
|
+
raise FileNotFoundError(path)
|
|
46
|
+
from huggingface_hub import hf_hub_download, snapshot_download
|
|
47
|
+
|
|
48
|
+
info = Path(hf_hub_download(repo_id=str(source), filename="config.json", revision=revision))
|
|
49
|
+
commit = info.parent.name if _COMMIT.fullmatch(info.parent.name) else revision
|
|
50
|
+
path = Path(
|
|
51
|
+
snapshot_download(
|
|
52
|
+
repo_id=str(source),
|
|
53
|
+
revision=commit,
|
|
54
|
+
allow_patterns=["model.ckpt", "model-long.ckpt", "tta.json"],
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
info = None
|
|
58
|
+
if path.is_dir():
|
|
59
|
+
if tta_config is not None:
|
|
60
|
+
raise ValueError("a model bundle uses its own tta.json")
|
|
61
|
+
info = path / "config.json" if (path / "config.json").is_file() else None
|
|
62
|
+
path, tta_config = path / "model.ckpt", path / "tta.json"
|
|
63
|
+
elif tta_config is None:
|
|
64
|
+
raise ValueError("a standalone checkpoint requires tta_config")
|
|
65
|
+
tta = Path(tta_config).expanduser()
|
|
66
|
+
for file in (path, tta):
|
|
67
|
+
if not file.is_file():
|
|
68
|
+
raise FileNotFoundError(file)
|
|
69
|
+
return path, tta, info
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _model_info(path: Path, forecaster: Forecaster) -> dict:
|
|
73
|
+
"""Read a bundle's ``config.json`` and check it describes the loaded weights."""
|
|
74
|
+
info = json.loads(path.read_text())
|
|
75
|
+
if not isinstance(info, dict):
|
|
76
|
+
raise ValueError("config.json must contain a JSON object")
|
|
77
|
+
missing = {"name", "parameters", "quantiles"} - set(info)
|
|
78
|
+
if missing:
|
|
79
|
+
raise ValueError(f"config.json is missing {sorted(missing)}")
|
|
80
|
+
model = forecaster._model
|
|
81
|
+
quantiles = forecaster.quantiles.tolist()
|
|
82
|
+
if (
|
|
83
|
+
not isinstance(info["quantiles"], list)
|
|
84
|
+
or len(info["quantiles"]) != len(quantiles)
|
|
85
|
+
or not all(
|
|
86
|
+
isinstance(given, (int, float)) and math.isclose(given, level, abs_tol=1e-6)
|
|
87
|
+
for given, level in zip(info["quantiles"], quantiles, strict=True)
|
|
88
|
+
)
|
|
89
|
+
):
|
|
90
|
+
raise ValueError("config.json quantiles do not match the checkpoint")
|
|
91
|
+
if info["parameters"] != sum(parameter.numel() for parameter in model.parameters()):
|
|
92
|
+
raise ValueError("config.json parameters do not match the checkpoint")
|
|
93
|
+
return info
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_model(
|
|
97
|
+
checkpoint: str | Path,
|
|
98
|
+
tta_config: str | Path | None = None,
|
|
99
|
+
device: str = "auto",
|
|
100
|
+
*,
|
|
101
|
+
revision: str | None = None,
|
|
102
|
+
) -> Forecaster:
|
|
103
|
+
"""Load a local model bundle, Hugging Face model ID, or checkpoint plus TTA file.
|
|
104
|
+
|
|
105
|
+
Bundles contain ``model.ckpt`` (weights and embedded architecture) and
|
|
106
|
+
``tta.json``. Hub bundles also contain ``config.json``, which describes the
|
|
107
|
+
model and is returned by ``Forecaster.info``; it is checked against the
|
|
108
|
+
weights. Hub files are fetched together from the same revision;
|
|
109
|
+
pass a commit SHA to pin it. Hub IDs use ``owner/model`` strings. Use a
|
|
110
|
+
``Path`` or ``./`` prefix to mark an ambiguous relative path as local.
|
|
111
|
+
An optional ``model-long.ckpt`` shares the bundle's TTA and is selected
|
|
112
|
+
when history exceeds the primary checkpoint's context length.
|
|
113
|
+
"""
|
|
114
|
+
placement = _device(device)
|
|
115
|
+
path, tta_path, info_path = _files(checkpoint, tta_config, revision)
|
|
116
|
+
payload = torch.load(path, map_location="cpu", weights_only=True, mmap=True)
|
|
117
|
+
from .ci_proto._checkpoint import from_checkpoint
|
|
118
|
+
|
|
119
|
+
forecaster = from_checkpoint(payload, tta_path, placement)
|
|
120
|
+
placement = forecaster.device
|
|
121
|
+
if info_path is not None:
|
|
122
|
+
forecaster._info = _model_info(info_path, forecaster)
|
|
123
|
+
long_path = path.with_name("model-long.ckpt")
|
|
124
|
+
if tta_config is None and long_path.is_file() and forecaster._model.config.context_len:
|
|
125
|
+
quantiles = forecaster.quantiles
|
|
126
|
+
future_len = forecaster._model.config.future_len
|
|
127
|
+
|
|
128
|
+
@cache
|
|
129
|
+
def load_long() -> Forecaster:
|
|
130
|
+
context = torch.cuda.device(placement) if placement.type == "cuda" else nullcontext()
|
|
131
|
+
with context:
|
|
132
|
+
long = load_model(long_path, tta_path, device=placement.type)
|
|
133
|
+
if not torch.equal(long.quantiles, quantiles) or (
|
|
134
|
+
long._model.config.future_len != future_len
|
|
135
|
+
):
|
|
136
|
+
raise ValueError("long checkpoint must have the same quantiles and forecast window")
|
|
137
|
+
return long
|
|
138
|
+
|
|
139
|
+
forecaster._long_model = load_long
|
|
140
|
+
return forecaster
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Plot observed history and forecast quantiles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import torch
|
|
10
|
+
from torch import Tensor
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from matplotlib.figure import Figure
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def plot_forecast(
|
|
17
|
+
history: Tensor,
|
|
18
|
+
prediction: Tensor,
|
|
19
|
+
quantiles: Tensor,
|
|
20
|
+
*,
|
|
21
|
+
timestamps: Sequence | Tensor | None = None,
|
|
22
|
+
ylabel: str = "Value",
|
|
23
|
+
output: str | Path | None = None,
|
|
24
|
+
) -> Figure:
|
|
25
|
+
"""Plot ``[variables, time]`` history and ``[variables, quantiles, horizon]`` predictions.
|
|
26
|
+
|
|
27
|
+
``quantiles`` is the model's probability grid. Each variable gets one panel,
|
|
28
|
+
showing the median and available nested intervals between 0.1 and 0.9.
|
|
29
|
+
``timestamps`` may supply a shared date axis covering history plus horizon;
|
|
30
|
+
otherwise the forecast starts at step zero. Returns a Matplotlib figure and
|
|
31
|
+
optionally saves it to ``output``.
|
|
32
|
+
"""
|
|
33
|
+
if any(not isinstance(value, Tensor) for value in (history, prediction, quantiles)):
|
|
34
|
+
raise ValueError("history, prediction and quantiles must be PyTorch tensors")
|
|
35
|
+
if any(value.is_complex() for value in (history, prediction, quantiles)):
|
|
36
|
+
raise ValueError("plot inputs must be real")
|
|
37
|
+
if history.ndim != 2 or min(history.shape) < 1:
|
|
38
|
+
raise ValueError("history must be a nonempty [variables, time] tensor")
|
|
39
|
+
if prediction.ndim != 3 or min(prediction.shape) < 1 or prediction.shape[0] != history.shape[0]:
|
|
40
|
+
raise ValueError("prediction must have shape [variables, quantiles, horizon]")
|
|
41
|
+
if (
|
|
42
|
+
quantiles.ndim != 1
|
|
43
|
+
or len(quantiles) != prediction.shape[1]
|
|
44
|
+
or not bool(((quantiles > 0) & (quantiles < 1)).all())
|
|
45
|
+
or not bool((quantiles.diff() > 0).all())
|
|
46
|
+
):
|
|
47
|
+
raise ValueError("quantiles must be a matching, strictly increasing probability grid")
|
|
48
|
+
if bool(history.isinf().any()) or not bool(prediction.isfinite().all()):
|
|
49
|
+
raise ValueError("history must be finite or NaN and predictions must be finite")
|
|
50
|
+
history, prediction, quantiles = (
|
|
51
|
+
value.detach().cpu().float() for value in (history, prediction, quantiles)
|
|
52
|
+
)
|
|
53
|
+
levels = torch.arange(1, 10) / 10
|
|
54
|
+
columns = (quantiles[:, None] - levels).abs().argmin(dim=0)
|
|
55
|
+
available = torch.isclose(quantiles[columns], levels, atol=1e-6, rtol=0)
|
|
56
|
+
if not available[4]:
|
|
57
|
+
raise ValueError("quantiles must include the median (0.5)")
|
|
58
|
+
import matplotlib.dates as mdates
|
|
59
|
+
import matplotlib.pyplot as plt
|
|
60
|
+
from matplotlib import font_manager
|
|
61
|
+
|
|
62
|
+
count, length = history.shape
|
|
63
|
+
horizon = prediction.shape[-1]
|
|
64
|
+
if timestamps is None:
|
|
65
|
+
times = torch.arange(-length, horizon).numpy()
|
|
66
|
+
else:
|
|
67
|
+
import pandas as pd
|
|
68
|
+
|
|
69
|
+
if isinstance(timestamps, Tensor):
|
|
70
|
+
timestamps = timestamps.detach().cpu().numpy()
|
|
71
|
+
times = pd.DatetimeIndex(pd.to_datetime(timestamps))
|
|
72
|
+
if len(times) != length + horizon or times.hasnans or not times.is_unique:
|
|
73
|
+
raise ValueError("timestamps must cover history plus horizon without missing dates")
|
|
74
|
+
if not times.is_monotonic_increasing:
|
|
75
|
+
raise ValueError("timestamps must be strictly increasing")
|
|
76
|
+
past, future = times[:length], times[length:]
|
|
77
|
+
background, panel, teal = "#173446", "#1d3d50", "#549a8b"
|
|
78
|
+
white, muted, grid = "#fafbfb", "#b6c3c9", "#2d4d5d"
|
|
79
|
+
fonts = {font.name for font in font_manager.fontManager.ttflist}
|
|
80
|
+
with plt.rc_context(
|
|
81
|
+
{
|
|
82
|
+
"font.family": "Aptos" if "Aptos" in fonts else "DejaVu Sans",
|
|
83
|
+
"font.size": 14,
|
|
84
|
+
"axes.labelsize": 16,
|
|
85
|
+
"text.color": white,
|
|
86
|
+
"axes.labelcolor": muted,
|
|
87
|
+
"xtick.color": muted,
|
|
88
|
+
"ytick.color": muted,
|
|
89
|
+
"savefig.dpi": 180,
|
|
90
|
+
}
|
|
91
|
+
):
|
|
92
|
+
figure, panels = plt.subplots(count, 1, figsize=(13.5, 5.8 * count), squeeze=False)
|
|
93
|
+
figure.set_facecolor(background)
|
|
94
|
+
figure.subplots_adjust(left=0.09, right=0.98, bottom=0.15 / count, top=0.97, hspace=0.3)
|
|
95
|
+
for index, axes in enumerate(panels[:, 0]):
|
|
96
|
+
axes.set_facecolor(panel)
|
|
97
|
+
axes.axvspan(future[0], future[-1], color="#214452", linewidth=0)
|
|
98
|
+
axes.axvline(future[0], color=teal, linewidth=1.2, linestyle=":")
|
|
99
|
+
axes.grid(color=grid, linewidth=0.7)
|
|
100
|
+
axes.set_axisbelow(True)
|
|
101
|
+
axes.spines[["top", "right", "left"]].set_visible(False)
|
|
102
|
+
axes.spines["bottom"].set_color(grid)
|
|
103
|
+
axes.tick_params(length=0, pad=8)
|
|
104
|
+
axes.set_xlim(past[0], future[-1])
|
|
105
|
+
axes.plot(past, history[index], color=white, linewidth=1.7, label="History")
|
|
106
|
+
for lo, hi, alpha in [(0, 8, 0.22), (1, 7, 0.20), (2, 6, 0.18), (3, 5, 0.16)]:
|
|
107
|
+
if available[lo] and available[hi]:
|
|
108
|
+
axes.fill_between(
|
|
109
|
+
future,
|
|
110
|
+
prediction[index, columns[lo]],
|
|
111
|
+
prediction[index, columns[hi]],
|
|
112
|
+
color=teal,
|
|
113
|
+
alpha=alpha,
|
|
114
|
+
linewidth=0,
|
|
115
|
+
label="10–90% quantiles" if lo == 0 else None,
|
|
116
|
+
)
|
|
117
|
+
median = prediction[index, columns[4]]
|
|
118
|
+
axes.plot(
|
|
119
|
+
future, median, color="#76c6b3", linewidth=2.4, label="Median forecast", zorder=4
|
|
120
|
+
)
|
|
121
|
+
axes.plot(
|
|
122
|
+
[past[-1], future[0]],
|
|
123
|
+
[float(history[index, -1]), float(median[0])],
|
|
124
|
+
color="#76c6b3",
|
|
125
|
+
linewidth=1.5,
|
|
126
|
+
linestyle=":",
|
|
127
|
+
)
|
|
128
|
+
axes.set_ylabel(ylabel)
|
|
129
|
+
if count > 1:
|
|
130
|
+
axes.set_title(f"Series {index + 1}", color=white)
|
|
131
|
+
low, high = axes.get_ylim()
|
|
132
|
+
if low < 0 < high:
|
|
133
|
+
axes.axhline(0, color=muted, alpha=0.3, linewidth=0.8)
|
|
134
|
+
if timestamps is not None:
|
|
135
|
+
if times[-1] - times[0] <= pd.Timedelta(days=14):
|
|
136
|
+
axes.xaxis.set_major_locator(mdates.DayLocator())
|
|
137
|
+
axes.xaxis.set_major_formatter(mdates.DateFormatter("%a %d %b"))
|
|
138
|
+
else:
|
|
139
|
+
locator = mdates.AutoDateLocator()
|
|
140
|
+
axes.xaxis.set_major_locator(locator)
|
|
141
|
+
axes.xaxis.set_major_formatter(mdates.ConciseDateFormatter(locator))
|
|
142
|
+
axes.legend(
|
|
143
|
+
loc="upper left",
|
|
144
|
+
frameon=True,
|
|
145
|
+
facecolor=background,
|
|
146
|
+
edgecolor=grid,
|
|
147
|
+
framealpha=0.95,
|
|
148
|
+
fontsize=15,
|
|
149
|
+
labelcolor=white,
|
|
150
|
+
borderpad=0.9,
|
|
151
|
+
)
|
|
152
|
+
if timestamps is None:
|
|
153
|
+
panels[-1, 0].set_xlabel("Time step relative to forecast origin")
|
|
154
|
+
if output is not None:
|
|
155
|
+
path = Path(output).expanduser()
|
|
156
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
figure.savefig(path, facecolor=figure.get_facecolor())
|
|
158
|
+
return figure
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Bundled third-party inference kernels."""
|