Merge pull request 'fix: feat: Push3 evolution — diverse seed population (#638)' (#656) from fix/issue-638 into master
This commit is contained in:
commit
709dfccf7e
4 changed files with 722 additions and 17 deletions
|
|
@ -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
|
||||
|
|
|
|||
71
tools/push3-evolution/seed-gen-cli.ts
Normal file
71
tools/push3-evolution/seed-gen-cli.ts
Normal 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');
|
||||
}
|
||||
311
tools/push3-evolution/seed-generator.ts
Normal file
311
tools/push3-evolution/seed-generator.ts
Normal 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)]);
|
||||
}
|
||||
267
tools/push3-evolution/test/seed-generator.test.ts
Normal file
267
tools/push3-evolution/test/seed-generator.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue