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.
Files changed (30) hide show
  1. continuousintelligence/__init__.py +9 -0
  2. continuousintelligence/_data/NOTICE +16 -0
  3. continuousintelligence/_data/bike_sharing.csv +145 -0
  4. continuousintelligence/_example.py +20 -0
  5. continuousintelligence/_loader.py +140 -0
  6. continuousintelligence/_plot.py +158 -0
  7. continuousintelligence/_vendor/__init__.py +1 -0
  8. continuousintelligence/_vendor/mamba3/LICENSE +201 -0
  9. continuousintelligence/_vendor/mamba3/__init__.py +6 -0
  10. continuousintelligence/_vendor/mamba3/angle_dt.py +213 -0
  11. continuousintelligence/_vendor/mamba3/layernorm_gated.py +115 -0
  12. continuousintelligence/_vendor/mamba3/mamba3_mimo_fwd.py +483 -0
  13. continuousintelligence/_vendor/mamba3/mamba3_mimo_utils.py +112 -0
  14. continuousintelligence/_vendor/mamba3/utils.py +25 -0
  15. continuousintelligence/ci_proto/__init__.py +5 -0
  16. continuousintelligence/ci_proto/_calendar.py +153 -0
  17. continuousintelligence/ci_proto/_checkpoint.py +106 -0
  18. continuousintelligence/ci_proto/_cuda.py +135 -0
  19. continuousintelligence/ci_proto/_forecast.py +708 -0
  20. continuousintelligence/ci_proto/_groups.py +198 -0
  21. continuousintelligence/ci_proto/_mamba.py +190 -0
  22. continuousintelligence/ci_proto/_model.py +489 -0
  23. continuousintelligence-0.1.0.dist-info/METADATA +29 -0
  24. continuousintelligence-0.1.0.dist-info/RECORD +30 -0
  25. continuousintelligence-0.1.0.dist-info/WHEEL +5 -0
  26. continuousintelligence-0.1.0.dist-info/licenses/LICENCE +95 -0
  27. continuousintelligence-0.1.0.dist-info/licenses/NOTICE +33 -0
  28. continuousintelligence-0.1.0.dist-info/licenses/src/continuousintelligence/_data/NOTICE +16 -0
  29. continuousintelligence-0.1.0.dist-info/licenses/src/continuousintelligence/_vendor/mamba3/LICENSE +201 -0
  30. 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."""