hypercube-esn 0.3.0__tar.gz
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_esn-0.3.0/.gitignore +6 -0
- hypercube_esn-0.3.0/CMakeLists.txt +67 -0
- hypercube_esn-0.3.0/PKG-INFO +76 -0
- hypercube_esn-0.3.0/README.md +50 -0
- hypercube_esn-0.3.0/bindings.cpp +302 -0
- hypercube_esn-0.3.0/hypercube_esn/__init__.py +698 -0
- hypercube_esn-0.3.0/pyproject.toml +73 -0
- hypercube_esn-0.3.0/tests/__init__.py +0 -0
- hypercube_esn-0.3.0/tests/test_basic.py +177 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.20)
|
|
2
|
+
project(HypercubeESNPython LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 23)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
7
|
+
|
|
8
|
+
# ── pybind11 ──
|
|
9
|
+
find_package(pybind11 CONFIG REQUIRED)
|
|
10
|
+
|
|
11
|
+
# ── Optimization flags (match main project) ──
|
|
12
|
+
# HYPERCUBE_ARCH controls -march. Defaults to "native" for local dev builds.
|
|
13
|
+
# cibuildwheel overrides to "x86-64-v2" (x86_64) or "none" (ARM, MSVC).
|
|
14
|
+
set(HYPERCUBE_ARCH "native" CACHE STRING "Target architecture for -march (native, x86-64-v2, none)")
|
|
15
|
+
|
|
16
|
+
if(MSVC)
|
|
17
|
+
add_compile_options(/O2 /fp:fast)
|
|
18
|
+
else()
|
|
19
|
+
add_compile_options(-O3 -ffast-math)
|
|
20
|
+
if(NOT HYPERCUBE_ARCH STREQUAL "none")
|
|
21
|
+
if(HYPERCUBE_ARCH STREQUAL "native")
|
|
22
|
+
add_compile_options(-march=native -mtune=native)
|
|
23
|
+
else()
|
|
24
|
+
add_compile_options(-march=${HYPERCUBE_ARCH} -mtune=generic)
|
|
25
|
+
endif()
|
|
26
|
+
endif()
|
|
27
|
+
add_compile_options(-Wall -Wextra -Wno-unknown-pragmas)
|
|
28
|
+
endif()
|
|
29
|
+
|
|
30
|
+
# ── Core sources compiled directly into the module (PIC required) ──
|
|
31
|
+
set(CORE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/..")
|
|
32
|
+
set(CORE_SOURCES
|
|
33
|
+
${CORE_DIR}/Reservoir.cpp
|
|
34
|
+
${CORE_DIR}/ESN.cpp
|
|
35
|
+
${CORE_DIR}/Readout.cpp
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# ── HypercubeCNN dependency ──
|
|
39
|
+
# Vendored read-only snapshot at ../third_party/HypercubeCNN (see VENDORED.md).
|
|
40
|
+
# No sibling checkout, no pre-built .a, no network fetch — offline & version-pinned.
|
|
41
|
+
# third_party/ lies outside this source tree, so a binary dir arg is required.
|
|
42
|
+
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../third_party/HypercubeCNN
|
|
43
|
+
${CMAKE_BINARY_DIR}/HypercubeCNN-build)
|
|
44
|
+
set(HCNN_LIBS HypercubeCNNCore)
|
|
45
|
+
|
|
46
|
+
# ── Build the Python extension module ──
|
|
47
|
+
# HypercubeCNNCore's PUBLIC include dir propagates through the link below, so no
|
|
48
|
+
# explicit HCNN include path is needed here.
|
|
49
|
+
pybind11_add_module(_core bindings.cpp ${CORE_SOURCES})
|
|
50
|
+
target_include_directories(_core PRIVATE ${CORE_DIR})
|
|
51
|
+
target_link_libraries(_core PRIVATE ${HCNN_LIBS})
|
|
52
|
+
|
|
53
|
+
# ── Linking ──
|
|
54
|
+
if(MINGW)
|
|
55
|
+
# Statically link MinGW runtime DLLs so the .pyd is self-contained.
|
|
56
|
+
target_link_options(_core PRIVATE -static-libgcc -static-libstdc++)
|
|
57
|
+
target_link_libraries(_core PRIVATE -Wl,-Bstatic pthread winpthread -Wl,-Bdynamic)
|
|
58
|
+
# Posix-model MinGW always needs libwinpthread-1.dll at runtime.
|
|
59
|
+
# Ship it inside the package so the .pyd loads without MinGW on PATH.
|
|
60
|
+
find_file(WINPTHREAD_DLL libwinpthread-1.dll PATHS ENV PATH NO_DEFAULT_PATH)
|
|
61
|
+
if(WINPTHREAD_DLL)
|
|
62
|
+
install(FILES ${WINPTHREAD_DLL} DESTINATION hypercube_esn)
|
|
63
|
+
endif()
|
|
64
|
+
endif()
|
|
65
|
+
|
|
66
|
+
# ── Install into the hypercube_esn package directory ──
|
|
67
|
+
install(TARGETS _core DESTINATION hypercube_esn)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hypercube-esn
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python bindings for HypercubeESN: reservoir computing on Boolean hypercube graphs
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
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 :: C++
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Project-URL: Homepage, https://github.com/dliptak001/HypercubeESN
|
|
19
|
+
Project-URL: Repository, https://github.com/dliptak001/HypercubeESN
|
|
20
|
+
Project-URL: Documentation, https://github.com/dliptak001/HypercubeESN/blob/main/docs/Python_SDK.md
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: numpy>=1.21
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# HypercubeESN
|
|
28
|
+
|
|
29
|
+
[](https://github.com/dliptak001/HypercubeESN/actions/workflows/wheels.yml)
|
|
30
|
+
|
|
31
|
+
Python bindings for a reservoir computer whose neurons live on a Boolean hypercube — a
|
|
32
|
+
DIM-dimensional graph where each vertex is addressed by a DIM-bit binary
|
|
33
|
+
index, with all connectivity defined by XOR operations on those indices.
|
|
34
|
+
**Neuron states are continuous real values** (driven through `tanh`
|
|
35
|
+
nonlinearity); only the *addressing scheme* is binary. No adjacency list
|
|
36
|
+
is stored. N = 2^DIM neurons (DIM 5-16, i.e. 32 to 65,536 neurons).
|
|
37
|
+
DIM-invariant hyperparameters: the same spectral radius and input_scaling
|
|
38
|
+
work at every DIM.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install hypercube-esn
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Pre-built wheels for Python 3.10-3.13 on Windows (x64), Linux (x86_64,
|
|
47
|
+
aarch64), and macOS (x86_64, arm64). No compiler required.
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import numpy as np
|
|
53
|
+
import hypercube_esn as he
|
|
54
|
+
|
|
55
|
+
# One-step-ahead sine prediction
|
|
56
|
+
signal = np.sin(np.linspace(0, 20 * np.pi, 2000)).astype(np.float32)
|
|
57
|
+
esn = he.ESN(dim=7)
|
|
58
|
+
esn.fit(signal, warmup=200)
|
|
59
|
+
print(f"R2 = {esn.r2():.6f}")
|
|
60
|
+
print(f"NRMSE = {esn.nrmse():.6f}")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
- **Simple API** -- `fit()` handles warmup, run, and train in one call
|
|
66
|
+
- **DIM 5-16** -- 32 to 65,536 neurons, DIM-invariant defaults
|
|
67
|
+
- **HCNN readout** -- learned convolutional readout on raw reservoir state
|
|
68
|
+
- **Multi-input** -- multiple input channels via contiguous-block driving
|
|
69
|
+
- **Streaming mode** -- online training for real-time applications
|
|
70
|
+
- **Model persistence** -- pickle, save/load to disk
|
|
71
|
+
|
|
72
|
+
## Documentation
|
|
73
|
+
|
|
74
|
+
Full API reference: [docs/Python_SDK.md](https://github.com/dliptak001/HypercubeESN/blob/main/docs/Python_SDK.md)
|
|
75
|
+
|
|
76
|
+
Project repository: [github.com/dliptak001/HypercubeESN](https://github.com/dliptak001/HypercubeESN)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# HypercubeESN
|
|
2
|
+
|
|
3
|
+
[](https://github.com/dliptak001/HypercubeESN/actions/workflows/wheels.yml)
|
|
4
|
+
|
|
5
|
+
Python bindings for a reservoir computer whose neurons live on a Boolean hypercube — a
|
|
6
|
+
DIM-dimensional graph where each vertex is addressed by a DIM-bit binary
|
|
7
|
+
index, with all connectivity defined by XOR operations on those indices.
|
|
8
|
+
**Neuron states are continuous real values** (driven through `tanh`
|
|
9
|
+
nonlinearity); only the *addressing scheme* is binary. No adjacency list
|
|
10
|
+
is stored. N = 2^DIM neurons (DIM 5-16, i.e. 32 to 65,536 neurons).
|
|
11
|
+
DIM-invariant hyperparameters: the same spectral radius and input_scaling
|
|
12
|
+
work at every DIM.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install hypercube-esn
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Pre-built wheels for Python 3.10-3.13 on Windows (x64), Linux (x86_64,
|
|
21
|
+
aarch64), and macOS (x86_64, arm64). No compiler required.
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import numpy as np
|
|
27
|
+
import hypercube_esn as he
|
|
28
|
+
|
|
29
|
+
# One-step-ahead sine prediction
|
|
30
|
+
signal = np.sin(np.linspace(0, 20 * np.pi, 2000)).astype(np.float32)
|
|
31
|
+
esn = he.ESN(dim=7)
|
|
32
|
+
esn.fit(signal, warmup=200)
|
|
33
|
+
print(f"R2 = {esn.r2():.6f}")
|
|
34
|
+
print(f"NRMSE = {esn.nrmse():.6f}")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Simple API** -- `fit()` handles warmup, run, and train in one call
|
|
40
|
+
- **DIM 5-16** -- 32 to 65,536 neurons, DIM-invariant defaults
|
|
41
|
+
- **HCNN readout** -- learned convolutional readout on raw reservoir state
|
|
42
|
+
- **Multi-input** -- multiple input channels via contiguous-block driving
|
|
43
|
+
- **Streaming mode** -- online training for real-time applications
|
|
44
|
+
- **Model persistence** -- pickle, save/load to disk
|
|
45
|
+
|
|
46
|
+
## Documentation
|
|
47
|
+
|
|
48
|
+
Full API reference: [docs/Python_SDK.md](https://github.com/dliptak001/HypercubeESN/blob/main/docs/Python_SDK.md)
|
|
49
|
+
|
|
50
|
+
Project repository: [github.com/dliptak001/HypercubeESN](https://github.com/dliptak001/HypercubeESN)
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#include <pybind11/pybind11.h>
|
|
2
|
+
#include <pybind11/numpy.h>
|
|
3
|
+
#include <pybind11/stl.h>
|
|
4
|
+
#include <cstring>
|
|
5
|
+
#include <memory>
|
|
6
|
+
#include "../ESN.h"
|
|
7
|
+
|
|
8
|
+
namespace py = pybind11;
|
|
9
|
+
|
|
10
|
+
// Single de-templated ESN binding. The hypercube dimension is a runtime
|
|
11
|
+
// constructor argument (cfg.reservoir.dim), so one C++ type and one Python
|
|
12
|
+
// class serve every dimension 5-16 — no per-DIM instantiations.
|
|
13
|
+
PYBIND11_MODULE(_core, m)
|
|
14
|
+
{
|
|
15
|
+
m.doc() = "HypercubeESN: reservoir computing on Boolean hypercube graphs";
|
|
16
|
+
|
|
17
|
+
py::class_<ESN>(m, "_ESN")
|
|
18
|
+
// ── Construction ──
|
|
19
|
+
// All reservoir + readout parameters fixed at construction time.
|
|
20
|
+
// The readout config is consumed by train() / init_online() — no
|
|
21
|
+
// per-call config overrides.
|
|
22
|
+
.def(py::init([](size_t dim, uint64_t seed, float spectral_radius, float input_scaling,
|
|
23
|
+
float leak_rate, size_t num_inputs, size_t history_depth,
|
|
24
|
+
float output_fraction,
|
|
25
|
+
int readout_num_outputs, const char* readout_task,
|
|
26
|
+
int readout_num_layers, int readout_conv_channels,
|
|
27
|
+
int readout_epochs, int readout_batch_size,
|
|
28
|
+
float readout_lr_max, float readout_lr_min_frac,
|
|
29
|
+
int readout_lr_decay_epochs, float readout_weight_decay,
|
|
30
|
+
unsigned readout_seed, bool readout_verbose) {
|
|
31
|
+
ESNConfig cfg;
|
|
32
|
+
cfg.reservoir.dim = dim;
|
|
33
|
+
cfg.reservoir.seed = seed;
|
|
34
|
+
cfg.reservoir.spectral_radius = spectral_radius;
|
|
35
|
+
cfg.reservoir.input_scaling = input_scaling;
|
|
36
|
+
cfg.reservoir.leak_rate = leak_rate;
|
|
37
|
+
cfg.reservoir.num_inputs = num_inputs;
|
|
38
|
+
cfg.reservoir.history_depth = history_depth;
|
|
39
|
+
cfg.output_fraction = output_fraction;
|
|
40
|
+
cfg.readout.num_outputs = readout_num_outputs;
|
|
41
|
+
cfg.readout.task = (std::strcmp(readout_task, "classification") == 0)
|
|
42
|
+
? ReadoutTask::Classification
|
|
43
|
+
: ReadoutTask::Regression;
|
|
44
|
+
cfg.readout.num_layers = readout_num_layers;
|
|
45
|
+
cfg.readout.conv_channels = readout_conv_channels;
|
|
46
|
+
cfg.readout.epochs = readout_epochs;
|
|
47
|
+
cfg.readout.batch_size = readout_batch_size;
|
|
48
|
+
cfg.readout.lr_max = readout_lr_max;
|
|
49
|
+
cfg.readout.lr_min_frac = readout_lr_min_frac;
|
|
50
|
+
cfg.readout.lr_decay_epochs = readout_lr_decay_epochs;
|
|
51
|
+
cfg.readout.weight_decay = readout_weight_decay;
|
|
52
|
+
cfg.readout.seed = readout_seed;
|
|
53
|
+
cfg.readout.verbose = readout_verbose;
|
|
54
|
+
return std::make_unique<ESN>(cfg);
|
|
55
|
+
}),
|
|
56
|
+
py::arg("dim"),
|
|
57
|
+
py::arg("seed") = 73895ULL,
|
|
58
|
+
py::arg("spectral_radius") = 0.99f,
|
|
59
|
+
py::arg("input_scaling") = 0.5f,
|
|
60
|
+
py::arg("leak_rate") = 1.0f,
|
|
61
|
+
py::arg("num_inputs") = 1ULL,
|
|
62
|
+
py::arg("history_depth") = 16ULL,
|
|
63
|
+
py::arg("output_fraction") = 1.0f,
|
|
64
|
+
py::arg("readout_num_outputs") = 1,
|
|
65
|
+
py::arg("readout_task") = "regression",
|
|
66
|
+
py::arg("readout_num_layers") = 0,
|
|
67
|
+
py::arg("readout_conv_channels") = 16,
|
|
68
|
+
py::arg("readout_epochs") = 200,
|
|
69
|
+
py::arg("readout_batch_size") = 32,
|
|
70
|
+
py::arg("readout_lr_max") = 0.005f,
|
|
71
|
+
py::arg("readout_lr_min_frac") = 0.1f,
|
|
72
|
+
py::arg("readout_lr_decay_epochs") = 0,
|
|
73
|
+
py::arg("readout_weight_decay") = 0.0f,
|
|
74
|
+
py::arg("readout_seed") = 42u,
|
|
75
|
+
py::arg("readout_verbose") = false)
|
|
76
|
+
|
|
77
|
+
// ── Reservoir driving ──
|
|
78
|
+
.def("warmup", [](ESN& self, py::array_t<float, py::array::c_style | py::array::forcecast> inputs) {
|
|
79
|
+
auto buf = inputs.request();
|
|
80
|
+
size_t total = static_cast<size_t>(buf.size);
|
|
81
|
+
size_t K = self.NumInputs();
|
|
82
|
+
if (total % K != 0)
|
|
83
|
+
throw std::invalid_argument("Input size must be divisible by num_inputs");
|
|
84
|
+
self.Warmup(static_cast<const float*>(buf.ptr), total / K);
|
|
85
|
+
}, py::arg("inputs"),
|
|
86
|
+
"Drive the reservoir without recording states (wash out initial transient).")
|
|
87
|
+
|
|
88
|
+
.def("run", [](ESN& self, py::array_t<float, py::array::c_style | py::array::forcecast> inputs) {
|
|
89
|
+
auto buf = inputs.request();
|
|
90
|
+
size_t total = static_cast<size_t>(buf.size);
|
|
91
|
+
size_t K = self.NumInputs();
|
|
92
|
+
if (total % K != 0)
|
|
93
|
+
throw std::invalid_argument("Input size must be divisible by num_inputs");
|
|
94
|
+
self.Run(static_cast<const float*>(buf.ptr), total / K);
|
|
95
|
+
}, py::arg("inputs"),
|
|
96
|
+
"Drive the reservoir and record states for training/evaluation.")
|
|
97
|
+
|
|
98
|
+
.def("clear_states", &ESN::ClearStates,
|
|
99
|
+
"Clear collected states and cached features. Keeps trained readout.")
|
|
100
|
+
|
|
101
|
+
.def("reset_reservoir_only", &ESN::ResetReservoirOnly,
|
|
102
|
+
"Zero only the reservoir state; collected states preserved.")
|
|
103
|
+
|
|
104
|
+
// ── Batch training ──
|
|
105
|
+
.def("train", [](ESN& self,
|
|
106
|
+
py::array_t<float, py::array::c_style | py::array::forcecast> targets) {
|
|
107
|
+
auto buf = targets.request();
|
|
108
|
+
size_t n = static_cast<size_t>(buf.size);
|
|
109
|
+
const float* ptr = static_cast<const float*>(buf.ptr);
|
|
110
|
+
|
|
111
|
+
if (n > self.NumCollected())
|
|
112
|
+
throw std::invalid_argument(
|
|
113
|
+
"train_size (" + std::to_string(n) +
|
|
114
|
+
") exceeds num_collected (" + std::to_string(self.NumCollected()) + ")");
|
|
115
|
+
|
|
116
|
+
self.Train(ptr, n);
|
|
117
|
+
},
|
|
118
|
+
py::arg("targets"),
|
|
119
|
+
"Train the HCNN readout on collected states.\n"
|
|
120
|
+
"Uses the readout config supplied at ESN construction.")
|
|
121
|
+
|
|
122
|
+
// ── Online (streaming) HCNN training ──
|
|
123
|
+
.def("init_online", [](ESN& self,
|
|
124
|
+
py::array_t<float, py::array::c_style | py::array::forcecast> warmup_inputs) {
|
|
125
|
+
auto buf = warmup_inputs.request();
|
|
126
|
+
size_t total = static_cast<size_t>(buf.size);
|
|
127
|
+
size_t K = self.NumInputs();
|
|
128
|
+
if (total % K != 0)
|
|
129
|
+
throw std::invalid_argument("warmup_inputs size must be divisible by num_inputs");
|
|
130
|
+
self.InitOnline(static_cast<const float*>(buf.ptr), total / K);
|
|
131
|
+
},
|
|
132
|
+
py::arg("warmup_inputs"),
|
|
133
|
+
"Initialize HCNN for online (streaming) training.\n\n"
|
|
134
|
+
"Runs warmup_inputs through reservoir to reach a representative state,\n"
|
|
135
|
+
"then builds CNN architecture. Uses the readout config supplied at construction.\n"
|
|
136
|
+
"Call before train_live_step/train_live_batch.")
|
|
137
|
+
|
|
138
|
+
.def("train_live_step", [](ESN& self, float target_class, float lr, float weight_decay) {
|
|
139
|
+
self.TrainLiveStep(target_class, lr, weight_decay);
|
|
140
|
+
},
|
|
141
|
+
py::arg("target_class"), py::arg("lr"), py::arg("weight_decay") = 0.0f,
|
|
142
|
+
"Single-step online classification training on the live reservoir state.")
|
|
143
|
+
|
|
144
|
+
.def("train_live_batch", [](ESN& self,
|
|
145
|
+
py::array_t<float, py::array::c_style | py::array::forcecast> states,
|
|
146
|
+
py::array_t<int, py::array::c_style | py::array::forcecast> targets,
|
|
147
|
+
float lr, float weight_decay) {
|
|
148
|
+
auto sbuf = states.request();
|
|
149
|
+
auto tbuf = targets.request();
|
|
150
|
+
size_t count = static_cast<size_t>(tbuf.size);
|
|
151
|
+
self.TrainLiveBatch(static_cast<const float*>(sbuf.ptr),
|
|
152
|
+
static_cast<const int*>(tbuf.ptr),
|
|
153
|
+
count, lr, weight_decay);
|
|
154
|
+
},
|
|
155
|
+
py::arg("states"), py::arg("targets"),
|
|
156
|
+
py::arg("lr"), py::arg("weight_decay") = 0.0f,
|
|
157
|
+
"Mini-batch online classification training on pre-accumulated states.\n"
|
|
158
|
+
"states: (count, num_output_verts) float array from copy_live_state.\n"
|
|
159
|
+
"targets: (count,) int array of class indices.")
|
|
160
|
+
|
|
161
|
+
.def("train_live_step_regression", [](ESN& self,
|
|
162
|
+
py::array_t<float, py::array::c_style | py::array::forcecast> target,
|
|
163
|
+
float lr, float weight_decay) {
|
|
164
|
+
auto buf = target.request();
|
|
165
|
+
self.TrainLiveStepRegression(static_cast<const float*>(buf.ptr), lr, weight_decay);
|
|
166
|
+
},
|
|
167
|
+
py::arg("target"), py::arg("lr"), py::arg("weight_decay") = 0.0f,
|
|
168
|
+
"Single-step online regression training on the live reservoir state.\n"
|
|
169
|
+
"target: (num_outputs,) float array.")
|
|
170
|
+
|
|
171
|
+
.def("train_live_batch_regression", [](ESN& self,
|
|
172
|
+
py::array_t<float, py::array::c_style | py::array::forcecast> states,
|
|
173
|
+
py::array_t<float, py::array::c_style | py::array::forcecast> targets,
|
|
174
|
+
float lr, float weight_decay) {
|
|
175
|
+
auto sbuf = states.request();
|
|
176
|
+
auto tbuf = targets.request();
|
|
177
|
+
size_t K = self.NumOutputs();
|
|
178
|
+
size_t count = static_cast<size_t>(tbuf.size) / K;
|
|
179
|
+
self.TrainLiveBatchRegression(static_cast<const float*>(sbuf.ptr),
|
|
180
|
+
static_cast<const float*>(tbuf.ptr),
|
|
181
|
+
count, lr, weight_decay);
|
|
182
|
+
},
|
|
183
|
+
py::arg("states"), py::arg("targets"),
|
|
184
|
+
py::arg("lr"), py::arg("weight_decay") = 0.0f,
|
|
185
|
+
"Mini-batch online regression training on pre-accumulated states.\n"
|
|
186
|
+
"states: (count, num_output_verts) float array from copy_live_state.\n"
|
|
187
|
+
"targets: (count, num_outputs) float array.")
|
|
188
|
+
|
|
189
|
+
.def("copy_live_state", [](const ESN& self) {
|
|
190
|
+
size_t M = self.NumOutputVerts();
|
|
191
|
+
py::array_t<float> arr(M);
|
|
192
|
+
self.CopyLiveState(arr.mutable_data());
|
|
193
|
+
return arr;
|
|
194
|
+
}, "Copy the current subsampled reservoir state for external accumulation.\n"
|
|
195
|
+
"Returns a (num_output_verts,) float array.")
|
|
196
|
+
|
|
197
|
+
// ── Prediction & evaluation ──
|
|
198
|
+
.def("predict_raw", [](const ESN& self, size_t timestep) {
|
|
199
|
+
if (timestep >= self.NumCollected())
|
|
200
|
+
throw std::out_of_range(
|
|
201
|
+
"timestep (" + std::to_string(timestep) +
|
|
202
|
+
") >= num_collected (" + std::to_string(self.NumCollected()) + ")");
|
|
203
|
+
return self.PredictRaw(timestep);
|
|
204
|
+
}, py::arg("timestep"),
|
|
205
|
+
"Return the raw continuous prediction for a collected timestep.")
|
|
206
|
+
|
|
207
|
+
.def("predict_live_raw", [](const ESN& self) {
|
|
208
|
+
return self.PredictLiveRaw();
|
|
209
|
+
}, "Predict from the reservoir's current live state (no cached states needed).\n"
|
|
210
|
+
"For autoregressive / streaming inference loops.")
|
|
211
|
+
|
|
212
|
+
.def("predict_live_raw_multi", [](const ESN& self) {
|
|
213
|
+
size_t K = self.NumOutputs();
|
|
214
|
+
py::array_t<float> arr(K);
|
|
215
|
+
self.PredictLiveRaw(arr.mutable_data());
|
|
216
|
+
return arr;
|
|
217
|
+
}, "Multi-output live predict: returns (num_outputs,) float array.")
|
|
218
|
+
|
|
219
|
+
.def("r2", [](const ESN& self,
|
|
220
|
+
py::array_t<float, py::array::c_style | py::array::forcecast> targets,
|
|
221
|
+
size_t start, size_t count) {
|
|
222
|
+
if (start + count > self.NumCollected())
|
|
223
|
+
throw std::out_of_range(
|
|
224
|
+
"start + count (" + std::to_string(start + count) +
|
|
225
|
+
") > num_collected (" + std::to_string(self.NumCollected()) + ")");
|
|
226
|
+
return self.R2(static_cast<const float*>(targets.request().ptr), start, count);
|
|
227
|
+
}, py::arg("targets"), py::arg("start"), py::arg("count"),
|
|
228
|
+
"Compute R-squared on a slice of collected states.")
|
|
229
|
+
|
|
230
|
+
.def("nrmse", [](const ESN& self,
|
|
231
|
+
py::array_t<float, py::array::c_style | py::array::forcecast> targets,
|
|
232
|
+
size_t start, size_t count) {
|
|
233
|
+
if (start + count > self.NumCollected())
|
|
234
|
+
throw std::out_of_range(
|
|
235
|
+
"start + count (" + std::to_string(start + count) +
|
|
236
|
+
") > num_collected (" + std::to_string(self.NumCollected()) + ")");
|
|
237
|
+
return self.NRMSE(static_cast<const float*>(targets.request().ptr), start, count);
|
|
238
|
+
}, py::arg("targets"), py::arg("start"), py::arg("count"),
|
|
239
|
+
"Compute Normalized RMSE on a slice of collected states.")
|
|
240
|
+
|
|
241
|
+
.def("accuracy", [](const ESN& self,
|
|
242
|
+
py::array_t<float, py::array::c_style | py::array::forcecast> labels,
|
|
243
|
+
size_t start, size_t count) {
|
|
244
|
+
if (start + count > self.NumCollected())
|
|
245
|
+
throw std::out_of_range(
|
|
246
|
+
"start + count (" + std::to_string(start + count) +
|
|
247
|
+
") > num_collected (" + std::to_string(self.NumCollected()) + ")");
|
|
248
|
+
return self.Accuracy(static_cast<const float*>(labels.request().ptr), start, count);
|
|
249
|
+
}, py::arg("labels"), py::arg("start"), py::arg("count"),
|
|
250
|
+
"Compute classification accuracy on a slice of collected states.")
|
|
251
|
+
|
|
252
|
+
// ── State access ──
|
|
253
|
+
.def("selected_states", [](const ESN& self) {
|
|
254
|
+
auto vec = self.SelectedStates();
|
|
255
|
+
size_t M = self.NumOutputVerts();
|
|
256
|
+
size_t T = self.NumCollected();
|
|
257
|
+
py::array_t<float> arr({T, M});
|
|
258
|
+
memcpy(arr.mutable_data(), vec.data(), vec.size() * sizeof(float));
|
|
259
|
+
return arr;
|
|
260
|
+
}, "Return stride-selected states as a (num_collected, M) array.")
|
|
261
|
+
|
|
262
|
+
.def("predictions", [](const ESN& self) {
|
|
263
|
+
size_t T = self.NumCollected();
|
|
264
|
+
py::array_t<float> arr(T);
|
|
265
|
+
float* ptr = arr.mutable_data();
|
|
266
|
+
for (size_t t = 0; t < T; ++t)
|
|
267
|
+
ptr[t] = self.PredictRaw(t);
|
|
268
|
+
return arr;
|
|
269
|
+
}, "Return predictions for all collected timesteps as a 1D array.")
|
|
270
|
+
|
|
271
|
+
// ── Properties ──
|
|
272
|
+
.def_property_readonly("num_collected", &ESN::NumCollected)
|
|
273
|
+
.def_property_readonly("num_outputs", &ESN::NumOutputs)
|
|
274
|
+
.def_property_readonly("output_fraction", [](const ESN& self) { return self.GetConfig().output_fraction; })
|
|
275
|
+
.def_property_readonly("num_output_verts", &ESN::NumOutputVerts)
|
|
276
|
+
.def_property_readonly("dim", &ESN::Dim)
|
|
277
|
+
.def_property_readonly("N", &ESN::Size)
|
|
278
|
+
.def_property_readonly("num_inputs", &ESN::NumInputs)
|
|
279
|
+
.def_property_readonly("history_depth", [](const ESN& self) { return self.GetConfig().reservoir.history_depth; })
|
|
280
|
+
.def_property_readonly("seed", [](const ESN& self) { return self.GetConfig().reservoir.seed; })
|
|
281
|
+
.def_property_readonly("spectral_radius", [](const ESN& self) { return self.GetConfig().reservoir.spectral_radius; })
|
|
282
|
+
.def_property_readonly("leak_rate", [](const ESN& self) { return self.GetConfig().reservoir.leak_rate; })
|
|
283
|
+
.def_property_readonly("input_scaling", [](const ESN& self) { return self.GetConfig().reservoir.input_scaling; })
|
|
284
|
+
|
|
285
|
+
// ── Persistence ──
|
|
286
|
+
.def("_get_readout_state", [](const ESN& self) -> py::dict {
|
|
287
|
+
auto s = self.GetReadoutState();
|
|
288
|
+
py::dict d;
|
|
289
|
+
d["is_trained"] = s.is_trained;
|
|
290
|
+
d["weights"] = py::array_t<double>(
|
|
291
|
+
{static_cast<py::ssize_t>(s.weights.size())}, s.weights.data());
|
|
292
|
+
return d;
|
|
293
|
+
})
|
|
294
|
+
.def("_set_readout_state", [](ESN& self, py::dict d) {
|
|
295
|
+
ESN::ReadoutState s;
|
|
296
|
+
s.is_trained = d["is_trained"].cast<bool>();
|
|
297
|
+
auto w = d["weights"].cast<py::array_t<double, py::array::c_style | py::array::forcecast>>();
|
|
298
|
+
s.weights.assign(w.data(), w.data() + w.size());
|
|
299
|
+
self.SetReadoutState(s);
|
|
300
|
+
})
|
|
301
|
+
;
|
|
302
|
+
}
|