hypercube-cascade 1.0.0__cp313-cp313-win_amd64.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.
- hypercube_cascade/__init__.py +720 -0
- hypercube_cascade/_core.cp313-win_amd64.pyd +0 -0
- hypercube_cascade/_version.py +5 -0
- hypercube_cascade-1.0.0.dist-info/DELVEWHEEL +2 -0
- hypercube_cascade-1.0.0.dist-info/METADATA +366 -0
- hypercube_cascade-1.0.0.dist-info/RECORD +8 -0
- hypercube_cascade-1.0.0.dist-info/WHEEL +5 -0
- hypercube_cascade.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
"""HypercubeCascade: frozen etalon transit + frozen reservoir orbit + HypercubeCNN.
|
|
2
|
+
|
|
3
|
+
Static length-N fields (no intrinsic time): one etalon transit, then a short
|
|
4
|
+
reservoir orbit per sample, then a HypercubeCNN readout trained on the
|
|
5
|
+
end-state features only.
|
|
6
|
+
|
|
7
|
+
Quick start::
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import hypercube_cascade as hc
|
|
11
|
+
|
|
12
|
+
# fields: (num_samples, N) float32, labels: (num_samples,) int
|
|
13
|
+
cas = hc.Cascade(dim=7, exciter_subcube_dim=5, readout_num_outputs=6,
|
|
14
|
+
readout_task="classification", readout_epochs=80)
|
|
15
|
+
cas.fit(fields_train, labels_train)
|
|
16
|
+
pred = cas.predict_class(fields_test[0])
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# start delvewheel patch
|
|
23
|
+
def _delvewheel_patch_1_13_0():
|
|
24
|
+
import os
|
|
25
|
+
if os.path.isdir(libs_dir := os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'hypercube_cascade.libs'))):
|
|
26
|
+
os.add_dll_directory(libs_dir)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_delvewheel_patch_1_13_0()
|
|
30
|
+
del _delvewheel_patch_1_13_0
|
|
31
|
+
# end delvewheel patch
|
|
32
|
+
|
|
33
|
+
import pathlib
|
|
34
|
+
import pickle
|
|
35
|
+
|
|
36
|
+
import numpy as np
|
|
37
|
+
|
|
38
|
+
from ._core import _Cascade
|
|
39
|
+
from ._version import __version__
|
|
40
|
+
|
|
41
|
+
__all__ = ["Cascade", "__version__"]
|
|
42
|
+
|
|
43
|
+
# Valid hypercube dimensions (matches C++ Cascade constructor [5, 12] check).
|
|
44
|
+
_DIM_MIN = 5
|
|
45
|
+
_DIM_MAX = 12
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _to_float32(arr):
|
|
49
|
+
"""Ensure array is C-contiguous float32."""
|
|
50
|
+
return np.ascontiguousarray(arr, dtype=np.float32)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _to_int32(arr):
|
|
54
|
+
"""Ensure array is C-contiguous int32 (labels)."""
|
|
55
|
+
return np.ascontiguousarray(arr, dtype=np.int32)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Cascade:
|
|
59
|
+
"""HypercubeCascade product: field → etalon transit → reservoir orbit → HCNN.
|
|
60
|
+
|
|
61
|
+
One cube dimension serves all three stages: N = 2^dim is the field length,
|
|
62
|
+
the transit output length, and the feature length. Each sample is a full
|
|
63
|
+
length-N field (you pack domain data on the host). A **map** runs one
|
|
64
|
+
frozen etalon transit, scales it by ``interstage_scale``, reloads a frozen
|
|
65
|
+
IC, drives the reservoir for T xor-re-addressed passes, and hands the end
|
|
66
|
+
state times ``readout_scale`` to the readout.
|
|
67
|
+
|
|
68
|
+
Typical lifecycle: :meth:`collect_batch` → :meth:`train` →
|
|
69
|
+
:meth:`predict` / :meth:`predict_class`, or the one-shot :meth:`fit`.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
dim : int
|
|
74
|
+
Hypercube dimension (5–12). N = 2^dim field length, all stages.
|
|
75
|
+
T : int
|
|
76
|
+
Reservoir drive-pass count per sample. Must be >= 1 (no auto value).
|
|
77
|
+
Default: 100.
|
|
78
|
+
interstage_scale : float
|
|
79
|
+
Gain on the transit output before the orbit. Finite, > 0. Default: 1.0.
|
|
80
|
+
readout_scale : float
|
|
81
|
+
Gain on the reservoir end state before the readout. Finite, > 0.
|
|
82
|
+
Default: 1.0.
|
|
83
|
+
ic_seed : int
|
|
84
|
+
Seed for the frozen episode initial condition (separate from the
|
|
85
|
+
weight seeds). Default: 1.
|
|
86
|
+
collect_threads : int
|
|
87
|
+
Workers for bulk collect / scoring: 0 = auto, 1 = serial, K = K workers.
|
|
88
|
+
exciter_seed : int
|
|
89
|
+
Exciter weight-init seed. Default matches C++ ``ExciterConfig``.
|
|
90
|
+
exciter_input_scaling : float
|
|
91
|
+
Scalar applied once to the field before the transit. Default: 0.02.
|
|
92
|
+
exciter_weight_scaling : float
|
|
93
|
+
Exciter neighbor weights are U(-1, 1) x this. Default: 0.02.
|
|
94
|
+
exciter_subcube_dim : int
|
|
95
|
+
Etalon face size; the walk covers 2^subcube_dim vertices. Valid
|
|
96
|
+
[1, dim] — **the default 6 throws below dim 6**; set it <= dim.
|
|
97
|
+
reservoir_seed : int
|
|
98
|
+
Reservoir weight-init seed. Default matches C++ ``ReservoirConfig``.
|
|
99
|
+
spectral_radius : float
|
|
100
|
+
Target spectral radius for recurrent weights. Default: 0.999.
|
|
101
|
+
reservoir_input_scaling : float
|
|
102
|
+
Reservoir input drive coefficient. Default: 0.02.
|
|
103
|
+
leak_rate : float
|
|
104
|
+
Leaky integrator; 1.0 = full replacement. Default: 1.0.
|
|
105
|
+
history_depth : int
|
|
106
|
+
Delay-line depth M in [1, 64]. Default: 16.
|
|
107
|
+
bias_scaling : float
|
|
108
|
+
Per-neuron bias scale after tanh; 0 disables. Default: 0.003.
|
|
109
|
+
verbose : bool
|
|
110
|
+
Print the reservoir construction banner. Default: False.
|
|
111
|
+
readout_num_outputs : int
|
|
112
|
+
Classes (classification) or regression width. Default: 1.
|
|
113
|
+
readout_task : str
|
|
114
|
+
``"regression"`` (default) or ``"classification"``.
|
|
115
|
+
readout_num_layers : int
|
|
116
|
+
Conv(+Pool) pairs. Default: 1. 0 = auto min(dim-2, 2).
|
|
117
|
+
readout_conv_channels : int
|
|
118
|
+
Base channel count. Default: 16.
|
|
119
|
+
readout_epochs : int
|
|
120
|
+
Batch train epochs. Default: 200.
|
|
121
|
+
readout_batch_size : int
|
|
122
|
+
Mini-batch size. Default: 32.
|
|
123
|
+
readout_lr_max : float
|
|
124
|
+
Cosine peak LR. Default: 0.0015.
|
|
125
|
+
readout_lr_min_frac : float
|
|
126
|
+
Floor as fraction of lr_max. Default: 0.01.
|
|
127
|
+
readout_lr_decay_epochs : int
|
|
128
|
+
Cosine horizon; 0 = use epochs. Default: 0.
|
|
129
|
+
readout_weight_decay : float
|
|
130
|
+
L2 on CNN weights. Default: 0.0.
|
|
131
|
+
readout_momentum : float
|
|
132
|
+
SGD momentum (ignored by Adam). Default: 0.9.
|
|
133
|
+
readout_activation : str
|
|
134
|
+
``"tanh"`` (default), ``"relu"``, ``"leaky_relu"``, or ``"none"``.
|
|
135
|
+
readout_seed : int
|
|
136
|
+
CNN weight-init seed. Default: 42.
|
|
137
|
+
readout_num_threads : int
|
|
138
|
+
HCNN pool: 0 = auto, 1 = single-threaded. Default: 0.
|
|
139
|
+
readout_restore_best_epoch : bool
|
|
140
|
+
Restore best-epoch weights after batch train. Default: True.
|
|
141
|
+
readout_best_epoch_holdout_frac : float
|
|
142
|
+
Tail hold-out for best-epoch scoring. Default: 0.0.
|
|
143
|
+
readout_use_pooling : bool
|
|
144
|
+
Antipodal pool after each conv. Default: True.
|
|
145
|
+
|
|
146
|
+
Notes
|
|
147
|
+
-----
|
|
148
|
+
This class is **not thread-safe** for concurrent public calls from multiple
|
|
149
|
+
host threads. Bulk collect / scoring parallelism is internal.
|
|
150
|
+
|
|
151
|
+
Examples
|
|
152
|
+
--------
|
|
153
|
+
>>> import numpy as np
|
|
154
|
+
>>> import hypercube_cascade as hc
|
|
155
|
+
>>> rng = np.random.default_rng(0)
|
|
156
|
+
>>> N = 32 # dim=5
|
|
157
|
+
>>> fields = rng.standard_normal((64, N), dtype=np.float32)
|
|
158
|
+
>>> labels = rng.integers(0, 3, size=64)
|
|
159
|
+
>>> cas = hc.Cascade(dim=5, exciter_subcube_dim=4, readout_num_outputs=3,
|
|
160
|
+
... readout_task="classification", readout_epochs=40,
|
|
161
|
+
... history_depth=4, T=16)
|
|
162
|
+
>>> cas.fit(fields, labels)
|
|
163
|
+
Cascade(dim=5, N=32, ...)
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
dim: int,
|
|
169
|
+
*,
|
|
170
|
+
T: int = 100,
|
|
171
|
+
interstage_scale: float = 1.0,
|
|
172
|
+
readout_scale: float = 1.0,
|
|
173
|
+
ic_seed: int = 1,
|
|
174
|
+
collect_threads: int = 0,
|
|
175
|
+
exciter_seed: int = 7934791766227647176,
|
|
176
|
+
exciter_input_scaling: float = 0.02,
|
|
177
|
+
exciter_weight_scaling: float = 0.02,
|
|
178
|
+
exciter_subcube_dim: int = 6,
|
|
179
|
+
reservoir_seed: int = 7934791766227647176,
|
|
180
|
+
spectral_radius: float = 0.999,
|
|
181
|
+
reservoir_input_scaling: float = 0.02,
|
|
182
|
+
leak_rate: float = 1.0,
|
|
183
|
+
history_depth: int = 16,
|
|
184
|
+
bias_scaling: float = 0.003,
|
|
185
|
+
verbose: bool = False,
|
|
186
|
+
readout_num_outputs: int = 1,
|
|
187
|
+
readout_task: str = "regression",
|
|
188
|
+
readout_num_layers: int = 1,
|
|
189
|
+
readout_conv_channels: int = 16,
|
|
190
|
+
readout_epochs: int = 200,
|
|
191
|
+
readout_batch_size: int = 32,
|
|
192
|
+
readout_lr_max: float = 0.0015,
|
|
193
|
+
readout_lr_min_frac: float = 0.01,
|
|
194
|
+
readout_lr_decay_epochs: int = 0,
|
|
195
|
+
readout_weight_decay: float = 0.0,
|
|
196
|
+
readout_momentum: float = 0.9,
|
|
197
|
+
readout_activation: str = "tanh",
|
|
198
|
+
readout_seed: int = 42,
|
|
199
|
+
readout_num_threads: int = 0,
|
|
200
|
+
readout_restore_best_epoch: bool = True,
|
|
201
|
+
readout_best_epoch_holdout_frac: float = 0.0,
|
|
202
|
+
readout_use_pooling: bool = True,
|
|
203
|
+
):
|
|
204
|
+
if not isinstance(dim, int) or not (_DIM_MIN <= dim <= _DIM_MAX):
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"dim must be an integer in [{_DIM_MIN}, {_DIM_MAX}], got {dim!r}"
|
|
207
|
+
)
|
|
208
|
+
if not isinstance(exciter_subcube_dim, int) or not (
|
|
209
|
+
1 <= exciter_subcube_dim <= dim
|
|
210
|
+
):
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"exciter_subcube_dim must be an integer in [1, {dim}] "
|
|
213
|
+
f"(got {exciter_subcube_dim!r}; the C++ default 6 is only "
|
|
214
|
+
f"legal when dim >= 6)"
|
|
215
|
+
)
|
|
216
|
+
if readout_task not in ("regression", "classification"):
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"readout_task must be 'regression' or 'classification', "
|
|
219
|
+
f"got {readout_task!r}"
|
|
220
|
+
)
|
|
221
|
+
if readout_activation not in ("tanh", "relu", "leaky_relu", "none"):
|
|
222
|
+
raise ValueError(
|
|
223
|
+
"readout_activation must be one of "
|
|
224
|
+
"'tanh', 'relu', 'leaky_relu', 'none' "
|
|
225
|
+
f"(got {readout_activation!r})"
|
|
226
|
+
)
|
|
227
|
+
self._verbose = verbose
|
|
228
|
+
self._ctor = {
|
|
229
|
+
"dim": dim,
|
|
230
|
+
"T": T,
|
|
231
|
+
"interstage_scale": interstage_scale,
|
|
232
|
+
"readout_scale": readout_scale,
|
|
233
|
+
"ic_seed": ic_seed,
|
|
234
|
+
"collect_threads": collect_threads,
|
|
235
|
+
"exciter_seed": exciter_seed,
|
|
236
|
+
"exciter_input_scaling": exciter_input_scaling,
|
|
237
|
+
"exciter_weight_scaling": exciter_weight_scaling,
|
|
238
|
+
"exciter_subcube_dim": exciter_subcube_dim,
|
|
239
|
+
"reservoir_seed": reservoir_seed,
|
|
240
|
+
"spectral_radius": spectral_radius,
|
|
241
|
+
"reservoir_input_scaling": reservoir_input_scaling,
|
|
242
|
+
"leak_rate": leak_rate,
|
|
243
|
+
"history_depth": history_depth,
|
|
244
|
+
"bias_scaling": bias_scaling,
|
|
245
|
+
"verbose": verbose,
|
|
246
|
+
"readout_num_outputs": readout_num_outputs,
|
|
247
|
+
"readout_task": readout_task,
|
|
248
|
+
"readout_num_layers": readout_num_layers,
|
|
249
|
+
"readout_conv_channels": readout_conv_channels,
|
|
250
|
+
"readout_epochs": readout_epochs,
|
|
251
|
+
"readout_batch_size": readout_batch_size,
|
|
252
|
+
"readout_lr_max": readout_lr_max,
|
|
253
|
+
"readout_lr_min_frac": readout_lr_min_frac,
|
|
254
|
+
"readout_lr_decay_epochs": readout_lr_decay_epochs,
|
|
255
|
+
"readout_weight_decay": readout_weight_decay,
|
|
256
|
+
"readout_momentum": readout_momentum,
|
|
257
|
+
"readout_activation": readout_activation,
|
|
258
|
+
"readout_seed": readout_seed,
|
|
259
|
+
"readout_num_threads": readout_num_threads,
|
|
260
|
+
"readout_restore_best_epoch": readout_restore_best_epoch,
|
|
261
|
+
"readout_best_epoch_holdout_frac": readout_best_epoch_holdout_frac,
|
|
262
|
+
"readout_use_pooling": readout_use_pooling,
|
|
263
|
+
}
|
|
264
|
+
self._impl = _Cascade(**self._ctor)
|
|
265
|
+
|
|
266
|
+
# ── Map (no training) ──
|
|
267
|
+
|
|
268
|
+
def run(self, x: np.ndarray) -> None:
|
|
269
|
+
"""Map one field (transit → orbit → features), no training-set append.
|
|
270
|
+
|
|
271
|
+
Updates :meth:`last_features` and the per-stage probes
|
|
272
|
+
:meth:`last_exciter` / :meth:`last_interstage` / :meth:`last_reservoir`.
|
|
273
|
+
|
|
274
|
+
Parameters
|
|
275
|
+
----------
|
|
276
|
+
x : ndarray
|
|
277
|
+
Length-N field (or shape that ravel-flattens to N). Converted to
|
|
278
|
+
float32.
|
|
279
|
+
"""
|
|
280
|
+
self._impl.run(_to_float32(np.ravel(x)))
|
|
281
|
+
|
|
282
|
+
def last_features(self) -> np.ndarray:
|
|
283
|
+
"""Features (length N) from the most recent completed map.
|
|
284
|
+
|
|
285
|
+
Updated by every map on this instance, including bulk calls
|
|
286
|
+
(last row).
|
|
287
|
+
"""
|
|
288
|
+
return self._impl.last_features()
|
|
289
|
+
|
|
290
|
+
def last_exciter(self) -> np.ndarray:
|
|
291
|
+
"""Etalon transit output from the last **serial** map (length N).
|
|
292
|
+
|
|
293
|
+
Not updated by bulk :meth:`collect_batch` / :meth:`accuracy` /
|
|
294
|
+
:meth:`r2`.
|
|
295
|
+
"""
|
|
296
|
+
return self._impl.last_exciter()
|
|
297
|
+
|
|
298
|
+
def last_interstage(self) -> np.ndarray:
|
|
299
|
+
"""Transit output × interstage_scale (the reservoir drive) from the
|
|
300
|
+
last **serial** map."""
|
|
301
|
+
return self._impl.last_interstage()
|
|
302
|
+
|
|
303
|
+
def last_reservoir(self) -> np.ndarray:
|
|
304
|
+
"""Reservoir end state (before readout_scale) from the last **serial**
|
|
305
|
+
map."""
|
|
306
|
+
return self._impl.last_reservoir()
|
|
307
|
+
|
|
308
|
+
def clear_collected(self) -> None:
|
|
309
|
+
"""Drop all samples collected for batch training."""
|
|
310
|
+
self._impl.clear_collected()
|
|
311
|
+
|
|
312
|
+
# ── Collect ──
|
|
313
|
+
|
|
314
|
+
def collect(
|
|
315
|
+
self,
|
|
316
|
+
x: np.ndarray,
|
|
317
|
+
target,
|
|
318
|
+
) -> None:
|
|
319
|
+
"""Serial collect one sample (classification label or regression target).
|
|
320
|
+
|
|
321
|
+
Parameters
|
|
322
|
+
----------
|
|
323
|
+
x : ndarray
|
|
324
|
+
Length-N field.
|
|
325
|
+
target : int or ndarray
|
|
326
|
+
Class index (classification) or length-``num_outputs`` floats
|
|
327
|
+
(regression).
|
|
328
|
+
"""
|
|
329
|
+
field = _to_float32(np.ravel(x))
|
|
330
|
+
if self._ctor["readout_task"] == "classification":
|
|
331
|
+
if isinstance(target, (bool, np.bool_)):
|
|
332
|
+
raise TypeError("class label must be an integer")
|
|
333
|
+
if isinstance(target, (int, np.integer)):
|
|
334
|
+
label = int(target)
|
|
335
|
+
else:
|
|
336
|
+
arr = np.asarray(target).ravel()
|
|
337
|
+
if arr.size != 1:
|
|
338
|
+
raise ValueError("classification target must be a single class index")
|
|
339
|
+
label = int(arr[0])
|
|
340
|
+
self._impl.collect_class(field, label)
|
|
341
|
+
else:
|
|
342
|
+
t = _to_float32(np.ravel(target))
|
|
343
|
+
self._impl.collect_reg(field, t)
|
|
344
|
+
|
|
345
|
+
def _check_bulk_fields(self, fields: np.ndarray):
|
|
346
|
+
fields = _to_float32(fields)
|
|
347
|
+
n = self.N
|
|
348
|
+
if fields.ndim == 2:
|
|
349
|
+
if fields.shape[1] != n:
|
|
350
|
+
raise ValueError(
|
|
351
|
+
f"fields.shape[1] ({fields.shape[1]}) must equal N ({n})"
|
|
352
|
+
)
|
|
353
|
+
count = int(fields.shape[0])
|
|
354
|
+
flat = np.ascontiguousarray(fields.reshape(-1))
|
|
355
|
+
elif fields.ndim == 1:
|
|
356
|
+
if fields.size % n != 0:
|
|
357
|
+
raise ValueError(
|
|
358
|
+
f"flat fields size ({fields.size}) must be a multiple of N ({n})"
|
|
359
|
+
)
|
|
360
|
+
count = int(fields.size // n)
|
|
361
|
+
flat = fields
|
|
362
|
+
else:
|
|
363
|
+
raise ValueError(
|
|
364
|
+
f"fields must be 1D or 2D, got ndim={fields.ndim}"
|
|
365
|
+
)
|
|
366
|
+
return flat, count
|
|
367
|
+
|
|
368
|
+
def _check_bulk_targets(self, targets: np.ndarray, count: int):
|
|
369
|
+
if self._ctor["readout_task"] == "classification":
|
|
370
|
+
labels = _to_int32(np.ravel(targets))
|
|
371
|
+
if labels.size != count:
|
|
372
|
+
raise ValueError(
|
|
373
|
+
f"labels length ({labels.size}) must equal sample count ({count})"
|
|
374
|
+
)
|
|
375
|
+
return labels
|
|
376
|
+
t = _to_float32(targets)
|
|
377
|
+
k = self.num_outputs
|
|
378
|
+
if t.ndim == 2:
|
|
379
|
+
if t.shape[0] != count or t.shape[1] != k:
|
|
380
|
+
raise ValueError(
|
|
381
|
+
f"targets shape must be ({count}, {k}), got {t.shape}"
|
|
382
|
+
)
|
|
383
|
+
t = np.ascontiguousarray(t.reshape(-1))
|
|
384
|
+
elif t.ndim == 1:
|
|
385
|
+
if t.size != count * k:
|
|
386
|
+
raise ValueError(
|
|
387
|
+
f"flat targets size ({t.size}) must equal "
|
|
388
|
+
f"count * num_outputs ({count * k})"
|
|
389
|
+
)
|
|
390
|
+
else:
|
|
391
|
+
raise ValueError(f"targets must be 1D or 2D, got ndim={t.ndim}")
|
|
392
|
+
return t
|
|
393
|
+
|
|
394
|
+
def collect_batch(
|
|
395
|
+
self,
|
|
396
|
+
fields: np.ndarray,
|
|
397
|
+
targets: np.ndarray,
|
|
398
|
+
) -> None:
|
|
399
|
+
"""Bulk parallel collect (appends to the training set).
|
|
400
|
+
|
|
401
|
+
Parameters
|
|
402
|
+
----------
|
|
403
|
+
fields : ndarray
|
|
404
|
+
Shape ``(count, N)`` preferred, or flat length ``count * N``.
|
|
405
|
+
targets : ndarray
|
|
406
|
+
Classification: shape ``(count,)`` integer labels.
|
|
407
|
+
Regression: shape ``(count, num_outputs)`` or flat ``count * num_outputs``.
|
|
408
|
+
"""
|
|
409
|
+
flat, count = self._check_bulk_fields(fields)
|
|
410
|
+
t = self._check_bulk_targets(targets, count)
|
|
411
|
+
if self._ctor["readout_task"] == "classification":
|
|
412
|
+
self._impl.collect_batch_class(flat, t)
|
|
413
|
+
else:
|
|
414
|
+
self._impl.collect_batch_reg(flat, t)
|
|
415
|
+
|
|
416
|
+
def fit(
|
|
417
|
+
self,
|
|
418
|
+
fields: np.ndarray,
|
|
419
|
+
targets: np.ndarray,
|
|
420
|
+
*,
|
|
421
|
+
clear: bool = True,
|
|
422
|
+
) -> "Cascade":
|
|
423
|
+
"""Collect then train the readout (one-shot).
|
|
424
|
+
|
|
425
|
+
Parameters
|
|
426
|
+
----------
|
|
427
|
+
fields : ndarray
|
|
428
|
+
Shape ``(count, N)`` length-N fields (host packing is your problem).
|
|
429
|
+
targets : ndarray
|
|
430
|
+
Labels or regression targets (see :meth:`collect_batch`).
|
|
431
|
+
clear : bool
|
|
432
|
+
If True (default), :meth:`clear_collected` first.
|
|
433
|
+
|
|
434
|
+
Returns
|
|
435
|
+
-------
|
|
436
|
+
Cascade
|
|
437
|
+
Self, for method chaining.
|
|
438
|
+
"""
|
|
439
|
+
if clear:
|
|
440
|
+
self.clear_collected()
|
|
441
|
+
self.collect_batch(fields, targets)
|
|
442
|
+
self.train()
|
|
443
|
+
return self
|
|
444
|
+
|
|
445
|
+
def train(self) -> None:
|
|
446
|
+
"""Batch-train the HCNN on all collected samples.
|
|
447
|
+
|
|
448
|
+
Does not clear the collected set — call again to continue training
|
|
449
|
+
from the current weights, or :meth:`clear_collected` first to start
|
|
450
|
+
over.
|
|
451
|
+
"""
|
|
452
|
+
self._impl.train()
|
|
453
|
+
|
|
454
|
+
# ── Inference ──
|
|
455
|
+
|
|
456
|
+
def predict(self, x: np.ndarray) -> np.ndarray:
|
|
457
|
+
"""Fresh map + readout forward.
|
|
458
|
+
|
|
459
|
+
Parameters
|
|
460
|
+
----------
|
|
461
|
+
x : ndarray
|
|
462
|
+
Length-N field.
|
|
463
|
+
|
|
464
|
+
Returns
|
|
465
|
+
-------
|
|
466
|
+
ndarray
|
|
467
|
+
Shape ``(num_outputs,)`` float32 logits / regression values.
|
|
468
|
+
"""
|
|
469
|
+
return self._impl.predict(_to_float32(np.ravel(x)))
|
|
470
|
+
|
|
471
|
+
def predict_class(self, x: np.ndarray) -> int:
|
|
472
|
+
"""Fresh map + argmax class (classification task only)."""
|
|
473
|
+
return int(self._impl.predict_class(_to_float32(np.ravel(x))))
|
|
474
|
+
|
|
475
|
+
def accuracy_on_collected(self) -> float:
|
|
476
|
+
"""Accuracy on the **collected training set** — not a test-set metric."""
|
|
477
|
+
return float(self._impl.accuracy_on_collected())
|
|
478
|
+
|
|
479
|
+
def r2_on_collected(self) -> float:
|
|
480
|
+
"""R² on the **collected training set** — not a test-set metric."""
|
|
481
|
+
return float(self._impl.r2_on_collected())
|
|
482
|
+
|
|
483
|
+
def accuracy(self, fields: np.ndarray, labels: np.ndarray) -> float:
|
|
484
|
+
"""Fresh map + accuracy on a caller-owned (held-out) set.
|
|
485
|
+
|
|
486
|
+
Maps every field through the cascade (bulk, parallel) and scores the
|
|
487
|
+
readout. Classification task only.
|
|
488
|
+
|
|
489
|
+
Parameters
|
|
490
|
+
----------
|
|
491
|
+
fields : ndarray
|
|
492
|
+
Shape ``(count, N)`` or flat ``count * N``.
|
|
493
|
+
labels : ndarray
|
|
494
|
+
Shape ``(count,)`` integer class indices.
|
|
495
|
+
"""
|
|
496
|
+
flat, count = self._check_bulk_fields(fields)
|
|
497
|
+
lab = _to_int32(np.ravel(labels))
|
|
498
|
+
if lab.size != count:
|
|
499
|
+
raise ValueError(
|
|
500
|
+
f"labels length ({lab.size}) must equal sample count ({count})"
|
|
501
|
+
)
|
|
502
|
+
return float(self._impl.accuracy(flat, lab))
|
|
503
|
+
|
|
504
|
+
def r2(self, fields: np.ndarray, targets: np.ndarray) -> float:
|
|
505
|
+
"""Fresh map + R² on a caller-owned (held-out) set.
|
|
506
|
+
|
|
507
|
+
Regression task only.
|
|
508
|
+
|
|
509
|
+
Parameters
|
|
510
|
+
----------
|
|
511
|
+
fields : ndarray
|
|
512
|
+
Shape ``(count, N)`` or flat ``count * N``.
|
|
513
|
+
targets : ndarray
|
|
514
|
+
Shape ``(count, num_outputs)`` or flat ``count * num_outputs``.
|
|
515
|
+
"""
|
|
516
|
+
flat, count = self._check_bulk_fields(fields)
|
|
517
|
+
t = self._check_bulk_targets(targets, count)
|
|
518
|
+
return float(self._impl.r2(flat, t))
|
|
519
|
+
|
|
520
|
+
# ── Properties ──
|
|
521
|
+
|
|
522
|
+
@property
|
|
523
|
+
def dim(self) -> int:
|
|
524
|
+
"""Hypercube dimension (all three stages)."""
|
|
525
|
+
return int(self._impl.dim)
|
|
526
|
+
|
|
527
|
+
@property
|
|
528
|
+
def N(self) -> int:
|
|
529
|
+
"""Field / feature length: 2^dim."""
|
|
530
|
+
return int(self._impl.N)
|
|
531
|
+
|
|
532
|
+
@property
|
|
533
|
+
def T(self) -> int:
|
|
534
|
+
"""Reservoir drive-pass count per sample."""
|
|
535
|
+
return int(self._impl.T)
|
|
536
|
+
|
|
537
|
+
@property
|
|
538
|
+
def M(self) -> int:
|
|
539
|
+
"""Delay-line depth (history_depth)."""
|
|
540
|
+
return int(self._impl.history_depth)
|
|
541
|
+
|
|
542
|
+
@property
|
|
543
|
+
def feature_size(self) -> int:
|
|
544
|
+
"""Floats per collected sample / last_features — always N."""
|
|
545
|
+
return int(self._impl.N)
|
|
546
|
+
|
|
547
|
+
@property
|
|
548
|
+
def num_collected(self) -> int:
|
|
549
|
+
"""Number of samples in the batch training set."""
|
|
550
|
+
return int(self._impl.num_collected)
|
|
551
|
+
|
|
552
|
+
@property
|
|
553
|
+
def num_outputs(self) -> int:
|
|
554
|
+
"""Readout output width."""
|
|
555
|
+
return int(self._impl.num_outputs)
|
|
556
|
+
|
|
557
|
+
@property
|
|
558
|
+
def collect_threads(self) -> int:
|
|
559
|
+
"""Configured collect-thread preference (0 = auto)."""
|
|
560
|
+
return int(self._impl.collect_threads)
|
|
561
|
+
|
|
562
|
+
@property
|
|
563
|
+
def interstage_scale(self) -> float:
|
|
564
|
+
"""Gain between transit output and reservoir drive."""
|
|
565
|
+
return float(self._impl.interstage_scale)
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
def readout_scale(self) -> float:
|
|
569
|
+
"""Gain between reservoir end state and readout."""
|
|
570
|
+
return float(self._impl.readout_scale)
|
|
571
|
+
|
|
572
|
+
@property
|
|
573
|
+
def ic_seed(self) -> int:
|
|
574
|
+
"""Frozen episode IC seed."""
|
|
575
|
+
return int(self._impl.ic_seed)
|
|
576
|
+
|
|
577
|
+
@property
|
|
578
|
+
def exciter_seed(self) -> int:
|
|
579
|
+
"""Exciter weight seed."""
|
|
580
|
+
return int(self._impl.exciter_seed)
|
|
581
|
+
|
|
582
|
+
@property
|
|
583
|
+
def exciter_input_scaling(self) -> float:
|
|
584
|
+
return float(self._impl.exciter_input_scaling)
|
|
585
|
+
|
|
586
|
+
@property
|
|
587
|
+
def exciter_weight_scaling(self) -> float:
|
|
588
|
+
return float(self._impl.exciter_weight_scaling)
|
|
589
|
+
|
|
590
|
+
@property
|
|
591
|
+
def subcube_dim(self) -> int:
|
|
592
|
+
"""Etalon face dimension."""
|
|
593
|
+
return int(self._impl.subcube_dim)
|
|
594
|
+
|
|
595
|
+
@property
|
|
596
|
+
def walk_size(self) -> int:
|
|
597
|
+
"""Vertices on one etalon out-and-back: 2^subcube_dim."""
|
|
598
|
+
return int(self._impl.walk_size)
|
|
599
|
+
|
|
600
|
+
@property
|
|
601
|
+
def reservoir_seed(self) -> int:
|
|
602
|
+
"""Reservoir weight seed."""
|
|
603
|
+
return int(self._impl.reservoir_seed)
|
|
604
|
+
|
|
605
|
+
@property
|
|
606
|
+
def spectral_radius(self) -> float:
|
|
607
|
+
"""Target spectral radius (config)."""
|
|
608
|
+
return float(self._impl.spectral_radius)
|
|
609
|
+
|
|
610
|
+
@property
|
|
611
|
+
def realized_spectral_radius(self) -> float:
|
|
612
|
+
"""Post-rescale realized spectral-radius estimate."""
|
|
613
|
+
return float(self._impl.realized_spectral_radius)
|
|
614
|
+
|
|
615
|
+
@property
|
|
616
|
+
def reservoir_input_scaling(self) -> float:
|
|
617
|
+
return float(self._impl.reservoir_input_scaling)
|
|
618
|
+
|
|
619
|
+
@property
|
|
620
|
+
def leak_rate(self) -> float:
|
|
621
|
+
return float(self._impl.leak_rate)
|
|
622
|
+
|
|
623
|
+
@property
|
|
624
|
+
def history_depth(self) -> int:
|
|
625
|
+
return int(self._impl.history_depth)
|
|
626
|
+
|
|
627
|
+
@property
|
|
628
|
+
def bias_scaling(self) -> float:
|
|
629
|
+
return float(self._impl.bias_scaling)
|
|
630
|
+
|
|
631
|
+
@property
|
|
632
|
+
def verbose(self) -> bool:
|
|
633
|
+
return bool(self._verbose)
|
|
634
|
+
|
|
635
|
+
@property
|
|
636
|
+
def readout_task(self) -> str:
|
|
637
|
+
return str(self._impl.readout_task)
|
|
638
|
+
|
|
639
|
+
@property
|
|
640
|
+
def readout_best_epoch(self) -> int:
|
|
641
|
+
"""1-based best epoch after restore_best_epoch train, else 0."""
|
|
642
|
+
return int(self._impl.readout_best_epoch)
|
|
643
|
+
|
|
644
|
+
def __repr__(self) -> str:
|
|
645
|
+
return (
|
|
646
|
+
f"Cascade(dim={self.dim}, N={self.N}, T={self.T}, "
|
|
647
|
+
f"subcube_dim={self.subcube_dim}, "
|
|
648
|
+
f"collected={self.num_collected}, outputs={self.num_outputs}, "
|
|
649
|
+
f"task={self.readout_task})"
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# ── Persistence ──
|
|
653
|
+
|
|
654
|
+
_PERSISTENCE_VERSION = 1
|
|
655
|
+
|
|
656
|
+
def __getstate__(self) -> dict:
|
|
657
|
+
"""Serialize constructor config + trained readout weights.
|
|
658
|
+
|
|
659
|
+
Collected samples are **not** saved.
|
|
660
|
+
"""
|
|
661
|
+
return {
|
|
662
|
+
"_version": self._PERSISTENCE_VERSION,
|
|
663
|
+
"ctor": dict(self._ctor),
|
|
664
|
+
"readout_state": self._impl._get_readout_state(),
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
def __setstate__(self, state: dict) -> None:
|
|
668
|
+
version = state.get("_version", 0)
|
|
669
|
+
if version > self._PERSISTENCE_VERSION:
|
|
670
|
+
raise ValueError(
|
|
671
|
+
f"Model was saved with persistence version {version}, "
|
|
672
|
+
f"but this version only supports up to "
|
|
673
|
+
f"{self._PERSISTENCE_VERSION}. Upgrade hypercube-cascade."
|
|
674
|
+
)
|
|
675
|
+
ctor = dict(state["ctor"])
|
|
676
|
+
self.__init__(**ctor)
|
|
677
|
+
self._impl._set_readout_state(state["readout_state"])
|
|
678
|
+
|
|
679
|
+
def save(self, path) -> None:
|
|
680
|
+
"""Save config + trained readout to a pickle file.
|
|
681
|
+
|
|
682
|
+
Collected samples are not saved. Prefer
|
|
683
|
+
:meth:`save_readout_hcnn_model` for portable HCNW + arch JSON.
|
|
684
|
+
"""
|
|
685
|
+
with open(pathlib.Path(path), "wb") as f:
|
|
686
|
+
pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL)
|
|
687
|
+
|
|
688
|
+
@classmethod
|
|
689
|
+
def load(cls, path) -> "Cascade":
|
|
690
|
+
"""Load a model saved by :meth:`save`.
|
|
691
|
+
|
|
692
|
+
.. warning::
|
|
693
|
+
|
|
694
|
+
Uses ``pickle.load``. Never load untrusted files.
|
|
695
|
+
"""
|
|
696
|
+
with open(pathlib.Path(path), "rb") as f:
|
|
697
|
+
obj = pickle.load(f)
|
|
698
|
+
if not isinstance(obj, cls):
|
|
699
|
+
raise TypeError(f"Expected Cascade, got {type(obj).__name__}")
|
|
700
|
+
return obj
|
|
701
|
+
|
|
702
|
+
def save_readout_hcnn_model(self, path_stem) -> None:
|
|
703
|
+
"""Export the HCNN readout as portable ``stem.hcnw`` + ``stem.arch.json``."""
|
|
704
|
+
self._impl.save_readout_hcnn_model(str(path_stem))
|
|
705
|
+
|
|
706
|
+
def load_readout_hcnn_model(self, path_stem, *, mode: str = "eval") -> None:
|
|
707
|
+
"""Load ``stem.hcnw`` (+ arch sidecar) into this instance's readout.
|
|
708
|
+
|
|
709
|
+
Parameters
|
|
710
|
+
----------
|
|
711
|
+
path_stem : str or Path
|
|
712
|
+
Path without extension.
|
|
713
|
+
mode : str
|
|
714
|
+
``"eval"`` (default) or ``"resume_train"``.
|
|
715
|
+
"""
|
|
716
|
+
self._impl.load_readout_hcnn_model(str(path_stem), mode)
|
|
717
|
+
|
|
718
|
+
def readout_arch_summary(self) -> str:
|
|
719
|
+
"""Human-readable HCNN readout architecture and parameter counts."""
|
|
720
|
+
return self._impl.readout_arch_summary()
|
|
Binary file
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Version: 1.13.0
|
|
2
|
+
Arguments: ['C:\\Users\\runneradmin\\AppData\\Local\\Temp\\cibw-run-jhn13f9f\\cp313-win_amd64\\build\\venv\\Scripts\\delvewheel', 'repair', '-w', 'C:\\Users\\runneradmin\\AppData\\Local\\Temp\\cibw-run-jhn13f9f\\cp313-win_amd64\\repaired_wheel', '-v', 'C:\\Users\\runneradmin\\AppData\\Local\\Temp\\cibw-run-jhn13f9f\\cp313-win_amd64\\built_wheel\\hypercube_cascade-1.0.0-cp313-cp313-win_amd64.whl']
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hypercube-cascade
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python bindings for HypercubeCascade: frozen etalon transit + frozen reservoir orbit + HypercubeCNN on end state
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
7
|
+
Classifier: Intended Audience :: Science/Research
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Programming Language :: C++
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Operating System :: MacOS
|
|
19
|
+
Project-URL: Homepage, https://github.com/dliptak001/HypercubeCascade
|
|
20
|
+
Project-URL: Repository, https://github.com/dliptak001/HypercubeCascade
|
|
21
|
+
Project-URL: Documentation, https://github.com/dliptak001/HypercubeCascade/blob/main/docs/Python_SDK.md
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: numpy>=1.21
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# HypercubeCascade
|
|
29
|
+
|
|
30
|
+
**HypercubeCascade** is for high-dimensional data that has no natural clock —
|
|
31
|
+
spectra, sensor frames, packed images, stills. Those are the same kinds of
|
|
32
|
+
static fields people usually feed a spatial CNN, an MLP, or a similar
|
|
33
|
+
feed-forward stack. HypercubeCascade puts **two frozen hypercube
|
|
34
|
+
preprocessors in series** in front of the CNN: first an **etalon transit**
|
|
35
|
+
(the [HypercubeEtalon](https://github.com/dliptak001/HypercubeEtalon)
|
|
36
|
+
mechanism — a deterministic wave swept across every vertex/antipode cavity of
|
|
37
|
+
the cube), then a short **reservoir orbit** (the
|
|
38
|
+
[HypercubeWTF](https://github.com/dliptak001/HypercubeWTF) mechanism — a
|
|
39
|
+
frozen recurrent core driven by re-addressing the same field for T synthetic
|
|
40
|
+
passes). A small
|
|
41
|
+
[HypercubeCNN](https://github.com/dliptak001/HypercubeCNN) head trains on the
|
|
42
|
+
**end state only**. The CNN never sees the original field — it sees what the
|
|
43
|
+
transit and the orbit leave behind.
|
|
44
|
+
|
|
45
|
+
That is the product idea: take a static field, pass it through two different
|
|
46
|
+
frozen nonlinearities, and train a spatial readout on what remains. The aim is
|
|
47
|
+
a preprocessor effective enough that the readout can be a single convolutional
|
|
48
|
+
layer with a single channel and no pooling.
|
|
49
|
+
|
|
50
|
+
This package is the **Python** surface for that product
|
|
51
|
+
(`import hypercube_cascade`).
|
|
52
|
+
Full API reference: **[docs/Python_SDK.md](https://github.com/dliptak001/HypercubeCascade/blob/main/docs/Python_SDK.md)**.
|
|
53
|
+
C++ integration guide: **[docs/CPP_SDK.md](https://github.com/dliptak001/HypercubeCascade/blob/main/docs/CPP_SDK.md)**.
|
|
54
|
+
Project home: **[github.com/dliptak001/HypercubeCascade](https://github.com/dliptak001/HypercubeCascade)**.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
<p align="center">
|
|
59
|
+
<strong>HypercubeAI ecosystem</strong><br/>
|
|
60
|
+
</p>
|
|
61
|
+
|
|
62
|
+
<p align="center">
|
|
63
|
+
<a href="https://github.com/dliptak001/HypercubeESN"><strong>HypercubeESN</strong></a>
|
|
64
|
+
·
|
|
65
|
+
<a href="https://github.com/dliptak001/HypercubeCNN"><strong>HypercubeCNN</strong></a>
|
|
66
|
+
·
|
|
67
|
+
<a href="https://github.com/dliptak001/HypercubeHopfield"><strong>HypercubeHopfield</strong></a>
|
|
68
|
+
·
|
|
69
|
+
<a href="https://github.com/dliptak001/HypercubeWTF"><strong>HypercubeWTF</strong></a>
|
|
70
|
+
·
|
|
71
|
+
<a href="https://github.com/dliptak001/HypercubeEtalon"><strong>HypercubeEtalon</strong></a>
|
|
72
|
+
·
|
|
73
|
+
<a href="https://github.com/dliptak001/HypercubeCascade"><strong>HypercubeCascade</strong></a>
|
|
74
|
+
</p>
|
|
75
|
+
|
|
76
|
+
HypercubeCascade is an experiment in the **HypercubeAI** project — our quest to
|
|
77
|
+
systematically re-implement classical neural architectures on a Boolean
|
|
78
|
+
hypercube topology instead of Euclidean grids or random graphs. The central
|
|
79
|
+
thesis is “topology-native intelligence”: the hypercube’s algebraic structure
|
|
80
|
+
(vertex-transitive symmetry, Hamming geometry, bitwise addressing) can serve
|
|
81
|
+
as a first-class computational substrate.
|
|
82
|
+
|
|
83
|
+
- **A topology you don’t store** — the graph is specified: connectivity is
|
|
84
|
+
implicit in the vertex indices; with a seed and a few config scalars the whole
|
|
85
|
+
preprocessor reconstructs mathematically.
|
|
86
|
+
- **Perfect homogeneity** — every vertex has the same degree and the same local
|
|
87
|
+
world, so local dynamics mean the same thing everywhere — no structural
|
|
88
|
+
favorites baked in by a random graph.
|
|
89
|
+
- **Cheap navigation** — each neighbor is a few bit operations on the vertex
|
|
90
|
+
index, not a pointer chase through a stored edge list, so walks stay
|
|
91
|
+
arithmetic and cache-friendly.
|
|
92
|
+
- **Topology-native pairing** — the readout consumes the preprocessor output
|
|
93
|
+
with zero geometric distortion, and the learned kernels exploit the same
|
|
94
|
+
locality that generated the dynamics. The data never leaves the hypercube it
|
|
95
|
+
was born on.
|
|
96
|
+
|
|
97
|
+
Each product in the family is a different architecture on that same foundation.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## What is HypercubeCascade?
|
|
102
|
+
|
|
103
|
+
[HypercubeEtalon](https://github.com/dliptak001/HypercubeEtalon) preprocesses a
|
|
104
|
+
static field with **one etalon transit**.
|
|
105
|
+
[HypercubeWTF](https://github.com/dliptak001/HypercubeWTF) preprocesses a
|
|
106
|
+
static field with **one reservoir orbit**. HypercubeCascade is **both of them,
|
|
107
|
+
in series, on one cube**: the transit output, times a gain, becomes the orbit
|
|
108
|
+
drive, and the orbit's end state, times a second gain, is what the CNN head
|
|
109
|
+
trains on.
|
|
110
|
+
|
|
111
|
+
In classical reservoir computing (and in both single-stage siblings):
|
|
112
|
+
|
|
113
|
+
- Preprocessor weights are **frozen**
|
|
114
|
+
- Only a **readout** is trained
|
|
115
|
+
- Nonlinear dynamics expand and mix the drive into a rich state
|
|
116
|
+
|
|
117
|
+
Whether the two-stage pipeline has **real product value** is still an open
|
|
118
|
+
question. Early studies suggest the second stage adds filtering on top of what
|
|
119
|
+
the first stage already adds (see
|
|
120
|
+
[Early observations](#early-observations-exploratory)).
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Pipeline
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
x (your length-N field — already on the cube, no natural time)
|
|
128
|
+
│
|
|
129
|
+
▼
|
|
130
|
+
frozen etalon transit (one wave over every cavity)
|
|
131
|
+
│
|
|
132
|
+
▼
|
|
133
|
+
× interstage_scale → frozen reservoir orbit (T re-addressed passes)
|
|
134
|
+
│
|
|
135
|
+
▼
|
|
136
|
+
end-of-orbit state × readout_scale → HypercubeCNN → logits / values
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- Cube size from **dim** (N = 2<sup>dim</sup>; dim 5…12). One dim serves all
|
|
140
|
+
three stages.
|
|
141
|
+
- Only the readout trains.
|
|
142
|
+
- Everyday loop in this package:
|
|
143
|
+
`collect_batch` → `train` → `predict` / `predict_class`,
|
|
144
|
+
or one-shot `fit` (collect + train).
|
|
145
|
+
|
|
146
|
+
Unlike HypercubeESN’s Python API, there is no stream of small samples over real
|
|
147
|
+
time and no next-step `fit` on a 1D signal. Each sample is one full field; the
|
|
148
|
+
“time” is the short synthetic orbit; the CNN only ever sees the state at the end.
|
|
149
|
+
|
|
150
|
+
Full method list and knobs:
|
|
151
|
+
**[docs/Python_SDK.md](https://github.com/dliptak001/HypercubeCascade/blob/main/docs/Python_SDK.md)**.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Early observations (exploratory)
|
|
156
|
+
|
|
157
|
+
On the MNIST white-noise study (train clean, test with Gaussian field noise),
|
|
158
|
+
the cascade behaves as a near-unity passthrough on clean fields and pulls
|
|
159
|
+
ahead of both the etalon-only path and the pack-only bypass from σ = 0.3
|
|
160
|
+
upward. On a Raman baseline-extraction regression it matches the etalon-only
|
|
161
|
+
sibling to within ~1% RMSE while training with a visibly more stable epoch
|
|
162
|
+
profile. The write-ups have the details and how we ran them:
|
|
163
|
+
|
|
164
|
+
| Document | Question |
|
|
165
|
+
|----------|----------|
|
|
166
|
+
| [WhiteNoiseFilter.md](https://github.com/dliptak001/HypercubeCascade/blob/main/examples/mnist/WhiteNoiseFilter.md) | Noisy test fields: do two stages help vs one stage vs pack-only → CNN? |
|
|
167
|
+
| [RamanBaselineExtraction/README.md](https://github.com/dliptak001/HypercubeCascade/blob/main/examples/RamanBaselineExtraction/README.md) | Baseline regression: cascade vs etalon-only, overlays and training profiles |
|
|
168
|
+
|
|
169
|
+
The MNIST study uses small cubes because they are handy to pack and run, not
|
|
170
|
+
because we are chasing digit accuracy. A more rigorous study is still needed
|
|
171
|
+
before treating any of those results as settled. You can reproduce the same
|
|
172
|
+
ideas from Python with this package (pack fields yourself, then collect,
|
|
173
|
+
train, and predict). The original write-ups and C++ demos that produced the
|
|
174
|
+
numbers live under
|
|
175
|
+
[`examples/`](https://github.com/dliptak001/HypercubeCascade/tree/main/examples).
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Installation
|
|
180
|
+
|
|
181
|
+
**Preferred:** install a pre-built wheel from PyPI (no compiler).
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
pip install hypercube-cascade
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
import hypercube_cascade as hc
|
|
189
|
+
print(hc.__version__)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Package name on PyPI: **`hypercube-cascade`**. Import name:
|
|
193
|
+
**`hypercube_cascade`**. Main type: **`hc.Cascade`**.
|
|
194
|
+
|
|
195
|
+
Wheels target Python 3.10–3.14 on common Windows, Linux, and macOS machines.
|
|
196
|
+
Runtime dependency: NumPy only.
|
|
197
|
+
|
|
198
|
+
### From source (full repository)
|
|
199
|
+
|
|
200
|
+
To compile the extension yourself, clone this **entire** repository (not a
|
|
201
|
+
minimal source-only download of the `python/` folder alone — the C++ core and
|
|
202
|
+
vendored HypercubeCNN live next to `python/`). You need Python 3.10+, a C++23
|
|
203
|
+
compiler, and CMake ≥ 3.20.
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
git clone https://github.com/dliptak001/HypercubeCascade.git
|
|
207
|
+
cd HypercubeCascade/python
|
|
208
|
+
pip install .
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
On Windows with CLion’s MinGW, put that compiler’s `bin` folder (and Ninja) on
|
|
212
|
+
your `PATH`, then:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
pip install . --no-build-isolation --force-reinstall --no-deps
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
(Exact CLion paths change with the version.) Step-by-step toolchain notes:
|
|
219
|
+
[docs/Python_SDK.md](https://github.com/dliptak001/HypercubeCascade/blob/main/docs/Python_SDK.md).
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Quick start
|
|
224
|
+
|
|
225
|
+
You bring each sample as a length-**N** float array (N = 2<sup>dim</sup>). How
|
|
226
|
+
you get there — pad an image, reshape a spectrum, invent a layout — is up to
|
|
227
|
+
you. This package does not pack 784 pixels or 300 bins for you.
|
|
228
|
+
|
|
229
|
+
Shapes that matter:
|
|
230
|
+
|
|
231
|
+
| Array | Shape | Notes |
|
|
232
|
+
|-------|-------|-------|
|
|
233
|
+
| `fields` | `(count, N)` | one length-N field per row |
|
|
234
|
+
| `labels` (classification) | `(count,)` | integer class indices |
|
|
235
|
+
| `targets` (regression) | `(count, num_outputs)` | float targets |
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
import numpy as np
|
|
239
|
+
import hypercube_cascade as hc
|
|
240
|
+
|
|
241
|
+
dim = 7
|
|
242
|
+
N = 2**dim
|
|
243
|
+
rng = np.random.default_rng(0)
|
|
244
|
+
fields = rng.standard_normal((200, N), dtype=np.float32)
|
|
245
|
+
labels = rng.integers(0, 4, size=200)
|
|
246
|
+
|
|
247
|
+
cas = hc.Cascade(
|
|
248
|
+
dim=dim,
|
|
249
|
+
exciter_subcube_dim=5,
|
|
250
|
+
history_depth=4,
|
|
251
|
+
T=50,
|
|
252
|
+
ic_seed=2,
|
|
253
|
+
readout_num_outputs=4,
|
|
254
|
+
readout_task="classification",
|
|
255
|
+
readout_epochs=80,
|
|
256
|
+
)
|
|
257
|
+
cas.fit(fields, labels) # collect_batch + train
|
|
258
|
+
|
|
259
|
+
print(cas.N, cas.T, cas.num_collected)
|
|
260
|
+
print(f"train sanity check: {cas.accuracy_on_collected():.3f}")
|
|
261
|
+
print(cas.predict_class(fields[0]), cas.predict(fields[0]).shape)
|
|
262
|
+
|
|
263
|
+
cas.save("model.pkl")
|
|
264
|
+
loaded = hc.Cascade.load("model.pkl")
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Step by step (same loop, more control)
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
cas = hc.Cascade(
|
|
271
|
+
dim=7,
|
|
272
|
+
exciter_subcube_dim=5,
|
|
273
|
+
readout_num_outputs=4,
|
|
274
|
+
readout_task="classification",
|
|
275
|
+
)
|
|
276
|
+
cas.collect_batch(fields_train, labels_train)
|
|
277
|
+
cas.train()
|
|
278
|
+
logits = cas.predict(fields_test[0]) # (num_outputs,) float32
|
|
279
|
+
cls = cas.predict_class(fields_test[0]) # int
|
|
280
|
+
test_acc = cas.accuracy(fields_test, labels_test) # held-out, fresh maps
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
For regression, set `readout_task="regression"` and pass float targets instead
|
|
284
|
+
of class labels. Then use `r2_on_collected()` / `r2(fields, targets)` the same
|
|
285
|
+
way.
|
|
286
|
+
|
|
287
|
+
`accuracy_on_collected` and `r2_on_collected` only look at the samples you
|
|
288
|
+
already trained on — they are a quick sanity check, not a test score. For real
|
|
289
|
+
evaluation, hold fields out and call `accuracy` / `r2` (or `predict` /
|
|
290
|
+
`predict_class` yourself).
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Features
|
|
295
|
+
|
|
296
|
+
- **One class** — `hypercube_cascade.Cascade` is the whole product surface
|
|
297
|
+
- **Map loop** — `collect` / `collect_batch` → `train` →
|
|
298
|
+
`predict` / `predict_class`
|
|
299
|
+
- **`fit`** — clear, collect, and train when your arrays are ready
|
|
300
|
+
- **dim 5–12** — field length N = 2<sup>dim</sup>; one dim for all three
|
|
301
|
+
stages; orbit length `T`; etalon face `exciter_subcube_dim`
|
|
302
|
+
- **Two gains** — `interstage_scale` (transit → orbit) and `readout_scale`
|
|
303
|
+
(orbit → readout)
|
|
304
|
+
- **Classification or regression** — `readout_task` fixed at construction
|
|
305
|
+
- **Held-out scoring** — `accuracy(fields, labels)` / `r2(fields, targets)`
|
|
306
|
+
map fresh in bulk
|
|
307
|
+
- **Bulk calls can parallelize** — `collect_threads` (0 = auto)
|
|
308
|
+
- **Inspect a map** — `run(x)` then `last_features()`, plus per-stage probes
|
|
309
|
+
`last_exciter()` / `last_interstage()` / `last_reservoir()` for gain tuning
|
|
310
|
+
- **Save / load** — `save` / `load` (pickle: config + readout weights;
|
|
311
|
+
collected samples are not stored). Optional `save_readout_hcnn_model` /
|
|
312
|
+
`load_readout_hcnn_model` for portable HCNW + arch JSON
|
|
313
|
+
- **NumPy float32** — arrays converted for you; prefer contiguous float32
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Examples
|
|
318
|
+
|
|
319
|
+
For a first try, paste the [Quick start](#quick-start) after
|
|
320
|
+
`pip install hypercube-cascade`. That is self-contained.
|
|
321
|
+
|
|
322
|
+
If you want a longer walk-through, the demo scripts on GitHub under
|
|
323
|
+
[`python/examples/`](https://github.com/dliptak001/HypercubeCascade/tree/main/python/examples)
|
|
324
|
+
are there to open or download — they are not added to your machine by pip.
|
|
325
|
+
|
|
326
|
+
| Script | What it is for |
|
|
327
|
+
|--------|----------------|
|
|
328
|
+
| [synthetic_classification.py](https://github.com/dliptak001/HypercubeCascade/blob/main/python/examples/synthetic_classification.py) | Multi-class toy fields: `fit`, then train and test accuracy |
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
# from a clone of HypercubeCascade, after: pip install hypercube-cascade
|
|
332
|
+
python python/examples/synthetic_classification.py
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
These use easy made-up fields so the API is obvious — not scores to publish.
|
|
336
|
+
More notes:
|
|
337
|
+
[python/examples/README.md](https://github.com/dliptak001/HypercubeCascade/blob/main/python/examples/README.md).
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Documentation
|
|
342
|
+
|
|
343
|
+
| Doc | Role |
|
|
344
|
+
|-----|------|
|
|
345
|
+
| **[docs/Python_SDK.md](https://github.com/dliptak001/HypercubeCascade/blob/main/docs/Python_SDK.md)** | Canonical Python API — every method, layout, pickle, limits |
|
|
346
|
+
| [python/examples/README.md](https://github.com/dliptak001/HypercubeCascade/blob/main/python/examples/README.md) | Demo scripts on GitHub |
|
|
347
|
+
| [Project README](https://github.com/dliptak001/HypercubeCascade#readme) | Product story and C++ demos from the repo root |
|
|
348
|
+
| [docs/CPP_SDK.md](https://github.com/dliptak001/HypercubeCascade/blob/main/docs/CPP_SDK.md) | Native library guide (same product, C++) |
|
|
349
|
+
| [docs/CascadeWhitePaper.md](https://github.com/dliptak001/HypercubeCascade/blob/main/docs/CascadeWhitePaper.md) | The two-stage concept, mechanism by mechanism |
|
|
350
|
+
| [WhiteNoiseFilter.md](https://github.com/dliptak001/HypercubeCascade/blob/main/examples/mnist/WhiteNoiseFilter.md) | Early white-noise study (MNIST as a test bed) |
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Ecosystem
|
|
355
|
+
|
|
356
|
+
- **[HypercubeEtalon](https://github.com/dliptak001/HypercubeEtalon)** — the etalon transit alone; Cascade’s first stage.
|
|
357
|
+
- **[HypercubeWTF](https://github.com/dliptak001/HypercubeWTF)** — the reservoir orbit alone; Cascade’s second stage.
|
|
358
|
+
- **[HypercubeCNN](https://github.com/dliptak001/HypercubeCNN)** — cube-native conv stack; Cascade’s trainable head.
|
|
359
|
+
- **[HypercubeESN](https://github.com/dliptak001/HypercubeESN)** — echo-state / reservoir computing on streams.
|
|
360
|
+
- **[HypercubeHopfield](https://github.com/dliptak001/HypercubeHopfield)** — Hopfield-style dynamics on the cube.
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## License
|
|
365
|
+
|
|
366
|
+
Apache 2.0. See [LICENSE](https://github.com/dliptak001/HypercubeCascade/blob/main/LICENSE).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
hypercube_cascade/_core.cp313-win_amd64.pyd,sha256=V-IcA478aG1rBvcVF2cibAg0deqyTt-Ccc6UizJB5oM,485888
|
|
2
|
+
hypercube_cascade/_version.py,sha256=V3_fncGsfYVTCIF5fWbPkm7cptk0McYg9O2Y6mZIo80,255
|
|
3
|
+
hypercube_cascade/__init__.py,sha256=MoxU1gjMxQDy4sliVr3zfKK7kwYwJM1KV90BAYTMM10,25981
|
|
4
|
+
hypercube_cascade-1.0.0.dist-info/DELVEWHEEL,sha256=3mO3bOluo6inW4JlfsJkiwG3fQvABBFrNGxhciNpQmY,416
|
|
5
|
+
hypercube_cascade-1.0.0.dist-info/METADATA,sha256=t48pABXMXdfDS5tIP3YakclFnTmc_bAmRpeQsl9hxpY,15443
|
|
6
|
+
hypercube_cascade-1.0.0.dist-info/RECORD,,
|
|
7
|
+
hypercube_cascade-1.0.0.dist-info/WHEEL,sha256=dXz53UX_wVrENU3pg7IjBHl1TfATF0jKSXtQDtrFnLw,105
|
|
8
|
+
hypercube_cascade.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll,sha256=pMIim9wqKmMKzcCVtNhgCOXD47x3cxdDVPPaT1vrnN4,575056
|
|
Binary file
|