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,198 @@
1
+ """Group context: what the other series of an evaluation window add to each series' forecast.
2
+
3
+ All three functions take the univariate series of one window, each as its observed history
4
+ (``histories``) and that history's epoch-ns timestamps (``stamps``), in the window's order. Every
5
+ value they return comes from those histories, and a series only pools with series whose history
6
+ ends at the same timestamp, its forecast origin, so no value after the origin is used.
7
+
8
+ - :func:`static_group_rows` gives each series past covariate rows: the mean of its static
9
+ group's other z-scored histories, as Chronos-2's groups pool series by their metadata.
10
+ - :func:`joint_neighbour_rows` gives each series extra target rows: the series whose z-scored
11
+ histories correlate best with its own, forecast jointly with it.
12
+ - :func:`chunk_groups` splits the window into chunks that are forecast jointly, every series a
13
+ target, as t0-gamma's ``cross_inference`` and Chronos-2's ``cross_learning`` do on fev-bench.
14
+
15
+ The z-scores are taken on a grid of the window's last ``span`` distinct timestamps. Each row
16
+ these functions return covers its series' own history timestamps and is missing where no value
17
+ is available.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from collections.abc import Sequence
23
+ from typing import Final
24
+
25
+ import numpy as np
26
+ from numpy.typing import NDArray
27
+
28
+ # The timestamps that z-scores and correlations are taken over.
29
+ SPAN: Final = 2048
30
+
31
+ # Two histories correlate only over at least this many common steps.
32
+ _MIN_OVERLAP: Final = 32
33
+
34
+ # A window whose largest origin group holds more series is left without joint neighbours, whose
35
+ # correlation matrix would grow with the square of the group.
36
+ _MAX_JOINT_GROUP: Final = 8000
37
+
38
+
39
+ def _codes(values: Sequence[object]) -> NDArray[np.int64]:
40
+ """Number each distinct value; a group's members share a number."""
41
+ return np.unique(np.asarray(values), return_inverse=True)[1].astype(np.int64)
42
+
43
+
44
+ def _origins(stamps: Sequence[NDArray[np.int64]]) -> NDArray[np.int64]:
45
+ """Number each series by the last timestamp of its history, its forecast origin."""
46
+ return _codes([int(s[-1]) if s.size else -1 for s in stamps])
47
+
48
+
49
+ def _z_scores(
50
+ histories: Sequence[NDArray[np.float64]], stamps: Sequence[NDArray[np.int64]], span: int
51
+ ) -> tuple[NDArray[np.int64], NDArray[np.float32]]:
52
+ """Return the timestamp grid and each history z-scored onto it.
53
+
54
+ A history without spread stays missing.
55
+ """
56
+ grid = np.unique(np.concatenate([s[-span:] for s in stamps]))[-span:]
57
+ z = np.full((len(histories), grid.size), np.nan, dtype=np.float32)
58
+ for i, (row, s) in enumerate(zip(histories, stamps, strict=True)):
59
+ with np.errstate(all="ignore"):
60
+ scale = np.nanstd(row)
61
+ if not np.isfinite(scale) or scale <= 0:
62
+ continue
63
+ at = np.searchsorted(grid, s)
64
+ ok = (at < grid.size) & (grid[np.minimum(at, grid.size - 1)] == s)
65
+ z[i, at[ok]] = ((row[ok] - np.nanmean(row)) / scale).astype(np.float32)
66
+ return grid, z
67
+
68
+
69
+ def _on_own_stamps(
70
+ values: NDArray[np.float64], grid: NDArray[np.int64], stamps: NDArray[np.int64]
71
+ ) -> NDArray[np.float64]:
72
+ """Read a grid row at a series' own history timestamps; missing off the grid."""
73
+ if (
74
+ values.shape == stamps.shape
75
+ and np.array_equal(grid, stamps)
76
+ and np.all(grid[1:] > grid[:-1])
77
+ ):
78
+ # Duplicate timestamps must retain searchsorted's first-occurrence semantics.
79
+ return values.copy()
80
+ at = np.searchsorted(grid, stamps)
81
+ ok = (at < grid.size) & (grid[np.minimum(at, grid.size - 1)] == stamps)
82
+ row = np.full(stamps.size, np.nan)
83
+ row[ok] = values[at[ok]]
84
+ return row
85
+
86
+
87
+ def static_group_rows(
88
+ histories: Sequence[NDArray[np.float64]],
89
+ stamps: Sequence[NDArray[np.int64]],
90
+ statics: Sequence[Sequence[object]],
91
+ columns: int,
92
+ span: int = SPAN,
93
+ ) -> list[NDArray[np.float64]] | None:
94
+ """Return each series' static-group rows, one per grouping column, or ``None`` if none groups.
95
+
96
+ ``statics`` holds each static column's value for every series. A column groups the window
97
+ when it has at least two distinct values, fewer than the window's series, and a median group
98
+ size of at least three. The ``columns`` of them with the most groups are kept, ties in column
99
+ order. Each gives every series one row: the mean of its same-origin group's other z-scored
100
+ histories. A missing value is a group of its own. A window of fewer than three series gets
101
+ no rows.
102
+ """
103
+ if columns < 1:
104
+ raise ValueError(f"static groups need at least one column, got {columns}")
105
+ count = len(histories)
106
+ if count < 3:
107
+ return None
108
+ groupings = []
109
+ for values in statics:
110
+ codes = _codes([repr(value) for value in values])
111
+ sizes = np.bincount(codes)
112
+ if 2 <= sizes.size < count and np.median(sizes[codes]) >= 3:
113
+ groupings.append((sizes.size, codes))
114
+ chosen = sorted(groupings, key=lambda grouping: -grouping[0])[:columns]
115
+ if not chosen:
116
+ return None
117
+ grid, z = _z_scores(histories, stamps, span)
118
+ seen = np.isfinite(z)
119
+ filled = np.where(seen, z, np.float32(0.0))
120
+ origin = _origins(stamps)
121
+ rows: list[list[NDArray[np.float64]]] = [[] for _ in histories]
122
+ for _, codes in chosen:
123
+ group = _codes((codes * (origin.max() + 1) + origin).tolist())
124
+ sums_by_group = np.zeros((group.max() + 1, grid.size), dtype=np.float32)
125
+ counts_by_group = np.zeros_like(sums_by_group)
126
+ np.add.at(sums_by_group, group, filled)
127
+ np.add.at(counts_by_group, group, seen.astype(np.float32))
128
+ sums = sums_by_group[group].astype(np.float64)
129
+ counts = counts_by_group[group].astype(np.float64) - seen
130
+ with np.errstate(all="ignore"):
131
+ means = (sums - filled) / counts
132
+ means[counts < 1] = np.nan
133
+ for i, s in enumerate(stamps):
134
+ rows[i].append(_on_own_stamps(means[i], grid, s))
135
+ return [np.stack(own) for own in rows]
136
+
137
+
138
+ def joint_neighbour_rows(
139
+ histories: Sequence[NDArray[np.float64]],
140
+ stamps: Sequence[NDArray[np.int64]],
141
+ size: int,
142
+ span: int = SPAN,
143
+ ) -> list[NDArray[np.float64]] | None:
144
+ """Return each series' history followed by its ``size - 1`` neighbours', or ``None``.
145
+
146
+ A series' neighbours are the same-origin series whose z-scored histories correlate best with
147
+ its own over at least 32 common grid steps, each placed on the series' own history
148
+ timestamps. A series with fewer neighbours repeats its own history in the remaining rows. A
149
+ window of fewer than two series, or whose largest origin group holds more than 8,000, gets
150
+ none.
151
+ """
152
+ if size < 2:
153
+ raise ValueError(f"joint neighbours need a group of at least two series, got {size}")
154
+ if len(histories) < 2:
155
+ return None
156
+ _, z = _z_scores(histories, stamps, span)
157
+ seen = np.isfinite(z)
158
+ filled = np.where(seen, z, np.float32(0.0))
159
+ origin = _origins(stamps)
160
+ if np.bincount(origin).max() > _MAX_JOINT_GROUP:
161
+ return None
162
+ neighbours: list[list[int]] = [[] for _ in histories]
163
+ for group in range(origin.max() + 1):
164
+ members = np.flatnonzero(origin == group)
165
+ if members.size < 2:
166
+ continue
167
+ zf, sf = filled[members], seen[members].astype(np.float32)
168
+ overlap = sf @ sf.T
169
+ corr = (zf @ zf.T) / np.maximum(overlap, 1.0)
170
+ corr[overlap < _MIN_OVERLAP] = -np.inf
171
+ np.fill_diagonal(corr, -np.inf)
172
+ for r, member in enumerate(members.tolist()):
173
+ order = np.argsort(-corr[r], kind="stable")[: size - 1]
174
+ neighbours[member] = [int(members[o]) for o in order if np.isfinite(corr[r, o])]
175
+ out = []
176
+ for i, (own, s) in enumerate(zip(histories, stamps, strict=True)):
177
+ rows = [own] + [_on_own_stamps(histories[j], stamps[j], s) for j in neighbours[i]]
178
+ rows += [own] * (size - len(rows))
179
+ out.append(np.stack(rows))
180
+ return out
181
+
182
+
183
+ def chunk_groups(origins: Sequence[int], size: int) -> list[list[int]]:
184
+ """Split a window's series into consecutive chunks of up to ``size`` with one origin.
185
+
186
+ ``origins`` holds each series' last history timestamp. Series keep the window's order within
187
+ a chunk, and a chunk may hold one series.
188
+ """
189
+ if size < 2:
190
+ raise ValueError(f"chunks need room for at least two series, got {size}")
191
+ by_origin: dict[int, list[int]] = {}
192
+ for index, origin in enumerate(origins):
193
+ by_origin.setdefault(origin, []).append(index)
194
+ return [
195
+ members[start : start + size]
196
+ for members in by_origin.values()
197
+ for start in range(0, len(members), size)
198
+ ]
@@ -0,0 +1,190 @@
1
+ """PyTorch Mamba-3 recurrence for CPU and CUDA inference.
2
+
3
+ Adapted by Continuous Intelligence from Mamba's module and
4
+ SISO/MIMO reference calculations:
5
+ https://github.com/state-spaces/mamba/tree/a14b1dff0454a3bc27d9eb31355dc01e4b2490ec
6
+
7
+ RMS normalisation is adapted from xLSTM Large:
8
+ https://github.com/NX-AI/xlstm/blob/ab22eadbd245f293dd8dec38ed29963d73758a12/xlstm/xlstm_large/components.py
9
+
10
+ Copyright (c) 2026, Dao AI Lab, Goombalab.
11
+ Copyright 2024 NXAI GmbH and its affiliates 2024
12
+ Upstream portions are licensed under Apache-2.0; see NOTICE.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+
19
+ import torch
20
+ import torch.nn.functional as F
21
+ from einops import rearrange
22
+ from torch import Tensor, nn
23
+
24
+
25
+ class RMSNorm(nn.Module):
26
+ """RMS norm with optional float32 reductions."""
27
+
28
+ def __init__(
29
+ self,
30
+ num_features: int,
31
+ eps: float = 1e-06,
32
+ force_float32_reductions: bool = True,
33
+ use_bias: bool = False,
34
+ ) -> None:
35
+ super().__init__()
36
+ self.eps = eps
37
+ self.force_float32_reductions = force_float32_reductions
38
+ self.weight = nn.Parameter(torch.ones(num_features))
39
+ self.bias = nn.Parameter(torch.zeros(num_features)) if use_bias else None
40
+
41
+ def forward(self, x: Tensor) -> Tensor:
42
+ in_dtype = x.dtype
43
+ if self.force_float32_reductions:
44
+ x = x.float()
45
+ x = x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
46
+ x = x.to(in_dtype)
47
+ x = x * self.weight
48
+ if self.bias is not None:
49
+ x = x + self.bias
50
+ return x
51
+
52
+
53
+ def _rotate_pairs(x: Tensor, cos: Tensor, sin: Tensor, interleaved: bool) -> Tensor:
54
+ """Rotate pairs of the last axis; angles beyond ``cos`` leave their pairs unrotated."""
55
+ if interleaved:
56
+ pairs = x.view(*x.shape[:-1], -1, 2)
57
+ first, second = (pairs[..., 0], pairs[..., 1])
58
+ else:
59
+ half = x.shape[-1] // 2
60
+ first, second = (x[..., :half], x[..., half:])
61
+ if cos.shape[-1] < first.shape[-1]:
62
+ pad = first.shape[-1] - cos.shape[-1]
63
+ cos = F.pad(cos, (0, pad), value=1.0)
64
+ sin = F.pad(sin, (0, pad), value=0.0)
65
+ rotated_first = first * cos - second * sin
66
+ rotated_second = first * sin + second * cos
67
+ if interleaved:
68
+ return torch.stack([rotated_first, rotated_second], dim=-1).view_as(x)
69
+ return torch.cat([rotated_first, rotated_second], dim=-1)
70
+
71
+
72
+ class Mamba3Reference(nn.Module):
73
+ """Mamba-3 block evaluated step by step in plain PyTorch."""
74
+
75
+ def __init__(
76
+ self,
77
+ d_model: int,
78
+ d_state: int = 128,
79
+ expand: int = 2,
80
+ headdim: int = 64,
81
+ is_mimo: bool = False,
82
+ mimo_rank: int = 4,
83
+ ) -> None:
84
+ super().__init__()
85
+ if type(d_state) is not int or d_state < 4 or d_state % 2:
86
+ raise ValueError("mamba_state_dim must be an even integer of at least 4")
87
+ self.d_state = d_state
88
+ self.is_mimo = is_mimo
89
+ self.mimo_rank = mimo_rank if is_mimo else 1
90
+ self.d_inner = expand * d_model
91
+ if self.d_inner % headdim != 0:
92
+ raise ValueError(f"expand * d_model {self.d_inner} not divisible by headdim {headdim}")
93
+ self.headdim = headdim
94
+ self.nheads = self.d_inner // headdim
95
+ split = d_state // 2 - d_state // 2 % 2
96
+ self.num_rope_angles = split // 2
97
+ bc = self.d_state * self.mimo_rank
98
+ d_in_proj = 2 * self.d_inner + 2 * bc + 3 * self.nheads + self.num_rope_angles
99
+ self.in_proj = nn.Linear(d_model, d_in_proj, bias=False)
100
+ dt = torch.exp(
101
+ torch.rand(self.nheads) * (math.log(0.1) - math.log(0.001)) + math.log(0.001)
102
+ ).clamp(min=1e-4)
103
+ self.dt_bias = nn.Parameter(dt + torch.log(-torch.expm1(-dt)))
104
+ self.B_bias = nn.Parameter(torch.ones(self.nheads, self.mimo_rank, d_state))
105
+ self.C_bias = nn.Parameter(torch.ones(self.nheads, self.mimo_rank, d_state))
106
+ self.B_norm = RMSNorm(d_state, eps=1e-05)
107
+ self.C_norm = RMSNorm(d_state, eps=1e-05)
108
+ if is_mimo:
109
+ shape = (self.nheads, self.mimo_rank, headdim)
110
+ self.mimo_x = nn.Parameter(torch.ones(shape) / self.mimo_rank)
111
+ self.mimo_z = nn.Parameter(torch.ones(shape))
112
+ self.mimo_o = nn.Parameter(torch.ones(shape) / self.mimo_rank)
113
+ self.D = nn.Parameter(torch.ones(self.nheads))
114
+ self.out_proj = nn.Linear(self.d_inner, d_model, bias=False)
115
+
116
+ def forward(self, u: Tensor) -> Tensor:
117
+ batch, seq, _ = u.shape
118
+ bc = self.d_state * self.mimo_rank
119
+ z, x, B, C, dd_dt, dd_A, trap, angles = torch.split(
120
+ self.in_proj(u),
121
+ [
122
+ self.d_inner,
123
+ self.d_inner,
124
+ bc,
125
+ bc,
126
+ self.nheads,
127
+ self.nheads,
128
+ self.nheads,
129
+ self.num_rope_angles,
130
+ ],
131
+ dim=-1,
132
+ )
133
+ z = rearrange(z, "b l (h p) -> b l h p", p=self.headdim).to(torch.float32)
134
+ x = rearrange(x, "b l (h p) -> b l h p", p=self.headdim).to(torch.float32)
135
+ B = rearrange(B, "b l (r g n) -> b l r g n", r=self.mimo_rank, g=1)
136
+ C = rearrange(C, "b l (r g n) -> b l r g n", r=self.mimo_rank, g=1)
137
+ A = torch.clamp(-F.softplus(dd_A.to(torch.float32)), max=-1e-4)
138
+ DT = F.softplus(dd_dt.to(torch.float32) + self.dt_bias)
139
+ alpha = torch.exp(A * DT)
140
+ gate = torch.sigmoid(trap.to(torch.float32))
141
+ beta = (1.0 - gate) * DT * alpha
142
+ gamma = gate * DT
143
+ k = self.B_norm(B).expand(-1, -1, -1, self.nheads, -1).to(torch.float32)
144
+ q = self.C_norm(C).expand(-1, -1, -1, self.nheads, -1).to(torch.float32)
145
+ k = k + rearrange(self.B_bias, "h r n -> r h n")
146
+ q = q + rearrange(self.C_bias, "h r n -> r h n")
147
+ turns = torch.tanh(angles.to(torch.float32))[:, :, None, :] * DT[..., None] * math.pi
148
+ cumulative = torch.cumsum(turns, dim=1)
149
+ cos, sin = (torch.cos(cumulative)[:, :, None], torch.sin(cumulative)[:, :, None])
150
+ q = _rotate_pairs(q, cos, sin, interleaved=not self.is_mimo)
151
+ k = _rotate_pairs(k, cos, sin, interleaved=not self.is_mimo)
152
+ if self.is_mimo:
153
+ v = torch.einsum("blhp,hrp->blrhp", x, self.mimo_x.to(torch.float32))
154
+ gates = torch.einsum("blhp,hrp->blrhp", z, self.mimo_z.to(torch.float32))
155
+ else:
156
+ v, gates = (x[:, :, None], z[:, :, None])
157
+ state = u.new_zeros((batch, self.nheads, self.headdim, self.d_state), dtype=torch.float32)
158
+ previous = torch.zeros_like(state)
159
+ outputs = []
160
+ for t in range(seq):
161
+ current = torch.einsum("brhn,brhp->bhpn", k[:, t], v[:, t])
162
+ state = (
163
+ alpha[:, t, :, None, None] * state
164
+ + beta[:, t, :, None, None] * previous
165
+ + gamma[:, t, :, None, None] * current
166
+ )
167
+ previous = current
168
+ step = torch.einsum("bhpn,brhn->brhp", state, q[:, t])
169
+ step = step + self.D[None, None, :, None] * v[:, t]
170
+ step = step * F.silu(gates[:, t])
171
+ if self.is_mimo:
172
+ step = torch.einsum("brhp,hrp->bhp", step, self.mimo_o.to(torch.float32))
173
+ else:
174
+ step = step[:, 0]
175
+ outputs.append(step)
176
+ y = rearrange(torch.stack(outputs, dim=1), "b l h p -> b l (h p)")
177
+ return self.out_proj(y.to(u.dtype))
178
+
179
+
180
+ class Mamba3(Mamba3Reference):
181
+ """Checkpoint-compatible cell with fused CUDA inference when configured by the loader."""
182
+
183
+ fused = False
184
+
185
+ def forward(self, u: Tensor) -> Tensor:
186
+ if self.fused and u.is_cuda and not torch.is_grad_enabled():
187
+ from ._cuda import mamba3_forward
188
+
189
+ return mamba3_forward(self, u)
190
+ return super().forward(u)