Skip to content

Columnar Programs

Rediscovers a causal program over a table of numeric columns. The search uses the vectorized primitive kit and the causal window primitives, and the fitness function masks the nan warmup that those windows produce.

See the Columnar Programs tutorial for a walkthrough of the pieces used here.

import numpy
from deap_er import Fitness, Toolbox, creator, gp, tools

tools.rng.seed(1234)  # disables randomization

COLUMNS = ["level", "flow", "noise"]


def make_columns(size=512):
    steps = numpy.linspace(0.0, 12.0, size)
    level = numpy.sin(steps) + 0.25 * steps
    flow = numpy.cos(steps * 0.7) * 2.0
    noise = numpy.abs(numpy.sin(steps * 3.1)) + 0.5
    return tuple(numpy.ascontiguousarray(c, dtype=numpy.float64) for c in (level, flow, noise))


def make_target(columns):
    # The program the search is expected to rediscover. It is causal,
    # so its first samples are nan and the fitness must ignore them.
    level, flow, _noise = columns
    return gp.rolling_mean(level, 4) - gp.delay(flow, 2)


def evaluate(individual, toolbox, columns, target):
    func = toolbox.compile(expr=individual)
    predicted = numpy.broadcast_to(numpy.asarray(func(*columns), dtype=numpy.float64), target.shape)
    valid = numpy.isfinite(predicted) & numpy.isfinite(target)
    if valid.sum() < target.size // 2:
        return (1.0e6,)  # too much warmup or too many holes to judge
    return (float(numpy.mean((predicted[valid] - target[valid]) ** 2)),)


def setup():
    columns = make_columns()
    target = make_target(columns)

    pset = gp.columnar_pset(COLUMNS, window=(2, 8))

    creator.create_type("FitnessMin", Fitness, weights=(-1.0,))
    creator.create_type("Individual", gp.PrimitiveTree, fitness=creator.FitnessMin)

    toolbox = Toolbox()
    gp.register_gp(
        toolbox,
        pset,
        individual=creator.Individual,
        min_depth=1,
        max_depth=3,
        height_limit=8,
    )
    toolbox.register("evaluate", evaluate, toolbox=toolbox, columns=columns, target=target)

    stats = tools.Statistics(lambda ind: ind.fitness.values)
    stats.register("avg", numpy.mean)
    stats.register("min", numpy.min)

    return toolbox, stats


def print_results(best_ind):
    print(f"\nBest program: {best_ind}")
    print(f"Mean squared error: {best_ind.fitness.values[0]:.6g}")


def main():
    toolbox, stats = setup()
    pop = toolbox.population(size=300)
    hof = tools.HallOfFame(1)
    args = {
        "toolbox": toolbox,
        "population": pop,
        "generations": 40,
        "cx_prob": 0.5,
        "mut_prob": 0.2,
        "hof": hof,
        "stats": stats,
        "verbose": True,  # prints stats
    }
    tools.ea_simple(**args)
    print_results(hof[0])


if __name__ == "__main__":
    main()

Batch evaluation

The same columnar contract, scored with evaluate_batch, interpret_tapes, and per-fold case_errors. Pair-window and time-series primitives are on the primitive set.

import numpy
from deap_er import Fitness, Toolbox, creator, gp, tools

tools.rng.seed(1234)  # disables randomization

COLUMNS = ["level", "flow"]
CASES = [(0, 256), (256, 512)]
N_ROWS = 512


def make_columns(size=N_ROWS):
    steps = numpy.linspace(0.0, 12.0, size)
    level = numpy.sin(steps) + 0.25 * steps
    flow = numpy.cos(steps * 0.7) * 2.0
    return tuple(numpy.ascontiguousarray(c, dtype=numpy.float64) for c in (level, flow))


def make_target(columns):
    level, flow = columns
    # Pair-window beta and a time-series rank — both causal.
    return gp.rolling_beta(level, flow, 4) + gp.ts_rank(level, 6)


def setup():
    columns = make_columns()
    target = make_target(columns)
    matrix = numpy.column_stack(columns)

    pset = gp.columnar_pset(COLUMNS, window=(2, 8), pair_windows=True, ts=True)

    creator.create_type("FitnessMin", Fitness, weights=(-1.0,))
    creator.create_type("Individual", gp.PrimitiveTree, fitness=creator.FitnessMin)

    toolbox = Toolbox()
    gp.register_gp(
        toolbox,
        pset,
        individual=creator.Individual,
        min_depth=1,
        max_depth=3,
        height_limit=8,
        backend="opcode",
    )
    toolbox.register(
        "evaluate_batch",
        gp.evaluate_columnar,
        pset=pset,
        matrix=matrix,
        target=target,
        cases=CASES,
    )

    stats = tools.Statistics(lambda ind: ind.fitness.values)
    stats.register("avg", numpy.mean)
    stats.register("min", numpy.min)
    return toolbox, stats, pset, matrix, target


def print_results(best_ind, toolbox, matrix, target):
    func = toolbox.compile(expr=best_ind)
    predicted = func(*[matrix[:, i] for i in range(matrix.shape[1])])
    errors = tools.case_errors(predicted, target, CASES)
    print(f"\nBest program: {best_ind}")
    print(f"Case MSE: {tuple(round(e, 6) for e in errors)}")
    print(f"Mean case MSE: {float(numpy.mean(errors)):.6g}")


def main():
    toolbox, stats, _pset, matrix, target = setup()
    pop = toolbox.population(size=80)
    hof = tools.HallOfFame(1)
    tools.ea_simple(
        toolbox,
        pop,
        generations=15,
        cx_prob=0.5,
        mut_prob=0.2,
        hof=hof,
        stats=stats,
        verbose=True,
    )
    print_results(hof[0], toolbox, matrix, target)


if __name__ == "__main__":
    main()