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 🙏

© 2026 – Pkg Stats / Ryan Hefner

goth-mommy

v0.0.3

Published

Small zero-dependency genetic algorithm library in TypeScript for hyperparameter search and ordering problems (TSP, scheduling).

Readme

GothMommy

A small genetic algorithm library in TypeScript. Two classes: GothMommy for searching good hyperparameters (numbers, integers, booleans, categories) and GothMommyPermutation for ordering problems (TSP, scheduling). You provide a starting point, or a size, plus a scoring function: the population evolves until it finds something better.

Zero dependencies. Works with Bun, or with Node (ESM).

Quick start

import { GothMommy, type HyperParams } from "goth-mommy";

// Find x such that x^2 + 2x + 1 = 0 (double root at x = -1)
const fx = (x: number) => x * x + 2 * x + 1;

// Higher is better: equals 1 when fx(x) = 0, tends to 0 otherwise
const score = (population: HyperParams[]) =>
	population.map((ind) => 1 / (1 + Math.abs(fx(ind.x as number))));

const ga = new GothMommy({ x: 5 }, 20, score);

for (const state of ga.iterator(2000, 1 / (1 + 1e-8))) {
	console.log(state.iteration, state.bestScore, state.best);
}

Repo examples live in examples/: bun run example runs the first one, bun run examples runs all of them.

| File | What it shows | |---|---| | basic.ts | The quick start above | | mixed-space.ts | Real, integer, category and boolean in one individual, with a learning rate searched on a log scale | | knapsack.ts | Knapsack: one boolean gene per item, compared against the exact brute-force solution | | tsp.ts | Traveling salesman over 20 cities with GothMommyPermutation | | custom-brick.ts | Writing a custom selector (truncation) and a custom mutator |

Encodings: the space

Each hyperparameter has a domain that controls how it is initialized, mutated and crossed over. Declare them in the space option:

import { GothMommy, bool, choice, int, real } from "goth-mommy";

const seed = { lr: 0.01, layers: 4, activation: "relu", batchNorm: true };

const ga = new GothMommy(seed, 30, score, {
	space: {
		lr: real(1e-5, 1),                         // bounded real
		layers: int(1, 8),                         // integer
		activation: choice(["relu", "tanh", "gelu"]), // category
		batchNorm: bool(),                         // boolean
	},
});

| Domain | Value | Initialization | Mutation | |---|---|---|---| | real(min?, max?) | number | uniform in [min, max] if bounded, else +/-amplitude around the seed | noise, then clamped to bounds | | int(min, max) | integer | uniform integer | rounded noise, always at least +/-1 | | bool() | boolean | coin flip | flip with probability flipRate | | choice([...]) | one value from the list | uniform draw | another value from the list with probability flipRate |

Without space, domains are inferred from the seed: a number is an unbounded real, a boolean is a bool(), and a string is a constant (never mutated). The seed must satisfy its domain, otherwise the constructor throws an error.

The bricks

GothMommy combines interchangeable bricks, each a plain function. Everything is exported from the package.

| Brick | Option | Available (default in bold) | |---|---|---| | Selection | selector | proportional() (roulette, scores >= 0), tournament(size = 3) (accepts negative scores) | | Crossover | crossover | none by default, uniform(), arithmetic() | | Mutation | mutator | perturb({ strength, flipRate }) (multi-scale uniform noise), gaussian({ sigma, flipRate }) | | Initialization | initializer | spread(amplitude = 10) |

import { GothMommy, arithmetic, gaussian, tournament } from "goth-mommy";

new GothMommy(seed, 20, score, {
	selector: tournament(3),
	crossover: arithmetic(),
	mutator: gaussian({ sigma: 0.5 }),
});

Without crossover, each child comes from a single parent (selection + mutation). With a crossover, the selector picks two parents per child.

Writing your own brick

These are the contracts from types.ts. A brick must never mutate its arguments.

type Selector    = (scores: number[], k: number) => number[];                        // k parent indices
type Mutator     = (individual: HyperParams, space: Space) => HyperParams;
type Crossover   = (a: HyperParams, b: HyperParams, space: Space) => HyperParams;
type Initializer = (seed: HyperParams, size: number, space: Space) => HyperParams[];

Ordering problems: GothMommyPermutation

When the solution is an order (city tour, task sequence), an individual is a permutation of the integers 0..size-1. You map them to your cities or tasks. Classic operators (segment crossover, swap mutation) would create duplicates: this class has its own, which always keep a valid permutation.

import { GothMommyPermutation, inversion, pmx } from "goth-mommy";

const cities = [[0, 0], [3, 1], [6, 0], [6, 4], [3, 5], [0, 4]] as const;

// closed tour length
const length = (tour: number[]) =>
	tour.reduce((sum, city, i) => {
		const [x1, y1] = cities[city]!;
		const [x2, y2] = cities[tour[(i + 1) % tour.length]!]!;
		return sum + Math.hypot(x2 - x1, y2 - y1);
	}, 0);

const score = (population: number[][]) =>
	population.map((tour) => 1 / (1 + length(tour)));

const ga = new GothMommyPermutation(cities.length, 50, score, {
	crossover: pmx(),
	mutator: inversion(),
});

let last;
for (const state of ga.iterator(200)) last = state;
console.log(last.best, length(last.best)); // [5, 0, 1, 2, 3, 4] 20.65

new GothMommyPermutation(size, populationSize?, scoringFunction?, options?) has the same iterator as GothMommy. Its options: mutationRate (0.5), crossoverRate (0.9, set 0 to disable), elitism (1), selector, and the two bricks below.

| Brick | Option | Available (default in bold) | |---|---|---| | Crossover | crossover | ox() (keeps relative order), pmx() (keeps positions) | | Mutation | mutator | swap() (swaps two genes), inversion() (reverses a segment, the TSP "2-opt") | | Selection | selector | tournament(3), proportional() |

A permutation crossover has contract (a: number[], b: number[]) => number[], a mutation (p: number[]) => number[].

API

new GothMommy(hyperparams?, populationSize?, scoringFunction?, options?)

| Parameter | Default | Role | |---|---|---| | hyperparams | {} | Starting individual (the seed). | | populationSize | 10 | Individuals per generation. | | scoringFunction | zero scores | Receives the whole population, returns one score per individual (same order). | | options | {} | See below. |

| Option | Default | Role | |---|---|---| | space | inferred from seed | Domain of each hyperparameter. | | mutationRate | 0.5 | Probability that a child is mutated (0 to 1). | | crossoverRate | 0.7 | Probability of crossing two parents (no effect without crossover). | | elitism | 1 | Number of best individuals copied as-is. | | selector, crossover, mutator, initializer | see bricks | |

iterator(maxIterations?, threshold?)

Generator yielding one state per generation. It stops when the best score reaches threshold or after maxIterations (defaults: 1000 and Infinity).

Each state holds population, scores, iteration, bestScore and best (the best individual).

Writing a good scoring function

This is the most important part:

  • Higher is better. To minimize an error, transform it, e.g. 1 / (1 + error).
  • Scores >= 0 with proportional(), since selection is proportional to score. With tournament(), negative scores are accepted.
  • Pick a realistic threshold for threshold, tied to the wanted precision rather than an arbitrary value.
  • The function is called once per generation, with the whole population.

How it works

Each generation:

  1. Evaluation: the scoring function is called once.
  2. Elitism: the elitism best individuals pass through as-is.
  3. Selection: the selector picks the parents of the remaining slots.
  4. Crossover (if configured): two parents produce one child, with probability crossoverRate.
  5. Mutation: each child is mutated with probability mutationRate, respecting each gene domain.

The initial population holds the seed as-is, plus individuals drawn from the gene domains for the rest.

API reference

Each function, type and option is documented with TSDoc, and the HTML reference is generated with TypeDoc:

bun run docs      # generates docs/ (open docs/index.html)

Development

bun install
bun test          # tests
bun run check     # lint + format (Biome)
bun run typecheck

Limits

  • Only top-level numbers, booleans and strings are supported: no nested objects.
  • No binary or Gray encodings yet, no constrained permutation (time windows, etc.).
  • choice values are compared with ===: no objects in the lists.
  • A genetic algorithm does not guarantee the global optimum. If results stall, raise populationSize or mutationRate, or try tournament().

License

MIT