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.
@@ -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()
@@ -0,0 +1,5 @@
1
+ # Single source of truth for the package version.
2
+ # - pyproject.toml reads this via scikit-build-core dynamic metadata
3
+ # - hypercube_cascade.__version__ imports it
4
+ # - bindings.cpp gets the same string at compile time via CMake
5
+ __version__ = "1.0.0"
@@ -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
+ &nbsp;·&nbsp;
65
+ <a href="https://github.com/dliptak001/HypercubeCNN"><strong>HypercubeCNN</strong></a>
66
+ &nbsp;·&nbsp;
67
+ <a href="https://github.com/dliptak001/HypercubeHopfield"><strong>HypercubeHopfield</strong></a>
68
+ &nbsp;·&nbsp;
69
+ <a href="https://github.com/dliptak001/HypercubeWTF"><strong>HypercubeWTF</strong></a>
70
+ &nbsp;·&nbsp;
71
+ <a href="https://github.com/dliptak001/HypercubeEtalon"><strong>HypercubeEtalon</strong></a>
72
+ &nbsp;·&nbsp;
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
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: scikit-build-core 1.0.3
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313-win_amd64
5
+