HypercubeEtalon C++ SDK

September 11, 2026 · View on GitHub

You place a fixed pattern on the hypercube — an image pack, a spectrum, or any field you built yourself. HypercubeEtalon runs one etalon transit over that field — a deterministic wave swept through every vertex/antipode cavity of the cube — then trains a small CNN on the transit output. One class does the whole loop: collect samples, train the head, predict.

You do not need to learn HypercubeWTF, HypercubeCascade, or HypercubeCNN first. Link HypercubeEtalonCore, include Etalon.h, and work with Etalon. Demos and packing helpers are optional recipes; they are not the product.

This guide matches the public headers for 1.0.x.

Who it is for: anyone embedding the Etalon in a host (collect → train → predict), and anyone learning the stack with the same API the demos use.

What you get: a C++23 static library. Headers sit at the repo root. A vendored HypercubeCNN builds the trainable readout; hosts usually never call HCNN themselves.

Section
1. Why explore HypercubeEtalonFamily role, what the transit buys
2. The big pictureWhere Etalon sits among WTF / Cascade / CNN
3. One sampleThe transit, mechanics, API words
4. Product surfaceHeaders, rules, the loop
5. BuildCMake, binaries
6. First programMinimal collect → train → predict
7. APIConfig and methods
8–13Boundaries, demos, pitfalls, cheat sheet

1. Why explore HypercubeEtalon

HypercubeEtalon processes spatial data through one frozen preprocessing stage in front of a small trainable CNN: an etalon transit. The transit's neighbor weights are drawn once at construction and never trained. Only the CNN readout learns.

The point of the experiment is to see whether that frozen stage makes the readout's job easier — better accuracy at the same readout size, or the same accuracy at a much smaller readout — than feeding the CNN the raw field (bypass_exciter runs exactly that ablation). On the MNIST white-noise study the transit path holds up markedly better than the bypass as test noise grows; see WhiteNoiseFilter.md for the numbers.

The aim is a preprocessor effective enough that the readout can be a single convolutional layer with a single channel and no pooling — fast to train, small in memory, and requiring essentially no CNN architecture engineering. The Raman baseline example already runs at exactly that readout size.

Whether the single-stage transit has real product value is still an open question.


2. The big picture

Most learning systems either see a stream (one small input every step) or a static pattern (classify an image once). The Etalon takes a static pattern and sweeps a deterministic wave through it:

  1. You give it one full-length field on the cube (packed however you like).
  2. The Exciter runs one etalon transit — same field length in, same out.
  3. It trains a CNN on that transit output.

The preprocessor never trains. Only the readout does. That is the whole product idea.

LibraryYou typically feed it…What runs
HypercubeEtalona static length-N fieldetalon transit → HCNN
HypercubeWTFa static length-N fieldreservoir orbit → end state → HCNN
HypercubeCascadea static length-N fieldtransit → orbit → end state → HCNN

Your data does not have to be a power of two

dim is the size knob for the whole pipeline: set cfg.exciter.dim (valid 4…12; prefer ≥ 5 so a pooled readout has room) and you get N = 2dim vertices. Leave cfg.readout.dim at 0 — construction fills it from the Exciter; any other value must equal exciter.dim or the constructor throws. The Etalon always expects a field of length N.

If your raw data is 784 pixels or 300 bins, you map it onto N floats first (pad, resize, spatial embed, custom layout — your choice). The Etalon does not invent that map. Demo helpers under examples/common/ are one MNIST-oriented recipe; skip them when you pack your own way.

What freezes vs what learns

PieceTrains?
Exciter neighbor weightsNo — drawn once at construct
The input gain (exciter.input_scaling)No — a config scalar you set
How you pack domain data into the fieldYour problem (outside the Etalon)
HCNN readoutYes

If you change only exciter.seed, the frozen transit weights change — and on the tasks measured so far, the results barely move (seed is not a tuning parameter). The gains (input_scaling, weight_scaling) are tuning parameters: they set how hard the tanh sites are driven.


3. One sample, step by step

Think of one map as: scale the field once, sweep a wave through every cavity of the cube, keep what stands at each start vertex when its wave comes back.

The etalon transit

The Exciter is not a reservoir: no leak, no delay line, no orbit. It is a fixed nonlinear map. At construction it draws one weight per (vertex, neighbor) pair — N × dim weights from U(-1, 1) × weight_scaling — and never updates them again.

An etalon here is a start vertex r and its face antipode treated as a pair of reflectors. The face is the subcube spanned by the low subcube_dim bits — M_walk = 2^subcube_dim vertices, with the high bits pinned by r. One transit scales the input once (by exciter.input_scaling), then for every start r reloads that scaled field, walks the face out to the antipode and back, and writes one output sample: the value standing at r after the second visit. Each site update is tanh of a full-star neighbor sum, applied sequentially — the disturbance propagates through the face and reflects back. Off-face neighbors are never updated during a walk, so every update also mixes in unmodified input.

N starts → N output samples → the transit output is a field with the same length and vertex indexing as the input.

Mechanics in order

  1. Copy the caller's field (the Etalon never writes your buffer).
  2. One etalon transit over the copy (the copy is scaled in place once, then swept start by start).
  3. The transit output (length N) times readout_scale is the feature row the CNN sees.
  4. Hand those features to the readout (collect, train, or predict).

With bypass_exciter = true, step 2 is skipped and the features are the field times readout_scale — the ablation path.

x  (length N, fixed for this sample)


 Etalon transit  (or a plain copy, when bypass_exciter)


 × readout_scale → features (N)


 HCNN readout → class logits or regression values

Words you will see in the API

WordPlain meaning
dimCube dimension you choose (4…12) via exciter.dim; readout.dim auto-fills
NField length = 2dim (input and features — both N)
subcube_dimFace size of one etalon cavity; walk covers 2subcube_dim vertices
input_scalingGain applied once to the field before the transit
weight_scalingNeighbor weights are U(-1, 1) × this
bypass_exciterSkip the transit; features = the field (ablation)
readout_scaleGain between transit output and readout

Unlike HypercubeWTF there is no T / B / readout_slices machinery: there is no orbit, the readout always sees exactly the transit output, and the feature size is always N.


4. What is the product (and what is not)

You care about…Use…
Integrating the libraryEtalon + EtalonConfig
Walk size, saving readout weightset.exciter() / et.readout()
Feature probeet.LastFeatures()
Learning by exampleetalon_synth, etalon_mnist, etalon_raman
MNIST paths / packing demosexamples/common/ (optional)
Raw HypercubeCNNAlmost never — that lives under the readout
Etalon.h           front door (Etalon + EtalonConfig)
Exciter.h          ExciterConfig (+ Exciter for inspection)
Readout.h          ReadoutConfig, enums, Readout
… .cpp files …
third_party/HypercubeCNN/    vendored; see VENDORED.md
examples/                    demos, not the SDK definition
docs/CPP_SDK.md              this guide

Link HypercubeEtalonCore (it pulls HypercubeCNNCore for you).

Rules that matter

These are product contracts, not implementation trivia.

  • Every field is length N. Wrong size throws.
  • Values are usually kept in [-1, 1]. The library trusts the host; it does not clamp.
  • You pack; the Etalon maps. No built-in image layout.
  • One dim. Set exciter.dim; leave readout.dim at 0 (auto). A nonzero mismatch throws.
  • The Exciter freezes at construct.
  • Predict returns raw logits (or regression values) — no softmax.
  • AccuracyOnCollected / R2OnCollected are training-set scores. For held-out data use Accuracy(fields, labels) / R2(fields, targets), which map fresh.
  • There is no built-in train-input noise knob. If a study needs noisy collect or noisy eval, the host adds the noise (see etalon_mnist).
  • One Etalon per thread of control. Bulk collect parallelizes inside one call; do not call public methods concurrently on the same object.
  • Etalon is not copyable but is movable. Moved-from objects may only be assigned to or destroyed.

The loop you will write

fill EtalonConfig
construct Etalon once
collect many samples      (Collect / CollectBatch)
TrainOnCollected
Predict / PredictClass    (always a fresh clean map)

Optional extras: Run + LastFeatures, train-set metrics, held-out Accuracy / R2, ClearCollected, collect_threads for faster bulk collect, bypass_exciter for the ablation.


5. Build and consume

You need C++23 and CMake ≥ 3.21. Prefer Release when you care about study numbers (Debug and Release float behavior can differ with this project's fast-math flags).

In CLion: open the project, reload CMake, build. From a shell with the toolchain available:

cmake --build cmake-build-release

When this repo is the top-level project you also get:

BinaryRole
HypercubeEtalonContract smoke (sizes, determinism, bypass, move)
etalon_synthMulti-class synthetic fields (no data files)
etalon_mnistMNIST recipe + test-noise sweep (IDX files under C:\HypercubeEtalon\data)
etalon_ramanRaman baseline regression (spectra under C:\HypercubeEtalon\RamanSpectraLCOHard)
etalon_raman_extractWrites selected baseline extracts from a saved readout

If you pull HypercubeEtalon in as a subdirectory, demos are skipped; you still get the library.

add_subdirectory(path/to/HypercubeEtalon)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE HypercubeEtalonCore)
#include "Etalon.h"

(HypercubeEtalonCore exports the repo root as a public include directory, so the one include line is enough.)


6. First program

A tiny two-class example — collect, train, predict. (Verified against the library: trains and predicts correctly on this toy task.)

#include "Etalon.h"
#include <cstdio>
#include <vector>

int main() {
    EtalonConfig cfg;
    cfg.exciter.dim = 5;                // N = 32; readout.dim auto-fills
    cfg.exciter.subcube_dim = 4;        // default 6 is illegal at dim 5
    cfg.exciter.seed = 1;
    cfg.exciter.input_scaling = 1.0f;   // defaults (0.02) barely tickle tanh
    cfg.exciter.weight_scaling = 0.5f;

    cfg.readout.num_outputs = 2;
    cfg.readout.task = ReadoutTask::Classification;
    cfg.readout.epochs = 80;
    cfg.readout.conv_channels = 4;
    cfg.readout.num_threads = 1;

    Etalon et(cfg);
    const size_t N = et.N();

    auto field = [&](int label) {
        std::vector<float> x(N, 0.f);
        const float s = (label == 0) ? 1.f : -1.f;
        for (size_t i = 0; i < N / 2; ++i)
            x[i] = s * (0.2f + 0.8f * float(i) / float(N));
        return x;
    };

    for (int i = 0; i < 24; ++i) {
        et.Collect(field(0), 0);
        et.Collect(field(1), 1);
    }
    et.TrainOnCollected();

    // AccuracyOnCollected is the *training* set, not test data.
    std::printf("train acc=%.3f  pred0=%d pred1=%d\n",
                et.AccuracyOnCollected(),
                et.PredictClass(field(0)),
                et.PredictClass(field(1)));
    return 0;
}

Habits that save pain later

  1. Build the config, construct one Etalon (weights freeze here).
  2. Collect a dataset, then train. You can call TrainOnCollected again without clearing — it continues from the current readout weights.
  3. Treat Predict / PredictClass as clean inference — each is a fresh map.
  4. Keep packing in your code (or a demo helper). The core only accepts length-N fields.

7. The API you actually use

Authoritative signatures and contracts live in Etalon.h (and the headers it pulls). This section is the host-oriented map.

Config at a glance

Everything interesting is set before Etalon is constructed.

struct EtalonConfig {
    ExciterConfig exciter{};         // exciter.dim is the size knob [4, 12]
    ReadoutConfig readout{};         // leave readout.dim = 0 (auto)

    bool bypass_exciter = false;     // features = a copy of the field (ablation)
    float readout_scale = 1.0f;      // transit → readout gain (finite, > 0)
    size_t collect_threads = 0;      // bulk maps: 0 = auto, 1 = serial, K = K
};

Exciter (frozen transit) — the preprocessing knobs:

FieldMeaningValid / notes
dimCube dimension; N = 2dim[4, 12]; prefer ≥ 5 for pooled readouts
seedNeighbor-weight drawsany uint64_t; results are seed-insensitive
input_scalingScales the field once before the transitdemos use 1.0 (the header default 0.02 barely drives tanh)
weight_scalingNeighbor weights are U(-1, 1) × thisdemos use 0.15…0.5
subcube_dimEtalon face size; walk covers 2subcube_dim vertices[1, dim] — the default 6 throws below dim 6

Readout (trainable head) — knobs most hosts touch:

FieldMeaning
num_outputsClasses, or regression width
taskReadoutTask::Classification or Regression
epochs, batch_sizeBatch training
lr_max, lr_min_frac, lr_decay_epochsCosine learning-rate schedule
num_layers, conv_channels, channel_growthConv stack size (the goal is 1 / 1 / 1)
use_pooling, pool_typeAntipodal pool after each conv
activationPer-conv activation (demos often NONE)
num_threadsHCNN workers — use 1 for simple determinism
restore_best_epochKeep best-epoch weights (default true)
seedReadout weight init

Deeper fields (batch-norm, optimizer, holdout fraction, epoch_tick) live on ReadoutConfig in Readout.h.

After construct — sizes and inspection

explicit Etalon(const EtalonConfig& cfg);

et.Dim();  et.N();
et.NumCollected();
et.CollectedFeatures();    // span, sample-major, NumCollected() * N
et.NumOutputs();
et.CollectThreads();       // configured preference (0 = auto)

et.config();               // resolved knobs (readout.dim filled in)
et.exciter();              // const — transit config, walk size
et.readout();              // mutable — weights, HCNW save/load, IsTrained

Construction checks the usual mistakes: dim out of [4, 12], subcube_dim out of [1, dim], a nonzero readout.dim that does not match, num_outputs < 1, plus every stage's own checks.

Run a map (no training)

et.Run(x);                         // x.size() == N; x is not modified
auto f = et.LastFeatures();        // length N — what the readout would see

LastFeatures updates on every completed map on the instance — Run, Collect, Predict, PredictClass, Accuracy, R2, and bulk collects (last row). The span is valid until the next map — copy what you keep.

The demos use this probe to print mean-|value| lines ("~1 is a live field, ~0 is crushed") — the fastest way to tune the two exciter gains.

Collect, train, predict

Classification

et.Collect(x, class_label);               // one sample
et.CollectBatch(fields_flat, labels);     // bulk, sample-major

Regression — same idea with target vectors (num_outputs floats per sample):

et.Collect(x, targets);
et.CollectBatch(fields_flat, targets_flat);
et.ClearCollected();             // drop training rows (keeps worker pool)
et.TrainOnCollected();           // needs at least one sample; does not clear

auto out = et.Predict(x);        // num_outputs floats; no softmax
int y = et.PredictClass(x);      // classification only

double acc = et.AccuracyOnCollected();    // train set
double r2  = et.R2OnCollected();          // train set, regression

Held-out scoring — these map every field fresh (bulk, parallel), then score:

double test_acc = et.Accuracy(test_fields_flat, test_labels);
double test_r2  = et.R2(test_fields_flat, test_targets_flat);

Bulk layout notes:

  • fields_flat is sample-major: sample i starts at i * N.
  • Labels are validated before any mapping starts; a throw leaves the collected set unchanged.
  • Wrong task (class API on a regression net, etc.) throws.

Faster bulk collect

EtalonConfig::collect_threads:

  • 0 — auto (leaves one or two cores free so the machine stays responsive)
  • 1 — serial
  • K — up to K workers

Worker 0 reuses the primary Exciter. Extra workers clone the frozen Exciter once (same seed → identical weights) — unless bypass_exciter, where there is nothing to clone. The internal thread pool grows for the life of the Etalon and does not shrink. Single-sample calls are always serial.


8. Please do not

TemptationBetter path
Drive Exciter yourself for product trainingUse Etalon maps
Call vendored hcnn::HCNN from the appLet Readout own it; export via et.readout() if needed
Depend on examples/common in productionCopy the idea; own your packing
Chain a second frozen stage by handThat experiment exists: HypercubeCascade
Size the readout separatelyLeave readout.dim = 0; construction fills it

Exciter and Readout headers are public so config and inspection work. The happy path is still collect → train → predict on Etalon.


9. Demos as recipes

Demos keep product knobs in MakeBaseConfig() / MakeConfig() and demo-only constants (k*) beside them:

config → Etalon → collect → train → score → predict
DemoWhen to open it
examples/synth/etalon_synth.cppFast multi-class gate without data files
examples/mnist/etalon_mnist.cppReal packing, bypass comparison, test-noise sweep
examples/RamanBaselineExtraction/etalon_raman.cppRegression at the goal readout size, save + reload check
examples/RamanBaselineExtraction/etalon_raman_extract.cppInference-only from a saved readout

More context: examples/README.md.


10. Threads, memory, and cost

  • Treat one Etalon as exclusive for public calls.
  • Bulk collect is where parallelism belongs; it clones the frozen Exciter as needed.
  • Per-sample cost is the transit: N × (2 × 2^subcube_dim − 1) × dim multiply-adds, plus an N-float reload per start. There is no orbit — one map here is the cheap end of the family.
  • Features are always N floats; the CNN scales with its own layers and channels.
  • If you already run many Etalon instances in parallel, set readout.num_threads = 1 so HCNN does not oversubscribe the machine.
  • Prefer Release when comparing accuracies across runs.

11. Common mistakes

Symptom / assumptionFix
Throw on collect / runField length must equal et.N()
"Why can't I pass 784 floats?"Pack to N first
Construct throws at small dimDefault exciter.subcube_dim = 6 needs dim ≥ 6 — set it ≤ dim
Construct throws on readout.dimLeave it 0 (auto); nonzero must equal exciter.dim
Softmax inside PredictYou get logits; use PredictClass or argmax
Expected a train-noise σ knobNot in this host — add noise in your collect loop
Great train accuracy, bad real testAccuracyOnCollected is the training set; use Accuracy(...) on held-out fields
Features look crushed (~0)Header defaults drive tanh weakly — the demos run input_scaling ≈ 1.0 and weight_scaling 0.15…0.5; probe with Run + LastFeatures
Racey results with shared EtalonOne instance, one host thread of control
Linked HypercubeCNN onlyLink HypercubeEtalonCore, include Etalon.h

12. Further reading

DocWhat it is
WhiteNoiseFilter.mdWhite-noise study: transit vs bypass
RamanBaselineExtraction/README.mdRegression task and results
examples/README.mdDemo map and data-file notes
VENDORED.mdWhich HypercubeCNN pin is in tree
HypercubeWTF / HypercubeCascadeThe reservoir sibling and the two-stage experiment

13. Cheat sheet

#include "Etalon.h"

EtalonConfig cfg;
cfg.exciter.dim = 7;                   // N = 128; readout.dim auto-fills
cfg.exciter.subcube_dim = 5;           // <= dim
cfg.exciter.input_scaling = 1.0f;
cfg.exciter.weight_scaling = 0.15f;
cfg.collect_threads = 0;               // auto

cfg.readout.num_outputs = K;
cfg.readout.task = ReadoutTask::Classification;
cfg.readout.epochs = 100;
cfg.readout.num_threads = 1;

Etalon et(cfg);

et.CollectBatch(fields_flat, labels);       // count * N floats, sample-major
et.TrainOnCollected();

int y = et.PredictClass(x);
auto logits = et.Predict(x);
double test_acc = et.Accuracy(test_flat, test_labels);

et.Run(x);                                  // probe one map
auto feats = et.LastFeatures();             // N

In one line: pack a field → frozen etalon transit → features → train the CNN head.