HypercubeESN C++ SDK

September 11, 2026 · View on GitHub

Static C++ library for reservoir computing on Boolean hypercube graphs: a fixed Reservoir plus a trainable HypercubeCNN Readout, wrapped by ESN.

Package version 2.0.2 (project(HypercubeESN VERSION 2.0.2)). Breaking changes and migration: CHANGELOG.md.

Deep dives: Reservoir.md · Readout.md.

House defaults (2.0): match ReservoirConfig / ReadoutConfig in headers — verbose = false, spectral_radius = 0.999, input_scaling = 0.02, bias_scaling = 0.003, num_layers = 1 (0 = auto), readout_slices = 1, external feedback off (D = 0).

Contents

What's in the SDK

After installation:

<prefix>/
  include/HypercubeESN/
    ESN.h              -- public API (the only header consumers need)
    Reservoir.h        -- included by ESN.h
    Readout.h          -- types used by the ESN API (ReadoutConfig, enums)
  lib/
    libHypercubeESNCore.a
  lib/cmake/HypercubeESN/
    HypercubeESNConfig.cmake
    HypercubeESNTargets.cmake
    HypercubeESNConfigVersion.cmake

Include <HypercubeESN/ESN.h> (installed) or "ESN.h" (FetchContent) and link HypercubeESN::HypercubeESNCore (or HypercubeESNCore in FetchContent builds). Reservoir.h / Readout.h come along transitively; their public types are part of the API surface.

The convolutional readout comes from HypercubeCNN, vendored at third_party/HypercubeCNN. HypercubeESNCore links it transitively — consumers do not name it. See Dependencies.

Building from source

Requirements: C++23 (GCC 13+, Clang 17+, MSVC 2022+), CMake 4.1+.

cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
cmake --install build --prefix /path/to/sdk

Using the SDK

cmake_minimum_required(VERSION 4.1)
project(MyApp)

set(CMAKE_CXX_STANDARD 23)

include(FetchContent)
FetchContent_Declare(
    HypercubeESN
    GIT_REPOSITORY https://github.com/dliptak001/HypercubeESN.git
    GIT_TAG        v2.0.0   # pin a release tag when cut; check GitHub Releases
)
FetchContent_MakeAvailable(HypercubeESN)

add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE HypercubeESNCore)
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build

Pin GIT_TAG to a release for reproducible builds. Include paths are set automatically — #include "ESN.h".

HypercubeCNN is vendored in-tree; no sibling checkout or network fetch.

Installed SDK (find_package)

cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
cmake --install build --prefix /path/to/sdk
cmake_minimum_required(VERSION 4.1)
project(MyApp)

set(CMAKE_CXX_STANDARD 23)

find_package(HypercubeESN REQUIRED)

add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE HypercubeESN::HypercubeESNCore)
cmake -B build -DCMAKE_PREFIX_PATH=/path/to/sdk
cmake --build build

Minimal example

FetchContent-style include ("ESN.h"). Installed SDK: <HypercubeESN/ESN.h>.

#include "ESN.h"
#include <cmath>
#include <vector>
#include <iostream>

int main()
{
    constexpr size_t dim = 7;         // N = 128 (= 2⁷) neurons
    constexpr size_t warmup = 200;
    constexpr size_t collect = 2000;

    std::vector<float> signal(warmup + collect + 1);
    for (size_t t = 0; t < signal.size(); ++t)
        signal[t] = std::sin(0.1f * static_cast<float>(t));

    ESNConfig cfg;
    cfg.reservoir.dim  = dim;              // hypercube dimension (5-16)
    cfg.reservoir.seed = 74119;            // per-task surveyed seed
    cfg.readout.epochs     = 25;
    cfg.readout.batch_size = 128;
    cfg.readout.lr_max     = 0.003f;
    // cfg.readout_slices = 1;             // default: newest reservoir slice only
    ESN esn(cfg);

    esn.ReservoirWarmup(signal.data(), warmup);
    esn.ReservoirRun(signal.data() + warmup, collect);

    std::vector<float> targets(collect);
    for (size_t t = 0; t < collect; ++t)
        targets[t] = signal[warmup + t + 1];  // next-step targets

    size_t train_size = 1400;
    size_t test_size = collect - train_size;

    esn.Train(targets.data(), train_size);

    double r2 = esn.R2(targets.data(), train_size, test_size);
    std::cout << "R2: " << r2 << "\n";

    return 0;
}

Pipeline vocabulary

  inputs [+ optional ext-fb]


  Reservoir (fixed)


  SliceAt(0 .. B-1)  ── pack B blocks of N ──▶  HCNN readout (trained) ──▶ y

Only the readout emits y. External feedback is an input into the reservoir (caller-owned closed-loop drive), not a second path to y.

TermMeaning
TimestepOne ReservoirStep
NReservoir neurons = 2dim (ReservoirNeuronCount)
Mhistory_depth — delay-line depth the recurrent gather uses
Breadout_slices — power of two, 1 ≤ B ≤ M; ages packed into the readout
Reservoir stateNewest slice only (Outputs / CopyReservoirState) — N floats
Readout inputWhat the HCNN sees: B blocks of N (ReadoutInputWidth)
Open loopTask input only
Closed loopAlso stage external_feedback on the reservoir

Not thread-safe. Const predict paths share a scratch buffer — one ESN per thread.


API Reference

Hypercube dimension: dim

ReservoirConfig::dim sets the reservoir hypercube size. N = 2dim neurons at construction. Valid range [5, 16] — out of range throws std::invalid_argument. One concrete Reservoir / ESN type serves every dimension (no per-dim templates).

dimNeuronsTypical use
532Fast prototyping, embedded
664Light benchmarks
7128Standard benchmarks
8256Production, complex tasks
9–12512–4096Research, high-capacity tasks
13–168192–65536Large-scale research

When readout_slices = B > 1, the HCNN start dimension is reservoir.dim + log2(B) (set by ESN — do not set readout.dim yourself).

Enums

Declared in Readout.h.

ReadoutTask

ValueDescription
RegressionMSE loss. Raw network outputs at inference (no automatic target centering). num_outputs = number of targets.
ClassificationSoftmax + cross-entropy in the loss only. num_outputs = number of classes. Labels are int class indices in [0, num_outputs); Predict returns raw logits (argmax for the label).

ReadoutActivation

Per-Conv activation (ReadoutConfig::activation).

ValueDescription
TANHHyperbolic tangent (default)
RELURectified linear
LEAKY_RELULeaky rectified linear
NONEIdentity

ReadoutPoolType

ValueDescription
MaxAntipodal max pool (default when pooling is on)
AvgAntipodal average pool

ReadoutOptimizer

ValueDescription
AdamDefault
SgdHeavy-ball SGD; uses momentum

ReadoutLoadMode

ValueDescription
EvalLoad parameters only (default; safe for inference)
ResumeTrainAlso reset optimizer moments for continued online training

ReservoirConfig

Construction-time reservoir parameters. Defaults are a sensible starting point; production callers set dim, seed, spectral radius, and history depth per task (surveyed offline).

struct ReservoirConfig
{
    size_t   dim             = 10;     // N = 1 << dim; range [5, 16]
    uint64_t seed            = 7934791766227647176ULL;
    float    spectral_radius = 0.999f; // target for recurrent block only
    float    leak_rate       = 1.0f;   // (0, 1]
    float    input_scaling   = 0.02f;  // weights × scaling/√dim
    size_t   num_inputs      = 1;      // must divide N
    size_t   history_depth   = 16;     // M in [1, 64]
    bool     verbose         = false;  // construction banner; demos may set true

    size_t   num_external_feedback_channels = 0;  // 0 = off; else [1, N]
    float    external_feedback_scaling      = 0.5f;

    float    bias_scaling    = 0.003f; // after tanh; 0 disables
};
FieldTypeDefaultDescription
dimsize_t10Hypercube dimension; N = 2^dim. [5, 16].
seeduint64_t7934791766227647176Master RNG seed (SplitMix64 substreams: recurrent / input / external-feedback / bias / SR probe). Screen per dim/task.
spectral_radiusfloat0.999Target ρ of the recurrent companion operator (MN×MN when M > 1). Drive ports are outside the rescale.
leak_ratefloat1.0state = (1 − leak) * old + leak * (tanh(s) + bias). (0, 1].
input_scalingfloat`0.02$\text{Input} \text{weights} \text{U}(−1{,}1) \text{then} \times $input_scaling / √dim` (fan-in variance). Local construction, not a universal optimum — retune per task/dim.
num_inputssize_t1Input channels; must divide N. Channel k drives [k·N/K, (k+1)·N/K).
history_depthsize_t16Delay-line depth M [1, 64]. Recurrent gather over M published slices. Independent of how many ages the readout packs (B). See Reservoir.md.
verboseboolfalseOne construction banner on stdout.
num_external_feedback_channelssize_t0D external-feedback channels. 0 = path off. Else [1, N] (need not divide N). See ReservoirFeedbackMechanism.md.
external_feedback_scalingfloat0.5Like input; only if D > 0. Outside SR rescale.
bias_scalingfloat0.003Per-neuron bias U(−1,1)×scale, after tanh. 0 disables. Survives Clear; not in snapshots.

GetConfig().spectral_radius / ESN::TargetSpectralRadius() is the target. Post-secant estimate: Reservoir::GetRealizedSpectralRadius() / ESN::RealizedSpectralRadius().


ReadoutConfig

HCNN architecture and training. Under ESN, dim is overwritten to reservoir.dim + log2(B) — leave it at 0.

struct ReadoutConfig {
    size_t dim           = 0;        // set by ESN — do not set
    int num_outputs      = 1;
    ReadoutTask task     = ReadoutTask::Regression;
    int num_layers       = 1;        // typical; 0 = auto min(dim-2, 2)
    bool use_pooling     = true;
    ReadoutPoolType pool_type = ReadoutPoolType::Max;
    int conv_channels    = 16;
    int channel_growth   = 2;
    bool use_batchnorm   = false;
    ReadoutOptimizer optimizer = ReadoutOptimizer::Adam;
    int epochs           = 200;
    int batch_size       = 32;
    float lr_max         = 0.0015f;  // keep ≤ ~0.005
    float lr_min_frac    = 0.01f;
    int   lr_decay_epochs = 0;       // 0 = use epochs
    float weight_decay   = 0.0f;
    float momentum       = 0.9f;     // SGD heavy-ball; 0 = plain SGD; ignored by Adam
    uint64_t seed        = 42;       // full 64-bit HCNN weight-init seed
    ReadoutActivation activation = ReadoutActivation::TANH;
    size_t num_threads   = 0;        // 0=auto, 1=ST, N=N workers
    bool restore_best_epoch = true;
    float best_epoch_holdout_frac = 0.0f;
};
FieldTypeDefaultDescription
dimsize_t0Features per sample = 2dim. Set by ESN from reservoir dim + log2(B).
num_outputsint1Regression targets or class count.
taskReadoutTaskRegressionTask head.
num_layersint1Conv(+Pool) stages. Default 1 (house default for most tasks). 0 → auto min(dim − 2, 2). With pooling: assert n ≤ dim − 2.
use_poolingbooltrueAntipodal pool after each conv (mixes every bit, including block-index bits when B > 1).
pool_typeReadoutPoolTypeMaxMax or Avg when pooling is on.
conv_channelsint16First-layer channels.
channel_growthint2Multiplier after each stage.
use_batchnormboolfalsePer-conv BN; grows the weight blob.
optimizerReadoutOptimizerAdamForwarded to HypercubeCNN.
epochsint200Batch-train epochs. Ignored by online TrainStep*.
batch_sizeint32Mini-batch size (batch mode).
lr_maxfloat0.0015Cosine peak. Keep ≤ ~0.005 to avoid NaN.
lr_min_fracfloat0.01Floor = lr_max * lr_min_frac.
lr_decay_epochsint0Cosine horizon; 0 = use epochs.
weight_decayfloat0.0L2 weight decay.
momentumfloat0.9SGD heavy-ball; 0 = plain SGD. Ignored by Adam (the default optimizer).
seeduint64_t42HCNN weight-init seed (full 64-bit).
activationReadoutActivationTANHAfter each Conv.
num_threadssize_t0HCNN workers: 0 auto, 1 single-threaded (use for multi-ESN hosts), N workers.
restore_best_epochbooltrueRestore best epoch (min MSE / max accuracy) at end of Train.
best_epoch_holdout_fracfloat0.0Tail hold-out for scoring; train on prefix. 0 = score full train set. Clamped to [0, 0.5].

See Readout.md and the vendor pin in ../third_party/HypercubeCNN/VENDORED.md.


ESNConfig

struct ESNConfig {
    ReservoirConfig reservoir;
    ReadoutConfig   readout;
    // B ages packed into the readout (power of two, 1 ≤ B ≤ history_depth).
    // ESN sets readout.dim = reservoir.dim + log2(B).
    size_t          readout_slices = 1;
};
FieldDescription
reservoirFixed dynamical core.
readoutHCNN architecture + training. Leave dim at 0.
readout_slicesB delay-line ages (newest first). Must be ≥ 1, a power of two, and ≤ reservoir.history_depth. B = 1 → readout input is one N-vector. B = 2 → two blocks, identity map. B > 2 → consecutive ages land on block indices two bits apart (pair map) so a Hamming-1 kernel can see both from midpoint vertices. Widening B does not change reservoir dynamics.

ESN

Complete pipeline: Reservoir → pack B slices → Readout. Constructed from one ESNConfig. Readout hyperparameters are fixed at construction — no per-call config overloads on Train. Move-only (not copyable). Pointer APIs have std::span overloads that check lengths.

ESN esn(cfg);

// Drive
esn.ReservoirStep(inputs, external_feedback /* optional */);
esn.ReservoirWarmup(inputs, num_steps);          // or span (count = size / NumInputs)
esn.ReservoirRun(inputs, num_steps);
esn.ReservoirRun(inputs, num_steps, /*clear_recorded=*/true);
esn.ReservoirClear();

// Batch train / score on recorded readout inputs
esn.Train(targets, train_size);
esn.R2(targets, start, count);            // full buffer covering [0, start+count)
esn.R2FromWindow(window, start, count);   // window-only targets
esn.NRMSE(targets, start, count);
esn.Accuracy(labels, start, count);       // int labels

// Streaming
esn.TrainStep(target, lr, weight_decay);
esn.TrainStepBatch(readout_inputs, targets, count, lr, weight_decay);
esn.CopyReadoutInput(out);      // B×N
esn.CopyReservoirState(out);    // N only (newest slice)

// Predict
esn.Predict();
esn.PredictFromRecorded(timestep);
esn.PredictFromReadoutInput(readout_input);  // B·N; PredictFromState is an alias

// Persist / inspect
esn.GetConfig();
esn.Dim();                       // == ReservoirHypercubeDimension()
esn.TargetSpectralRadius();
esn.RealizedSpectralRadius();
esn.GetReadoutState();
esn.SetReadoutState(state, mode);
esn.SaveReadoutHcnnModel(stem);
esn.LoadReadoutHcnnModel(stem, mode);
esn.ReadoutArchSummary();
esn.ReadoutBestEpoch();

Construction

explicit ESN(const ESNConfig& cfg);

Builds the reservoir (Create) and the HCNN eagerly (MakeReadoutConfig fills readout.dim). Both weight sets are ready before the first Train / TrainStep.

ESNConfig cfg;
cfg.reservoir.dim             = 8;
cfg.reservoir.seed            = 74119;
cfg.reservoir.spectral_radius = 0.99f;
cfg.readout_slices            = 1;       // or 2, 4, … ≤ history_depth
cfg.readout.epochs     = 1000;
cfg.readout.batch_size = 512;
cfg.readout.lr_max     = 0.001f;
ESN esn(cfg);

Reservoir driving

ReservoirStep

Pointer and std::span overloads (span form validates lengths):

void ReservoirStep(const float* inputs, const float* external_feedback = nullptr);
void ReservoirStep(std::span<const float> inputs,
                   std::span<const float> external_feedback = {});

One timestep: stage task inputs (NumInputs() floats), optionally stage external feedback (NumExternalFeedbackChannels() floats, or nullptr to skip), then Reservoir::Step. No learning.

Throws if external_feedback is non-null when D = 0.

ReservoirWarmup
void ReservoirWarmup(const float* inputs, size_t num_steps);
void ReservoirWarmup(std::span<const float> inputs);  // count = size / NumInputs()

Drive without recording (wash out zero initial state). Layout: num_steps × NumInputs() row-major. No external feedback — use ReservoirStep if needed. Typical warmup: 100–500 steps. Span form requires inputs.size() to be a multiple of NumInputs().

Values are not clamped; pass already-bounded signals.

ReservoirRun
void ReservoirRun(const float* inputs, size_t num_steps, bool clear_recorded = false);
void ReservoirRun(std::span<const float> inputs, bool clear_recorded = false);

Drive and append each assembled readout input (B×N) to the internal buffer for Train / metrics. Same input layout as warmup. No external feedback.

clear_recorded = true discards prior rows first (live reservoir and readout weights untouched).

ReservoirClear
void ReservoirClear();

Zero reservoir dynamics (state + history). Recorded rows and readout weights are preserved.


Batch training

Train
// Regression
void Train(const float* targets, size_t train_size);
void Train(std::span<const float> targets, size_t train_size);
// Classification
void Train(const int* class_labels, size_t train_size);
void Train(std::span<const int> class_labels, size_t train_size);

Fit the HCNN on recorded timesteps [0, train_size). A second call continues from current weights — construct a new ESN for a fresh init. Task is fixed at construction; the wrong pointer / span type throws std::logic_error.

  • Regression: train_size × NumOutputs() floats, row-major. Span size must equal that product.
  • Classification: train_size ints (class indices in [0, NumOutputs())). Span size must equal train_size.

Throws if train_size > NumCollectedStates().


Streaming training

CNN is built at construction — no separate init. Warm up the reservoir, then interleave ReservoirStep with TrainStep / Predict. epochs is ignored; loop length is the caller's.

TrainStep
void TrainStep(const float* target, float lr, float weight_decay = 0.0f); // regression
void TrainStep(std::span<const float> target, float lr, float weight_decay = 0.0f);
void TrainStep(int class_label, float lr, float weight_decay = 0.0f);     // classification

One gradient step on the current readout input (assembled after your last drive). Regression: NumOutputs() floats. Classification: one integer class index. Online hosts typically schedule lr with CosineLR / ExponentialDecayLR from Readout.h (epochs is ignored here).

TrainStepBatch
void TrainStepBatch(const float* readout_inputs, const float* targets, size_t count,
                    float lr, float weight_decay = 0.0f);  // regression
void TrainStepBatch(const float* readout_inputs, const int* class_labels, size_t count,
                    float lr, float weight_decay = 0.0f);  // classification
void TrainStepBatch(std::span<const float> readout_inputs, std::span<const float> targets,
                    float lr, float weight_decay = 0.0f);
void TrainStepBatch(std::span<const float> readout_inputs, std::span<const int> class_labels,
                    float lr, float weight_decay = 0.0f);

Mini-batch of caller-supplied readout inputs (count × ReadoutInputWidth()). Assemble rows with CopyReadoutInput (not CopyReservoirState, unless B = 1). Span forms infer count from readout_inputs.size() / ReadoutInputWidth().

CopyReadoutInput / CopyReservoirState
void CopyReadoutInput(float* out) const;     // ReadoutInputWidth() = B×N
void CopyReadoutInput(std::span<float> out) const;
void CopyReservoirState(float* out) const;   // N (newest slice only)
void CopyReservoirState(std::span<float> out) const;

Prediction and evaluation

Recorded window
std::vector<float> PredictFromRecorded(size_t timestep) const;
double R2(const float* targets, size_t start, size_t count) const;
double R2(std::span<const float> targets, size_t start, size_t count) const;
double R2FromWindow(std::span<const float> targets_window, size_t start, size_t count) const;
double NRMSE(const float* targets, size_t start, size_t count) const;
double NRMSEFromWindow(std::span<const float> targets_window, size_t start, size_t count) const;
double Accuracy(const int* labels, size_t start, size_t count) const;
double AccuracyFromWindow(std::span<const int> labels_window, size_t start, size_t count) const;
  • R2 / NRMSE / Accuracy: targets must cover [0, start+count) — pass the full array; methods index from start. Do not pre-slice.
  • *FromWindow: targets / labels are only the scored rows (count samples). Recorded states still use start. Use these when you already hold a sliced buffer.
  • R²: average of per-output coefficients of determination. 1.0 = perfect. Regression layout: stride = NumOutputs().
  • NRMSE: mean over outputs of RMSE / std(target). 0 = perfect. Degenerate target variance → +inf on that output.
  • Accuracy: integer class labels; multi-class argmax; single-output thresholds the logit at 0.
Live / caller-supplied
std::vector<float> Predict() const;                    // assemble live, then forward
void Predict(float* out) const;
void Predict(std::span<float> out) const;
std::vector<float> PredictFromReadoutInput(const float* readout_input) const;
void PredictFromReadoutInput(const float* readout_input, float* out) const;
std::vector<float> PredictFromReadoutInput(std::span<const float> readout_input) const;
void PredictFromReadoutInput(std::span<const float> readout_input, std::span<float> out) const;
// Historical aliases — same as PredictFromReadoutInput:
std::vector<float> PredictFromState(const float* readout_input) const;
void PredictFromState(const float* readout_input, float* out) const;

PredictFromReadoutInput never reads the reservoir — pass a ReadoutInputWidth() buffer (e.g. from CopyReadoutInput). Softmax is not applied; classification returns logits.


State access and accessors

std::vector<float> CollectedStates() const;  // T × ReadoutInputWidth(), row-major

CollectedStates is the recorded readout inputs (B×N per row), not just the newest reservoir slice. Name is historical.

MethodReturns
NumCollectedStates()Rows recorded by ReservoirRun
NumInputs()Input channels per timestep
NumOutputs()Readout width (targets or classes)
NumExternalFeedbackChannels()D (0 = no ext-fb port)
ReservoirHypercubeDimension() / Dim()cfg.reservoir.dim
ReservoirNeuronCount()N = 2dim
ReadoutInputWidth()B × N
ReadoutBlockCount()B
ReadoutBlockOf(slot)Physical block index for logical age slot
TargetSpectralRadius()Configured recurrent ρ target
RealizedSpectralRadius()Post-rescale estimate from construction
GetConfig()ESNConfig with derived readout.dim filled

Readout persistence

Reservoir weights are deterministic from config + seed. Persist GetConfig() and the readout.

ESN::ReadoutState

FieldDescription
weightsOpaque vector<double> (unversioned HCNN blob). Round-trip only.
is_trainedTrue if the network exists (true after construction — not “has seen data”).
MethodDescription
GetReadoutState()Snapshot weights
SetReadoutState(state, mode=Eval)Inject into the live net. No-op if !is_trained
ReadoutBestEpoch()1-based best epoch after last batch Train with restore, else 0
SaveReadoutHcnnModel(stem)Portable stem.hcnw + stem.arch.json
LoadReadoutHcnnModel(stem, mode=Eval)Load after arch sidecar validation
ReadoutArchSummary()Human-readable stack + parameter counts
ESNConfig cfg   = esn.GetConfig();
auto      state = esn.GetReadoutState();
// serialize cfg + state …

ESN restored(cfg);
restored.SetReadoutState(state);

Standalone Reservoir and Readout

Most adopters only construct ESN. Reservoir.h and Readout.h are still public (included by ESN.h).

ReservoirReservoir::Create(cfg) returns unique_ptr<Reservoir> (non-copyable, non-movable). Per-step contract: InjectInput / optional InjectExternalFeedback, then Step(), then Outputs() / SliceAt(age). Clear() zeros dynamics. TakeSnapshot / RestoreSnapshot round-trip the delay line (not weights). GetConfig() + seed rebuilds matching weights; GetRealizedSpectralRadius() is the post-rescale estimate.

Readout — construct from ReadoutConfig (set dim yourself if you are not going through ESN). Train / TrainStep* / PredictRaw / PredictClass / R2 / Accuracy / Weights / SetState / SaveHcnnModel. Online hosts schedule lr with the free functions in Readout.h:

float CosineLR(float progress, float lr_max, float lr_min);
float ExponentialDecayLR(float progress, float lr_max, float lr_min);

progress is clamped to [0, 1]. Batch Train uses HCNN's own cosine schedule from epochs / lr_decay_epochs instead.


Dependencies

HypercubeCNN — hypercube convolutional stack used by Readout.

  • Vendored read-only snapshot at third_party/HypercubeCNN (see VENDORED.md).
  • Built transitively via add_subdirectory; offline and version-pinned.
  • Linked through HypercubeESNCore — consumers only link HypercubeESNCore.
  • Public HypercubeESN surface is ESN / Reservoir / Readout types; full HCNN API is not re-exported (hcnn::HCNN is PIMPL'd inside Readout).

No other external dependencies beyond the C++ standard library.