@anonympins/fingerprint 0.4.2 → 0.4.3
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/CHANGELOG.md +17 -0
- package/README.md +66 -60
- package/package.json +1 -1
- package/src/js/fingerprint.js +10 -2
- package/src/js/pow.solver.js +531 -497
- package/src/js/problem-manager.js +24 -0
- package/src/js/tests/fingerprint.test.js +2351 -2319
- package/src/js/tests/pow.solver.test.js +233 -197
- package/src/js/tests/problem-manager.test.js +358 -322
- package/src/php/FingerprintEngine.php +10 -2
- package/src/php/Optimization/FunctionRegistry.php +63 -62
- package/src/php/Optimization/OptimizationOperators.php +401 -304
- package/src/php/ProblemManager.php +29 -0
- package/src/php/Tests/FingerprintEngineTest.php +330 -299
- package/src/php/Tests/ProblemManagerTest.php +376 -296
- package/src/php/Tests/RequestUtilsTest.php +256 -253
- package/src/php/Tests/problems.config.json +3 -3
- package/src/php/Utils/RequestUtils.php +1 -1
|
@@ -1,323 +1,359 @@
|
|
|
1
|
-
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
|
2
|
-
import {promises as fs} from 'node:fs';
|
|
3
|
-
import {__internal as problemManagerInternal, getProblemManager} from '../problem-manager.js';
|
|
4
|
-
import {Optimization} from '../library.js';
|
|
5
|
-
|
|
6
|
-
// Mock the in-memory store for testing
|
|
7
|
-
const inMemoryStore = {
|
|
8
|
-
_map: new Map(),
|
|
9
|
-
async get(key) { return this._map.get(key); },
|
|
10
|
-
async set(key, value, ttl) { this._map.set(key, value); },
|
|
11
|
-
async has(key) { return this._map.has(key); },
|
|
12
|
-
async delete(key) { this._map.delete(key); },
|
|
13
|
-
clear() { this._map.clear(); }
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// Mock the 'fs' module
|
|
17
|
-
vi.mock('node:fs', async () => {
|
|
18
|
-
const actualFs = await vi.importActual('node:fs');
|
|
19
|
-
return {
|
|
20
|
-
...actualFs, // Import all actual functions first
|
|
21
|
-
promises: {
|
|
22
|
-
...actualFs.promises, // Spread the actual promises implementation
|
|
23
|
-
readFile: vi.fn()
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// Mock the specific utility function used for energy calculation
|
|
29
|
-
vi.spyOn(Optimization.Utils, 'evaluatePathDistance').mockImplementation(path => {
|
|
30
|
-
// For tests, the energy is the sum of the IDs in the solution path.
|
|
31
|
-
return path.reduce((sum, val) => sum + (val.id || 0), 0);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe('ProblemManager', () => {
|
|
35
|
-
let manager;
|
|
36
|
-
let mockConfig;
|
|
37
|
-
const configPath = 'fake/path.json';
|
|
38
|
-
const store = inMemoryStore;
|
|
39
|
-
|
|
40
|
-
beforeEach(async () => {
|
|
41
|
-
// Reset mocks before each test
|
|
42
|
-
// This also resets the singleton instance of the problem manager.
|
|
43
|
-
problemManagerInternal.resetManager();
|
|
44
|
-
|
|
45
|
-
vi.clearAllMocks();
|
|
46
|
-
store.clear();
|
|
47
|
-
|
|
48
|
-
// Default mock config for most tests
|
|
49
|
-
mockConfig = [
|
|
50
|
-
{
|
|
51
|
-
"id": "tsp_10_cities",
|
|
52
|
-
"workUnit": {
|
|
53
|
-
"type": "simulated_annealing_iterations",
|
|
54
|
-
"scoreFunction": "tsp.calculateEnergy", // Add the score function
|
|
55
|
-
"baseIterations": 1000, // Lower value for testing
|
|
56
|
-
"initialSolutionSource": "points", // Specify where to get the initial solution from
|
|
57
|
-
"scalingFactor": 2.0
|
|
58
|
-
},
|
|
59
|
-
"payload": {
|
|
60
|
-
// In tests, we use a more generic 'points' to match the implementation
|
|
61
|
-
// of _ensureInitialSolution
|
|
62
|
-
"points": {
|
|
63
|
-
"$init": "generate:randomPoints",
|
|
64
|
-
"params": { "count": 10 }
|
|
65
|
-
},
|
|
66
|
-
"options": { "initialTemperature": 1000, "coolingRate": 0.99 }
|
|
67
|
-
},
|
|
68
|
-
"state": { "bestSolution": null, "bestEnergy": "Infinity" }
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"id": "portfolio_5_assets",
|
|
72
|
-
"workUnit": {
|
|
73
|
-
"type": "genetic_algorithm_generations",
|
|
74
|
-
"baseGenerations": 10, // Lower value for testing
|
|
75
|
-
"scalingFactor": 1.5
|
|
76
|
-
},
|
|
77
|
-
"payload": {
|
|
78
|
-
"assets": {
|
|
79
|
-
"$init": "generate:randomAssets",
|
|
80
|
-
"params": { "count": 5 }
|
|
81
|
-
},
|
|
82
|
-
"maxVolatility": 0.25
|
|
83
|
-
},
|
|
84
|
-
"state": { "population": null }
|
|
85
|
-
}
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
// Mock readFile to return our config. This needs to be in the top-level
|
|
89
|
-
// beforeEach to apply to all test suites within this describe block.
|
|
90
|
-
fs.readFile.mockResolvedValue(JSON.stringify(mockConfig));
|
|
91
|
-
manager = await getProblemManager({ configPath }, store);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe('Initialization and Loading', () => {
|
|
95
|
-
it('should load and parse problems from the config file', async () => {
|
|
96
|
-
expect(fs.readFile).toHaveBeenCalledWith(configPath, 'utf-8');
|
|
97
|
-
expect(manager.problems.length).toBe(mockConfig.length);
|
|
98
|
-
expect(manager.problems[0].id).toBe('tsp_10_cities');
|
|
99
|
-
// Check if initial state was saved to the store
|
|
100
|
-
const storedState = await store.get('problem-state:tsp_10_cities');
|
|
101
|
-
expect(storedState).toEqual(mockConfig[0].state);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('should dynamically generate cities and assets', async () => {
|
|
105
|
-
const tspProblem = manager.problems.find(p => p.id === 'tsp_10_cities');
|
|
106
|
-
const portfolioProblem = manager.problems.find(p => p.id === 'portfolio_5_assets');
|
|
107
|
-
|
|
108
|
-
expect(tspProblem.payload.points.length).toBe(10);
|
|
109
|
-
expect(tspProblem.payload.points[0]).toHaveProperty('x');
|
|
110
|
-
expect(tspProblem.payload.points[0]).toHaveProperty('y');
|
|
111
|
-
|
|
112
|
-
expect(Array.isArray(portfolioProblem.payload.assets)).toBe(true);
|
|
113
|
-
expect(portfolioProblem.payload.assets.length).toBe(5);
|
|
114
|
-
expect(portfolioProblem.payload.assets[0]).toHaveProperty('expectedReturn');
|
|
115
|
-
expect(portfolioProblem.payload.assets[0]).toHaveProperty('volatility');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should handle file read errors gracefully', async () => {
|
|
119
|
-
fs.readFile.mockRejectedValue(new Error('File not found'));
|
|
120
|
-
const manager = await getProblemManager({ configPath: 'nonexistent.json' }, store);
|
|
121
|
-
expect(manager.problems).toEqual([]);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe('dispatchWork', () => {
|
|
126
|
-
it('should return null if no problems are loaded', async () => {
|
|
127
|
-
fs.readFile.mockRejectedValue(new Error('File read error'));
|
|
128
|
-
const manager = await getProblemManager({ configPath: 'bad.json' }, store);
|
|
129
|
-
expect(manager.dispatchWork(0.5)).toBeNull();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should cycle through problems in a round-robin fashion', async () => {
|
|
133
|
-
const work1 = manager.dispatchWork(0.1);
|
|
134
|
-
const work2 = manager.dispatchWork(0.1);
|
|
135
|
-
const work3 = manager.dispatchWork(0.1);
|
|
136
|
-
|
|
137
|
-
expect(work1.problemId).toBe('tsp_10_cities');
|
|
138
|
-
expect(work2.problemId).toBe('portfolio_5_assets');
|
|
139
|
-
expect(work3.problemId).toBe('tsp_10_cities'); // Cycled back
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('should calculate exponential difficulty based on suspicionFactor', async () => {
|
|
143
|
-
const suspicionFactor = 0.5;
|
|
144
|
-
|
|
145
|
-
// Test for TSP problem
|
|
146
|
-
const { task: tspTask } = manager.dispatchWork(suspicionFactor);
|
|
147
|
-
|
|
148
|
-
// The base is now the MAX of the config and the hardcoded minimum (15000)
|
|
149
|
-
const expectedBaseIterations = Math.max(15000, mockConfig[0].workUnit.baseIterations);
|
|
150
|
-
const expectedTspIterations = Math.floor(expectedBaseIterations * Math.pow(2.0, suspicionFactor));
|
|
151
|
-
expect(tspTask.iterations).toBe(expectedTspIterations);
|
|
152
|
-
|
|
153
|
-
// Test for Portfolio problem
|
|
154
|
-
const { task: portfolioTask } = manager.dispatchWork(suspicionFactor);
|
|
155
|
-
const expectedBaseGenerations = Math.max(50, mockConfig[1].workUnit.baseGenerations);
|
|
156
|
-
const expectedPortfolioGenerations = Math.floor(expectedBaseGenerations * Math.pow(1.5, suspicionFactor));
|
|
157
|
-
expect(portfolioTask.generations).toBe(expectedPortfolioGenerations);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
describe('integrateSolution', () => {
|
|
162
|
-
it('should update the best solution if a better one is provided', async () => {
|
|
163
|
-
const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
|
|
164
|
-
problem.state.bestEnergy = 1000; // Set a high initial energy
|
|
165
|
-
|
|
166
|
-
const newBetterSolution = { solution: [{ id: 1 }], energy: 500 };
|
|
167
|
-
// Le mock de `evaluatePathDistance` va retourner 1 (la somme des IDs).
|
|
168
|
-
// C'est cette valeur qui doit être stockée, pas 500.
|
|
169
|
-
const expectedRecalculatedEnergy = 1;
|
|
170
|
-
|
|
171
|
-
await manager.integrateSolution('tsp_10_cities', newBetterSolution);
|
|
172
|
-
|
|
173
|
-
expect(problem.state.bestSolution).toEqual(newBetterSolution.solution);
|
|
174
|
-
expect(problem.state.bestEnergy).toBe(expectedRecalculatedEnergy); // Vérifier le score recalculé
|
|
175
|
-
const storedState = await store.get('problem-state:tsp_10_cities');
|
|
176
|
-
expect(storedState.bestEnergy).toBe(expectedRecalculatedEnergy);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('should not update the best solution if a worse one is provided', async () => {
|
|
180
|
-
const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
|
|
181
|
-
const initialSolution = [{ id: 10 }];
|
|
182
|
-
problem.state.bestSolution = initialSolution;
|
|
183
|
-
problem.state.bestEnergy = 10; // Le score initial est 10 (calculé à partir de l'ID)
|
|
184
|
-
|
|
185
|
-
const newWorseSolution = { solution: [{ id: 20 }], energy: 200 };
|
|
186
|
-
// Le mock de `evaluatePathDistance` va retourner 20.
|
|
187
|
-
// Comme 20 n'est pas meilleur que 10, la solution ne doit pas changer.
|
|
188
|
-
|
|
189
|
-
await manager.integrateSolution('tsp_10_cities', newWorseSolution);
|
|
190
|
-
|
|
191
|
-
expect(problem.state.bestSolution).toEqual(initialSolution);
|
|
192
|
-
expect(problem.state.bestEnergy).toBe(10);
|
|
193
|
-
const storedState = await store.get('problem-state:tsp_10_cities');
|
|
194
|
-
expect(storedState.bestEnergy).toBe(10);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('should
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
"
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
expect(problem.state.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
problem.state.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
expect(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
1
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
|
2
|
+
import {promises as fs} from 'node:fs';
|
|
3
|
+
import {__internal as problemManagerInternal, getProblemManager} from '../problem-manager.js';
|
|
4
|
+
import {Optimization} from '../library.js';
|
|
5
|
+
|
|
6
|
+
// Mock the in-memory store for testing
|
|
7
|
+
const inMemoryStore = {
|
|
8
|
+
_map: new Map(),
|
|
9
|
+
async get(key) { return this._map.get(key); },
|
|
10
|
+
async set(key, value, ttl) { this._map.set(key, value); },
|
|
11
|
+
async has(key) { return this._map.has(key); },
|
|
12
|
+
async delete(key) { this._map.delete(key); },
|
|
13
|
+
clear() { this._map.clear(); }
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Mock the 'fs' module
|
|
17
|
+
vi.mock('node:fs', async () => {
|
|
18
|
+
const actualFs = await vi.importActual('node:fs');
|
|
19
|
+
return {
|
|
20
|
+
...actualFs, // Import all actual functions first
|
|
21
|
+
promises: {
|
|
22
|
+
...actualFs.promises, // Spread the actual promises implementation
|
|
23
|
+
readFile: vi.fn()
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Mock the specific utility function used for energy calculation
|
|
29
|
+
vi.spyOn(Optimization.Utils, 'evaluatePathDistance').mockImplementation(path => {
|
|
30
|
+
// For tests, the energy is the sum of the IDs in the solution path.
|
|
31
|
+
return path.reduce((sum, val) => sum + (val.id || 0), 0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('ProblemManager', () => {
|
|
35
|
+
let manager;
|
|
36
|
+
let mockConfig;
|
|
37
|
+
const configPath = 'fake/path.json';
|
|
38
|
+
const store = inMemoryStore;
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
// Reset mocks before each test
|
|
42
|
+
// This also resets the singleton instance of the problem manager.
|
|
43
|
+
problemManagerInternal.resetManager();
|
|
44
|
+
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
store.clear();
|
|
47
|
+
|
|
48
|
+
// Default mock config for most tests
|
|
49
|
+
mockConfig = [
|
|
50
|
+
{
|
|
51
|
+
"id": "tsp_10_cities",
|
|
52
|
+
"workUnit": {
|
|
53
|
+
"type": "simulated_annealing_iterations",
|
|
54
|
+
"scoreFunction": "tsp.calculateEnergy", // Add the score function
|
|
55
|
+
"baseIterations": 1000, // Lower value for testing
|
|
56
|
+
"initialSolutionSource": "points", // Specify where to get the initial solution from
|
|
57
|
+
"scalingFactor": 2.0
|
|
58
|
+
},
|
|
59
|
+
"payload": {
|
|
60
|
+
// In tests, we use a more generic 'points' to match the implementation
|
|
61
|
+
// of _ensureInitialSolution
|
|
62
|
+
"points": {
|
|
63
|
+
"$init": "generate:randomPoints",
|
|
64
|
+
"params": { "count": 10 }
|
|
65
|
+
},
|
|
66
|
+
"options": { "initialTemperature": 1000, "coolingRate": 0.99 }
|
|
67
|
+
},
|
|
68
|
+
"state": { "bestSolution": null, "bestEnergy": "Infinity" }
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "portfolio_5_assets",
|
|
72
|
+
"workUnit": {
|
|
73
|
+
"type": "genetic_algorithm_generations",
|
|
74
|
+
"baseGenerations": 10, // Lower value for testing
|
|
75
|
+
"scalingFactor": 1.5
|
|
76
|
+
},
|
|
77
|
+
"payload": {
|
|
78
|
+
"assets": {
|
|
79
|
+
"$init": "generate:randomAssets",
|
|
80
|
+
"params": { "count": 5 }
|
|
81
|
+
},
|
|
82
|
+
"maxVolatility": 0.25
|
|
83
|
+
},
|
|
84
|
+
"state": { "population": null }
|
|
85
|
+
}
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// Mock readFile to return our config. This needs to be in the top-level
|
|
89
|
+
// beforeEach to apply to all test suites within this describe block.
|
|
90
|
+
fs.readFile.mockResolvedValue(JSON.stringify(mockConfig));
|
|
91
|
+
manager = await getProblemManager({ configPath }, store);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('Initialization and Loading', () => {
|
|
95
|
+
it('should load and parse problems from the config file', async () => {
|
|
96
|
+
expect(fs.readFile).toHaveBeenCalledWith(configPath, 'utf-8');
|
|
97
|
+
expect(manager.problems.length).toBe(mockConfig.length);
|
|
98
|
+
expect(manager.problems[0].id).toBe('tsp_10_cities');
|
|
99
|
+
// Check if initial state was saved to the store
|
|
100
|
+
const storedState = await store.get('problem-state:tsp_10_cities');
|
|
101
|
+
expect(storedState).toEqual(mockConfig[0].state);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should dynamically generate cities and assets', async () => {
|
|
105
|
+
const tspProblem = manager.problems.find(p => p.id === 'tsp_10_cities');
|
|
106
|
+
const portfolioProblem = manager.problems.find(p => p.id === 'portfolio_5_assets');
|
|
107
|
+
|
|
108
|
+
expect(tspProblem.payload.points.length).toBe(10);
|
|
109
|
+
expect(tspProblem.payload.points[0]).toHaveProperty('x');
|
|
110
|
+
expect(tspProblem.payload.points[0]).toHaveProperty('y');
|
|
111
|
+
|
|
112
|
+
expect(Array.isArray(portfolioProblem.payload.assets)).toBe(true);
|
|
113
|
+
expect(portfolioProblem.payload.assets.length).toBe(5);
|
|
114
|
+
expect(portfolioProblem.payload.assets[0]).toHaveProperty('expectedReturn');
|
|
115
|
+
expect(portfolioProblem.payload.assets[0]).toHaveProperty('volatility');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle file read errors gracefully', async () => {
|
|
119
|
+
fs.readFile.mockRejectedValue(new Error('File not found'));
|
|
120
|
+
const manager = await getProblemManager({ configPath: 'nonexistent.json' }, store);
|
|
121
|
+
expect(manager.problems).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('dispatchWork', () => {
|
|
126
|
+
it('should return null if no problems are loaded', async () => {
|
|
127
|
+
fs.readFile.mockRejectedValue(new Error('File read error'));
|
|
128
|
+
const manager = await getProblemManager({ configPath: 'bad.json' }, store);
|
|
129
|
+
expect(manager.dispatchWork(0.5)).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should cycle through problems in a round-robin fashion', async () => {
|
|
133
|
+
const work1 = manager.dispatchWork(0.1);
|
|
134
|
+
const work2 = manager.dispatchWork(0.1);
|
|
135
|
+
const work3 = manager.dispatchWork(0.1);
|
|
136
|
+
|
|
137
|
+
expect(work1.problemId).toBe('tsp_10_cities');
|
|
138
|
+
expect(work2.problemId).toBe('portfolio_5_assets');
|
|
139
|
+
expect(work3.problemId).toBe('tsp_10_cities'); // Cycled back
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should calculate exponential difficulty based on suspicionFactor', async () => {
|
|
143
|
+
const suspicionFactor = 0.5;
|
|
144
|
+
|
|
145
|
+
// Test for TSP problem
|
|
146
|
+
const { task: tspTask } = manager.dispatchWork(suspicionFactor);
|
|
147
|
+
|
|
148
|
+
// The base is now the MAX of the config and the hardcoded minimum (15000)
|
|
149
|
+
const expectedBaseIterations = Math.max(15000, mockConfig[0].workUnit.baseIterations);
|
|
150
|
+
const expectedTspIterations = Math.floor(expectedBaseIterations * Math.pow(2.0, suspicionFactor));
|
|
151
|
+
expect(tspTask.iterations).toBe(expectedTspIterations);
|
|
152
|
+
|
|
153
|
+
// Test for Portfolio problem
|
|
154
|
+
const { task: portfolioTask } = manager.dispatchWork(suspicionFactor);
|
|
155
|
+
const expectedBaseGenerations = Math.max(50, mockConfig[1].workUnit.baseGenerations);
|
|
156
|
+
const expectedPortfolioGenerations = Math.floor(expectedBaseGenerations * Math.pow(1.5, suspicionFactor));
|
|
157
|
+
expect(portfolioTask.generations).toBe(expectedPortfolioGenerations);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('integrateSolution', () => {
|
|
162
|
+
it('should update the best solution if a better one is provided', async () => {
|
|
163
|
+
const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
|
|
164
|
+
problem.state.bestEnergy = 1000; // Set a high initial energy
|
|
165
|
+
|
|
166
|
+
const newBetterSolution = { solution: [{ id: 1 }], energy: 500 };
|
|
167
|
+
// Le mock de `evaluatePathDistance` va retourner 1 (la somme des IDs).
|
|
168
|
+
// C'est cette valeur qui doit être stockée, pas 500.
|
|
169
|
+
const expectedRecalculatedEnergy = 1;
|
|
170
|
+
|
|
171
|
+
await manager.integrateSolution('tsp_10_cities', newBetterSolution);
|
|
172
|
+
|
|
173
|
+
expect(problem.state.bestSolution).toEqual(newBetterSolution.solution);
|
|
174
|
+
expect(problem.state.bestEnergy).toBe(expectedRecalculatedEnergy); // Vérifier le score recalculé
|
|
175
|
+
const storedState = await store.get('problem-state:tsp_10_cities');
|
|
176
|
+
expect(storedState.bestEnergy).toBe(expectedRecalculatedEnergy);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should not update the best solution if a worse one is provided', async () => {
|
|
180
|
+
const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
|
|
181
|
+
const initialSolution = [{ id: 10 }];
|
|
182
|
+
problem.state.bestSolution = initialSolution;
|
|
183
|
+
problem.state.bestEnergy = 10; // Le score initial est 10 (calculé à partir de l'ID)
|
|
184
|
+
|
|
185
|
+
const newWorseSolution = { solution: [{ id: 20 }], energy: 200 };
|
|
186
|
+
// Le mock de `evaluatePathDistance` va retourner 20.
|
|
187
|
+
// Comme 20 n'est pas meilleur que 10, la solution ne doit pas changer.
|
|
188
|
+
|
|
189
|
+
await manager.integrateSolution('tsp_10_cities', newWorseSolution);
|
|
190
|
+
|
|
191
|
+
expect(problem.state.bestSolution).toEqual(initialSolution);
|
|
192
|
+
expect(problem.state.bestEnergy).toBe(10);
|
|
193
|
+
const storedState = await store.get('problem-state:tsp_10_cities');
|
|
194
|
+
expect(storedState.bestEnergy).toBe(10);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should update the best facility location solution with recalculated energy', async () => {
|
|
198
|
+
problemManagerInternal.resetManager();
|
|
199
|
+
mockConfig.push({
|
|
200
|
+
"id": "facility_location_test",
|
|
201
|
+
"workUnit": {
|
|
202
|
+
"type": "simulated_annealing_iterations",
|
|
203
|
+
"scoreFunction": "facility.calculateEnergy"
|
|
204
|
+
},
|
|
205
|
+
"payload": {
|
|
206
|
+
"customers": [{"x": 100, "y": 100}, {"x": 200, "y": 200}],
|
|
207
|
+
"options": {"fixedCostPerFacility": 1500}
|
|
208
|
+
},
|
|
209
|
+
"state": { "bestSolution": null, "bestEnergy": "Infinity" }
|
|
210
|
+
});
|
|
211
|
+
fs.readFile.mockResolvedValue(JSON.stringify(mockConfig));
|
|
212
|
+
manager = await getProblemManager(configPath, store);
|
|
213
|
+
|
|
214
|
+
const problem = manager.problems.find(p => p.id === 'facility_location_test');
|
|
215
|
+
problem.state.bestEnergy = 99999.0;
|
|
216
|
+
|
|
217
|
+
const newBetterSolution = {
|
|
218
|
+
solution: [
|
|
219
|
+
{ x: 100, y: 100 },
|
|
220
|
+
{ x: 200, y: 200 }
|
|
221
|
+
],
|
|
222
|
+
energy: 1500.0
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
await manager.integrateSolution('facility_location_test', newBetterSolution);
|
|
226
|
+
|
|
227
|
+
expect(problem.state.bestSolution).toEqual(newBetterSolution.solution);
|
|
228
|
+
expect(problem.state.bestEnergy).toBeLessThan(99999.0);
|
|
229
|
+
const storedState = await store.get('problem-state:facility_location_test');
|
|
230
|
+
expect(storedState.bestEnergy).toBeLessThan(99999.0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should handle solutions for non-existent problems gracefully', async () => {
|
|
234
|
+
// This should not throw an error
|
|
235
|
+
await manager.integrateSolution('non_existent_problem', { energy: 1 });
|
|
236
|
+
expect(await store.has('problem-state:non_existent_problem')).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should integrate a new Pareto front for multi-objective problems', async () => {
|
|
240
|
+
// **LA CORRECTION** : Réinitialiser le singleton avant de modifier la configuration.
|
|
241
|
+
problemManagerInternal.resetManager();
|
|
242
|
+
|
|
243
|
+
// Add a multi-objective problem to the config for this test
|
|
244
|
+
mockConfig.push({
|
|
245
|
+
"id": "multi_obj_test",
|
|
246
|
+
"workUnit": { "type": "multi_objective_genetic_algorithm" },
|
|
247
|
+
"state": { "paretoFront": [{ solution: 'A', objectives: [10, 20] }] }
|
|
248
|
+
});
|
|
249
|
+
fs.readFile.mockResolvedValue(JSON.stringify(mockConfig));
|
|
250
|
+
manager = await getProblemManager(configPath, store);
|
|
251
|
+
|
|
252
|
+
const problem = manager.problems.find(p => p.id === 'multi_obj_test');
|
|
253
|
+
|
|
254
|
+
// The new front contains a solution that dominates the old one.
|
|
255
|
+
const newFrontFromClient = [{ solution: 'B', objectives: [5, 15] }];
|
|
256
|
+
|
|
257
|
+
await manager.integrateSolution('multi_obj_test', { paretoFront: newFrontFromClient });
|
|
258
|
+
|
|
259
|
+
// The new front should contain only the new, dominant solution.
|
|
260
|
+
// We check the content instead of object equality for robustness.
|
|
261
|
+
expect(problem.state.paretoFront).toHaveLength(1);
|
|
262
|
+
expect(problem.state.paretoFront[0]).toEqual({ solution: 'B', objectives: [5, 15] });
|
|
263
|
+
expect(problem.state.lastUpdate).toBeDefined();
|
|
264
|
+
const storedState = await store.get('problem-state:multi_obj_test');
|
|
265
|
+
expect(storedState.paretoFront[0].solution).toBe('B');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('getBestSolutions', () => {
|
|
270
|
+
beforeEach(async () => {
|
|
271
|
+
// Mock the initial solution generation to be predictable
|
|
272
|
+
mockConfig[0].payload.points = [
|
|
273
|
+
{ id: 1, x: 10, y: 10 },
|
|
274
|
+
{ id: 2, x: 20, y: 20 },
|
|
275
|
+
{ id: 3, x: 30, y: 30 }
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
// The function that is actually called is `Optimization.Utils.evaluatePathDistance`.
|
|
279
|
+
// We need to mock its return value to be predictable for the test.
|
|
280
|
+
// The mock implementation `solution.reduce(...)` will sum the `id` properties.
|
|
281
|
+
// For the points above, the sum is 1 + 2 + 3 = 6.
|
|
282
|
+
vi.spyOn(Optimization.Utils, 'evaluatePathDistance').mockReturnValue(6);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should generate an initial solution if none exists', async () => {
|
|
286
|
+
const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
|
|
287
|
+
problem.state.bestSolution = null; // Ensure no solution exists
|
|
288
|
+
|
|
289
|
+
await manager.getBestSolutions('tsp_10_cities');
|
|
290
|
+
|
|
291
|
+
expect(problem.state.bestSolution).not.toBeNull();
|
|
292
|
+
expect(problem.state.bestEnergy).toBe(6); // Mocked energy value
|
|
293
|
+
const storedState = await store.get('problem-state:tsp_10_cities');
|
|
294
|
+
expect(storedState.bestEnergy).toBe(6);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should return the best solution for a specific problem ID', async () => {
|
|
298
|
+
const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
|
|
299
|
+
problem.state.bestSolution = [{ id: 'A' }];
|
|
300
|
+
problem.state.bestEnergy = 123;
|
|
301
|
+
problem.state.lastUpdate = '2023-01-01T00:00:00.000Z';
|
|
302
|
+
|
|
303
|
+
const result = await manager.getBestSolutions('tsp_10_cities');
|
|
304
|
+
|
|
305
|
+
expect(result).toEqual({
|
|
306
|
+
id: 'tsp_10_cities',
|
|
307
|
+
solution: [{ id: 'A' }],
|
|
308
|
+
score: 123,
|
|
309
|
+
lastUpdate: '2023-01-01T00:00:00.000Z'
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should return an array of all best solutions if no ID is provided', async () => {
|
|
314
|
+
const problem1 = manager.problems.find(p => p.id === 'tsp_10_cities');
|
|
315
|
+
problem1.state.bestSolution = [{ id: 'A' }];
|
|
316
|
+
problem1.state.bestEnergy = 123;
|
|
317
|
+
|
|
318
|
+
// The portfolio problem has no solution, so it should be filtered out
|
|
319
|
+
const results = await manager.getBestSolutions();
|
|
320
|
+
|
|
321
|
+
expect(Array.isArray(results)).toBe(true);
|
|
322
|
+
expect(results.length).toBe(1);
|
|
323
|
+
expect(results[0].id).toBe('tsp_10_cities');
|
|
324
|
+
expect(results[0].score).toBe(123);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should return null if a non-existent problem ID is requested', async () => {
|
|
328
|
+
const result = await manager.getBestSolutions('non_existent_problem');
|
|
329
|
+
expect(result).toBeNull();
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('updateProblemPayload', () => {
|
|
334
|
+
it('should update the payload of a specific problem', async () => {
|
|
335
|
+
const newPayload = {
|
|
336
|
+
"points": [{ "x": 0, "y": 0 }],
|
|
337
|
+
"options": { "initialTemperature": 500 }
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const success = await manager.updateProblemPayload('tsp_10_cities', newPayload);
|
|
341
|
+
expect(success).toBe(true);
|
|
342
|
+
|
|
343
|
+
const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
|
|
344
|
+
expect(problem.payload).toEqual(newPayload);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should reset the state of the updated problem', async () => {
|
|
348
|
+
const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
|
|
349
|
+
problem.state.bestSolution = [{ id: 1 }];
|
|
350
|
+
problem.state.bestEnergy = 10;
|
|
351
|
+
|
|
352
|
+
await manager.updateProblemPayload('tsp_10_cities', { new: 'payload' });
|
|
353
|
+
expect(problem.state.bestSolution).toBeNull();
|
|
354
|
+
expect(problem.state.bestEnergy).toBe("Infinity");
|
|
355
|
+
const storedState = await store.get('problem-state:tsp_10_cities');
|
|
356
|
+
expect(storedState.bestEnergy).toBe("Infinity");
|
|
357
|
+
});
|
|
358
|
+
});
|
|
323
359
|
});
|