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,153 @@
1
+ """The calendar covariates used by ci-proto training and inference."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ from numpy.typing import NDArray
10
+
11
+ _DAY_NS = 86_400 * 10**9
12
+ _HOUR_NS = 3_600 * 10**9
13
+ _HOLIDAY_DATES = ((1, 1), (12, 24), (12, 25), (12, 26), (12, 31))
14
+ _LEGACY_ALIASES = {
15
+ "T": "min",
16
+ "H": "h",
17
+ "S": "s",
18
+ "L": "ms",
19
+ "U": "us",
20
+ "N": "ns",
21
+ "M": "ME",
22
+ "Q": "QE",
23
+ "A": "YE",
24
+ "Y": "YE",
25
+ }
26
+
27
+
28
+ def frequency_step_ns(freq: str | None) -> int:
29
+ """Return the step of a pandas frequency in nanoseconds, or ``0`` for none or an irregular one.
30
+
31
+ A month counts 30 days and a quarter 91, so they select the monthly and quarterly calendar
32
+ rows. A step of several weeks, months or quarters returns ``0``.
33
+ """
34
+ if not freq:
35
+ return 0
36
+ try:
37
+ offset = pd.tseries.frequencies.to_offset(freq)
38
+ except ValueError:
39
+ match = re.fullmatch(r"(\d*)([A-Za-z]+)(-.*)?", freq)
40
+ if match is None or match[2] not in _LEGACY_ALIASES:
41
+ return 0
42
+ offset = pd.tseries.frequencies.to_offset(
43
+ match[1] + _LEGACY_ALIASES[match[2]] + (match[3] or "")
44
+ )
45
+ if isinstance(offset, pd.offsets.Day):
46
+ return int(offset.n) * _DAY_NS
47
+ for kind, days in (
48
+ (pd.offsets.Week, 7),
49
+ (pd.offsets.MonthBegin, 30),
50
+ (pd.offsets.MonthEnd, 30),
51
+ (pd.offsets.QuarterBegin, 91),
52
+ (pd.offsets.QuarterEnd, 91),
53
+ ):
54
+ if isinstance(offset, kind):
55
+ return days * _DAY_NS if offset.n == 1 else 0
56
+ if isinstance(offset, pd.offsets.Tick):
57
+ return int(pd.Timedelta(offset).value)
58
+ return 0
59
+
60
+
61
+ def _easter_day(year: int) -> int:
62
+ """Days since 1970-01-01 of Easter Sunday, by the anonymous Gregorian algorithm."""
63
+ a, b, c = year % 19, year // 100, year % 100
64
+ d, e = b // 4, b % 4
65
+ g = (b - (b + 8) // 25 + 1) // 3
66
+ h = (19 * a + b - d - g + 15) % 30
67
+ i, k = c // 4, c % 4
68
+ w = (32 + 2 * e + 2 * i - h - k) % 7
69
+ m = (a + 11 * h + 22 * w) // 451
70
+ month, day = divmod(h + w - 7 * m + 114, 31)
71
+ return int(np.datetime64(f"{year:04d}-{month:02d}-{day + 1:02d}").astype(np.int64))
72
+
73
+
74
+ def _holiday_flags(days: NDArray[np.int64]) -> NDArray[np.bool_]:
75
+ """Whether each day (days since 1970-01-01) is a holiday.
76
+
77
+ The holidays are New Year's Day, Christmas Eve to Boxing Day, New Year's Eve, Good Friday,
78
+ Easter Monday and US Thanksgiving; the movable ones only for years 1800 to 2300.
79
+ """
80
+ dates = days.astype("datetime64[D]")
81
+ months = dates.astype("datetime64[M]").astype(np.int64) % 12 + 1
82
+ day_of_month = (dates - dates.astype("datetime64[M]")).astype(np.int64) + 1
83
+ flags = np.zeros(days.shape, dtype=bool)
84
+ for month, day in _HOLIDAY_DATES:
85
+ flags |= (months == month) & (day_of_month == day)
86
+ years = dates.astype("datetime64[Y]").astype(np.int64) + 1970
87
+ for year in np.unique(years).tolist():
88
+ if not 1800 <= year <= 2300:
89
+ continue
90
+ easter = _easter_day(year)
91
+ november = int(np.datetime64(f"{year:04d}-11-01").astype(np.int64))
92
+ # The fourth Thursday; 1970-01-01, day 0, was a Thursday.
93
+ thanksgiving = november + (3 - (november + 3) % 7) % 7 + 21
94
+ flags |= np.isin(days, (easter - 2, easter + 1, thanksgiving))
95
+ return flags
96
+
97
+
98
+ def _business_days(timestamps: NDArray[np.int64], step_ns: int) -> NDArray[np.float32] | None:
99
+ """Return the weekdays that are not holidays in each step's month or quarter.
100
+
101
+ Monthly steps (28 to 31 days) count their calendar month and quarterly steps (89 to 92
102
+ days) their calendar quarter; any other step returns ``None``.
103
+ """
104
+ months = (timestamps // _DAY_NS).astype("datetime64[D]").astype("datetime64[M]")
105
+ if 28 * _DAY_NS <= step_ns <= 31 * _DAY_NS:
106
+ starts = months
107
+ ends = months + np.timedelta64(1, "M")
108
+ elif 89 * _DAY_NS <= step_ns <= 92 * _DAY_NS:
109
+ starts = months - (months.astype(np.int64) % 3).astype("timedelta64[M]")
110
+ ends = starts + np.timedelta64(3, "M")
111
+ else:
112
+ return None
113
+ first, last = starts.astype("datetime64[D]"), ends.astype("datetime64[D]")
114
+ span = np.arange(first.min(), last.max(), dtype="datetime64[D]")
115
+ holidays = span[_holiday_flags(span.astype(np.int64))]
116
+ return np.busday_count(first, last, holidays=holidays).astype(np.float32)
117
+
118
+
119
+ def calendar_channels(timestamps: NDArray[np.int64], step_ns: int) -> NDArray[np.float32]:
120
+ """``[channels, time]`` calendar features of a series sampled every ``step_ns``.
121
+
122
+ Steps that divide the day give hour of day (sub-daily steps only), day of week (Monday
123
+ ``0``), day of year (``0``-``365``) and a holiday flag; weekly steps give week of year
124
+ (``0``-``52``) and whether a holiday falls within three days; monthly steps (28 to 31 days)
125
+ give month of year (``0``-``11``); quarterly steps (89 to 92 days) give the quarter
126
+ (``0``-``3``). Both also include the period's business-day count. Any other step gives
127
+ no channels.
128
+ """
129
+ days = timestamps // _DAY_NS
130
+ dates = days.astype("datetime64[D]")
131
+ day_of_year = (dates - dates.astype("datetime64[Y]")).astype(np.int64)
132
+ rows: list[NDArray[np.int64] | NDArray[np.bool_]]
133
+ if 0 < step_ns <= _DAY_NS and _DAY_NS % step_ns == 0:
134
+ # 1970-01-01, day 0, was a Thursday.
135
+ rows = [(days + 3) % 7, day_of_year, _holiday_flags(days)]
136
+ if step_ns < _DAY_NS:
137
+ rows.insert(0, (timestamps % _DAY_NS) // _HOUR_NS)
138
+ elif step_ns == 7 * _DAY_NS:
139
+ window = (days[:, None] + np.arange(-3, 4)).ravel()
140
+ rows = [day_of_year // 7, _holiday_flags(window).reshape(days.shape[0], 7).any(axis=1)]
141
+ elif 28 * _DAY_NS <= step_ns <= 31 * _DAY_NS:
142
+ rows = [
143
+ dates.astype("datetime64[M]").astype(np.int64) % 12,
144
+ _business_days(timestamps, step_ns),
145
+ ]
146
+ elif 89 * _DAY_NS <= step_ns <= 92 * _DAY_NS:
147
+ rows = [
148
+ (dates.astype("datetime64[M]").astype(np.int64) % 12) // 3,
149
+ _business_days(timestamps, step_ns),
150
+ ]
151
+ else:
152
+ return np.empty((0, timestamps.shape[0]), dtype=np.float32)
153
+ return np.stack([row.astype(np.float32) for row in rows])
@@ -0,0 +1,106 @@
1
+ """Reconstruct ci-proto weights, calibration and inference backend."""
2
+
3
+ import json
4
+ import math
5
+ from pathlib import Path
6
+
7
+ import torch
8
+
9
+ from ._forecast import Forecaster
10
+ from ._model import Model
11
+
12
+
13
+ def _load_tta(path: str | Path) -> dict[str, float]:
14
+ booleans = {
15
+ "postprocess",
16
+ "tta_diff",
17
+ "tta_sign_flip",
18
+ "tta_running_totals",
19
+ "target_encode_categoricals",
20
+ "clip_nonnegative",
21
+ "integer_round",
22
+ "bfloat16",
23
+ "calendar_covariates",
24
+ }
25
+ constants = {
26
+ "trend_threshold",
27
+ "trend_min_r2",
28
+ "trend_window",
29
+ "diff_band_exponent",
30
+ "diff_band_scale",
31
+ "raw_band_exponent",
32
+ "raw_band_scale",
33
+ "differencing_max_horizon",
34
+ "static_groups",
35
+ "joint_neighbours",
36
+ "chunk_joint",
37
+ }
38
+ required = booleans | constants
39
+ raw = json.loads(Path(path).read_text())
40
+ if not isinstance(raw, dict):
41
+ raise ValueError("tta config must contain a JSON object")
42
+ missing = required - raw.keys()
43
+ unknown = raw.keys() - required
44
+ if missing or unknown:
45
+ raise ValueError(
46
+ f"invalid tta fields: missing={sorted(missing)}, unknown={sorted(unknown)}"
47
+ )
48
+ result = {}
49
+ for key in booleans:
50
+ if type(raw[key]) is not bool:
51
+ raise ValueError(f"{key} must be a boolean")
52
+ result[key] = raw[key]
53
+ for key in constants:
54
+ value = raw[key]
55
+ if key in ("differencing_max_horizon", "static_groups", "joint_neighbours", "chunk_joint"):
56
+ valid = (
57
+ type(value) is int
58
+ and value >= (1 if key == "differencing_max_horizon" else 0)
59
+ and not (key in ("joint_neighbours", "chunk_joint") and value == 1)
60
+ )
61
+ else:
62
+ if type(value) not in (int, float) or not math.isfinite(value):
63
+ raise ValueError(f"{key} must be a finite number")
64
+ valid = value >= 0 if key.endswith("exponent") else value > 0
65
+ if key in ("trend_min_r2", "trend_window"):
66
+ valid = 0 < value <= 1
67
+ value = float(value)
68
+ if not valid:
69
+ raise ValueError(f"invalid {key}: {value}")
70
+ result[key] = value
71
+ return result
72
+
73
+
74
+ def from_checkpoint(payload: object, tta_config: str | Path, device: torch.device) -> Forecaster:
75
+ """Build ci-proto from a loaded checkpoint and its calibration."""
76
+ tta = _load_tta(tta_config)
77
+ metadata = payload.get("hyper_parameters") if isinstance(payload, dict) else None
78
+ if not isinstance(metadata, dict) or not isinstance(metadata.get("model"), dict):
79
+ raise ValueError("checkpoint must contain a hyper_parameters.model mapping")
80
+ try:
81
+ model = Model(metadata["model"])
82
+ except (KeyError, AttributeError, TypeError, ZeroDivisionError, OverflowError) as error:
83
+ raise ValueError(f"invalid checkpoint model configuration: {error}") from error
84
+ state = payload.get("state_dict")
85
+ if not isinstance(state, dict) or not state or any(not isinstance(key, str) for key in state):
86
+ raise ValueError("checkpoint must contain a nonempty state_dict with string keys")
87
+ if all(key.startswith("model.") for key in state):
88
+ state = {key.removeprefix("model."): value for key, value in state.items()}
89
+ model.load_state_dict(state, strict=True)
90
+ levels = model.quantiles
91
+ if (
92
+ len(levels) % 2 == 0
93
+ or not torch.isclose(levels[len(levels) // 2], torch.tensor(0.5), atol=1e-6, rtol=0)
94
+ or not torch.allclose(
95
+ levels,
96
+ 1 - levels.flip(0),
97
+ atol=1e-6,
98
+ rtol=0,
99
+ )
100
+ ):
101
+ raise ValueError("ci-proto TTA requires symmetric quantiles centred on the median")
102
+ model = model.to(device).eval()
103
+ from ._cuda import configure_backend
104
+
105
+ configure_backend(model)
106
+ return Forecaster(model, tta)
@@ -0,0 +1,135 @@
1
+ """Automatic selection and inference wrapper for the bundled Mamba-3 MIMO kernels.
2
+
3
+ Includes Mamba-3 forward integration adapted by Continuous Intelligence:
4
+ https://github.com/state-spaces/mamba/tree/a14b1dff0454a3bc27d9eb31355dc01e4b2490ec
5
+
6
+ Copyright (c) 2026, Dao AI Lab, Goombalab.
7
+ Upstream portions are licensed under Apache-2.0; see NOTICE.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import warnings
13
+
14
+ import torch
15
+ import torch.nn.functional as F
16
+ from einops import rearrange
17
+ from torch import Tensor, nn
18
+
19
+ from ._mamba import Mamba3
20
+
21
+
22
+ def configure_backend(model: nn.Module) -> None:
23
+ """Enable fused inference only for supported hardware and cell geometry."""
24
+ model.backend = "torch"
25
+ device = next(model.parameters()).device
26
+ if device.type != "cuda" or torch.version.hip is not None:
27
+ return
28
+ cells = [cell for cell in model.modules() if isinstance(cell, Mamba3)]
29
+ if torch.cuda.get_device_capability(device)[0] < 8 or not cells:
30
+ return
31
+ # Float32 tiles need more shared memory than bf16. Use smaller chunks on
32
+ # Ampere and consumer GPUs.
33
+ chunk_budget = (
34
+ 64
35
+ if torch.cuda.get_device_properties(device).shared_memory_per_block_optin >= 196608
36
+ else 32
37
+ )
38
+ if any(
39
+ not cell.is_mimo
40
+ or cell.mimo_rank not in (1, 2, 4, 8)
41
+ or cell.d_state not in (32, 64, 128)
42
+ or cell.headdim not in (32, 64, 128)
43
+ or max(8, chunk_budget // cell.mimo_rank) * cell.mimo_rank > chunk_budget
44
+ for cell in cells
45
+ ):
46
+ return
47
+ try:
48
+ from .._vendor.mamba3.mamba3_mimo_fwd import mamba_mimo_forward # noqa: F401
49
+ except (ImportError, OSError) as error:
50
+ warnings.warn(
51
+ f"Fused Mamba-3 is unavailable; using portable PyTorch on CUDA: {error}",
52
+ RuntimeWarning,
53
+ stacklevel=2,
54
+ )
55
+ return
56
+ for cell in cells:
57
+ cell.chunk_size = max(8, chunk_budget // cell.mimo_rank)
58
+ cell.fused = True
59
+ model.backend = "mamba3"
60
+
61
+
62
+ def mamba3_forward(cell: Mamba3, u: Tensor) -> Tensor:
63
+ """Run the forward path, including rank-scaled chunk padding."""
64
+ from .._vendor.mamba3.angle_dt import angle_dt_fwd
65
+ from .._vendor.mamba3.layernorm_gated import _layer_norm_fwd
66
+ from .._vendor.mamba3.mamba3_mimo_fwd import mamba_mimo_forward
67
+ from .._vendor.mamba3.mamba3_mimo_utils import compute_dacs_segsum_triton
68
+
69
+ seq = u.shape[1]
70
+ chunk = cell.chunk_size
71
+ u = F.pad(u, (0, 0, 0, (-seq) % chunk))
72
+ bc = cell.d_state * cell.mimo_rank
73
+ z, x, B, C, dd_dt, dd_A, trap, angles = torch.split(
74
+ cell.in_proj(u),
75
+ [
76
+ cell.d_inner,
77
+ cell.d_inner,
78
+ bc,
79
+ bc,
80
+ cell.nheads,
81
+ cell.nheads,
82
+ cell.nheads,
83
+ cell.num_rope_angles,
84
+ ],
85
+ dim=-1,
86
+ )
87
+ x = rearrange(x, "b l (h p) -> b l h p", p=cell.headdim)
88
+ z = rearrange(z, "b l (h p) -> b l h p", p=cell.headdim)
89
+ B = rearrange(B, "b l (r g n) -> b l r g n", r=cell.mimo_rank, g=1)
90
+ C = rearrange(C, "b l (r g n) -> b l r g n", r=cell.mimo_rank, g=1)
91
+ # Keep the kernel's reduction and rounding order, including 1 / sqrt.
92
+ B = _layer_norm_fwd(
93
+ B.reshape(-1, cell.d_state),
94
+ cell.B_norm.weight,
95
+ None,
96
+ cell.B_norm.eps,
97
+ is_rms_norm=True,
98
+ )[0].view_as(B)
99
+ C = _layer_norm_fwd(
100
+ C.reshape(-1, cell.d_state),
101
+ cell.C_norm.weight,
102
+ None,
103
+ cell.C_norm.eps,
104
+ is_rms_norm=True,
105
+ )[0].view_as(C)
106
+ A = (-F.softplus(dd_A.float())).clamp(max=-1e-4)
107
+ dt = F.softplus(dd_dt + cell.dt_bias)
108
+ adt = rearrange(A * dt, "b l h -> b h l").contiguous()
109
+ dt = rearrange(dt, "b l h -> b h l").contiguous()
110
+ angles = angles[:, :, None].expand(-1, -1, cell.nheads, -1).float().contiguous()
111
+ with torch.cuda.device(u.device):
112
+ cumulative, _ = angle_dt_fwd(angles, dt, chunk_size=chunk, return_output_state=True)
113
+ da_cs, da_cs_rev, segsum = compute_dacs_segsum_triton(adt, chunk)
114
+ y, _, _ = mamba_mimo_forward(
115
+ C.contiguous(),
116
+ B.contiguous(),
117
+ x.contiguous(),
118
+ cell.C_bias,
119
+ cell.B_bias,
120
+ cell.mimo_x,
121
+ cell.mimo_o,
122
+ z.contiguous(),
123
+ cell.D,
124
+ cell.mimo_z,
125
+ cumulative,
126
+ da_cs,
127
+ da_cs_rev,
128
+ dt,
129
+ rearrange(trap, "b l h -> b h l").contiguous(),
130
+ segsum,
131
+ chunk_size=chunk,
132
+ rotary_dim_divisor=4,
133
+ dtype=x.dtype,
134
+ )
135
+ return cell.out_proj(rearrange(y, "b l h p -> b l (h p)").to(u.dtype))[:, :seq]