npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

gp-lite

v2.4.1

Published

Tiny GA/GP engine for TypeScript/JS with first-class types, Zod validation, deterministic RNG, and budget-aware runs.

Readme

gp-lite

CI npm license types size

Genetic algorithm/programming library for TypeScript. Evolves solutions through selection, crossover, and mutation.

Install

# npm
npm i gp-lite

# pnpm
pnpm add gp-lite

# bun
bun add gp-lite

Quick Start

import { GPLite, type GPProblem, mulberry32 } from "gp-lite"

// Define your genome type and problem
const target = "hello world"
const alphabet = "abcdefghijklmnopqrstuvwxyz "

const TextEvolution: GPProblem<string> = {
  createRandom: (rng) =>
    Array.from({ length: target.length },
      () => alphabet[rng.int(alphabet.length)]
    ).join(''),

  fitness: (text) =>
    Array.from(text).reduce((sum, ch, i) =>
      sum + (ch === target[i] ? 1 : 0), 0
    ),

  mutate: (text, rng) => {
    const i = rng.int(text.length)
    const ch = alphabet[rng.int(alphabet.length)]
    return text.slice(0, i) + ch + text.slice(i + 1)
  },

  crossover: (a, b, rng) => {
    const cut = rng.int(a.length)
    return [
      a.slice(0, cut) + b.slice(cut),
      b.slice(0, cut) + a.slice(cut)
    ]
  },
}

const gp = new GPLite(TextEvolution, {
  popSize: 100,
  generations: 300,
  rng: mulberry32(42), // deterministic
})

const result = gp.run()
console.log(result.bestGenome) // "hello world"

API

GPProblem

Define how to create, evaluate, and evolve genomes:

interface GPProblem<T> {
  createRandom: (rng: RNG) => T
  fitness: (genome: T) => number
  mutate: (genome: T, rng: RNG) => T
  crossover: (a: T, b: T, rng: RNG) => T | T[]  // single child or array
}

All operators must return new instances (immutable).

Crossover patterns:

// Single child (direct return)
crossover: (a, b, rng) => rng.float() < 0.5 ? a : b

// Single child (wrapped)
crossover: (a, b, rng) => [combine(a, b)]

// Two children (classic)
crossover: (a, b, rng) => [child1, child2]

GPLite Constructor

new GPLite(problem: GPProblem<T>, config?: GPConfig<T>)

Config options:

{
  popSize: 100,           // population size
  generations: 1000,      // max iterations

  // Operators (must sum to 1)
  cxProb: 0.8,           // crossover probability
  mutProb: 0.2,          // mutation probability
  immigration: 0.02,     // random newcomers per gen

  // Selection
  tournament: 3,         // tournament size
  elite: 2,              // preserve N best

  // Early stopping
  targetFitness: 100,    // stop if reached
  stall: 50,             // stop after N gens without improvement
  maxWallMs: 5000,       // time budget (ms)
  maxEvaluations: 10000, // evaluation budget

  // Utilities
  rng: mulberry32(42),   // seeded RNG for reproducibility
  cacheFitness: true,    // memoize fitness calls
  fitnessKey: (g) => g,  // custom cache key function

  // Hooks
  hooks: {
    onIteration: ({ generation, bestFitness }) => {},
    onGenerationStart: ({ generation }) => {},
    onGenerationEnd: ({ generation, bestGenome }) => {},
  },

  // Logging
  verbose: true,           // log progress to console
  exposePopulation: true,  // include full population in hooks
}

Run

const result = gp.run()

Returns:

{
  bestGenome: T,
  bestFitness: number,
  generations: number,
  stopReason: "generations" | "target" | "stall" | "time" | "evaluations",
  bestGenomeHistory: T[],    // best genome at each generation
  metrics: {
    evaluations: number,
    elapsedMs: number,
    immigrants: number,      // random genomes added
  }
}

Async

GPLite accepts both sync and async problems:

import { GPLite, type GPProblemAsync } from "gp-lite"

const problem: GPProblemAsync<T> = {
  createRandom: async (rng) => ...,
  fitness: async (genome) => ...,
  mutate: async (genome, rng) => ...,
  crossover: async (a, b, rng) => ...,
}

const gp = new GPLite(problem, {
  concurrency: 4,         // parallel evaluations
  fitnessTimeoutMs: 1000, // per-eval timeout
  signal: abortController.signal,
})

const result = await gp.run()

For pure synchronous code without async overhead, use GPLiteSync.

Examples

Schedule Optimization

type Schedule = number[] // employee ID per shift

const problem: GPProblem<Schedule> = {
  createRandom: (rng) =>
    Array.from({ length: 21 }, () => rng.int(10)),

  fitness: (schedule) => {
    const hours = new Array(10).fill(0)
    let score = 0

    schedule.forEach((emp, shift) => {
      hours[emp] += 8
      if (shift >= 15) score += 10 // weekend bonus
    })

    hours.forEach(h => {
      if (h > 40) score -= (h - 40) * 20 // overtime penalty
    })

    return score
  },

  mutate: (s, rng) => {
    const copy = s.slice()
    copy[rng.int(s.length)] = rng.int(10)
    return copy
  },

  crossover: (a, b, rng) => {
    const cut = rng.int(a.length)
    return [
      [...a.slice(0, cut), ...b.slice(cut)],
      [...b.slice(0, cut), ...a.slice(cut)]
    ]
  },
}

Neural Architecture Search

type Architecture = {
  layers: number[]
  activations: string[]
  dropout: number[]
}

const problem: GPProblemAsync<Architecture> = {
  fitness: async (arch) => {
    const model = buildModel(arch)
    const { accuracy } = await trainAndValidate(model)
    return accuracy
  },
  // ...
}

const gp = new GPLite(problem, {
  concurrency: 8,
  fitnessTimeoutMs: 30000,
})

Utilities

Simple run() function

For quick experiments without a full engine:

import { run } from "gp-lite"

const result = run<string>({
  initialPopulation: ["...", "..."], // starting population
  iterations: 300,
  fitness: (g) => score(g),
  mutate: (g, rng) => mutated(g),
  crossover: (a, b, rng) => child,  // single child or array
  cxProb: 0.8,
  verbose: true,
})

Async version: runAsync() with same API.

Progress tracking

Built-in logging:

const gp = new GPLite(problem, {
  verbose: true,  // logs every generation
})

// Or customize:
const gp = new GPLite(problem, {
  verbose: { every: 10 },  // log every 10 generations
})

Custom tracking with hooks:

const gp = new GPLite(problem, {
  hooks: {
    onIteration: ({ generation, bestFitness, avgFitness }) => {
      console.log(`Gen ${generation}: ${bestFitness}`)
    }
  }
})

Access full population in hooks:

const gp = new GPLite(problem, {
  exposePopulation: true,
  hooks: {
    onGenerationEnd: ({ population }) => {
      // population is sorted by fitness (best first)
      console.log('Top 3:', population.slice(0, 3))
    }
  }
})

Cost estimation

import { estimateRun } from "gp-lite"

const estimate = estimateRun(
  { popSize: 200, generations: 500 },
  { units: {
    perEvaluationMs: 10,
    perEvaluationCost: 0.001
  }}
)

console.log(`Time: ${estimate.timeMs.expectedTotal}ms`)
console.log(`Cost: $${estimate.monetary.expectedTotal}`)

Unit tests

Test your fitness function:

const problem: GPProblem<number[]> = {
  // ... operators ...

  unitTests: [
    {
      name: "optimal solution",
      genome: [1, 1, 1, 1, 1],
      test: (fitness) => fitness === 5,
      expect: { exact: 5 }
    },
    {
      name: "fitness is non-negative",
      genome: [-1, -1, -1],
      test: (fitness) => fitness >= 0,
      expect: { min: 0 }
    }
  ]
}

const results = gp.runUnitTests()

Validation

Uses Zod for runtime validation:

import { validateConfig, validateProblem } from "gp-lite"

validateConfig(config)   // throws on invalid
validateProblem(problem) // throws on invalid

CLI

Estimate costs from command line:

npm run build

gp-lite-estimate --config examples/run.json
gp-lite-estimate --popSize 200 --generations 500 \
  --perEvaluationMs 10 --perEvaluationCost 0.001 --json

Tips

Stuck at local optimum?

  • Increase mutProb or immigration
  • Decrease tournament size
  • Increase popSize

Too many invalid genomes?

  • Relax fitness constraints
  • Add repair logic in mutate
  • Return -Infinity for invalid genomes

Non-deterministic results?

  • Always use seeded RNG: rng: mulberry32(seed)
  • Ensure pure functions (no external state/randomness)

Slow performance?

  • Enable cacheFitness: true
  • Reduce popSize or generations
  • Use maxWallMs for time limits
  • Profile your fitness function

Advanced

Dynamic parameters

const gp = new GPLite(problem, {
  hooks: {
    onGenerationEnd: ({ generation }) => {
      gp.cfg.mutProb *= 0.99 // decay mutation
    }
  }
})

Multi-objective (NSGA-II)

For true Pareto optimization, use GPLiteMulti:

import { GPLiteMulti, type GPProblemMulti, type VectorFitness } from "gp-lite"

interface MyFitness extends VectorFitness {
  speed: number
  accuracy: number
  cost: number
}

const problem: GPProblemMulti<Solution, MyFitness> = {
  createRandom: (rng) => ...,
  fitness: (solution) => ({
    speed: measureSpeed(solution),
    accuracy: measureAccuracy(solution),
    cost: measureCost(solution),
  }),
  mutate: (solution, rng) => ...,
  crossover: (a, b, rng) => ...,
}

const gp = new GPLiteMulti(problem, {
  popSize: 100,
  generations: 200,
  objectives: { speed: "max", accuracy: "max", cost: "min" },
})

const result = await gp.run()
console.log(result.paretoFront) // non-dominated solutions
console.log(result.hypervolume) // convergence metric

For simple weighted-sum approach (single-objective):

fitness: (solution) => {
  const speed = measureSpeed(solution)
  const accuracy = measureAccuracy(solution)
  const cost = measureCost(solution)
  return speed * 0.3 + accuracy * 0.5 - cost * 0.2
}

Repair invalid solutions

mutate: (genome, rng) => {
  let mutated = doMutation(genome, rng)
  return repairConstraints(mutated)
}

Or mark as invalid:

fitness: (genome) => {
  if (!isValid(genome)) return -Infinity
  return score(genome)
}

Resources

License

MIT