Merge pull request 'fix: feat: Push3 evolution — diverse seed population (#638)' (#656) from fix/issue-638 into master

This commit is contained in:
johba 2026-03-13 06:44:34 +01:00
commit 709dfccf7e
4 changed files with 722 additions and 17 deletions

View file

@ -11,7 +11,15 @@
# --generations 5 \
# --mutation-rate 2 \
# --elites 2 \
# --output evolved/
# --output evolved/ \
# [--diverse-seeds]
#
# --diverse-seeds Use the seed generator to initialise gen_0 with parametric
# variants (different staked% thresholds, bull/bear outputs,
# penalty thresholds, and tax distributions) instead of N
# copies of the seed each independently mutated. When the
# generator produces fewer variants than --population the
# remaining slots are filled with mutations of the seed.
#
# Algorithm:
# 1. Initialize population: N copies of seed, each with M random mutations.
@ -48,6 +56,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
FITNESS_SH="$SCRIPT_DIR/fitness.sh"
BATCH_EVAL_SH="$SCRIPT_DIR/revm-evaluator/batch-eval.sh"
MUTATE_CLI="$SCRIPT_DIR/mutate-cli.ts"
SEED_GEN_CLI="$SCRIPT_DIR/seed-gen-cli.ts"
# EVAL_MODE controls which fitness backend is used:
# anvil (default) — per-candidate Anvil+forge-script pipeline (fitness.sh)
@ -65,6 +74,7 @@ GENERATIONS=5
MUTATION_RATE=2
ELITES=2
OUTPUT_DIR=""
DIVERSE_SEEDS=false
while [[ $# -gt 0 ]]; do
case $1 in
@ -74,6 +84,7 @@ while [[ $# -gt 0 ]]; do
--mutation-rate) MUTATION_RATE="$2"; shift 2 ;;
--elites) ELITES="$2"; shift 2 ;;
--output) OUTPUT_DIR="$2"; shift 2 ;;
--diverse-seeds) DIVERSE_SEEDS=true; shift ;;
*) echo "Unknown option: $1" >&2; exit 2 ;;
esac
done
@ -138,6 +149,11 @@ run_mutate_cli() {
(cd "$SCRIPT_DIR" && $TSX_CMD "$MUTATE_CLI" "$@")
}
# Run the seed-gen-cli.ts with the given arguments.
run_seed_gen_cli() {
(cd "$SCRIPT_DIR" && $TSX_CMD "$SEED_GEN_CLI" "$@")
}
# Integer min/max/mean via python3 (bash arithmetic overflows on wei values).
py_stats() {
# Args: space-separated integers on stdin as a Python list literal
@ -206,6 +222,10 @@ done
[ -f "$MUTATE_CLI" ] || fail "mutate-cli.ts not found at $MUTATE_CLI"
[ -x "$FITNESS_SH" ] || chmod +x "$FITNESS_SH"
if [ "$DIVERSE_SEEDS" = "true" ]; then
[ -f "$SEED_GEN_CLI" ] || fail "seed-gen-cli.ts not found at $SEED_GEN_CLI"
fi
if [ "$EVAL_MODE" = "revm" ]; then
[ -f "$BATCH_EVAL_SH" ] || fail "batch-eval.sh not found at $BATCH_EVAL_SH"
[ -x "$BATCH_EVAL_SH" ] || chmod +x "$BATCH_EVAL_SH"
@ -232,14 +252,15 @@ trap cleanup EXIT
log "========================================================"
log "Push3 Evolution — $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
log " Seed: $SEED"
log " Population: $POPULATION"
log " Generations: $GENERATIONS"
log " Mutation rate: $MUTATION_RATE"
log " Elites: $ELITES"
log " Output: $OUTPUT_DIR"
log " TSX: $TSX_CMD"
log " Eval mode: $EVAL_MODE"
log " Seed: $SEED"
log " Population: $POPULATION"
log " Generations: $GENERATIONS"
log " Mutation rate: $MUTATION_RATE"
log " Elites: $ELITES"
log " Diverse seeds: $DIVERSE_SEEDS"
log " Output: $OUTPUT_DIR"
log " TSX: $TSX_CMD"
log " Eval mode: $EVAL_MODE"
log "========================================================"
# =============================================================================
@ -254,15 +275,50 @@ log "=== Initializing population ==="
GEN_DIR="$WORK_DIR/gen_0"
mkdir -p "$GEN_DIR"
for i in $(seq 0 $((POPULATION - 1))); do
CAND_FILE="$GEN_DIR/candidate_$(printf '%03d' $i).push3"
MUTATED=$(run_mutate_cli mutate "$SEED" "$MUTATION_RATE") \
|| fail "Failed to mutate seed for initial candidate $i"
printf '%s\n' "$MUTATED" > "$CAND_FILE"
printf '%d\n' "$MUTATION_RATE" > "${CAND_FILE%.push3}.ops"
done
if [ "$DIVERSE_SEEDS" = "true" ]; then
# --- Diverse-seeds mode: use seed-gen-cli to produce parametric variants ---
# Generate up to POPULATION variants; any shortfall is filled by mutating the seed.
SEED_VARIANTS_DIR="$WORK_DIR/seed_variants"
SEED_VARIANTS_LIST="$WORK_DIR/seed_variants_list.txt"
VARIANT_IDX=0
log "Initialized ${POPULATION} candidates in gen_0"
# Run seed-gen-cli as a direct command (not inside <(...)) so its exit code is
# checked by the parent shell and fail() aborts the entire script on error.
# Stderr goes to the log file for diagnostics rather than being discarded.
run_seed_gen_cli --count "$POPULATION" --output-dir "$SEED_VARIANTS_DIR" \
> "$SEED_VARIANTS_LIST" 2>>"$LOG" \
|| fail "seed-gen-cli.ts failed to generate variants"
while IFS= read -r VARIANT_FILE && [ "$VARIANT_IDX" -lt "$POPULATION" ]; do
CAND_FILE="$GEN_DIR/candidate_$(printf '%03d' $VARIANT_IDX).push3"
cp "$VARIANT_FILE" "$CAND_FILE"
printf '0\n' > "${CAND_FILE%.push3}.ops"
VARIANT_IDX=$((VARIANT_IDX + 1))
done < "$SEED_VARIANTS_LIST"
# Fill any remaining slots with mutations of the seed (fallback)
while [ "$VARIANT_IDX" -lt "$POPULATION" ]; do
CAND_FILE="$GEN_DIR/candidate_$(printf '%03d' $VARIANT_IDX).push3"
MUTATED=$(run_mutate_cli mutate "$SEED" "$MUTATION_RATE") \
|| fail "Failed to mutate seed for fallback candidate $VARIANT_IDX"
printf '%s\n' "$MUTATED" > "$CAND_FILE"
printf '%d\n' "$MUTATION_RATE" > "${CAND_FILE%.push3}.ops"
VARIANT_IDX=$((VARIANT_IDX + 1))
done
log "Initialized ${POPULATION} candidates in gen_0 (diverse-seeds mode)"
else
# --- Default mode: N copies of the seed, each independently mutated ---
for i in $(seq 0 $((POPULATION - 1))); do
CAND_FILE="$GEN_DIR/candidate_$(printf '%03d' $i).push3"
MUTATED=$(run_mutate_cli mutate "$SEED" "$MUTATION_RATE") \
|| fail "Failed to mutate seed for initial candidate $i"
printf '%s\n' "$MUTATED" > "$CAND_FILE"
printf '%d\n' "$MUTATION_RATE" > "${CAND_FILE%.push3}.ops"
done
log "Initialized ${POPULATION} candidates in gen_0"
fi
# =============================================================================
# Step 2 — Evolution loop

View file

@ -0,0 +1,71 @@
/**
* seed-gen-cli.ts CLI for generating diverse Push3 seed variants (#638).
*
* Usage:
* tsx seed-gen-cli.ts --count <N> --output-dir <dir>
*
* Writes N Push3 variant files to <dir>/variant_000.push3 ... variant_NNN.push3
* and prints each file path to stdout (one per line).
*
* The variants are systematically diverse: different staked% thresholds,
* penalty thresholds, bull/bear output params, and tax rate distributions.
*
* Options:
* --count <N> Number of variants to generate (required, positive integer)
* --output-dir <dir> Directory to write variant files (required, created if absent)
*/
import { mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { selectVariants, generateSeedVariant } from './seed-generator';
function usage(): void {
process.stderr.write('Usage: seed-gen-cli --count <N> --output-dir <dir>\n');
}
const args = process.argv.slice(2);
let count: number | undefined;
let outputDir: string | undefined;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--count' && args[i + 1] !== undefined) {
count = parseInt(args[++i]!, 10);
} else if (args[i] === '--output-dir' && args[i + 1] !== undefined) {
outputDir = args[++i];
} else {
process.stderr.write(`Unknown argument: ${args[i]!}\n`);
usage();
process.exit(1);
}
}
if (count === undefined || isNaN(count) || count < 1) {
process.stderr.write('Error: --count must be a positive integer\n');
usage();
process.exit(1);
}
if (!outputDir) {
process.stderr.write('Error: --output-dir is required\n');
usage();
process.exit(1);
}
mkdirSync(outputDir, { recursive: true });
const variants = selectVariants(count);
if (variants.length < count) {
process.stderr.write(
`[seed-gen] Note: --count ${count} exceeds the 1152-variant parameter space;` +
` generating ${variants.length} variants. The remaining ${count - variants.length}` +
` slots in evolve.sh will be filled by mutating the seed.\n`,
);
}
for (let i = 0; i < variants.length; i++) {
const text = generateSeedVariant(variants[i]!);
const filename = `variant_${String(i).padStart(3, '0')}.push3`;
const filepath = join(outputDir, filename);
writeFileSync(filepath, text + '\n', 'utf8');
process.stdout.write(filepath + '\n');
}

View file

@ -0,0 +1,311 @@
/**
* seed-generator.ts Generate diverse seed variants for Push3 evolution (#638).
*
* Produces parametric Push3 programs that implement the same binary bear/bull
* logic as optimizer_seed.push3, with systematically varied constants:
* - stakedThreshold: the staked% cutoff for entering the penalty branch
* - penaltyThreshold: the penalty value below which bull params are used
* - bull/bear params: capitalInefficiency, anchorShare, anchorWidth, discoveryDepth
* - taxDistribution: spacing of the 30-bucket tax-rate lookup thresholds
*
* All generated programs:
* 1. Consume 8 inputs (same DYADIC stack interface as the seed)
* 2. Produce 4 outputs in valid ranges [0..1e18] for fractions, positive int for anchorWidth
* 3. Have identical structure to optimizer_seed.push3 (only constants differ)
*/
const ONE_E18 = 1_000_000_000_000_000_000n;
export interface BullBearParams {
capitalInefficiency: bigint; // 0..1e18
anchorShare: bigint; // 0..1e18
anchorWidth: number; // tick units (positive integer)
discoveryDepth: bigint; // 0..1e18
}
export type TaxDistribution = 'exponential' | 'linear' | 'sqrt';
export interface SeedVariantParams {
stakedThreshold: number; // integer 0-100: staked% above which penalty branch is entered
penaltyThreshold: number; // penalty < N → bull output; else → bear output
bull: BullBearParams;
bear: BullBearParams;
taxDistribution: TaxDistribution;
}
// ---- Parameter sets --------------------------------------------------------
/** Staked% thresholds: when staked > threshold, enter penalty computation. */
export const STAKED_THRESHOLDS: readonly number[] = [80, 85, 88, 91, 94, 97];
/** Penalty thresholds: penalty < N → bull; else → bear. */
export const PENALTY_THRESHOLDS: readonly number[] = [30, 50, 70, 100];
/**
* Bull output parameter variants (higher confidence / tighter ranges).
*
* capitalInefficiency is intentionally fixed at 0 across all variants the
* current seed (optimizer_v3.push3) uses CI=0 in all branches, and varying it
* is left to the mutation operators once evolution starts.
*/
export const BULL_VARIANTS: readonly BullBearParams[] = [
// Aggressive bull (current seed): maximum confidence, tight anchor
{ capitalInefficiency: 0n, anchorShare: ONE_E18, anchorWidth: 20, discoveryDepth: ONE_E18 },
// Tight bull: slightly lower confidence, narrower anchor
{ capitalInefficiency: 0n, anchorShare: 900_000_000_000_000_000n, anchorWidth: 15, discoveryDepth: 900_000_000_000_000_000n },
// Moderate bull: wider anchor, moderate confidence
{ capitalInefficiency: 0n, anchorShare: 800_000_000_000_000_000n, anchorWidth: 30, discoveryDepth: 800_000_000_000_000_000n },
// Mild bull: broad discovery, lower confidence
{ capitalInefficiency: 0n, anchorShare: 700_000_000_000_000_000n, anchorWidth: 50, discoveryDepth: 700_000_000_000_000_000n },
];
/**
* Bear output parameter variants (defensive / wide ranges).
*
* capitalInefficiency is intentionally fixed at 0 for the same reason as BULL_VARIANTS.
*/
export const BEAR_VARIANTS: readonly BullBearParams[] = [
// Standard bear (current seed)
{ capitalInefficiency: 0n, anchorShare: 300_000_000_000_000_000n, anchorWidth: 100, discoveryDepth: 300_000_000_000_000_000n },
// Strong bear: very defensive, very wide anchor
{ capitalInefficiency: 0n, anchorShare: 200_000_000_000_000_000n, anchorWidth: 150, discoveryDepth: 200_000_000_000_000_000n },
// Mild bear: slightly wider than neutral
{ capitalInefficiency: 0n, anchorShare: 400_000_000_000_000_000n, anchorWidth: 80, discoveryDepth: 400_000_000_000_000_000n },
// Very mild bear: approaching neutral
{ capitalInefficiency: 0n, anchorShare: 500_000_000_000_000_000n, anchorWidth: 60, discoveryDepth: 500_000_000_000_000_000n },
];
export const TAX_DISTRIBUTIONS: readonly TaxDistribution[] = [
'exponential',
'linear',
'sqrt',
];
// ---- Tax threshold distributions -------------------------------------------
/**
* Current seed thresholds exponentially spaced from ~2e14 to ~8.9e17.
* These are the exact values from optimizer_seed.push3.
*/
function exponentialThresholds(): bigint[] {
return [
206185567010309n,
412371134020618n,
618556701030927n,
1030927835051546n,
1546391752577319n,
2164948453608247n,
2783505154639175n,
3608247422680412n,
4639175257731958n,
5670103092783505n,
7216494845360824n,
9278350515463917n,
11855670103092783n,
15979381443298969n,
22164948453608247n,
29381443298969072n,
38144329896907216n,
49484536082474226n,
63917525773195876n,
83505154639175257n,
109278350515463917n,
144329896907216494n,
185567010309278350n,
237113402061855670n,
309278350515463917n,
402061855670103092n,
520618556701030927n,
680412371134020618n,
886597938144329896n,
];
}
/**
* Linear thresholds 29 boundaries evenly spaced across [0, 1e18].
* Gives equal bucket widths: each bucket covers 1/30 of the tax rate range.
*/
function linearThresholds(): bigint[] {
return Array.from({ length: 29 }, (_, i) =>
(BigInt(i + 1) * ONE_E18) / 30n,
);
}
/**
* Square-root thresholds more buckets at low tax rates.
* Threshold[i] = sqrt((i+1)/30) * 1e18.
* Compresses the high-tax range and spreads the low-tax range.
*/
function sqrtThresholds(): bigint[] {
return Array.from({ length: 29 }, (_, i) => {
// Work in units of 1e9 for float precision, then scale with BigInt
const scaled = Math.sqrt((i + 1) / 30) * 1e9;
return BigInt(Math.round(scaled)) * 1_000_000_000n;
});
}
/** Return the 29 boundary values for the given tax distribution. */
export function getTaxThresholds(dist: TaxDistribution): bigint[] {
switch (dist) {
case 'exponential': return exponentialThresholds();
case 'linear': return linearThresholds();
case 'sqrt': return sqrtThresholds();
}
}
// ---- Code generation -------------------------------------------------------
/**
* Generate the output-push fragment for a bear/bull param set.
* Pushes 4 values bottom-first so stack top = CI, bottom = DD.
*/
function outputBlock(p: BullBearParams): string {
return `${p.discoveryDepth} ${p.anchorWidth} ${p.anchorShare} ${p.capitalInefficiency}`;
}
/**
* Generate the 30-way nested EXEC.IF tax-rate lookup.
*
* Returns inline Push3 tokens that, when placed inside a list, leave one
* integer (0..29) on the DYADIC stack representing the tax rate bucket index.
*
* Structure (right-recursive):
* TAXRATE t0 DYADIC.<= EXEC.IF 0 ( TAXRATE t1 DYADIC.<= EXEC.IF 1 ( ... ) )
*/
function generateTaxLookup(thresholds: bigint[]): string {
if (thresholds.length !== 29) {
throw new Error(`Expected 29 tax thresholds, got ${thresholds.length}`);
}
function nested(i: number): string {
if (i === 28) {
// Innermost: threshold[28] → index 28 or 29
return `TAXRATE ${thresholds[i]} DYADIC.<= EXEC.IF ${i} ${i + 1}`;
}
return `TAXRATE ${thresholds[i]} DYADIC.<= EXEC.IF ${i} ( ${nested(i + 1)} )`;
}
return nested(0);
}
/**
* Generate Push3 source text for one seed variant.
*
* The generated program has the same structure as optimizer_seed.push3:
* 1. Bind inputs 0-1; discard 2-7
* 2. Scale percentageStaked integer 0-100
* 3. If staked > stakedThreshold: compute penalty, choose bull or bear
* 4. Else: bear outputs
*/
export function generateSeedVariant(params: SeedVariantParams): string {
const { stakedThreshold, penaltyThreshold, bull, bear, taxDistribution } = params;
const thresholds = getTaxThresholds(taxDistribution);
const taxLookup = generateTaxLookup(thresholds);
const bullOut = outputBlock(bull);
const bearOut = outputBlock(bear);
return [
`;; Generated seed variant (#638)`,
`;; staked_threshold=${stakedThreshold} penalty_threshold=${penaltyThreshold}`,
`;; bull: CI=${bull.capitalInefficiency} AS=${bull.anchorShare} AW=${bull.anchorWidth} DD=${bull.discoveryDepth}`,
`;; bear: CI=${bear.capitalInefficiency} AS=${bear.anchorShare} AW=${bear.anchorWidth} DD=${bear.discoveryDepth}`,
`;; tax_distribution=${taxDistribution}`,
`(`,
` PERCENTAGESTAKED DYADIC.DEFINE`,
` TAXRATE DYADIC.DEFINE`,
` DYADIC.POP`,
` DYADIC.POP`,
` DYADIC.POP`,
` DYADIC.POP`,
` DYADIC.POP`,
` DYADIC.POP`,
` PERCENTAGESTAKED`,
` 100 DYADIC.*`,
` 1000000000000000000 DYADIC./`,
` STAKED DYADIC.DEFINE`,
` STAKED ${stakedThreshold} DYADIC.>`,
` EXEC.IF`,
` (`,
` 100 STAKED DYADIC.-`,
` DELTAS DYADIC.DEFINE`,
` ${taxLookup}`,
` DYADIC.DUP 14 DYADIC.>=`,
` EXEC.IF`,
` (`,
` 1 DYADIC.+`,
` DYADIC.DUP 29 DYADIC.>`,
` EXEC.IF`,
` ( DYADIC.POP 29 )`,
` ( )`,
` )`,
` ( )`,
` EFFIDX DYADIC.DEFINE`,
` DELTAS DELTAS DYADIC.*`,
` DELTAS DYADIC.*`,
` EFFIDX DYADIC.*`,
` 20 DYADIC./`,
` ${penaltyThreshold} DYADIC.<`,
` EXEC.IF`,
` ( ${bullOut} )`,
` ( ${bearOut} )`,
` )`,
` ( ${bearOut} )`,
`)`,
].join('\n');
}
// ---- Variant enumeration ---------------------------------------------------
/**
* Enumerate all parameter combinations in a deterministic order.
*
* STAKED_THRESHOLDS is the outermost loop so that even-stride sampling in
* selectVariants(n) naturally covers all staked% values: each block of
* (4 penalty × 4 bull × 4 bear × 3 tax) = 192 entries maps to one threshold.
*
* Total = |STAKED_THRESHOLDS| × |PENALTY_THRESHOLDS| × |BULL_VARIANTS|
* × |BEAR_VARIANTS| × |TAX_DISTRIBUTIONS|
* = 6 × 4 × 4 × 4 × 3 = 1152
*/
export function enumerateVariants(): SeedVariantParams[] {
const result: SeedVariantParams[] = [];
for (const stakedThreshold of STAKED_THRESHOLDS) {
for (const penaltyThreshold of PENALTY_THRESHOLDS) {
for (const bull of BULL_VARIANTS) {
for (const bear of BEAR_VARIANTS) {
for (const taxDistribution of TAX_DISTRIBUTIONS) {
result.push({
stakedThreshold,
penaltyThreshold,
bull,
bear,
taxDistribution,
});
}
}
}
}
}
return result;
}
/**
* Select `n` diverse variants from the full parameter space.
*
* When n total combinations (1152), samples evenly across the enumeration so
* each axis of variation is represented. Because STAKED_THRESHOLDS is the
* outermost loop, selectVariants(6) picks one representative per staked%
* threshold (stride = 1152/6 = 192, one block per threshold value).
*
* When n > total, returns all 1152 combinations.
*/
export function selectVariants(n: number): SeedVariantParams[] {
if (n < 1) throw new RangeError('n must be at least 1');
const all = enumerateVariants();
if (n >= all.length) return all;
// Even-stride sampling across the full enumeration
const stride = all.length / n;
return Array.from({ length: n }, (_, i) => all[Math.floor(i * stride)]);
}

View file

@ -0,0 +1,267 @@
import { describe, it, expect } from 'vitest';
import { parse } from '../../push3-transpiler/src/parser';
import { isValid } from '../mutate';
import {
generateSeedVariant,
selectVariants,
enumerateVariants,
getTaxThresholds,
STAKED_THRESHOLDS,
PENALTY_THRESHOLDS,
BULL_VARIANTS,
BEAR_VARIANTS,
TAX_DISTRIBUTIONS,
type SeedVariantParams,
type TaxDistribution,
} from '../seed-generator';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function parseAndValidate(src: string): boolean {
try {
const program = parse(src);
return isValid(program);
} catch {
return false;
}
}
const DEFAULT_PARAMS: SeedVariantParams = {
stakedThreshold: 91,
penaltyThreshold: 50,
bull: BULL_VARIANTS[0]!,
bear: BEAR_VARIANTS[0]!,
taxDistribution: 'exponential',
};
// ---------------------------------------------------------------------------
// getTaxThresholds
// ---------------------------------------------------------------------------
describe('getTaxThresholds', () => {
for (const dist of TAX_DISTRIBUTIONS) {
it(`returns 29 values for ${dist}`, () => {
const t = getTaxThresholds(dist as TaxDistribution);
expect(t).toHaveLength(29);
});
it(`all ${dist} thresholds are positive`, () => {
const t = getTaxThresholds(dist as TaxDistribution);
for (const v of t) {
expect(v).toBeGreaterThan(0n);
}
});
it(`${dist} thresholds are strictly increasing`, () => {
const t = getTaxThresholds(dist as TaxDistribution);
for (let i = 1; i < t.length; i++) {
expect(t[i]).toBeGreaterThan(t[i - 1]!);
}
});
it(`${dist} thresholds are below 1e18`, () => {
const t = getTaxThresholds(dist as TaxDistribution);
const ONE_E18 = 1_000_000_000_000_000_000n;
for (const v of t) {
expect(v).toBeLessThan(ONE_E18);
}
});
}
it('linear thresholds are evenly spaced', () => {
const t = getTaxThresholds('linear');
const ONE_E18 = 1_000_000_000_000_000_000n;
// Each gap should be approximately 1e18/30
const expected = ONE_E18 / 30n;
for (let i = 1; i < t.length; i++) {
const gap = t[i]! - t[i - 1]!;
// Allow ±1 for integer rounding
expect(gap - expected).toBeGreaterThanOrEqual(-1n);
expect(gap - expected).toBeLessThanOrEqual(1n);
}
});
});
// ---------------------------------------------------------------------------
// generateSeedVariant — structure and validity
// ---------------------------------------------------------------------------
describe('generateSeedVariant', () => {
it('generates parseable Push3 with default seed params', () => {
const src = generateSeedVariant(DEFAULT_PARAMS);
expect(() => parse(src)).not.toThrow();
});
it('generates a valid program with default seed params', () => {
const src = generateSeedVariant(DEFAULT_PARAMS);
expect(parseAndValidate(src)).toBe(true);
});
it('generates valid programs for all staked thresholds', () => {
for (const st of STAKED_THRESHOLDS) {
const src = generateSeedVariant({ ...DEFAULT_PARAMS, stakedThreshold: st });
expect(parseAndValidate(src)).toBe(true);
}
});
it('generates valid programs for all penalty thresholds', () => {
for (const pt of PENALTY_THRESHOLDS) {
const src = generateSeedVariant({ ...DEFAULT_PARAMS, penaltyThreshold: pt });
expect(parseAndValidate(src)).toBe(true);
}
});
it('generates valid programs for all bull variants', () => {
for (const bull of BULL_VARIANTS) {
const src = generateSeedVariant({ ...DEFAULT_PARAMS, bull });
expect(parseAndValidate(src)).toBe(true);
}
});
it('generates valid programs for all bear variants', () => {
for (const bear of BEAR_VARIANTS) {
const src = generateSeedVariant({ ...DEFAULT_PARAMS, bear });
expect(parseAndValidate(src)).toBe(true);
}
});
it('generates valid programs for all tax distributions', () => {
for (const taxDistribution of TAX_DISTRIBUTIONS) {
const src = generateSeedVariant({ ...DEFAULT_PARAMS, taxDistribution });
expect(parseAndValidate(src)).toBe(true);
}
});
it('default params reproduce optimizer_seed.push3 constants', () => {
const src = generateSeedVariant(DEFAULT_PARAMS);
// Staked threshold
expect(src).toContain('STAKED 91 DYADIC.>');
// Penalty threshold
expect(src).toContain('50 DYADIC.<');
// Bull outputs (DD=1e18, AW=20, AS=1e18, CI=0)
expect(src).toContain('1000000000000000000 20 1000000000000000000 0');
// Bear outputs (DD=0.3e18, AW=100, AS=0.3e18, CI=0)
expect(src).toContain('300000000000000000 100 300000000000000000 0');
});
it('different staked thresholds produce distinct programs', () => {
const programs = STAKED_THRESHOLDS.map(st =>
generateSeedVariant({ ...DEFAULT_PARAMS, stakedThreshold: st }),
);
const unique = new Set(programs);
expect(unique.size).toBe(STAKED_THRESHOLDS.length);
});
it('different tax distributions produce distinct programs', () => {
const programs = TAX_DISTRIBUTIONS.map(td =>
generateSeedVariant({ ...DEFAULT_PARAMS, taxDistribution: td }),
);
const unique = new Set(programs);
expect(unique.size).toBe(TAX_DISTRIBUTIONS.length);
});
});
// ---------------------------------------------------------------------------
// enumerateVariants
// ---------------------------------------------------------------------------
describe('enumerateVariants', () => {
it('returns 1152 combinations', () => {
const all = enumerateVariants();
const expected =
STAKED_THRESHOLDS.length *
PENALTY_THRESHOLDS.length *
BULL_VARIANTS.length *
BEAR_VARIANTS.length *
TAX_DISTRIBUTIONS.length;
expect(all).toHaveLength(expected);
});
it('all combinations are distinct', () => {
const all = enumerateVariants();
const keys = new Set(
all.map(v =>
JSON.stringify([
v.stakedThreshold,
v.penaltyThreshold,
v.bull.anchorWidth,
v.bear.anchorWidth,
v.taxDistribution,
]),
),
);
expect(keys.size).toBe(all.length);
});
it('covers all staked thresholds', () => {
const all = enumerateVariants();
const seen = new Set(all.map(v => v.stakedThreshold));
for (const st of STAKED_THRESHOLDS) {
expect(seen.has(st)).toBe(true);
}
});
it('covers all tax distributions', () => {
const all = enumerateVariants();
const seen = new Set(all.map(v => v.taxDistribution));
for (const td of TAX_DISTRIBUTIONS) {
expect(seen.has(td)).toBe(true);
}
});
});
// ---------------------------------------------------------------------------
// selectVariants
// ---------------------------------------------------------------------------
describe('selectVariants', () => {
it('returns exactly n variants for n < total', () => {
expect(selectVariants(1)).toHaveLength(1);
expect(selectVariants(10)).toHaveLength(10);
expect(selectVariants(50)).toHaveLength(50);
});
it('returns all variants when n >= total', () => {
const total = enumerateVariants().length;
expect(selectVariants(total)).toHaveLength(total);
expect(selectVariants(total + 100)).toHaveLength(total);
});
it('throws for n < 1', () => {
expect(() => selectVariants(0)).toThrow();
expect(() => selectVariants(-1)).toThrow();
});
it('all selected variants produce valid programs', () => {
const variants = selectVariants(20);
for (const v of variants) {
const src = generateSeedVariant(v);
expect(parseAndValidate(src)).toBe(true);
}
});
it('n=6 covers all staked thresholds via even stride', () => {
// enumerateVariants puts STAKED_THRESHOLDS outermost (192 entries per threshold).
// stride = 1152/6 = 192 → one representative per staked% value.
const variants = selectVariants(6);
expect(variants).toHaveLength(6);
const stakedValues = new Set(variants.map(v => v.stakedThreshold));
expect(stakedValues.size).toBe(STAKED_THRESHOLDS.length);
for (const st of STAKED_THRESHOLDS) {
expect(stakedValues.has(st)).toBe(true);
}
// Each representative is a valid Push3 program
for (const v of variants) {
expect(parseAndValidate(generateSeedVariant(v))).toBe(true);
}
});
it('produces diverse programs (no duplicates in typical pop size)', () => {
const variants = selectVariants(24);
const programs = variants.map(v => generateSeedVariant(v));
const unique = new Set(programs);
expect(unique.size).toBe(24);
});
});