harb/tools/push3-evolution/seed-generator.ts
openhands 89a2734bff fix: address review findings for diverse seed population (#638)
- evolve.sh: fix fail-in-subshell bug — run seed-gen-cli as a direct
  command so its exit code is checked by the parent shell and fail()
  aborts the script correctly; redirect stderr to log file instead of
  discarding it with 2>/dev/null
- seed-generator.ts: reorder enumerateVariants() to put
  STAKED_THRESHOLDS outermost (192 entries/block) so that
  selectVariants(6) with stride=192 covers all 6 staked% thresholds;
  remove false doc claim about "first variant is current seed config";
  add comments explaining CI=0n is intentional in all presets
- seed-gen-cli.ts: emit a stderr diagnostic when count exceeds the
  1152-variant cap so the cap is visible rather than silently producing
  fewer files than requested
- test: strengthen n=6 test to assert all STAKED_THRESHOLDS values are
  represented in the selected variants

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 05:21:05 +00:00

311 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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)]);
}