From 850131b74f7567695f043d8c6de73040b6a285e0 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 13 Mar 2026 04:48:04 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20feat:=20Push3=20evolution=20=E2=80=94=20?= =?UTF-8?q?diverse=20seed=20population=20(#638)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add seed-generator.ts module and seed-gen-cli.ts CLI that produce parametric Push3 variants for initial population seeding. Variants systematically cover: - Staked% thresholds: 80, 85, 88, 91, 94, 97 - Penalty thresholds: 30, 50, 70, 100 - Bull params: 4 presets (aggressive → mild) - Bear params: 4 presets (standard → very mild) - Tax distributions: exponential (seed), linear, sqrt Total combination space: 6×4×4×4×3 = 1152 variants. selectVariants(n) samples evenly so every axis is represented. evolve.sh gains --diverse-seeds flag: when set, gen_0 is seeded with parametric variants instead of N copies of the same mutated seed. Remaining slots (if population > generated variants) fall back to mutations of the base seed. All generated programs pass transpiler stack validation (33 new tests). Co-Authored-By: Claude Sonnet 4.6 --- tools/push3-evolution/evolve.sh | 83 ++++- tools/push3-evolution/seed-gen-cli.ts | 63 ++++ tools/push3-evolution/seed-generator.ts | 296 ++++++++++++++++++ .../test/seed-generator.test.ts | 261 +++++++++++++++ 4 files changed, 686 insertions(+), 17 deletions(-) create mode 100644 tools/push3-evolution/seed-gen-cli.ts create mode 100644 tools/push3-evolution/seed-generator.ts create mode 100644 tools/push3-evolution/test/seed-generator.test.ts diff --git a/tools/push3-evolution/evolve.sh b/tools/push3-evolution/evolve.sh index 20a23db..2870f19 100755 --- a/tools/push3-evolution/evolve.sh +++ b/tools/push3-evolution/evolve.sh @@ -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,43 @@ 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" + VARIANT_IDX=0 -log "Initialized ${POPULATION} candidates in gen_0" + 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 < <(run_seed_gen_cli --count "$POPULATION" --output-dir "$SEED_VARIANTS_DIR" 2>/dev/null \ + || fail "seed-gen-cli.ts failed to generate variants") + + # 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 diff --git a/tools/push3-evolution/seed-gen-cli.ts b/tools/push3-evolution/seed-gen-cli.ts new file mode 100644 index 0000000..362b069 --- /dev/null +++ b/tools/push3-evolution/seed-gen-cli.ts @@ -0,0 +1,63 @@ +/** + * seed-gen-cli.ts — CLI for generating diverse Push3 seed variants (#638). + * + * Usage: + * tsx seed-gen-cli.ts --count --output-dir + * + * Writes N Push3 variant files to /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 Number of variants to generate (required, positive integer) + * --output-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 --output-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); +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'); +} diff --git a/tools/push3-evolution/seed-generator.ts b/tools/push3-evolution/seed-generator.ts new file mode 100644 index 0000000..1d599c5 --- /dev/null +++ b/tools/push3-evolution/seed-generator.ts @@ -0,0 +1,296 @@ +/** + * 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). */ +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). */ +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. + * + * 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 taxDistribution of TAX_DISTRIBUTIONS) { + for (const stakedThreshold of STAKED_THRESHOLDS) { + for (const penaltyThreshold of PENALTY_THRESHOLDS) { + for (const bull of BULL_VARIANTS) { + for (const bear of BEAR_VARIANTS) { + 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 so each axis of variation + * is represented. When n > total, returns all combinations. + * + * The first variant is always the current seed configuration + * (stakedThreshold=91, penaltyThreshold=50, bull/bear=defaults, exponential). + */ +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)]); +} diff --git a/tools/push3-evolution/test/seed-generator.test.ts b/tools/push3-evolution/test/seed-generator.test.ts new file mode 100644 index 0000000..4538175 --- /dev/null +++ b/tools/push3-evolution/test/seed-generator.test.ts @@ -0,0 +1,261 @@ +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', () => { + // 1152 / 6 = stride of 192; the 6 selected entries span the full combination space + const variants = selectVariants(6); + expect(variants).toHaveLength(6); + // Each variant is valid + 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); + }); +});