├── .eslintrc.js ├── .github ├── genetic-classic-console.png ├── island.png └── logo.png ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── README.md ├── package.json ├── rollup.config.js ├── src ├── genetic.ts ├── index.ts ├── island-model.ts └── utils.ts ├── test ├── comparison.ts ├── genetic.ts ├── island.ts ├── test-classic-single.ts └── test-iland-single.ts └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 5 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 6 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 10 | sourceType: 'module', // Allows for the use of imports 11 | }, 12 | 13 | rules: { 14 | // note you must disable the base rule as it can report incorrect errors 15 | indent: ['error', 4, { SwitchCase: 1 }], 16 | 'no-use-before-define': 'off', 17 | 'no-empty-function': 'off', 18 | '@typescript-eslint/no-use-before-define': 'off', 19 | '@typescript-eslint/explicit-module-boundary-types': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/no-empty-function': 'off', 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.github/genetic-classic-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coin-unknown/async-genetic/1a916f19cc6036fcf94c91b6b4c3abb2e94c2aab/.github/genetic-classic-console.png -------------------------------------------------------------------------------- /.github/island.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coin-unknown/async-genetic/1a916f19cc6036fcf94c91b6b4c3abb2e94c2aab/.github/island.png -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coin-unknown/async-genetic/1a916f19cc6036fcf94c91b6b4c3abb2e94c2aab/.github/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | npm-debug.log 4 | lib 5 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .nyc_output 3 | coverage 4 | .github 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 4, 7 | endOfLine: 'lf', 8 | }; 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blazing fast Genetic Algorithm 2 | 3 | **Async Genetic** its crossplatform implementation of genetic algorithms. It's pretty asyncronous and use `Promises`. Genetic algorithms allow solving problems such as game balance optimization, solving equations, creating visual effects, optimizing system parameters, and others. 4 | 5 | 6 | 7 | # Abstract 8 | 9 | Genetic Algorithm (GA) is one of the most well-regarded evolutionary algorithms in the history. This algorithm mimics Darwinian theory of survival of the fittest in nature. This chapter presents the most fundamental concepts, operators, and mathematical models of this algorithm. The most popular improvements in the main component of this algorithm (selection, crossover, and mutation) are given too. The chapter also investigates the application of this technique in the field of image processing. In fact, the GA algorithm is employed to reconstruct a binary image from a completely random image. 10 | 11 | # Island Model 12 | 13 | The simulation model of the behavior of population settlement on islands helps to create species diversity. On the islands, the degree of mutation and isolation of the population from the main part allows the creation of local dominant genes. 14 | 15 | In the local implementation of this model, the mainland is also used to cross all populations. You can manually manipulate the population migrations to the mainland and islands as often as you like. 16 | 17 | 18 | 19 | ## Installation 20 | 21 | Releases are available under Node Package Manager (npm): 22 | 23 | npm install async-genetic 24 | 25 | ## Examples 26 | 27 | **Gnetic guess text phrase** 28 | 29 | [Classic Model Test](./test/genetic.ts) 30 | [Island Model Test](./test/island.ts) 31 | 32 | ![Genetic console](./.github/genetic-classic-console.png) 33 | ## How to use 34 | 35 | ### GeneticAlgorithm constructor 36 | ```js 37 | import { Genetic } from 'async-genetic'; 38 | 39 | const config = {...}; 40 | const population = [...]; 41 | const genetic = new Genetic(config); 42 | await genetic.seed(population); 43 | 44 | ``` 45 | The minimal configuration for constructing an GeneticAlgorithm calculator is like so: 46 | 47 | ```js 48 | const config = { 49 | mutationFunction: (phenotype: T) => Promise; // you custom mutation fn 50 | crossoverFunction: (a: T, b: T) => Promise>; // you custom crossover fn 51 | fitnessFunction: (phenotype: T, isLast: boolean) => Promise<{ fitness: number, state?: any }>; // // you custom fitness fn 52 | randomFunction: () => Promise; // you custom random phenotype generator fn 53 | populationSize: number; // constant size of population 54 | mutateProbablity?: number; // perturb prob random phenotype DNA 55 | crossoverProbablity?: number; // crossover prob 56 | fittestNSurvives?: number; // good old boys, fittest are not crossing in current generation 57 | select1?: (pop) => T; // Select one phenotype by Selection method e.g. Select.Random or Select.Fittest 58 | select2?: (pop) => T; // Select for crossover by Selection method e.g. Select.Tournament2 or Select.Tournament3 59 | deduplicate?: (phenotype: T) => boolean; // Remove duplicates (not recommended to use) 60 | } 61 | 62 | const settings = {...}; 63 | const population = [...]; 64 | const genetic = new Genetic(config); 65 | ``` 66 | 67 | That creates one instance of an GeneticAlgorithm calculator which uses the initial configuration you supply. All configuration options are optional except *population*. If you don't specify a crossover function then GeneticAlgorithm will only do mutations and similarly if you don't specify the mutation function it will only do crossovers. If you don't specify either then no evolution will happen, go figure. 68 | 69 | ### genetic.estimate( ) 70 | Estimate current generation by fitnessFunction 71 | ```js 72 | await geneticalgorithm.estimate( ) 73 | ``` 74 | The *.estimate()* add score number per each phenotype in population 75 | ### genetic.breed(); 76 | ```js 77 | async function solve() { 78 | await genetic.seed(); // filled by random function or passed pre defined population T[] 79 | 80 | for (let i = 0; i <= GENERATIONS; i++) { 81 | console.count('gen'); 82 | await genetic.estimate(); // estimate i generation 83 | await genetic.breed(); // breed (apply crossover or mutations) 84 | 85 | const bestOne = genetic.best()[0]; // get best one 86 | console.log(bestOne); 87 | 88 | if (bestOne.entity === solution) { 89 | break; 90 | } 91 | } 92 | } 93 | ``` 94 | to do two evolutions and then get the best N phenoTypes with scores (see *.scoredPopulation(N)* below). 95 | 96 | ### genetic.best(N) 97 | Retrieve the Phenotype with the highest fitness score like so. You can get directly N best scored items 98 | ```js 99 | const best = genetic.best(1) 100 | // best = [{...}]; 101 | ``` 102 | 103 | # Functions 104 | This is the specification of the configuration functions you pass to GeneticAlgorithm 105 | 106 | ### mutationFunction(phenotype) 107 | > Must return a phenotype 108 | 109 | The mutation function that you provide. It is a synchronous function that mutates the phenotype that you provide like so: 110 | ```js 111 | async function mutationFunction (oldPhenotype) { 112 | var resultPhenotype = {} 113 | // use oldPhenotype and some random 114 | // function to make a change to your 115 | // phenotype 116 | return resultPhenotype 117 | } 118 | ``` 119 | 120 | ### crossoverFunction (phenoTypeA, phenoTypeB) 121 | > Must return an array [] with 2 phenotypes 122 | 123 | The crossover function that you provide. It is a synchronous function that swaps random sections between two phenotypes. Construct it like so: 124 | ```js 125 | async function crossoverFunction(phenoTypeA, phenoTypeB) { 126 | var result = {} 127 | // result should me created by merge phenoTypeA and phenoTypeB in custom rules 128 | return result; 129 | } 130 | ``` 131 | 132 | ### fitnessFunction (phenotype) [async] 133 | > Must return a promise with number 134 | 135 | ```js 136 | async function fitnessFunction(phenotype) { 137 | var fitness = 0 138 | // use phenotype and possibly some other information 139 | // to determine the fitness number. Higher is better, lower is worse. 140 | return { fitness, state: { foo: 'bar' } }; 141 | } 142 | ``` 143 | 144 | ### crossoverFunction (phenotypeA, phenotypeB) 145 | > Must return childs phenotypes after breeding phenotypeA and phenotypeB 146 | 147 | ```js 148 | async function crossoverFunction(mother: string, father: string) { 149 | // two-point crossover 150 | const len = mother.length; 151 | let ca = Math.floor(Math.random() * len); 152 | let cb = Math.floor(Math.random() * len); 153 | if (ca > cb) { 154 | [ca, cb] = [cb, ca]; 155 | } 156 | 157 | const son = father.substr(0, ca) + mother.substr(ca, cb - ca) + father.substr(cb); 158 | const daughter = mother.substr(0, ca) + father.substr(ca, cb - ca) + mother.substr(cb); 159 | 160 | return [son, daughter]; 161 | } 162 | ``` 163 | 164 | ### Configuring 165 | > Next T - is your custom phenotype 166 | 167 | | Parameter | Type | Description | 168 | | ------------- | ------------- | ------------- | 169 | | mutationFunction | (phenotype: T) => Promise | Mutate you phenotype as you describe | 170 | | crossoverFunction | (a: T, b: T) => Promise> | Cross two different phenotypes in to once (merge) | 171 | | fitnessFunction | (phenotype: T) => Promise | Train you phenotype to get result (scores more - better) | 172 | | randomFunction | () => Promise | Function generate random phenotype to complete the generation | 173 | | populationSize | number | Number phenotypes in population | 174 | | mutateProbablity | number [0...1] | Each crossover may be changed to mutation with this chance | 175 | | fittestNSurvives | number [0...population.length -1] | Each generation fittest guys will survive | 176 | | select1 | Select | select one phenotype from population for mutate or cloning | 177 | | select2 | Select | select two or more phenotype from population for crossing over | 178 | | optimize | (a: T, b:T) => boolean | order function for popultaion | 179 | | deduplicate | boolean | Remove duplicates from phenotypes | 180 | 181 | 182 | ### Selection method 183 | > Should be used for select1, select2 parameters 184 | 185 | | Type | Description | 186 | | ------------- | ------------- | 187 | | Select.Random | Select random phenotype from population | 188 | | Select.RandomLinear | Select random phenotype from population | 189 | | Select.Fittest | Select best one phenotype from population | 190 | | Select.FittestLinear | Select linear best one phenotypes from population | 191 | | Select.Tournament2 | Select 2 random phenotypes from population and take best of 2 | 192 | | Select.Tournament3 | Select 3 random phenotype from population and take best of 3| 193 | | Select.RandomLinearRank | Select random phenotype from population with linear rank | 194 | | Select.Sequential | Select phenotype from population by linear function | 195 | 196 | 197 | # Island Model 198 | 199 | Island model have absolutely same interface with classic genetic. 200 | 201 | ```typescript 202 | // Use Island model imports 203 | import { IslandGeneticModel, IslandGeneticModelOptions, Migrate, GeneticOptions } from 'async-genetic'; 204 | 205 | // Island configuration 206 | const islandOptions: IslandGeneticModelOptions = { 207 | islandCount: 8, // count of islands 208 | islandMutationProbability: 0.8, // mutation on island are different from continental 209 | islandCrossoverProbability: 0.8, // same for crossover, because island area are small 210 | migrationProbability: 0.1, // migration to another island chance 211 | migrationFunction: Migrate.FittestLinear, // select migrated phenotype 212 | }; 213 | 214 | // Move to continent after each 50 generations 215 | const continentBreedAfter = 50; 216 | // How many generations to breed at continent left 217 | let continentGenerationsCount = 0; 218 | 219 | const genetic = new IslandGeneticModel(islandOptions, geneticOptions); 220 | await genetic.seed(); 221 | 222 | for (let i = 0; i <= GENERATIONS; i++) { 223 | if (log) { 224 | console.count('gen'); 225 | } 226 | 227 | if (i !== 0 && i % continentBreedAfter === 0) { 228 | // Move to continent 229 | genetic.moveAllToContinent(); 230 | // Setup next 10 generations to breed at continent 231 | continentGenerationsCount = 10; 232 | } 233 | 234 | if (continentGenerationsCount) { 235 | // Reduce continent generations 236 | continentGenerationsCount--; 237 | 238 | // If continent generations over, move to islands 239 | if (continentGenerationsCount === 0) { 240 | // Move to islands 241 | genetic.migrateToIslands(); 242 | } 243 | } 244 | 245 | // Estimate on island or continent, by configuration 246 | await genetic.estimate(); 247 | 248 | const bestOne = genetic.best()[0]; 249 | 250 | if (log) { 251 | console.log(`${bestOne.entity} - ${bestOne.fitness}`); 252 | } 253 | 254 | await genetic.breed(); 255 | 256 | if (bestOne.entity === solution) { 257 | return i; 258 | } 259 | } 260 | 261 | ``` 262 | 263 | ### Migration method 264 | > Should be used for selection Phenotype and move to another island (migrate) 265 | 266 | | Type | Description | 267 | | ------------- | ------------- | 268 | | Migrate.Random | Select random phenotype from population | 269 | | Migrate.RandomLinearelect random phenotype from population | 270 | | Migrate.Fittest | Select best one phenotype from population | 271 | | Migrate.FittestLinear | Select linear best one phenotypes from population | 272 | 273 | 274 | 275 | ```javascript 276 | // Move to continent, islands has no populations after that 277 | genetic.moveAllToContinent(); 278 | // Split population and move to islands (each island got same of total population part) 279 | genetic.migrateToIslands(); 280 | ``` 281 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Dmitriy Yurov ", 3 | "name": "async-genetic", 4 | "description": "Implementation of genetic algorithms for nodejs and browser", 5 | "version": "1.6.8", 6 | "homepage": "https://github.com/BusinessDuck/async-genetic", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/BusinessDuck/async-genetic" 10 | }, 11 | "scripts": { 12 | "start": "ts-node --", 13 | "build": "rollup -c ./rollup.config.js", 14 | "version": "npm run build", 15 | "postversion": "git push && git push --tags" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/BusinessDuck/async-geneticissues" 19 | }, 20 | "main": "lib/index.cjs.js", 21 | "module": "lib/index.esm.js", 22 | "browser": "lib/index.umd.js", 23 | "types": "lib/index.d.ts", 24 | "dependencies": { 25 | "object-path-immutable": "^4.1.0" 26 | }, 27 | "devDependencies": { 28 | "@rollup/plugin-buble": "^0.21.3", 29 | "@rollup/plugin-commonjs": "^13.0.0", 30 | "@rollup/plugin-node-resolve": "^8.1.0", 31 | "@typescript-eslint/eslint-plugin": "^4.1.0", 32 | "@typescript-eslint/parser": "^4.1.0", 33 | "eslint": "^7.8.1", 34 | "eslint-config-prettier": "^6.11.0", 35 | "eslint-plugin-prettier": "^3.1.4", 36 | "prettier": "^2.1.1", 37 | "rollup": "^2.18.0", 38 | "rollup-plugin-gzip": "^2.2.0", 39 | "rollup-plugin-terser": "^5.1.2", 40 | "rollup-plugin-typescript2": "^0.27.1", 41 | "rollup-plugin-uglify-es": "0.0.1", 42 | "ts-node": "^8.10.2", 43 | "tslib": "^2.0.0", 44 | "typescript": "^3.9.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | import pkg from './package.json'; 5 | import gzipPlugin from 'rollup-plugin-gzip'; 6 | import buble from '@rollup/plugin-buble'; 7 | import typescript from 'rollup-plugin-typescript2'; 8 | 9 | export default [ 10 | // browser-friendly UMD build 11 | { 12 | input: 'src/index.ts', 13 | output: { 14 | name: 'index', 15 | file: pkg.browser, 16 | format: 'umd', 17 | }, 18 | plugins: [ 19 | resolve(), // so Rollup can find `ms` 20 | commonjs(), // so Rollup can convert `ms` to an ES module 21 | typescript({ 22 | tsconfigOverride: { 23 | compilerOptions: { 24 | module: 'ESNext', 25 | }, 26 | }, 27 | }), 28 | buble({ 29 | transforms: { forOf: false }, 30 | objectAssign: 'Object.assign', 31 | asyncAwait: false, 32 | }), 33 | terser(), // uglify 34 | gzipPlugin(), 35 | ], 36 | }, 37 | { 38 | input: 'src/index.ts', 39 | external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})], 40 | plugins: [ 41 | typescript({ 42 | tsconfigOverride: { 43 | compilerOptions: { 44 | module: 'ESNext', 45 | target: 'ES2020', 46 | }, 47 | }, 48 | }), 49 | ], 50 | output: [ 51 | { file: pkg.main, format: 'cjs' }, 52 | { file: pkg.module, format: 'es' }, 53 | ], 54 | }, 55 | ]; 56 | -------------------------------------------------------------------------------- /src/genetic.ts: -------------------------------------------------------------------------------- 1 | export const Select = { 2 | Fittest, 3 | FittestLinear, 4 | FittestRandom, 5 | Random, 6 | RandomLinearRank, 7 | Sequential, 8 | Tournament2, 9 | Tournament3, 10 | }; 11 | export interface GeneticOptions { 12 | mutationFunction: (phenotype: T) => Promise; 13 | crossoverFunction: (a: T, b: T) => Promise>; 14 | fitnessFunction: (phenotype: T, isLast?: boolean) => Promise<{ fitness: number; state?: Record }>; 15 | randomFunction: () => Promise; 16 | populationSize: number; 17 | mutateProbablity?: number; 18 | crossoverProbablity?: number; 19 | fittestNSurvives?: number; 20 | select1?: (pop: Array>) => T; 21 | select2?: (pop: Array>) => T; 22 | deduplicate?: (phenotype: T) => boolean; 23 | optimize?: (a: Phenotype, b: Phenotype) => boolean; 24 | } 25 | 26 | export interface Phenotype { 27 | fitness: number; 28 | entity: T; 29 | state: Record; 30 | } 31 | 32 | export class Genetic { 33 | public stats = {}; 34 | public options: GeneticOptions; 35 | public population: Array> = []; 36 | protected internalGenState = {}; /* Used for random linear */ 37 | 38 | constructor(options: GeneticOptions) { 39 | const defaultOptions: Partial> = { 40 | populationSize: 250, 41 | mutateProbablity: 0.2, 42 | crossoverProbablity: 0.9, 43 | fittestNSurvives: 1, 44 | select1: Select.Fittest, 45 | select2: Select.Tournament2, 46 | optimize: (phenotypeA: Phenotype, phenotypeB: Phenotype) => { 47 | return phenotypeA.fitness >= phenotypeB.fitness; 48 | }, 49 | }; 50 | 51 | this.options = { ...defaultOptions, ...options }; 52 | } 53 | 54 | /** 55 | * Startup population, if not passed than will be random generated by randomFunction() 56 | */ 57 | public async seed(entities: Array = []) { 58 | this.population = entities.map((entity) => ({ fitness: null, entity, state: {} })); 59 | 60 | // seed the population 61 | await this.fill(this.population); 62 | } 63 | 64 | public best(count = 1) { 65 | return this.population.slice(0, count); 66 | } 67 | 68 | /** 69 | * Breed population with optional breed settings 70 | */ 71 | public async breed() { 72 | // crossover and mutate 73 | let newPop: Array> = []; 74 | 75 | // lets the best solution fall through 76 | if (this.options.fittestNSurvives) { 77 | const cutted = this.cutPopulation(this.options.fittestNSurvives); 78 | 79 | for (const item of cutted) { 80 | newPop.push({ ...item, fitness: null, state: {} }); 81 | } 82 | } 83 | 84 | // Lenght may be change dynamically, because fittest and some pairs from crossover 85 | while (newPop.length <= this.options.populationSize) { 86 | const crossed = await this.tryCrossover(); 87 | 88 | newPop.push(...crossed.map((entity) => ({ fitness: null, entity, state: {} }))); 89 | } 90 | 91 | if (this.options.deduplicate) { 92 | newPop = newPop.filter((ph) => this.options.deduplicate(ph.entity)); 93 | } 94 | 95 | await this.fill(newPop); 96 | this.population = newPop; 97 | } 98 | 99 | /** 100 | * Estimate population before breeding 101 | */ 102 | public async estimate() { 103 | const { fitnessFunction } = this.options; 104 | // reset for each generation 105 | this.internalGenState = {}; 106 | 107 | const tasks = await Promise.all( 108 | this.population.map(({ entity }, idx) => { 109 | const isLast = idx === this.population.length - 1; 110 | 111 | return fitnessFunction(entity, isLast); 112 | }), 113 | ); 114 | 115 | for (let i = 0; i < this.population.length; i++) { 116 | const target = this.population[i]; 117 | 118 | target.fitness = tasks[i].fitness; 119 | target.state = tasks[i].state; 120 | } 121 | 122 | this.reorderPopulation(); 123 | 124 | const popLen = this.population.length; 125 | const mean = this.getMean(); 126 | 127 | this.stats = { 128 | population: this.population.length, 129 | maximum: this.population[0].fitness, 130 | minimum: this.population[popLen - 1].fitness, 131 | mean, 132 | stdev: this.getStdev(mean), 133 | }; 134 | } 135 | 136 | /** 137 | * Appli population sorting 138 | */ 139 | public reorderPopulation() { 140 | this.population = this.population.sort((a, b) => (this.options.optimize(a, b) ? -1 : 1)); 141 | } 142 | 143 | /** Fill population if is not full */ 144 | private async fill(arr: Phenotype[]) { 145 | while (arr.length < this.options.populationSize) { 146 | const entity = await this.options.randomFunction(); 147 | 148 | arr.push({ entity, fitness: null, state: {} }); 149 | } 150 | } 151 | 152 | /** 153 | * Try cross a pair or one selected phenotypes 154 | */ 155 | private tryCrossover = async () => { 156 | const { crossoverProbablity, crossoverFunction } = this.options; 157 | let selected = crossoverFunction && Math.random() <= crossoverProbablity ? this.selectPair() : this.selectOne(); 158 | 159 | if (selected.length > 1) { 160 | selected = await crossoverFunction(selected[0], selected[1]); 161 | } 162 | 163 | for (let i = 0; i < selected.length; i++) { 164 | selected[i] = await this.tryMutate(selected[i]); 165 | } 166 | 167 | return selected; 168 | }; 169 | 170 | /** 171 | * Try mutate entity with optional probabilty 172 | */ 173 | private tryMutate = async (entity: T) => { 174 | // applies mutation based on mutation probability 175 | if (this.options.mutationFunction && Math.random() <= this.options.mutateProbablity) { 176 | return this.options.mutationFunction(entity); 177 | } 178 | 179 | return entity; 180 | }; 181 | 182 | /** 183 | * Mean deviation 184 | */ 185 | private getMean() { 186 | return this.population.reduce((a, b) => a + b.fitness, 0) / this.population.length; 187 | } 188 | 189 | /** 190 | * Standart deviation 191 | */ 192 | private getStdev(mean: number) { 193 | const { population: pop } = this; 194 | const l = pop.length; 195 | 196 | return Math.sqrt(pop.map(({ fitness }) => (fitness - mean) * (fitness - mean)).reduce((a, b) => a + b, 0) / l); 197 | } 198 | 199 | /** 200 | * Select one phenotype from population 201 | */ 202 | private selectOne(): T[] { 203 | const { select1 } = this.options; 204 | 205 | return [select1.call(this, this.population)]; 206 | } 207 | 208 | /** 209 | * Select two phenotypes from population for crossover 210 | */ 211 | private selectPair(): T[] { 212 | const { select2 } = this.options; 213 | 214 | return [select2.call(this, this.population), select2.call(this, this.population)]; 215 | } 216 | 217 | /** 218 | * Return population without an estimate (fitness) 219 | */ 220 | private cutPopulation(count: number) { 221 | return this.population.slice(0, count).map((ph) => ({ fitness: null, entity: ph.entity })); 222 | } 223 | } 224 | 225 | /** Utility */ 226 | 227 | function Tournament2(this: Genetic, pop: Array>) { 228 | const n = pop.length; 229 | const a = pop[Math.floor(Math.random() * n)]; 230 | const b = pop[Math.floor(Math.random() * n)]; 231 | 232 | return this.options.optimize(a, b) ? a.entity : b.entity; 233 | } 234 | 235 | function Tournament3(this: Genetic, pop: Array>) { 236 | const n = pop.length; 237 | const a = pop[Math.floor(Math.random() * n)]; 238 | const b = pop[Math.floor(Math.random() * n)]; 239 | const c = pop[Math.floor(Math.random() * n)]; 240 | let best = this.options.optimize(a, b) ? a : b; 241 | best = this.options.optimize(best, c) ? best : c; 242 | 243 | return best.entity; 244 | } 245 | 246 | function Fittest(this: Genetic, pop: Array>) { 247 | return pop[0].entity; 248 | } 249 | 250 | function FittestLinear(this: Genetic, pop: Array>) { 251 | this.internalGenState['flr'] = this.internalGenState['flr'] >= pop.length ? 0 : this.internalGenState['flr'] || 0; 252 | 253 | return pop[this.internalGenState['flr']++].entity; 254 | } 255 | 256 | function FittestRandom(this: Genetic, pop: Array>) { 257 | return pop[Math.floor(Math.random() * pop.length * 0.2)].entity; 258 | } 259 | 260 | function Random(this: Genetic, pop: Array>) { 261 | return pop[Math.floor(Math.random() * pop.length)].entity; 262 | } 263 | 264 | function RandomLinearRank(this: Genetic, pop: Array>) { 265 | this.internalGenState['rlr'] = this.internalGenState['rlr'] >= pop.length ? 0 : this.internalGenState['rlr'] || 0; 266 | 267 | return pop[Math.floor(Math.random() * Math.min(pop.length, this.internalGenState['rlr']++))].entity; 268 | } 269 | 270 | function Sequential(this: Genetic, pop: Array>) { 271 | this.internalGenState['seq'] = this.internalGenState['seq'] >= pop.length ? 0 : this.internalGenState['seq'] || 0; 272 | 273 | return pop[this.internalGenState['seq']++ % pop.length].entity; 274 | } 275 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './genetic'; 2 | export * from './island-model'; 3 | export * from './utils'; 4 | -------------------------------------------------------------------------------- /src/island-model.ts: -------------------------------------------------------------------------------- 1 | import { Genetic, GeneticOptions, Phenotype } from './genetic'; 2 | 3 | export const Migrate = { 4 | Fittest, 5 | FittestLinear, 6 | FittestRandom, 7 | Random, 8 | RandomLinearRank, 9 | Sequential, 10 | }; 11 | 12 | export interface IslandGeneticModelOptions { 13 | islandCount: number; 14 | islandMutationProbability: number; 15 | islandCrossoverProbability: number; 16 | migrationProbability: number; 17 | migrationFunction: (pop: Array>) => number; 18 | } 19 | 20 | /** 21 | * Genetical island evolution model implementation 22 | * @see https://www.researchgate.net/figure/Parallel-genetic-algorithm-with-island-model_fig3_332715538 23 | * @see https://www.researchgate.net/figure/Plot-of-multi-island-genetic-algorithm_fig1_318073651 24 | */ 25 | export class IslandGeneticModel { 26 | protected internalGenState = {}; /* Used for random linear */ 27 | 28 | private populationOnContinent = false; 29 | private islands: Array> = []; 30 | private continent: Genetic; 31 | private options: IslandGeneticModelOptions; 32 | private geneticOptions: GeneticOptions; 33 | 34 | /** 35 | * Population getter for full compatibility with classic genetic interface 36 | */ 37 | get population() { 38 | // If population on continent get from last one 39 | if (this.continent.population.length) { 40 | return this.continent.population; 41 | } 42 | 43 | const totalPopulation: Array> = []; 44 | 45 | for (let i = 0; i < this.options.islandCount; i++) { 46 | const island = this.islands[i]; 47 | 48 | // Copy and reset population on island 49 | totalPopulation.push(...island.population); 50 | } 51 | 52 | return totalPopulation; 53 | } 54 | 55 | /** 56 | * Stats compatibility method, aggregate stats from all islands 57 | */ 58 | get stats() { 59 | // If population on continent get from last one 60 | if (this.continent.population.length) { 61 | return this.continent.stats; 62 | } 63 | 64 | let stats = {}; 65 | 66 | for (let i = 0; i < this.options.islandCount; i++) { 67 | const island = this.islands[i]; 68 | const islandStats = island.stats; 69 | 70 | if (i === 0) { 71 | stats = { ...islandStats }; 72 | } else { 73 | for (const key in islandStats) { 74 | stats[key] += islandStats[key]; 75 | } 76 | } 77 | } 78 | 79 | for (const key in stats) { 80 | if (key !== 'population') { 81 | stats[key] /= this.options.islandCount; 82 | } 83 | } 84 | 85 | return stats; 86 | } 87 | 88 | constructor(options: Partial>, geneticOptions: GeneticOptions) { 89 | const defaultOptions: IslandGeneticModelOptions = { 90 | islandCount: 6, 91 | islandMutationProbability: 0.5, 92 | islandCrossoverProbability: 0.8, 93 | migrationProbability: 0.05, 94 | migrationFunction: Migrate.Random, 95 | }; 96 | 97 | this.options = { ...defaultOptions, ...options }; 98 | this.geneticOptions = { 99 | optimize: (phenotypeA: Phenotype, phenotypeB: Phenotype) => { 100 | return phenotypeA.fitness >= phenotypeB.fitness; 101 | }, 102 | ...geneticOptions, 103 | // Should be more than continent, because environment are special 104 | mutateProbablity: options.islandMutationProbability, 105 | // Should be more than continent, because area is small 106 | crossoverProbablity: options.islandCrossoverProbability, 107 | // Reduce population size for each island (sum of all phenotypes should be equal to total population count) 108 | populationSize: Math.round(geneticOptions.populationSize / this.options.islandCount), 109 | }; 110 | 111 | this.createIslands(); 112 | this.continent = new Genetic(geneticOptions); 113 | } 114 | 115 | /** 116 | * Get best results from eash islands (one by one) 117 | * count should be more than islands count 118 | */ 119 | public best(count = 5): Array> { 120 | // If population on continent get from last one 121 | if (this.continent.population.length) { 122 | return this.continent.best(count); 123 | } 124 | 125 | if (count < this.options.islandCount) { 126 | count = this.options.islandCount; 127 | } 128 | 129 | const results: Array> = []; 130 | const idxMap = {}; 131 | let activeIsland = 0; 132 | 133 | while (results.length < count) { 134 | const island = this.islands[activeIsland]; 135 | results.push(island.population[idxMap[activeIsland] || 0]); 136 | idxMap[activeIsland] = (idxMap[activeIsland] || 0) + 1; 137 | activeIsland++; 138 | 139 | // Circullar reset index 140 | if (activeIsland >= this.islands.length) { 141 | activeIsland = 0; 142 | } 143 | } 144 | 145 | return results.sort((a, b) => (this.geneticOptions.optimize(a, b) ? -1 : 1)); 146 | } 147 | 148 | /** 149 | * Seed populations 150 | */ 151 | public async seed(entities?: T[]) { 152 | for (let i = 0; i < this.options.islandCount; i++) { 153 | const island = this.islands[i]; 154 | await island.seed(entities); 155 | } 156 | } 157 | /** 158 | * Breed each island 159 | */ 160 | public async breed() { 161 | if (this.populationOnContinent) { 162 | return this.continent.breed(); 163 | } 164 | 165 | this.migration(); 166 | 167 | for (let i = 0; i < this.options.islandCount; i++) { 168 | const island = this.islands[i]; 169 | 170 | await island.breed(); 171 | } 172 | } 173 | 174 | /** 175 | * Estimate each island 176 | */ 177 | public async estimate() { 178 | if (this.populationOnContinent) { 179 | return this.continent.estimate(); 180 | } 181 | 182 | const tasks: Array> = []; 183 | 184 | for (let i = 0; i < this.options.islandCount; i++) { 185 | const island = this.islands[i]; 186 | tasks.push(island.estimate()); 187 | } 188 | 189 | return Promise.all(tasks); 190 | } 191 | 192 | /** 193 | * island migrations alorithm 194 | */ 195 | private migration() { 196 | for (let i = 0; i < this.options.islandCount; i++) { 197 | const island = this.islands[i]; 198 | 199 | for (let j = 0; j < island.population.length; j++) { 200 | if (Math.random() <= this.options.migrationProbability) { 201 | const selectedIndex = this.selectOne(island); 202 | const migratedPhonotype = this.peekPhenotye(island, selectedIndex); 203 | const newIsland = this.getRandomIsland(i); 204 | 205 | // Move phenotype from old to new island 206 | this.insertPhenotype(newIsland, migratedPhonotype); 207 | } 208 | } 209 | } 210 | 211 | this.reorderIslands(); 212 | } 213 | 214 | /** 215 | * Move all population to one continent 216 | */ 217 | public moveAllToContinent() { 218 | // Population already on continent 219 | if (this.populationOnContinent) { 220 | return; 221 | } 222 | 223 | const totalPopulation: Array> = []; 224 | 225 | for (let i = 0; i < this.options.islandCount; i++) { 226 | const island = this.islands[i]; 227 | 228 | // Copy and reset population on island 229 | totalPopulation.push(...island.population); 230 | island.population = []; 231 | } 232 | 233 | this.continent.population = totalPopulation; 234 | this.populationOnContinent = true; 235 | } 236 | 237 | /** 238 | * Move continent population to islands 239 | */ 240 | public migrateToIslands() { 241 | let activeIsland = 0; 242 | 243 | while (this.continent.population.length) { 244 | const phenotype = this.continent.population.pop(); 245 | const island = this.islands[activeIsland]; 246 | 247 | island.population.push(phenotype); 248 | activeIsland++; 249 | 250 | if (activeIsland >= this.options.islandCount) { 251 | activeIsland = 0; 252 | } 253 | } 254 | 255 | this.populationOnContinent = false; 256 | } 257 | 258 | /** 259 | * Create a lot of islands to use in evolution progress 260 | */ 261 | private createIslands() { 262 | for (let i = 0; i < this.options.islandCount; i++) { 263 | this.islands.push(new Genetic(this.geneticOptions)); 264 | } 265 | } 266 | 267 | /** 268 | * Apply ordering to island populations (use after all migrations) 269 | */ 270 | private reorderIslands() { 271 | for (let i = 0; i < this.options.islandCount; i++) { 272 | this.islands[i].reorderPopulation(); 273 | } 274 | } 275 | 276 | /** 277 | * Select one phenotype from population 278 | */ 279 | private selectOne(island: Genetic): number { 280 | const { migrationFunction } = this.options; 281 | 282 | return migrationFunction.call(this, island.population); 283 | } 284 | 285 | /** 286 | * Returns a random picked island 287 | * TODO: Improve island selection by selection function 288 | */ 289 | private getRandomIsland(exclude: number) { 290 | const targetIdx = Math.floor(Math.random() * this.options.islandCount); 291 | 292 | if (targetIdx !== exclude) { 293 | return this.islands[targetIdx]; 294 | } 295 | 296 | return this.getRandomIsland(exclude); 297 | } 298 | 299 | /** 300 | * Peek phenotype from island 301 | */ 302 | private peekPhenotye(island: Genetic, idx: number): Phenotype { 303 | return island.population.splice(idx, 1).pop(); 304 | } 305 | 306 | /** 307 | * Insert phenotype to island with custom index 308 | */ 309 | private insertPhenotype(island: Genetic, phenotype: Phenotype): void { 310 | island.population.push(phenotype); 311 | } 312 | } 313 | 314 | function Fittest(this: IslandGeneticModel): number { 315 | return 0; 316 | } 317 | 318 | function FittestLinear(this: IslandGeneticModel, pop: Array>): number { 319 | this.internalGenState['flr'] = this.internalGenState['flr'] >= pop.length ? 0 : this.internalGenState['flr'] || 0; 320 | 321 | return this.internalGenState['flr']++; 322 | } 323 | 324 | function FittestRandom(this: IslandGeneticModel, pop: Array>): number { 325 | return Math.floor(Math.random() * pop.length * 0.2); 326 | } 327 | 328 | function Random(this: IslandGeneticModel, pop: Array>): number { 329 | return Math.floor(Math.random() * pop.length); 330 | } 331 | 332 | function RandomLinearRank(this: IslandGeneticModel, pop: Array>): number { 333 | this.internalGenState['rlr'] = this.internalGenState['rlr'] >= pop.length ? 0 : this.internalGenState['rlr'] || 0; 334 | 335 | return Math.floor(Math.random() * Math.min(pop.length, this.internalGenState['rlr']++)); 336 | } 337 | 338 | function Sequential(this: IslandGeneticModel, pop: Array>): number { 339 | this.internalGenState['seq'] = this.internalGenState['seq'] >= pop.length ? 0 : this.internalGenState['seq'] || 0; 340 | 341 | return this.internalGenState['seq']++ % pop.length; 342 | } 343 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function clone>(obj: T): T { 2 | if (obj == null || typeof obj != 'object') return obj; 3 | 4 | return JSON.parse(JSON.stringify(obj)); 5 | } -------------------------------------------------------------------------------- /test/comparison.ts: -------------------------------------------------------------------------------- 1 | import { classicGenetic } from './genetic'; 2 | import { islandGenetic } from './island'; 3 | 4 | const resultsClassic: number[] = []; 5 | const resultsIsland: number[] = []; 6 | 7 | async function main() { 8 | for (let i = 0; i < 50; i++) { 9 | const islandGens = await islandGenetic(false); 10 | const classicGens = await classicGenetic(false); 11 | 12 | resultsIsland.push(islandGens); 13 | resultsClassic.push(classicGens); 14 | 15 | console.log('In progress...', i, islandGens, classicGens); 16 | } 17 | 18 | const avgClassic = average(resultsClassic); 19 | const avgIsland = average(resultsIsland); 20 | console.log('---- Average generation count needed to solve task ----\n'); 21 | console.log('Classic:', avgClassic); 22 | console.log('Island:', avgIsland); 23 | console.log('Percent diff is:', isWhatPercentOf(avgIsland, avgClassic)); 24 | } 25 | 26 | main(); 27 | 28 | function average(arr: number[]) { 29 | return arr.reduce((a, b) => a + b, 0) / arr.length; 30 | } 31 | 32 | function isWhatPercentOf(numA: number, numB: number) { 33 | return (1 - numA / numB) * 100; 34 | } 35 | -------------------------------------------------------------------------------- /test/genetic.ts: -------------------------------------------------------------------------------- 1 | // benchmarked vs https://subprotocol.com/system/genetic-hello-world.html 2 | // local genetic is x2 faster 3 | 4 | import { Genetic, Select } from '../src/genetic'; 5 | 6 | const GENERATIONS = 4000; 7 | const POPULATION = 4000; 8 | const solution = 'Insanity is doing the same thing over and over again and expecting different results'; 9 | 10 | export async function classicGenetic(log?: boolean) { 11 | function randomString(len: number) { 12 | let text = ''; 13 | const charset = 'abcdefghijklmnopqrstuvwxyz0123456789'; 14 | for (let i = 0; i < len; i++) text += charset.charAt(Math.floor(Math.random() * charset.length)); 15 | 16 | return text; 17 | } 18 | 19 | function replaceAt(str, index, character) { 20 | return str.substr(0, index) + character + str.substr(index + character.length); 21 | } 22 | 23 | async function randomFunction() { 24 | // create random strings that are equal in length to solution 25 | return randomString(solution.length); 26 | } 27 | 28 | async function mutationFunction(entity: string) { 29 | // chromosomal drift 30 | const i = Math.floor(Math.random() * entity.length); 31 | return replaceAt( 32 | entity, 33 | i, 34 | String.fromCharCode(entity.charCodeAt(i) + (Math.floor(Math.random() * 2) ? 1 : -1)), 35 | ); 36 | } 37 | 38 | async function crossoverFunction(mother: string, father: string) { 39 | // two-point crossover 40 | const len = mother.length; 41 | let ca = Math.floor(Math.random() * len); 42 | let cb = Math.floor(Math.random() * len); 43 | if (ca > cb) { 44 | const tmp = cb; 45 | cb = ca; 46 | ca = tmp; 47 | } 48 | 49 | const son = father.substr(0, ca) + mother.substr(ca, cb - ca) + father.substr(cb); 50 | const daughter = mother.substr(0, ca) + father.substr(ca, cb - ca) + mother.substr(cb); 51 | 52 | return [son, daughter]; 53 | } 54 | 55 | async function fitnessFunction(entity: string) { 56 | let fitness = 0; 57 | 58 | for (let i = 0; i < entity.length; ++i) { 59 | // increase fitness for each character that matches 60 | if (entity[i] == solution[i]) fitness += 1; 61 | 62 | // award fractions of a point as we get warmer 63 | fitness += (127 - Math.abs(entity.charCodeAt(i) - solution.charCodeAt(i))) / 50; 64 | } 65 | 66 | return { fitness }; 67 | } 68 | 69 | const population: Promise[] = []; 70 | 71 | for (let i = 0; i < POPULATION; i++) { 72 | population.push(randomFunction()); 73 | } 74 | 75 | const genetic = new Genetic({ 76 | mutationFunction, 77 | crossoverFunction, 78 | fitnessFunction, 79 | randomFunction, 80 | populationSize: POPULATION, 81 | fittestNSurvives: 1, 82 | select1: Select.FittestLinear, 83 | select2: Select.Tournament3, 84 | mutateProbablity: 0.8, 85 | crossoverProbablity: 0.8, 86 | }); 87 | 88 | async function solve() { 89 | await genetic.seed(); 90 | 91 | for (let i = 0; i <= GENERATIONS; i++) { 92 | if (log) { 93 | console.count('gen'); 94 | } 95 | 96 | await genetic.estimate(); 97 | const bestOne = genetic.best()[0]; 98 | 99 | if (log) { 100 | console.log(`${bestOne.entity} - ${bestOne.fitness}`); 101 | } 102 | 103 | await genetic.breed(); 104 | 105 | if (bestOne.entity === solution) { 106 | return i; 107 | } 108 | } 109 | } 110 | 111 | return solve(); 112 | } 113 | -------------------------------------------------------------------------------- /test/island.ts: -------------------------------------------------------------------------------- 1 | // benchmarked vs https://subprotocol.com/system/genetic-hello-world.html 2 | // local genetic is x2 faster 3 | 4 | import { GeneticOptions, Select } from '../src/genetic'; 5 | import { IslandGeneticModel, IslandGeneticModelOptions, Migrate } from '../src/island-model'; 6 | 7 | const GENERATIONS = 4000; 8 | const POPULATION = 4000; 9 | const solution = 'Insanity is doing the same thing over and over again and expecting different results'; 10 | 11 | export async function islandGenetic(log: boolean) { 12 | function randomString(len: number) { 13 | let text = ''; 14 | const charset = 'abcdefghijklmnopqrstuvwxyz0123456789'; 15 | for (let i = 0; i < len; i++) text += charset.charAt(Math.floor(Math.random() * charset.length)); 16 | 17 | return text; 18 | } 19 | 20 | function replaceAt(str, index, character) { 21 | return str.substr(0, index) + character + str.substr(index + character.length); 22 | } 23 | 24 | async function randomFunction() { 25 | // create random strings that are equal in length to solution 26 | return randomString(solution.length); 27 | } 28 | 29 | async function mutationFunction(entity: string) { 30 | // chromosomal drift 31 | const i = Math.floor(Math.random() * entity.length); 32 | return replaceAt( 33 | entity, 34 | i, 35 | String.fromCharCode(entity.charCodeAt(i) + (Math.floor(Math.random() * 2) ? 1 : -1)), 36 | ); 37 | } 38 | 39 | async function crossoverFunction(mother: string, father: string) { 40 | // two-point crossover 41 | const len = mother.length; 42 | let ca = Math.floor(Math.random() * len); 43 | let cb = Math.floor(Math.random() * len); 44 | if (ca > cb) { 45 | const tmp = cb; 46 | cb = ca; 47 | ca = tmp; 48 | } 49 | 50 | const son = father.substr(0, ca) + mother.substr(ca, cb - ca) + father.substr(cb); 51 | const daughter = mother.substr(0, ca) + father.substr(ca, cb - ca) + mother.substr(cb); 52 | 53 | return [son, daughter]; 54 | } 55 | 56 | async function fitnessFunction(entity: string) { 57 | let fitness = 0; 58 | 59 | for (let i = 0; i < entity.length; ++i) { 60 | // increase fitness for each character that matches 61 | if (entity[i] == solution[i]) fitness += 1; 62 | 63 | // award fractions of a point as we get warmer 64 | fitness += (127 - Math.abs(entity.charCodeAt(i) - solution.charCodeAt(i))) / 50; 65 | } 66 | 67 | return { fitness }; 68 | } 69 | 70 | const population: Promise[] = []; 71 | 72 | for (let i = 0; i < POPULATION; i++) { 73 | population.push(randomFunction()); 74 | } 75 | 76 | const geneticOptions: GeneticOptions = { 77 | mutationFunction, 78 | crossoverFunction, 79 | fitnessFunction, 80 | randomFunction, 81 | populationSize: POPULATION, 82 | fittestNSurvives: 1, 83 | select1: Select.FittestLinear, 84 | select2: Select.Tournament3, 85 | }; 86 | 87 | const ilandOptions: IslandGeneticModelOptions = { 88 | islandCount: 8, 89 | islandMutationProbability: 0.8, 90 | islandCrossoverProbability: 0.8, 91 | migrationProbability: 0.1, 92 | migrationFunction: Migrate.FittestLinear, 93 | }; 94 | 95 | const continentBreedAfter = 50; 96 | let continentGenerationsCount = 0; 97 | 98 | const genetic = new IslandGeneticModel(ilandOptions, geneticOptions); 99 | 100 | async function solve() { 101 | await genetic.seed(); 102 | 103 | for (let i = 0; i <= GENERATIONS; i++) { 104 | if (log) { 105 | console.count('gen'); 106 | } 107 | 108 | if (i !== 0 && i % continentBreedAfter === 0) { 109 | // Move to continent 110 | genetic.moveAllToContinent(); 111 | continentGenerationsCount = 10; 112 | } 113 | 114 | if (continentGenerationsCount) { 115 | continentGenerationsCount--; 116 | 117 | if (continentGenerationsCount === 0) { 118 | // Move to ilands 119 | genetic.migrateToIslands(); 120 | } 121 | } 122 | 123 | await genetic.estimate(); 124 | 125 | const bestOne = genetic.best()[0]; 126 | 127 | if (log) { 128 | console.log(`${bestOne.entity} - ${bestOne.fitness}`); 129 | } 130 | 131 | await genetic.breed(); 132 | 133 | if (bestOne.entity === solution) { 134 | return i; 135 | } 136 | } 137 | } 138 | 139 | return solve(); 140 | } 141 | -------------------------------------------------------------------------------- /test/test-classic-single.ts: -------------------------------------------------------------------------------- 1 | import { classicGenetic } from './genetic'; 2 | 3 | classicGenetic(true); 4 | -------------------------------------------------------------------------------- /test/test-iland-single.ts: -------------------------------------------------------------------------------- 1 | import { islandGenetic } from './island'; 2 | 3 | islandGenetic(true); 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "allowJs": true, 7 | "outDir": "./lib", 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true, 10 | "sourceMap": true, 11 | "allowSyntheticDefaultImports": true, 12 | "declaration": true, 13 | "lib": ["ESNext"] 14 | }, 15 | "include": [ 16 | "src/**/*.ts" 17 | ], 18 | "exclude": ["node_modules"], 19 | } 20 | --------------------------------------------------------------------------------