All notable changes to this project will be documented in this file.
The format follows Keep a Changelog, and this project adheres to Semantic Versioning.
Major release. Most changes are bug fixes that were observable in long optimization runs (island starvation, stale fitness, selection degeneracy) but some user-facing surface was reshaped — read the Breaking section before upgrading.
- Public
cloneremoved.utils.tsand its re-export are gone. The library's internal deep-clone is private. If you importedclonefromasync-genetic, switch tostructuredClone(Node 17+, modern browsers). - UMD global name renamed:
window.index→window.asyncGenetic. The previous name was a build artefact and conflicted with anything else exporting underindex. Update<script>-tag usage. estimate()now throws on non-finite fitness. If yourfitnessFunctionreturnsNaN,Infinity,undefinedor an object missing thefitnessfield, you'll get a descriptive error instead of a corrupted sort and a silent crash several lines later. Fix your fitness function or guard against bad backtest results explicitly.Migrate.Fittestsemantics: was a constant0(which, combined with the new per-pass index uniqueness check, would have exported only one individual per generation). Now walks top-N sequentially within a generation — i.e. it actually migrates the fittest cohort, as the name suggests.Migrate.Fittestsignature: now correctly acceptspopper themigrationFunction: (pop) => numberinterface.IslandGeneticModelstats aggregation:maximumFitnessis now the max across islands (was: average of island maxes),minimumFitnessis min (was: average of mins),fitnessPopulationis the sum (was: average), andaverageFitness/fitnessStdDevare weighted by island population size. Anyone parsing these numbers will see different values.- Contract clarified — "one GA instance = one fixed fitness landscape":
fitnessFunctionmust be a pure function ofentityand external state captured at construction time. For walk-forward / sliding windows, instantiate a separateGeneticper data window and aggregate winners externally. Mutating data drivingfitnessFunctionbetween generations was never sound and is now explicitly out of scope. fittestNSurvivesdefault is documented as 1 (unchanged from 1.x in value, but the elitism mechanics are now specified): elite phenotypes carry their genome to the next generation; theirfitness/stateare reset and re-scored eachestimate(). No more wasted cycles on preserved-but-stale scores; no more frozen champions on stale data.
clone()now deep-clones arrays and nested objects (viastructuredClonewith a JSON fallback). The previous shallow{...val}returned the same reference for arrays, letting user-providedcrossoverFunction/mutationFunctioncorrupt parents that still lived in the population.IslandGeneticModelconstructor now readsmutateProbablity/crossoverProbablityfrom merged options instead of the rawPartial<>. Previously, omitting these fields silently leakedundefinedinto each island'sGenetic, disabling mutation and crossover entirely.migration()is now two-phase (collect candidates, then apply moves) and uses round-robin destination selection. The previous implementation spliced inside afor (j < population.length)loop, which skipped entries as the length changed and could fully deplete an island whenevermigrationFunctionhappened to return constant indexes.migration()withislandCount === 1now early-returns instead of blowing the stack via the previous infinite-recursivegetRandomIsland.migration()reserves at least one individual per island so a highmigrationProbabilitycannot empty an island and starve the subsequentbreed().IslandGeneticModel.seed()distributes provided entities round-robin across islands (was: every island got the same set, killing initial diversity). It also resets continent state from previous runs so a freshseed()after amoveAllToContinent()no longer leaves the model in continent mode.Select.RandomLinearRank/Migrate.RandomLinearRank: growing window now starts at 1 and grows topop.length. The previous version usedMath.random() * min(pop.length, rlr++), producing a zero window on the first call and pinning every early call topop[0].selectPair()uses structural equality (cached JSON key) when retrying the second parent pick, so two distinct-but-value-identical object parents are still detected as the same genome.===only caught reference equality.tryCrossover()logic clarified — thecrossoverFunctioncheck is hoisted into a singledoCrossoverboolean instead of repeated checks scattered across branches.- Walk-forward correctness: with elitism re-evaluating each
generation, there is no scenario where a "champion" from an old data
window persists with a frozen score on new data.
population[0]afterestimate()is always the best on the current landscape.
- Vitest test suite with 31 unit tests covering: deep-clone isolation,
selector distributions, deduplicate filter, elite genome carry-forward,
fitness validation, structural-equality
selectPair, island migration invariants, stats aggregation, single-island edge case,Migrate.Fittestsequential-top-N contract, continent round-trip, best() ordering, and convergence smoke test. npm run bench:classic | bench:island | bench:comparescripts run the original benchmark harnesses viavite-node(these used to require the now-removedts-node).- JSDoc contracts on
Geneticclass,estimate(),IslandGeneticModel.populationgetter, and the island per-population rounding caveat.
utils.tsand its publicclonere-export. See Breaking.object-path-immutableruntime dependency. Was declared but never imported anywhere insrc/.- Dead helpers:
getRandomIslandIndex,peekPhenotye,insertPhenotype,cutPopulation. Replaced by inlined logic in the callers. - rollup + buble + ts-node and the associated rollup plugins.
@rollup/plugin-bubletransformation ofasync/await(no longer needed; esbuild handles modern syntax natively).
- All selector internal-state accesses (
FittestLinear,Sequential,RandomLinearRankin bothSelectandMigrate) now use a uniformconst state = this.internalGenState as { ... }+??pattern instead of the historicalthis.internalGenState['...'] || 0indirection. Behaviour is unchanged; the code is easier to read and harder to off-by-one. IslandGeneticModel.migration()resetsinternalGenStateper-island so sequence-based selectors (Fittest,FittestLinear,Sequential,RandomLinearRank) start fresh on each island instead of carrying indexes from the previous one.
- Build pipeline:
rollup→vite(build.libproduces ESM, CJS, UMD with sourcemaps). - Type declarations: bundled to a single
lib/index.d.tsviavite-plugin-dts(rollupTypes: true). - Test runner: home-grown
node:test/ts-nodesetup →vitest. - TypeScript:
3.9.10→5.9.x.tsconfig.jsontargetsES2020withmoduleResolution: bundler. - Node tooling:
ts-node→vite-nodefor ad-hoc script execution (benches). - New scripts:
test,test:watch,typecheck,build,bench:classic,bench:island,bench:compare.
- Stop importing
clonefromasync-genetic. UsestructuredClone(Node 17+, all evergreen browsers) or your own helper. - Update UMD consumers to read
window.asyncGeneticinstead ofwindow.index. - Audit your
fitnessFunctionfor cases where it could returnNaN,Infinity, orundefined(failed backtests, divide-by-zero, etc.).estimate()will now throw on those. - If you were relying on changing data between generations of a single
GA instance (walk-forward, online learning, sliding windows): switch
to one
Geneticinstance per data window. Aggregate the per-window winners (population[0]after the lastestimate()) externally according to your validation strategy. - If you parsed
IslandGeneticModel.stats:maximumFitness,minimumFitness,fitnessPopulationnow reflect proper aggregations (max, min, sum) instead of averages. - If you used
Migrate.Fittestexpecting exactly one migrant per generation: it now exports the fittest cohort sequentially.