@gkucmierz/utils 1.30.2 → 1.32.1
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.
- package/README.md +1 -0
- package/main.mjs +25 -5
- package/package.json +30 -22
- package/src/consume-iterator.mjs +26 -0
- package/src/math3d.mjs +66 -0
- package/src/nelder-mead.mjs +95 -0
- package/src/particle-swarm.mjs +84 -0
- package/src/simulated-annealing.mjs +57 -0
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# @gkucmierz/utils
|
|
2
2
|
|
|
3
|
+
[](https://socket.dev/npm/package/@gkucmierz/utils)
|
|
3
4
|

|
|
4
5
|

|
|
5
6
|

|
package/main.mjs
CHANGED
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
binarySearchArr, binarySearchGE, binarySearchLE, binarySearchRangeIncl
|
|
16
16
|
} from './src/binary-search.mjs'
|
|
17
|
+
import {
|
|
18
|
+
consumeIteratorNonBlocking
|
|
19
|
+
} from './src/consume-iterator.mjs'
|
|
17
20
|
import {
|
|
18
21
|
copyCase
|
|
19
22
|
} from './src/copy-case.mjs'
|
|
@@ -47,6 +50,9 @@ import {
|
|
|
47
50
|
import {
|
|
48
51
|
ListNode
|
|
49
52
|
} from './src/list-node.mjs'
|
|
53
|
+
import {
|
|
54
|
+
axisAngleToMatrix4, crossProduct, dotProduct, getRotationMatrixFromVectors, multiplyMatrix4, normalize, projectToTrackball
|
|
55
|
+
} from './src/math3d.mjs'
|
|
50
56
|
import {
|
|
51
57
|
matrixAsArray
|
|
52
58
|
} from './src/matrix.mjs'
|
|
@@ -56,6 +62,15 @@ import {
|
|
|
56
62
|
import {
|
|
57
63
|
mod, modBI
|
|
58
64
|
} from './src/mod.mjs'
|
|
65
|
+
import {
|
|
66
|
+
naturalSearch
|
|
67
|
+
} from './src/natural-search.mjs'
|
|
68
|
+
import {
|
|
69
|
+
nelderMead
|
|
70
|
+
} from './src/nelder-mead.mjs'
|
|
71
|
+
import {
|
|
72
|
+
particleSwarmOptimization
|
|
73
|
+
} from './src/particle-swarm.mjs'
|
|
59
74
|
import {
|
|
60
75
|
phi, phiBI
|
|
61
76
|
} from './src/phi.mjs'
|
|
@@ -65,21 +80,22 @@ import {
|
|
|
65
80
|
import {
|
|
66
81
|
array2range, range2array
|
|
67
82
|
} from './src/range-array.mjs'
|
|
83
|
+
import {
|
|
84
|
+
simulatedAnnealing
|
|
85
|
+
} from './src/simulated-annealing.mjs'
|
|
68
86
|
import {
|
|
69
87
|
squareRoot, squareRootBI
|
|
70
88
|
} from './src/square-root.mjs'
|
|
71
89
|
import {
|
|
72
90
|
tonelliShanksBI
|
|
73
91
|
} from './src/tonelli-shanks.mjs'
|
|
74
|
-
import {
|
|
75
|
-
naturalSearch
|
|
76
|
-
} from './src/natural-search.mjs'
|
|
77
92
|
|
|
78
93
|
export * from './src/SetCnt.mjs';
|
|
79
94
|
export * from './src/Trie.mjs';
|
|
80
95
|
export * from './src/base64.mjs';
|
|
81
96
|
export * from './src/bijective-numeration.mjs';
|
|
82
97
|
export * from './src/binary-search.mjs';
|
|
98
|
+
export * from './src/consume-iterator.mjs';
|
|
83
99
|
export * from './src/copy-case.mjs';
|
|
84
100
|
export * from './src/egcd.mjs';
|
|
85
101
|
export * from './src/factors.mjs';
|
|
@@ -91,16 +107,20 @@ export * from './src/heap.mjs';
|
|
|
91
107
|
export * from './src/herons-formula.mjs';
|
|
92
108
|
export * from './src/lcm.mjs';
|
|
93
109
|
export * from './src/list-node.mjs';
|
|
110
|
+
export * from './src/math3d.mjs';
|
|
94
111
|
export * from './src/matrix.mjs';
|
|
95
112
|
export * from './src/memoize.mjs';
|
|
96
113
|
export * from './src/mod.mjs';
|
|
114
|
+
export * from './src/natural-search.mjs';
|
|
115
|
+
export * from './src/nelder-mead.mjs';
|
|
116
|
+
export * from './src/particle-swarm.mjs';
|
|
97
117
|
export * from './src/phi.mjs';
|
|
98
118
|
export * from './src/pow-mod.mjs';
|
|
99
119
|
export * from './src/range-array.mjs';
|
|
120
|
+
export * from './src/simulated-annealing.mjs';
|
|
100
121
|
export * from './src/square-root.mjs';
|
|
101
122
|
export * from './src/tonelli-shanks.mjs';
|
|
102
|
-
export * from './src/natural-search.mjs';
|
|
103
123
|
|
|
104
124
|
export default [
|
|
105
|
-
SetCnt, Trie, fromBase64, fromBase64Url, toBase64, toBase64Url, bijective2num, bijective2numBI, num2bijective, num2bijectiveBI, binarySearchArr, binarySearchGE, binarySearchLE, binarySearchRangeIncl, copyCase, egcd, factors, factorsBI, formatBigNumber, formatBigNumberBI, wrapFn, gcd, gcdBI, getType, gpn, gpnBI, Heap, heronsFormula, heronsFormulaBI, lcm, lcmBI, ListNode, matrixAsArray, memoize, mod, modBI, phi, phiBI, powMod, powModBI, array2range, range2array, squareRoot, squareRootBI, tonelliShanksBI
|
|
125
|
+
SetCnt, Trie, fromBase64, fromBase64Url, toBase64, toBase64Url, bijective2num, bijective2numBI, num2bijective, num2bijectiveBI, binarySearchArr, binarySearchGE, binarySearchLE, binarySearchRangeIncl, consumeIteratorNonBlocking, copyCase, egcd, factors, factorsBI, formatBigNumber, formatBigNumberBI, wrapFn, gcd, gcdBI, getType, gpn, gpnBI, Heap, heronsFormula, heronsFormulaBI, lcm, lcmBI, ListNode, axisAngleToMatrix4, crossProduct, dotProduct, getRotationMatrixFromVectors, multiplyMatrix4, normalize, projectToTrackball, matrixAsArray, memoize, mod, modBI, naturalSearch, nelderMead, particleSwarmOptimization, phi, phiBI, powMod, powModBI, array2range, range2array, simulatedAnnealing, squareRoot, squareRootBI, tonelliShanksBI
|
|
106
126
|
];
|
package/package.json
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gkucmierz/utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.1",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Usefull functions for solving programming tasks",
|
|
5
6
|
"keywords": [
|
|
6
|
-
"utils",
|
|
7
7
|
"algorithms",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"base64",
|
|
9
|
+
"bijective",
|
|
10
|
+
"binary-search",
|
|
11
|
+
"competitive-programming",
|
|
12
|
+
"data-structures",
|
|
13
|
+
"factors",
|
|
10
14
|
"gcd",
|
|
15
|
+
"heap",
|
|
16
|
+
"iterator",
|
|
17
|
+
"javascript",
|
|
11
18
|
"lcm",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
19
|
+
"math",
|
|
20
|
+
"memoize",
|
|
14
21
|
"mod",
|
|
22
|
+
"node",
|
|
23
|
+
"non-blocking",
|
|
24
|
+
"number-theory",
|
|
25
|
+
"phi",
|
|
15
26
|
"pow-mod",
|
|
16
|
-
"binary-search",
|
|
17
|
-
"heap",
|
|
18
|
-
"trie",
|
|
19
|
-
"base64",
|
|
20
|
-
"bijective",
|
|
21
|
-
"tonelli-shanks",
|
|
22
|
-
"square-root",
|
|
23
|
-
"memoize",
|
|
24
27
|
"range-array",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
28
|
+
"square-root",
|
|
29
|
+
"tonelli-shanks",
|
|
30
|
+
"trie",
|
|
31
|
+
"utils"
|
|
29
32
|
],
|
|
30
33
|
"files": [
|
|
31
34
|
"main.mjs",
|
|
@@ -35,20 +38,25 @@
|
|
|
35
38
|
"scripts": {
|
|
36
39
|
"test": "jasmine",
|
|
37
40
|
"watch": "nodemon --exec 'npm test'",
|
|
38
|
-
"main": "node scripts/generate_main.mjs",
|
|
39
41
|
"docs": "npm run docs:build; npm run docs:open",
|
|
42
|
+
"build:main": "node scripts/generate_main.mjs",
|
|
40
43
|
"docs:build": "jsdoc -c jsdoc.json --readme README.md",
|
|
41
|
-
"docs:open": "open docs/index.html"
|
|
44
|
+
"docs:open": "open docs/index.html",
|
|
45
|
+
"lint": "eslint .",
|
|
46
|
+
"prepare": "husky"
|
|
42
47
|
},
|
|
43
48
|
"repository": {
|
|
44
49
|
"type": "git",
|
|
45
|
-
"url": "git+https://
|
|
50
|
+
"url": "git+https://gitea.7u.pl/gkucmierz/utils.git"
|
|
46
51
|
},
|
|
47
52
|
"author": "Grzegorz Kućmierz",
|
|
48
53
|
"license": "MIT",
|
|
49
54
|
"devDependencies": {
|
|
55
|
+
"@eslint/js": "^10.0.1",
|
|
50
56
|
"docdash": "^2.0.2",
|
|
51
|
-
"
|
|
57
|
+
"eslint": "^10.1.0",
|
|
58
|
+
"globals": "^17.4.0",
|
|
59
|
+
"husky": "^8.0.3",
|
|
52
60
|
"jasmine": "^4.4.0",
|
|
53
61
|
"jsdoc": "^4.0.4",
|
|
54
62
|
"nodemon": "^3.1.14"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consume iterator values asynchronously utilizing macro-task queue
|
|
3
|
+
* to prevent Event Loop blocking.
|
|
4
|
+
*
|
|
5
|
+
* @param {Iterator} iter - Iterator object with .next() method returning {value, done}
|
|
6
|
+
* @param {function} progressFn - Callback function executed for each iteration Step
|
|
7
|
+
* @returns {Promise} - Resolves when the iterator is fully consumed
|
|
8
|
+
*/
|
|
9
|
+
export const consumeIteratorNonBlocking = (iter, progressFn) => {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const loop = () => {
|
|
12
|
+
try {
|
|
13
|
+
const { value, done } = iter.next();
|
|
14
|
+
if (done) {
|
|
15
|
+
resolve();
|
|
16
|
+
} else {
|
|
17
|
+
progressFn(value);
|
|
18
|
+
setTimeout(loop, 0);
|
|
19
|
+
}
|
|
20
|
+
} catch (err) {
|
|
21
|
+
reject(err);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
loop();
|
|
25
|
+
});
|
|
26
|
+
};
|
package/src/math3d.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure 3D Math Engine for Arcball/Trackball raw matrix transformations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const crossProduct = (u, v) => [
|
|
6
|
+
u[1]*v[2] - u[2]*v[1],
|
|
7
|
+
u[2]*v[0] - u[0]*v[2],
|
|
8
|
+
u[0]*v[1] - u[1]*v[0]
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export const dotProduct = (u, v) => u[0]*v[0] + u[1]*v[1] + u[2]*v[2];
|
|
12
|
+
|
|
13
|
+
export const normalize = (v) => {
|
|
14
|
+
const len = Math.hypot(v[0], v[1], v[2]);
|
|
15
|
+
if (len === 0) return [0, 0, 0];
|
|
16
|
+
return [v[0]/len, v[1]/len, v[2]/len];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const projectToTrackball = (x, y, radius = 1) => {
|
|
20
|
+
const d2 = x*x + y*y;
|
|
21
|
+
const r2 = radius * radius;
|
|
22
|
+
if (d2 <= r2 * 0.5) {
|
|
23
|
+
return normalize([x, y, Math.sqrt(r2 - d2)]);
|
|
24
|
+
} else {
|
|
25
|
+
return normalize([x, y, (r2 * 0.5) / Math.sqrt(d2)]);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const axisAngleToMatrix4 = (axis, angle) => {
|
|
30
|
+
const [x, y, z] = normalize(axis);
|
|
31
|
+
const c = Math.cos(angle);
|
|
32
|
+
const s = Math.sin(angle);
|
|
33
|
+
const t = 1 - c;
|
|
34
|
+
|
|
35
|
+
// Column-major array mapping (standard for WebGL and Three.js matrices)
|
|
36
|
+
return [
|
|
37
|
+
c + x*x*t, y*x*t + z*s, z*x*t - y*s, 0,
|
|
38
|
+
x*y*t - z*s, c + y*y*t, z*y*t + x*s, 0,
|
|
39
|
+
x*z*t + y*s, y*z*t - x*s, c + z*z*t, 0,
|
|
40
|
+
0, 0, 0, 1
|
|
41
|
+
];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const multiplyMatrix4 = (a, b) => {
|
|
45
|
+
const out = new Array(16);
|
|
46
|
+
for (let c = 0; c < 4; c++) {
|
|
47
|
+
for (let r = 0; r < 4; r++) {
|
|
48
|
+
let sum = 0;
|
|
49
|
+
for (let i = 0; i < 4; i++) {
|
|
50
|
+
sum += a[i*4 + r] * b[c*4 + i]; // Note: column-major alignment
|
|
51
|
+
}
|
|
52
|
+
out[c*4 + r] = sum;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const getRotationMatrixFromVectors = (u, v) => {
|
|
59
|
+
const axis = crossProduct(u, v);
|
|
60
|
+
if (axis[0] === 0 && axis[1] === 0 && axis[2] === 0) {
|
|
61
|
+
return [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]; // Identity
|
|
62
|
+
}
|
|
63
|
+
const dot = dotProduct(u, v);
|
|
64
|
+
const angle = Math.acos(Math.max(-1, Math.min(1, dot)));
|
|
65
|
+
return axisAngleToMatrix4(axis, angle);
|
|
66
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nelder-Mead Simplex Algorithm (Direct Search Gradient-Free Optimization)
|
|
3
|
+
* Maximizes continuous N-dimensional spaces through contraction and expansion.
|
|
4
|
+
*
|
|
5
|
+
* @param {Function} evaluate - function(paramsArray) => Number (score to maximize)
|
|
6
|
+
* @param {Array<Array<Number>>} bounds - [[min1, max1], ... ] bounds per dimension
|
|
7
|
+
* @param {Object} options - { steps: 30, onProgress: async (state) => {} }
|
|
8
|
+
* @returns {Promise<{bestParams: Array<Number>, bestScore: Number}>}
|
|
9
|
+
*/
|
|
10
|
+
export const nelderMead = async (evaluate, bounds, options = {}) => {
|
|
11
|
+
const steps = options.steps || 30;
|
|
12
|
+
const onProgress = options.onProgress || (async () => {});
|
|
13
|
+
const dim = bounds.length;
|
|
14
|
+
|
|
15
|
+
let simplex = [];
|
|
16
|
+
let baseCoords = bounds.map(b => b[0] + (b[1] - b[0]) * 0.5); // Start directly centered
|
|
17
|
+
|
|
18
|
+
// To build an initial N-dimensional simplex, we require N+1 vertices
|
|
19
|
+
for (let i = 0; i <= dim; i++) {
|
|
20
|
+
let p = [...baseCoords];
|
|
21
|
+
if (i > 0) {
|
|
22
|
+
// Offset each dimension successively to form the simplex
|
|
23
|
+
let range = bounds[i-1][1] - bounds[i-1][0];
|
|
24
|
+
p[i-1] += range * 0.2; // 20% span stretch
|
|
25
|
+
}
|
|
26
|
+
for(let d=0; d<dim; d++) p[d] = Math.max(bounds[d][0], Math.min(bounds[d][1], p[d]));
|
|
27
|
+
|
|
28
|
+
simplex.push({ position: p, score: evaluate(p) });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const alpha = 1.0; // Reflection
|
|
32
|
+
const gamma = 2.0; // Expansion
|
|
33
|
+
const rho = 0.5; // Contraction
|
|
34
|
+
const sigma = 0.5; // Shrink
|
|
35
|
+
|
|
36
|
+
for (let step = 0; step < steps; step++) {
|
|
37
|
+
// Sort descending layout (Simplex optimization seeks maximization)
|
|
38
|
+
simplex.sort((a, b) => b.score - a.score);
|
|
39
|
+
|
|
40
|
+
let best = simplex[0];
|
|
41
|
+
let worst = simplex[dim];
|
|
42
|
+
let secondWorst = simplex[dim - 1];
|
|
43
|
+
|
|
44
|
+
// Compute the spatial centroid of all nodes excluding the worst
|
|
45
|
+
let centroid = new Array(dim).fill(0);
|
|
46
|
+
for (let i = 0; i < dim; i++) {
|
|
47
|
+
for (let d = 0; d < dim; d++) centroid[d] += simplex[i].position[d];
|
|
48
|
+
}
|
|
49
|
+
for (let d = 0; d < dim; d++) centroid[d] /= dim;
|
|
50
|
+
|
|
51
|
+
// Reflection Hook
|
|
52
|
+
let xr = centroid.map((c, d) => c + alpha * (c - worst.position[d]));
|
|
53
|
+
xr = xr.map((v, d) => Math.max(bounds[d][0], Math.min(bounds[d][1], v)));
|
|
54
|
+
let rScore = evaluate(xr);
|
|
55
|
+
|
|
56
|
+
if (rScore > best.score) {
|
|
57
|
+
// Expansion Hook
|
|
58
|
+
let xe = centroid.map((c, d) => c + gamma * (xr[d] - c));
|
|
59
|
+
xe = xe.map((v, d) => Math.max(bounds[d][0], Math.min(bounds[d][1], v)));
|
|
60
|
+
let eScore = evaluate(xe);
|
|
61
|
+
simplex[dim] = eScore > rScore ? { position: xe, score: eScore } : { position: xr, score: rScore };
|
|
62
|
+
} else if (rScore > secondWorst.score) {
|
|
63
|
+
// Reflected vertex outperforms second-worst node
|
|
64
|
+
simplex[dim] = { position: xr, score: rScore };
|
|
65
|
+
} else {
|
|
66
|
+
// Contraction Hook (if reflection completely failed)
|
|
67
|
+
let xc = centroid.map((c, d) => c + rho * (worst.position[d] - c));
|
|
68
|
+
xc = xc.map((v, d) => Math.max(bounds[d][0], Math.min(bounds[d][1], v)));
|
|
69
|
+
let cScore = evaluate(xc);
|
|
70
|
+
if (cScore > worst.score) {
|
|
71
|
+
simplex[dim] = { position: xc, score: cScore };
|
|
72
|
+
} else {
|
|
73
|
+
// Complete Simplex Shrinkage towards the primary maximum best point
|
|
74
|
+
for (let i = 1; i <= dim; i++) {
|
|
75
|
+
simplex[i].position = simplex[i].position.map((v, d) => best.position[d] + sigma * (v - best.position[d]));
|
|
76
|
+
simplex[i].position = simplex[i].position.map((v, d) => Math.max(bounds[d][0], Math.min(bounds[d][1], v)));
|
|
77
|
+
simplex[i].score = evaluate(simplex[i].position);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
simplex.sort((a, b) => b.score - a.score);
|
|
83
|
+
|
|
84
|
+
await onProgress({
|
|
85
|
+
step,
|
|
86
|
+
totalSteps: steps,
|
|
87
|
+
bestScore: simplex[0].score,
|
|
88
|
+
bestParams: simplex[0].position,
|
|
89
|
+
simplexVisNodes: simplex.map(s => s.position)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
simplex.sort((a, b) => b.score - a.score);
|
|
94
|
+
return { bestParams: simplex[0].position, bestScore: simplex[0].score };
|
|
95
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Particle Swarm Optimization (PSO)
|
|
3
|
+
* A stochastic, population-based metaheuristic for maximizing complex multidimensional bounds.
|
|
4
|
+
*
|
|
5
|
+
* @param {Function} evaluate - function(paramsArray) => Number (score to maximize)
|
|
6
|
+
* @param {Array<Array<Number>>} bounds - [[min1, max1], [min2, max2], ...] bounds per dimension
|
|
7
|
+
* @param {Object} options - { particles: 10, steps: 30, w: 0.5, c1: 1.5, c2: 1.5, onProgress: async (state) => {} }
|
|
8
|
+
* @returns {Promise<{bestParams: Array<Number>, bestScore: Number}>}
|
|
9
|
+
*/
|
|
10
|
+
export const particleSwarmOptimization = async (evaluate, bounds, options = {}) => {
|
|
11
|
+
const numParticles = options.particles || 12;
|
|
12
|
+
const steps = options.steps || 30;
|
|
13
|
+
const w = options.w || 0.5; // Inertia weight
|
|
14
|
+
const c1 = options.c1 || 1.5; // Cognitive (local) best weight
|
|
15
|
+
const c2 = options.c2 || 1.5; // Social (global) best weight
|
|
16
|
+
const onProgress = options.onProgress || (async () => {});
|
|
17
|
+
|
|
18
|
+
let swarm = [];
|
|
19
|
+
let globalBestParams = null;
|
|
20
|
+
let globalBestScore = -Infinity;
|
|
21
|
+
|
|
22
|
+
// Initialize swarm
|
|
23
|
+
for (let i = 0; i < numParticles; i++) {
|
|
24
|
+
let position = bounds.map(b => b[0] + Math.random() * (b[1] - b[0]));
|
|
25
|
+
let velocity = bounds.map(b => (Math.random() * 2 - 1) * (b[1] - b[0]) * 0.1); // Small initial velocity
|
|
26
|
+
let score = evaluate(position);
|
|
27
|
+
|
|
28
|
+
let particle = {
|
|
29
|
+
position,
|
|
30
|
+
velocity,
|
|
31
|
+
bestPosition: [...position],
|
|
32
|
+
bestScore: score
|
|
33
|
+
};
|
|
34
|
+
swarm.push(particle);
|
|
35
|
+
|
|
36
|
+
if (score > globalBestScore) {
|
|
37
|
+
globalBestScore = score;
|
|
38
|
+
globalBestParams = [...position];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < steps; i++) {
|
|
43
|
+
for (let p of swarm) {
|
|
44
|
+
for (let d = 0; d < bounds.length; d++) {
|
|
45
|
+
let r1 = Math.random();
|
|
46
|
+
let r2 = Math.random();
|
|
47
|
+
|
|
48
|
+
// Velocity update matrix
|
|
49
|
+
p.velocity[d] = w * p.velocity[d] +
|
|
50
|
+
c1 * r1 * (p.bestPosition[d] - p.position[d]) +
|
|
51
|
+
c2 * r2 * (globalBestParams[d] - p.position[d]);
|
|
52
|
+
|
|
53
|
+
// Position update
|
|
54
|
+
p.position[d] = p.position[d] + p.velocity[d];
|
|
55
|
+
|
|
56
|
+
// Enforce physical parameter bounds
|
|
57
|
+
p.position[d] = Math.max(bounds[d][0], Math.min(bounds[d][1], p.position[d]));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let score = evaluate(p.position);
|
|
61
|
+
|
|
62
|
+
// Update local cognitive best
|
|
63
|
+
if (score > p.bestScore) {
|
|
64
|
+
p.bestScore = score;
|
|
65
|
+
p.bestPosition = [...p.position];
|
|
66
|
+
}
|
|
67
|
+
// Update social swarm global best
|
|
68
|
+
if (score > globalBestScore) {
|
|
69
|
+
globalBestScore = score;
|
|
70
|
+
globalBestParams = [...p.position];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await onProgress({
|
|
75
|
+
step: i,
|
|
76
|
+
totalSteps: steps,
|
|
77
|
+
bestScore: globalBestScore,
|
|
78
|
+
bestParams: globalBestParams,
|
|
79
|
+
swarm: swarm // Expose for 3D visualizers (Radar scatter generation)
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { bestParams: globalBestParams, bestScore: globalBestScore };
|
|
84
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulated Annealing (Derivative-Free Stochastic Optimization)
|
|
3
|
+
* Maximizes the evaluate function payload.
|
|
4
|
+
*
|
|
5
|
+
* @param {Function} evaluate - function(paramsArray) => Number (score to maximize)
|
|
6
|
+
* @param {Array<Array<Number>>} bounds - [[min1, max1], [min2, max2], ...] bounds per dimension
|
|
7
|
+
* @param {Object} options - { steps: 100, initialTemp: 100, finalTemp: 0.1, onProgress: async (state) => {} }
|
|
8
|
+
* @returns {Promise<{bestParams: Array<Number>, bestScore: Number}>}
|
|
9
|
+
*/
|
|
10
|
+
export const simulatedAnnealing = async (evaluate, bounds, options = {}) => {
|
|
11
|
+
const steps = options.steps || 100;
|
|
12
|
+
const initialTemp = options.initialTemp || 100.0;
|
|
13
|
+
const finalTemp = options.finalTemp || 0.1;
|
|
14
|
+
const onProgress = options.onProgress || (async () => {});
|
|
15
|
+
|
|
16
|
+
let currentParams = bounds.map(b => (b[0] + b[1]) / 2);
|
|
17
|
+
let currentScore = evaluate(currentParams);
|
|
18
|
+
|
|
19
|
+
let bestParams = [...currentParams];
|
|
20
|
+
let bestScore = currentScore;
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < steps; i++) {
|
|
23
|
+
let temp = initialTemp * Math.pow((finalTemp / initialTemp), (i / steps));
|
|
24
|
+
let noiseScale = temp / initialTemp; // Gradually reduce the noise field radius
|
|
25
|
+
|
|
26
|
+
let candidateParams = currentParams.map((p, idx) => {
|
|
27
|
+
let b = bounds[idx];
|
|
28
|
+
let range = b[1] - b[0];
|
|
29
|
+
let nv = p + (Math.random() * 2 - 1) * (range * 0.2) * noiseScale;
|
|
30
|
+
return Math.max(b[0], Math.min(b[1], nv)); // enforce bounds constraint
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
let candidateScore = evaluate(candidateParams);
|
|
34
|
+
|
|
35
|
+
// Acceptance criteria (accept worse scores with diminishing probability to escape local maxima)
|
|
36
|
+
if (candidateScore > currentScore || Math.random() < Math.exp((candidateScore - currentScore) / temp)) {
|
|
37
|
+
currentParams = candidateParams;
|
|
38
|
+
currentScore = candidateScore;
|
|
39
|
+
|
|
40
|
+
if (candidateScore > bestScore) {
|
|
41
|
+
bestScore = candidateScore;
|
|
42
|
+
bestParams = [...currentParams];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await onProgress({
|
|
47
|
+
step: i,
|
|
48
|
+
totalSteps: steps,
|
|
49
|
+
score: currentScore,
|
|
50
|
+
bestScore: bestScore,
|
|
51
|
+
params: currentParams,
|
|
52
|
+
bestParams: bestParams
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { bestParams, bestScore };
|
|
57
|
+
};
|