@thi.ng/gp 0.4.81 → 0.4.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (6) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/api.js +0 -1
  3. package/ast.js +142 -128
  4. package/mep.js +89 -95
  5. package/package.json +12 -9
  6. package/utils.js +24 -16
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2023-12-09T19:12:03Z
3
+ - **Last updated**: 2023-12-11T10:07:09Z
4
4
  - **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
5
5
 
6
6
  All notable changes to this project will be documented in this file.
package/api.js CHANGED
@@ -1 +0,0 @@
1
- export {};
package/ast.js CHANGED
@@ -6,134 +6,148 @@ import { repeatedly } from "@thi.ng/transducers/repeatedly";
6
6
  import { takeWhile } from "@thi.ng/transducers/take-while";
7
7
  import { zipper } from "@thi.ng/zipper/zipper";
8
8
  import { opNode, probabilities, terminalNode } from "./utils.js";
9
- export class AST {
10
- opts;
11
- choices;
12
- probTerminal;
13
- constructor(opts) {
14
- this.opts = { rnd: SYSTEM, ...opts };
15
- assert(this.opts.probMutate < 1, "mutation probability must be < 1.0");
16
- const probs = probabilities(this.opts);
17
- this.probTerminal = probs.probTerminal;
18
- this.choices = probs.iter;
19
- }
20
- /**
21
- * Returns a random AST with given max tree depth. The max depth
22
- * provided in {@link ASTOpts} is used by default.
23
- *
24
- * @param maxDepth -
25
- */
26
- randomAST(maxDepth = this.opts.maxDepth) {
27
- return this.randomASTNode(0, maxDepth);
28
- }
29
- /**
30
- * Immutably transplants a randomly chosen subtree of `parent2` into
31
- * a randomly chosen location in `parent1` and vice versa. Returns 2
32
- * new trees.
33
- *
34
- * @param parent1 -
35
- * @param parent2 -
36
- */
37
- crossoverSingle(parent1, parent2) {
38
- return [
39
- this.selectRandomNode(parent1).replace(this.selectRandomNode(parent2).node).root,
40
- this.selectRandomNode(parent2).replace(this.selectRandomNode(parent1).node).root,
41
- ];
42
- }
43
- /**
44
- * Probilistically replaces randomly chosen tree nodes with a new random AST
45
- * of given `maxDepth` (default: 1). Never mutates root.
46
- *
47
- * @remarks
48
- * The AST's pre-configured max tree depth will always be respected, so
49
- * depending on the depth of the selected mutation location, the randomly
50
- * inserted/replaced subtree might be more shallow than given `maxDepth`.
51
- * I.e. if a tree node at max depth is selected for mutation, it will always
52
- * result in a randomly chosen terminal node only.
53
- *
54
- * @param tree -
55
- * @param maxDepth -
56
- */
57
- mutate(tree, maxDepth = 1) {
58
- const { rnd, probMutate, maxDepth: limit } = this.opts;
59
- let loc = this.asZipper(tree).next;
60
- if (!loc)
61
- return tree;
62
- while (true) {
63
- let nextLoc;
64
- if (rnd.probability(probMutate)) {
65
- loc = loc.replace(this.randomASTNode(0, Math.min(limit - loc.depth, maxDepth)));
66
- nextLoc = loc.right;
67
- if (!nextLoc) {
68
- nextLoc = loc.up;
69
- if (nextLoc) {
70
- nextLoc = nextLoc.right;
71
- }
72
- }
73
- }
74
- else {
75
- nextLoc = loc.next;
76
- }
77
- if (!nextLoc)
78
- return loc.root;
79
- loc = nextLoc;
9
+ class AST {
10
+ opts;
11
+ choices;
12
+ probTerminal;
13
+ constructor(opts) {
14
+ this.opts = { rnd: SYSTEM, ...opts };
15
+ assert(this.opts.probMutate < 1, "mutation probability must be < 1.0");
16
+ const probs = probabilities(this.opts);
17
+ this.probTerminal = probs.probTerminal;
18
+ this.choices = probs.iter;
19
+ }
20
+ /**
21
+ * Returns a random AST with given max tree depth. The max depth
22
+ * provided in {@link ASTOpts} is used by default.
23
+ *
24
+ * @param maxDepth -
25
+ */
26
+ randomAST(maxDepth = this.opts.maxDepth) {
27
+ return this.randomASTNode(0, maxDepth);
28
+ }
29
+ /**
30
+ * Immutably transplants a randomly chosen subtree of `parent2` into
31
+ * a randomly chosen location in `parent1` and vice versa. Returns 2
32
+ * new trees.
33
+ *
34
+ * @param parent1 -
35
+ * @param parent2 -
36
+ */
37
+ crossoverSingle(parent1, parent2) {
38
+ return [
39
+ this.selectRandomNode(parent1).replace(
40
+ this.selectRandomNode(parent2).node
41
+ ).root,
42
+ this.selectRandomNode(parent2).replace(
43
+ this.selectRandomNode(parent1).node
44
+ ).root
45
+ ];
46
+ }
47
+ /**
48
+ * Probilistically replaces randomly chosen tree nodes with a new random AST
49
+ * of given `maxDepth` (default: 1). Never mutates root.
50
+ *
51
+ * @remarks
52
+ * The AST's pre-configured max tree depth will always be respected, so
53
+ * depending on the depth of the selected mutation location, the randomly
54
+ * inserted/replaced subtree might be more shallow than given `maxDepth`.
55
+ * I.e. if a tree node at max depth is selected for mutation, it will always
56
+ * result in a randomly chosen terminal node only.
57
+ *
58
+ * @param tree -
59
+ * @param maxDepth -
60
+ */
61
+ mutate(tree, maxDepth = 1) {
62
+ const { rnd, probMutate, maxDepth: limit } = this.opts;
63
+ let loc = this.asZipper(tree).next;
64
+ if (!loc)
65
+ return tree;
66
+ while (true) {
67
+ let nextLoc;
68
+ if (rnd.probability(probMutate)) {
69
+ loc = loc.replace(
70
+ this.randomASTNode(0, Math.min(limit - loc.depth, maxDepth))
71
+ );
72
+ nextLoc = loc.right;
73
+ if (!nextLoc) {
74
+ nextLoc = loc.up;
75
+ if (nextLoc) {
76
+ nextLoc = nextLoc.right;
77
+ }
80
78
  }
79
+ } else {
80
+ nextLoc = loc.next;
81
+ }
82
+ if (!nextLoc)
83
+ return loc.root;
84
+ loc = nextLoc;
81
85
  }
82
- /**
83
- * Returns linearized/flat version of given AST as an array of [zipper
84
- * locations](https://docs.thi.ng/umbrella/zipper/classes/Location.html).
85
- *
86
- * @param tree -
87
- */
88
- linearizedAST(tree) {
89
- return [
90
- ...iterator(takeWhile((x) => !!x), iterate((x) => x.next, this.asZipper(tree))),
91
- ];
92
- }
93
- /**
94
- * Returns random {@link ASTNode} from given tree, using the user provided
95
- * PRNG (via `opts`) or
96
- * [`SYSTEM`](https://docs.thi.ng/umbrella/random/variables/SYSTEM.html).
97
- * The returned value is a [zipper
98
- * location](https://docs.thi.ng/umbrella/zipper/classes/Location.html) of
99
- * the selected node.
100
- *
101
- * @remarks
102
- * The actual `ASTNode` can be obtained via `.node`.
103
- *
104
- * Only nodes in the linearized index range `[min..max)` are returned
105
- * (default: entire tree range). Since the linear tree length isn't known
106
- * beforehand, `max` < 0 (default) is equivalent to the linearized tree end.
107
- *
108
- * @param opts -
109
- * @param tree -
110
- * @param min -
111
- * @param max -
112
- */
113
- selectRandomNode(tree, min = 0, max = -1) {
114
- const rnd = this.opts.rnd;
115
- const linTree = this.linearizedAST(tree);
116
- let node;
117
- max < 0 && (max = linTree.length);
118
- node = linTree[rnd.minmax(min, max) | 0];
119
- return node;
120
- }
121
- randomASTNode(d, maxDepth) {
122
- const rnd = this.opts.rnd;
123
- const geneID = this.choices.next().value;
124
- if (geneID === 0 || d >= maxDepth)
125
- return terminalNode(this.opts.terminal(rnd));
126
- const op = this.opts.ops[geneID - 1];
127
- const children = [
128
- ...repeatedly(() => this.randomASTNode(d + 1, maxDepth), op.arity),
129
- ];
130
- return opNode(op.fn(rnd, children), children);
131
- }
132
- asZipper(tree) {
133
- return zipper({
134
- branch: (x) => x.type === "op",
135
- children: (x) => x.args,
136
- factory: (n, args) => opNode(n.op, args),
137
- }, tree);
138
- }
86
+ }
87
+ /**
88
+ * Returns linearized/flat version of given AST as an array of [zipper
89
+ * locations](https://docs.thi.ng/umbrella/zipper/classes/Location.html).
90
+ *
91
+ * @param tree -
92
+ */
93
+ linearizedAST(tree) {
94
+ return [
95
+ ...iterator(
96
+ takeWhile((x) => !!x),
97
+ iterate((x) => x.next, this.asZipper(tree))
98
+ )
99
+ ];
100
+ }
101
+ /**
102
+ * Returns random {@link ASTNode} from given tree, using the user provided
103
+ * PRNG (via `opts`) or
104
+ * [`SYSTEM`](https://docs.thi.ng/umbrella/random/variables/SYSTEM.html).
105
+ * The returned value is a [zipper
106
+ * location](https://docs.thi.ng/umbrella/zipper/classes/Location.html) of
107
+ * the selected node.
108
+ *
109
+ * @remarks
110
+ * The actual `ASTNode` can be obtained via `.node`.
111
+ *
112
+ * Only nodes in the linearized index range `[min..max)` are returned
113
+ * (default: entire tree range). Since the linear tree length isn't known
114
+ * beforehand, `max` < 0 (default) is equivalent to the linearized tree end.
115
+ *
116
+ * @param opts -
117
+ * @param tree -
118
+ * @param min -
119
+ * @param max -
120
+ */
121
+ selectRandomNode(tree, min = 0, max = -1) {
122
+ const rnd = this.opts.rnd;
123
+ const linTree = this.linearizedAST(tree);
124
+ let node;
125
+ max < 0 && (max = linTree.length);
126
+ node = linTree[rnd.minmax(min, max) | 0];
127
+ return node;
128
+ }
129
+ randomASTNode(d, maxDepth) {
130
+ const rnd = this.opts.rnd;
131
+ const geneID = this.choices.next().value;
132
+ if (geneID === 0 || d >= maxDepth)
133
+ return terminalNode(this.opts.terminal(rnd));
134
+ const op = this.opts.ops[geneID - 1];
135
+ const children = [
136
+ ...repeatedly(() => this.randomASTNode(d + 1, maxDepth), op.arity)
137
+ ];
138
+ return opNode(op.fn(rnd, children), children);
139
+ }
140
+ asZipper(tree) {
141
+ return zipper(
142
+ {
143
+ branch: (x) => x.type === "op",
144
+ children: (x) => x.args,
145
+ factory: (n, args) => opNode(n.op, args)
146
+ },
147
+ tree
148
+ );
149
+ }
139
150
  }
151
+ export {
152
+ AST
153
+ };
package/mep.js CHANGED
@@ -2,104 +2,98 @@ import { inRange } from "@thi.ng/math/interval";
2
2
  import { SYSTEM } from "@thi.ng/random/system";
3
3
  import { repeatedly } from "@thi.ng/transducers/repeatedly";
4
4
  import { opNode, probabilities, terminalNode } from "./utils.js";
5
- export class MEP {
6
- opts;
7
- probTerminal;
8
- choices;
9
- constructor(opts) {
10
- this.opts = { rnd: SYSTEM, ...opts };
11
- const probs = probabilities(this.opts);
12
- this.probTerminal = probs.probTerminal;
13
- this.choices = probs.iter;
5
+ class MEP {
6
+ opts;
7
+ probTerminal;
8
+ choices;
9
+ constructor(opts) {
10
+ this.opts = { rnd: SYSTEM, ...opts };
11
+ const probs = probabilities(this.opts);
12
+ this.probTerminal = probs.probTerminal;
13
+ this.choices = probs.iter;
14
+ }
15
+ /**
16
+ * Returns a new random {@link MEPChromosome} with the configured
17
+ * probabilities of operator and terminal nodes (constants).
18
+ *
19
+ * @remarks
20
+ * See {@link MEP.decodeChromosome} for conversion to
21
+ * {@link ASTNode}s.
22
+ *
23
+ */
24
+ randomChromosome() {
25
+ const res = [];
26
+ for (let i = 0, n = this.opts.chromoSize; i < n; i++) {
27
+ res[i] = this.randomGene(i);
14
28
  }
15
- /**
16
- * Returns a new random {@link MEPChromosome} with the configured
17
- * probabilities of operator and terminal nodes (constants).
18
- *
19
- * @remarks
20
- * See {@link MEP.decodeChromosome} for conversion to
21
- * {@link ASTNode}s.
22
- *
23
- */
24
- randomChromosome() {
25
- const res = [];
26
- for (let i = 0, n = this.opts.chromoSize; i < n; i++) {
27
- res[i] = this.randomGene(i);
28
- }
29
- return res;
29
+ return res;
30
+ }
31
+ /**
32
+ * Decodes given chromosome into an array of {@link ASTNode}s and
33
+ * optionally applies tree depth filter (by default includes all).
34
+ *
35
+ * @remarks
36
+ * A {@link MEPChromosome} encodes multiple solutions (one per gene
37
+ * slot), therefore a chromosome of length `n` will produce the same
38
+ * number ASTs (less if min/max tree depth filters are applied).
39
+ *
40
+ * @param chromosome -
41
+ * @param minDepth -
42
+ * @param maxDepth -
43
+ */
44
+ decodeChromosome(chromosome, minDepth = 0, maxDepth = Infinity) {
45
+ const res = [];
46
+ const depths = [];
47
+ for (let i = 0; i < chromosome.length; i++) {
48
+ const gene = chromosome[i];
49
+ if (gene.type == "term") {
50
+ res[i] = gene;
51
+ depths[i] = 1;
52
+ } else {
53
+ res[i] = opNode(
54
+ gene.op,
55
+ gene.args.map((g) => res[g])
56
+ );
57
+ depths[i] = 1 + gene.args.reduce((d, a) => Math.max(d, depths[a]), 0);
58
+ }
30
59
  }
31
- /**
32
- * Decodes given chromosome into an array of {@link ASTNode}s and
33
- * optionally applies tree depth filter (by default includes all).
34
- *
35
- * @remarks
36
- * A {@link MEPChromosome} encodes multiple solutions (one per gene
37
- * slot), therefore a chromosome of length `n` will produce the same
38
- * number ASTs (less if min/max tree depth filters are applied).
39
- *
40
- * @param chromosome -
41
- * @param minDepth -
42
- * @param maxDepth -
43
- */
44
- decodeChromosome(chromosome, minDepth = 0, maxDepth = Infinity) {
45
- const res = [];
46
- const depths = [];
47
- for (let i = 0; i < chromosome.length; i++) {
48
- const gene = chromosome[i];
49
- if (gene.type == "term") {
50
- res[i] = gene;
51
- depths[i] = 1;
52
- }
53
- else {
54
- res[i] = opNode(gene.op, gene.args.map((g) => res[g]));
55
- depths[i] =
56
- 1 + gene.args.reduce((d, a) => Math.max(d, depths[a]), 0);
57
- }
58
- }
59
- return res.filter((_, i) => inRange(depths[i], minDepth, maxDepth));
60
+ return res.filter((_, i) => inRange(depths[i], minDepth, maxDepth));
61
+ }
62
+ crossoverSingle(chromo1, chromo2, cut) {
63
+ cut = cut !== void 0 ? cut : this.opts.rnd.int() % Math.min(chromo1.length, chromo2.length);
64
+ return [
65
+ chromo1.slice(0, cut).concat(chromo2.slice(cut)),
66
+ chromo2.slice(0, cut).concat(chromo1.slice(cut))
67
+ ];
68
+ }
69
+ crossoverUniform(chromo1, chromo2) {
70
+ const rnd = this.opts.rnd;
71
+ const res = [];
72
+ const minLen = Math.min(chromo1.length, chromo2.length);
73
+ for (let i = 0; i < minLen; i++) {
74
+ res[i] = rnd.probability(0.5) ? chromo1[i] : chromo2[i];
60
75
  }
61
- crossoverSingle(chromo1, chromo2, cut) {
62
- cut =
63
- cut !== undefined
64
- ? cut
65
- : this.opts.rnd.int() %
66
- Math.min(chromo1.length, chromo2.length);
67
- return [
68
- chromo1.slice(0, cut).concat(chromo2.slice(cut)),
69
- chromo2.slice(0, cut).concat(chromo1.slice(cut)),
70
- ];
76
+ return chromo1.length > minLen ? res.concat(chromo1.slice(minLen)) : chromo2.length > minLen ? res.concat(chromo2.slice(minLen)) : res;
77
+ }
78
+ mutate(chromo) {
79
+ const { rnd, probMutate } = this.opts;
80
+ const res = new Array(chromo.length);
81
+ for (let i = chromo.length; i-- > 0; ) {
82
+ res[i] = rnd.probability(probMutate) ? this.randomGene(i) : chromo[i];
71
83
  }
72
- crossoverUniform(chromo1, chromo2) {
73
- const rnd = this.opts.rnd;
74
- const res = [];
75
- const minLen = Math.min(chromo1.length, chromo2.length);
76
- for (let i = 0; i < minLen; i++) {
77
- res[i] = rnd.probability(0.5) ? chromo1[i] : chromo2[i];
78
- }
79
- return chromo1.length > minLen
80
- ? res.concat(chromo1.slice(minLen))
81
- : chromo2.length > minLen
82
- ? res.concat(chromo2.slice(minLen))
83
- : res;
84
- }
85
- mutate(chromo) {
86
- const { rnd, probMutate } = this.opts;
87
- const res = new Array(chromo.length);
88
- for (let i = chromo.length; i-- > 0;) {
89
- res[i] = rnd.probability(probMutate)
90
- ? this.randomGene(i)
91
- : chromo[i];
92
- }
93
- return res;
94
- }
95
- randomGene(i) {
96
- const geneID = this.choices.next().value;
97
- const rnd = this.opts.rnd;
98
- if (i === 0 || geneID === 0) {
99
- return terminalNode(this.opts.terminal(rnd));
100
- }
101
- const op = this.opts.ops[geneID - 1];
102
- const args = [...repeatedly(() => rnd.int() % i, op.arity)];
103
- return opNode(op.fn(rnd, args), args);
84
+ return res;
85
+ }
86
+ randomGene(i) {
87
+ const geneID = this.choices.next().value;
88
+ const rnd = this.opts.rnd;
89
+ if (i === 0 || geneID === 0) {
90
+ return terminalNode(this.opts.terminal(rnd));
104
91
  }
92
+ const op = this.opts.ops[geneID - 1];
93
+ const args = [...repeatedly(() => rnd.int() % i, op.arity)];
94
+ return opNode(op.fn(rnd, args), args);
95
+ }
105
96
  }
97
+ export {
98
+ MEP
99
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/gp",
3
- "version": "0.4.81",
3
+ "version": "0.4.82",
4
4
  "description": "Genetic programming helpers & strategies (tree based & multi-expression programming)",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -24,7 +24,9 @@
24
24
  "author": "Karsten Schmidt (https://thi.ng)",
25
25
  "license": "Apache-2.0",
26
26
  "scripts": {
27
- "build": "yarn clean && tsc --declaration",
27
+ "build": "yarn build:esbuild && yarn build:decl",
28
+ "build:decl": "tsc --declaration --emitDeclarationOnly",
29
+ "build:esbuild": "esbuild --format=esm --platform=neutral --target=es2022 --tsconfig=tsconfig.json --outdir=. src/**/*.ts",
28
30
  "clean": "rimraf --glob '*.js' '*.d.ts' '*.map' doc",
29
31
  "doc": "typedoc --excludePrivate --excludeInternal --out doc src/index.ts",
30
32
  "doc:ae": "mkdir -p .ae/doc .ae/temp && api-extractor run --local --verbose",
@@ -33,15 +35,16 @@
33
35
  "test": "bun test"
34
36
  },
35
37
  "dependencies": {
36
- "@thi.ng/api": "^8.9.11",
37
- "@thi.ng/errors": "^2.4.5",
38
- "@thi.ng/math": "^5.7.6",
39
- "@thi.ng/random": "^3.6.17",
40
- "@thi.ng/transducers": "^8.8.14",
41
- "@thi.ng/zipper": "^2.1.68"
38
+ "@thi.ng/api": "^8.9.12",
39
+ "@thi.ng/errors": "^2.4.6",
40
+ "@thi.ng/math": "^5.7.7",
41
+ "@thi.ng/random": "^3.6.18",
42
+ "@thi.ng/transducers": "^8.8.15",
43
+ "@thi.ng/zipper": "^2.1.69"
42
44
  },
43
45
  "devDependencies": {
44
46
  "@microsoft/api-extractor": "^7.38.3",
47
+ "esbuild": "^0.19.8",
45
48
  "rimraf": "^5.0.5",
46
49
  "tools": "^0.0.1",
47
50
  "typedoc": "^0.25.4",
@@ -106,5 +109,5 @@
106
109
  "status": "alpha",
107
110
  "year": 2019
108
111
  },
109
- "gitHead": "25f2ac8ff795a432a930119661b364d4d93b59a0\n"
112
+ "gitHead": "5e7bafedfc3d53bc131469a28de31dd8e5b4a3ff\n"
110
113
  }
package/utils.js CHANGED
@@ -1,23 +1,31 @@
1
- // thing:no-export
2
1
  import { assert } from "@thi.ng/errors/assert";
3
2
  import { add } from "@thi.ng/transducers/add";
4
3
  import { choices } from "@thi.ng/transducers/choices";
5
4
  import { range } from "@thi.ng/transducers/range";
6
- export const terminalNode = (value) => ({
7
- type: "term",
8
- value,
5
+ const terminalNode = (value) => ({
6
+ type: "term",
7
+ value
9
8
  });
10
- export const opNode = (op, args) => ({
11
- type: "op",
12
- op,
13
- args,
9
+ const opNode = (op, args) => ({
10
+ type: "op",
11
+ op,
12
+ args
14
13
  });
15
- export const probabilities = (opts) => {
16
- const probabilities = opts.ops.map((op) => op.prob);
17
- const psum = add(probabilities);
18
- assert(psum < 1, "total op probabilities MUST be < 1");
19
- return {
20
- iter: choices([...range(probabilities.length + 1)], [1 - psum, ...probabilities], opts.rnd),
21
- probTerminal: 1 - psum,
22
- };
14
+ const probabilities = (opts) => {
15
+ const probabilities2 = opts.ops.map((op) => op.prob);
16
+ const psum = add(probabilities2);
17
+ assert(psum < 1, "total op probabilities MUST be < 1");
18
+ return {
19
+ iter: choices(
20
+ [...range(probabilities2.length + 1)],
21
+ [1 - psum, ...probabilities2],
22
+ opts.rnd
23
+ ),
24
+ probTerminal: 1 - psum
25
+ };
26
+ };
27
+ export {
28
+ opNode,
29
+ probabilities,
30
+ terminalNode
23
31
  };