Combinatorics for the rest of us

Ither is a toolkit for combinatorial mapping of structured and text-based data. A formula defines a family of structures and a reversible mapping that gives each structure an exact numerical coordinate. The compiled space encodes and decodes one structure at a time; queries explore many coordinates as grids, rays or lists; analysis inspects spaces and checks live or saved datasets.

With integer axes, a design space can be enumerated, searched, sampled, paged through and stored by position — without generating the whole family first.

Status: Ither is a young project. 0.3 is the first public release. The API may still change before 1.0, and its manual and API reference are still being written.

This desk

  • Examples — spaces to move through: drag a ruler, click a neighbour
  • Workbench — write a formula, query its space, read the analysis
  • Library — Ither in brief, from its README, and where the rest is

Drag a block by its title, close it with ×; auto layout puts everything back.

npm install @labofbabel/ither · GitHub · npm

Examples

Each one is a space of its own. Move through it, then open its formula in the workbench.

2d colour palette
sentence
3d composition
polygon
css button
title card

Workbench

Ither's three tiers, on one space:

  • formula — the one object that defines a space; compile it into a Space
  • query — walk part of the space and get a record for every point
  • analysis — capsules that check the space and the records

Pick a formula — the site's spaces, or the library's formula bank — edit it and compile. The query runs by itself whenever it can; the records, the request and the report show what the library answered, and the diagnostics read it.

formula
query
records
space summary
serialization
axes
layers
checks
geometry
integrity
coverage
distribution
roundtrip
console
request
report

Library

Ither in brief: the README, block by block. The rest lives with the library:

  • Manual — concepts, workflows and integration
  • API — exact request and return contracts
  • Changes

Status: version 0.3.0, the first public release — very young. The formula, query and analysis tiers each have their contract; the API may still change before 1.0. The manual and the API reference are still being written. MPL 2.0.

install
npm install ither
import { Ither } from 'ither';

ES modules, no runtime dependencies, no consumer build step; Node ≥ 20.10, modern browsers and Deno. The same API works in server processes, command-line scripts and browser apps. Ither does not start a server, read arguments or choose storage; those stay with the application. Without a package manager, vendor dist/ither.min.js.

quick start

A space is defined by one object — a formula.

import { Ither } from 'ither';

const space = Ither.from({
  layers: { f: { type: 'AST', domains: {
    x: ['a', 'b', 'c', '0', '1'], o2: ['add', 'mul', 'sub'] } } },
});

space.encode({ f: 'add(mul(a,b),c)' });   // [1337n]
space.decode([1337n]);                    // { f: 'add(mul(a,b),c)' }
space.decode([1338n]);                    // a neighbouring expression

A coordinate is an array with one BigInt per axis; a structure is an object keyed by layer name — always, also for one axis or one layer.

the core idea

A design space normally has no addresses: you edit structures, but cannot ask what lives at a particular point. Ither gives it integer axes. That makes it possible to enumerate, search, sample, page through and store positions in the space without generating the whole family first.

For a bounded contiguous axis, coordinate + 1 means the next structure in the ordering chosen by the formula. With several axes, a coordinate is an array of those integers. Projection and combine determine whether adjacency preserves a useful notion of locality, deliberately destroys it, or uses a sparse range that can contain holes.

Manual, chapter 1 · choosing a combine

three tiers
tierinputdoesoutput
1. formulaa formula objectdefines the coordinate ↔ structure mapping, strictlya Space
2. querya Space and coordinates (or structures)decodes (or encodes) them, one record contract for every inputrecords, progress and final reports, and on request a manifest
3. analysisa space, a geometry, records (live or saved), a manifestruns capsules, each on the inputs it declares; one that cannot run says soan analysis report with provenance
formula ──▶ Space ──query──▶ records + manifest ──analysis──▶ report

Each tier is useful alone: a Space answers single encode / decode calls without a query, and analysis can read records saved months ago — with the manifest saying which formula and version produced them. The workbench is all three, on one space.

concepts
  • formula — the one object that defines a space: { layers, projection?, options? }
  • space — the compiled formula: Ither.from(formula); you encode, decode and query it
  • coordinate — one BigInt per projection axis; a unique address in the space
  • structure — what a coordinate decodes to, keyed by layer name
  • layer — the encoder/decoder for one structure family
  • projection — how layer coordinates are laid out across the axes
  • bound — an axis's exclusive upper limit, where the formula fixes one
  • outside / hole — a coordinate past its axis's range / in range but naming no structure
  • query — decoding a grid, a ray or a list of coordinates (or encoding a list of structures) as records
  • manifest — what a query walked, with which formula and version, and every ordinal without a record

Convention: anything that is an array is ordered and the order is meaningful; anything that is an object is named and its key order is irrelevant.

layers
typeencodes
ASTexpression trees (topology + symbols jointly)
OBJkey/value objects
ARRfixed-shape arrays
STRfill-in templates
LSML-system grammar spaces
TRIconvex-polygon triangulations (Catalan)
NUMreal-valued axes (quantized)
PERMpermutations of [0,n) (Lehmer)

Every layer spec uses the same envelope — { type, domains?, shape?, from?, options? }. Anything a formula does not declare correctly is refused, naming the key and the layer.

composition

Name the layers and project them onto axes. A projection entry is one axis; a reference is a layer ('topo') or a part ('fill.o1'). Several references on one axis combine — by the formula's one options.combine: box (the default), gray, morton, hilbert, scramble or cantor — and from chains one layer's output into another's template.

const space = Ither.from({
  layers: {
    topo: { type: 'AST', domains: { x: ['x'], o1: ['o1'], o2: ['o2'], o3: ['o3'] } },
    fill: { type: 'STR', from: 'topo',     // template = topo's output
            domains: { x: ['F','I','1.0'], o1: ['nrm','sin'], o2: ['dot','add'], o3: ['mix'] } },
  },
  projection: [ ['topo'], ['fill.x'], ['fill.o1','fill.o2','fill.o3'] ],
});

space.decode([7n, 4n, 1n]);
// { topo: 'o2(x,o1(x))', fill: 'dot(I,sin(I))' }

space.recover({ fill: 'dot(I,sin(I))' });
// the topology read back from the text

Manual, chapter 4 · API §1.3

query many points
const { query, manifest } = await Ither({
  space,
  query: {
    decode: { type: 'grid', axes: [{ start: 0n, end: 9n }, { start: 0n, end: 19n }] },
    write(record) {
      // { ordinal, coordinate, status: 'structure' | 'hole' | 'error', structure | error }
    },
    progress(report) { /* fraction, records, outside, remainingMs, … */ },
    manifest: true,
  },
});

Inputs: a grid, a ray (start, direction, count), a list of coordinates, or encode a list of structures. A point past its axis's range is never answered with another structure: it is omitted, counted in outside, and listed in the manifest. offset and limit page in ordinals; write returning false or an AbortSignal stops cleanly; nextOffset resumes exactly.

Manual, chapter 5 · API, chapter 2

analysis
const ray = { type: 'ray', start: [0n, 0n], direction: [1n, 1n], count: 100n };

const dryRun = await Ither({ space, analysis: { suite: ['geometry-summary'], geometry: ray } });
const live = await Ither({ space, query: { decode: ray }, analysis: { suite: ['query-suite'] } });
const saved = await Ither({ space, analysis: { records, manifest, suite: ['query-suite', 'lib-suite'] } });

saved.analysis.provenance.mapping;   // 'manifest-matches' — or 'manifest-differs'

Each capsule declares the inputs it needs (space, geometry, records, manifest). One that cannot run on what a request supplies gives an inapplicable result naming why, and the rest run. Ither owns no storage format; an analysis.adapt(row, position, context) converts stored rows back into records.

Manual, chapter 6 · API, chapter 3

formula bank

A versioned set of example formulas, shared by the library's tests and client projects — the workbench's formula list includes them.

import { listFormulas, loadFormula } from 'ither/formulas/bank.mjs';
const space = Ither.from(await loadFormula('ast-shader-expression'));

The bank's format, and how to add one

determinism

For a given formula, the coordinate ↔ structure mapping is deterministic and identical on any engine or OS — a pure function of (version, canonical formula, structure). Golden coordinates pin it in the tests.

The version travels with what it produces — space.pack() and every query manifest record it — so data from another version is detectable, and analysis reports it instead of refusing it.