@anonympins/fingerprint 0.2.3 → 0.3.0

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/library.js CHANGED
@@ -1,1694 +1,1727 @@
1
- /**
2
- * @file @/library.js
3
- * @description Une bibliothèque d'outils basés sur le principe fondamental de la dichotomie (division en deux).
4
- * Inclut des algorithmes pour les tableaux triés, des structures de données et des solveurs de problèmes conceptuels.
5
- *
6
- * @template T
7
- * @callback Comparator
8
- * @param {T} element - L'élément du tableau.
9
- * @param {any} target - La valeur cible.
10
- * @returns {number} -1 si element < target, 0 si element == target, 1 si element > target.
11
- *
12
- */
13
-
14
- import { Worker } from "worker_threads";
15
- import os from "os";
16
-
17
- const Optimization = {
18
- // eslint-disable-line no-unused-vars
19
- /**
20
- * Trouve une bonne solution à un problème d'optimisation en utilisant le Recuit Simulé.
21
- * Cet algorithme est efficace pour trouver un optimum global dans un grand espace de recherche
22
- * avec de nombreux optima locaux (plusieurs "pics" ou "vallées").
23
- * @template TSolution - Le type de la solution (peut être un nombre, un tableau, un objet...).
24
- * @param {TSolution} initialSolution - Le point de départ de la recherche.
25
- * @param {function(TSolution): number} evaluator - Fonction qui évalue une solution. L'objectif est de minimiser ce score.
26
- * @param {function(TSolution): TSolution} neighbor - Fonction qui génère une solution "voisine" aléatoire.
27
- * @param {number} [initialTemperature=1000] - La température de départ.
28
- * @param {number} [coolingRate=0.995] - Le taux de refroidissement (proche de 1).
29
- * @param {number} [maxIterations=10000] - Le nombre total d'itérations.
30
- * @returns {{solution: TSolution, energy: number}} Le meilleur couple solution/score trouvé.
31
- */
32
- simulatedAnnealing(
33
- initialSolution,
34
- evaluator,
35
- neighbor,
36
- initialTemperature = 1000,
37
- coolingRate = 0.995,
38
- maxIterations = 10000,
39
- ) {
40
- let currentSolution = initialSolution;
41
- let currentEnergy = evaluator(currentSolution);
42
-
43
- let bestSolution = currentSolution;
44
- let bestEnergy = currentEnergy;
45
-
46
- let temperature = initialTemperature;
47
-
48
- for (let i = 0; i < maxIterations; i++) {
49
- const newSolution = neighbor(currentSolution);
50
- const newEnergy = evaluator(newSolution);
51
-
52
- // Calcule la probabilité d'accepter une moins bonne solution.
53
- const acceptanceProbability = Math.exp(
54
- (currentEnergy - newEnergy) / temperature,
55
- );
56
-
57
- // Décide si on se déplace vers la nouvelle solution.
58
- if (newEnergy < currentEnergy || Math.random() < acceptanceProbability) {
59
- currentSolution = newSolution;
60
- currentEnergy = newEnergy;
61
- }
62
-
63
- // Met à jour la meilleure solution trouvée jusqu'à présent.
64
- if (currentEnergy < bestEnergy) {
65
- bestSolution = currentSolution;
66
- bestEnergy = currentEnergy;
67
- }
68
-
69
- // Refroidit la température.
70
- temperature *= coolingRate;
71
- }
72
-
73
- return { solution: bestSolution, energy: bestEnergy };
74
- },
75
-
76
- /**
77
- * Résout un problème d'optimisation en utilisant un Algorithme Génétique.
78
- * Idéal pour les problèmes complexes où l'espace de recherche est vaste et non-linéaire.
79
- * @template TChromosome - Le type de la solution (un "chromosome").
80
- * @param {function(): TChromosome} createIndividual - Fonction pour créer un individu aléatoire.
81
- * @param {function(TChromosome): number} fitnessFunction - Évalue un individu. L'objectif est de MINIMISER ce score.
82
- * @param {function(TChromosome, TChromosome): TChromosome} crossover - Croise deux parents pour créer un enfant.
83
- * @param {function(TChromosome): TChromosome} mutate - Applique une mutation aléatoire à un individu.
84
- * @param {object} options - Options de l'algorithme.
85
- * @param {number} [options.populationSize=100] - Taille de la population.
86
- * @param {number} [options.generations=100] - Nombre de générations à simuler.
87
- * @param {number} [options.crossoverRate=0.8] - Probabilité de croisement.
88
- * @param {number} [options.mutationRate=0.1] - Probabilité de mutation.
89
- * @param {function} [options.selectionFunction] - Fonction de sélection des parents. Par défaut, un tournoi.
90
- * @param {boolean} [options.returnPopulation=false] - Si true, retourne la population finale au lieu du meilleur individu.
91
- * @returns {{solution: TChromosome, fitness: number}} Le meilleur individu trouvé.
92
- */
93
- geneticAlgorithm(
94
- createIndividual,
95
- fitnessFunction,
96
- crossover,
97
- mutate,
98
- options = {},
99
- ) {
100
- const populationSize = options.populationSize || 100;
101
- const generations = options.generations || 100;
102
- const crossoverRate =
103
- options.crossoverRate !== undefined ? options.crossoverRate : 0.8;
104
- const mutationRate =
105
- options.mutationRate !== undefined ? options.mutationRate : 0.1;
106
- const selectionFunction =
107
- options.selectionFunction ||
108
- this.Operators.createTournamentSelection({ size: 5 });
109
- const returnPopulation = options.returnPopulation || false;
110
-
111
- // 1. Initialisation
112
- // La population est un tableau d'objets { chromosome, fitness }
113
- // La fitness est calculée une seule fois par individu.
114
- let population = Array.from({ length: populationSize }, () => {
115
- const chromosome = createIndividual();
116
- return { chromosome, fitness: fitnessFunction(chromosome) };
117
- });
118
-
119
- // Trier la population initiale pour trouver le meilleur
120
- population.sort((a, b) => a.fitness - b.fitness);
121
- let bestOverall = population[0];
122
-
123
- // 2. Boucle des générations
124
- for (let gen = 0; gen < generations; gen++) {
125
- const newPopulation = [];
126
-
127
- // Élitisme : le meilleur individu de la génération précédente est conservé.
128
- // Il est déjà à l'index 0 grâce au tri à la fin de la boucle précédente.
129
- newPopulation.push(population[0]);
130
-
131
- while (newPopulation.length < populationSize) {
132
- // 3. Sélection
133
- const parent1 = selectionFunction(population);
134
- const parent2 = selectionFunction(population);
135
-
136
- let offspringChromosome;
137
- // 4. Croisement
138
- if (Math.random() < crossoverRate) {
139
- offspringChromosome = crossover(
140
- parent1.chromosome,
141
- parent2.chromosome,
142
- );
143
- } else {
144
- offspringChromosome = parent1.chromosome;
145
- }
146
-
147
- // 5. Mutation
148
- if (Math.random() < mutationRate) {
149
- offspringChromosome = mutate(offspringChromosome);
150
- }
151
-
152
- // S'assurer que les opérateurs ont bien retourné un individu
153
- if (offspringChromosome) {
154
- newPopulation.push({
155
- chromosome: offspringChromosome,
156
- fitness: fitnessFunction(offspringChromosome),
157
- });
158
- } else {
159
- // Si le croisement/mutation échoue, on réinsère un parent pour garder la taille de la population
160
- newPopulation.push(parent1);
161
- }
162
- }
163
-
164
- population = newPopulation;
165
-
166
- // Trier la nouvelle population pour la prochaine génération (élitisme) et la mise à jour du meilleur
167
- population.sort((a, b) => a.fitness - b.fitness);
168
-
169
- if (population[0].fitness < bestOverall.fitness) {
170
- bestOverall = population[0];
171
- }
172
- }
173
-
174
- if (returnPopulation) {
175
- return population;
176
- }
177
-
178
- return { solution: bestOverall.chromosome, fitness: bestOverall.fitness };
179
- },
180
-
181
- /**
182
- * Exécute un solveur stochastique plusieurs fois et retourne le meilleur résultat.
183
- * C'est une méta-heuristique pour augmenter la probabilité de trouver un optimum global
184
- * en échange d'un temps de calcul plus long.
185
- * @param {function(): {solution: any, energy?: number, fitness?: number}} solverFunction - Une fonction qui, lorsqu'elle est appelée, exécute un algorithme d'optimisation et retourne un objet résultat.
186
- * @param {number} numCycles - Le nombre de fois où exécuter le solveur.
187
- * @param {boolean} [logProgress=false] - Si true, affiche le score de chaque cycle dans la console.
188
- * @returns {{bestResult: object, stats: {scores: Array<number>, average: number, stdDev: number}}} Le meilleur résultat et des statistiques sur les exécutions.
189
- */
190
- runMultiple(solverFunction, numCycles, logProgress = false) {
191
- let bestResult = null;
192
- const allScores = [];
193
-
194
- for (let i = 0; i < numCycles; i++) {
195
- const currentResult = solverFunction();
196
-
197
- // Gère les résultats du Recuit Simulé (energy) et des Algorithmes Génétiques (fitness).
198
- // On suppose que pour les deux, un score plus bas est meilleur.
199
- const currentScore =
200
- currentResult.energy !== undefined
201
- ? currentResult.energy
202
- : currentResult.fitness;
203
- allScores.push(currentScore);
204
-
205
- if (logProgress) {
206
- console.log(
207
- ` -> Cycle ${i + 1}/${numCycles}: Score trouvé = ${currentScore.toFixed(2)}`,
208
- );
209
- }
210
-
211
- if (
212
- !bestResult ||
213
- currentScore <
214
- (bestResult.energy !== undefined
215
- ? bestResult.energy
216
- : bestResult.fitness)
217
- ) {
218
- bestResult = currentResult;
219
- }
220
- }
221
-
222
- // Calcul des statistiques
223
- const sum = allScores.reduce((a, b) => a + b, 0);
224
- const average = sum / numCycles;
225
- const variance =
226
- allScores.reduce((a, b) => a + Math.pow(b - average, 2), 0) / numCycles;
227
- const stdDev = Math.sqrt(variance);
228
-
229
- return {
230
- bestResult,
231
- stats: {
232
- scores: allScores,
233
- average: average,
234
- stdDev: stdDev,
235
- },
236
- };
237
- },
238
-
239
- /**
240
- * Trouve un minimum local d'une fonction en utilisant l'algorithme de Descente de Gradient.
241
- * Nécessite que la fonction soit différentiable et que son gradient soit connu.
242
- * @template TSolution - Le type de la solution (nombre ou tableau de nombres).
243
- * @param {TSolution} initialSolution - Le point de départ.
244
- * @param {function(TSolution): TSolution} gradientFunction - Fonction qui calcule le gradient au point donné.
245
- * @param {object} options - Options de l'algorithme.
246
- * @param {number} [options.learningRate=0.01] - Le "pas" de la descente.
247
- * @param {number} [options.maxIterations=1000] - Nombre d'itérations.
248
- * @param {number} [options.tolerance=1e-6] - Seuil pour arrêter si la solution ne change plus beaucoup.
249
- * @returns {TSolution} La solution (minimum local) trouvée.
250
- */
251
- gradientDescent(initialSolution, gradientFunction, options = {}) {
252
- const {
253
- learningRate = 0.01,
254
- maxIterations = 1000,
255
- tolerance = 1e-6,
256
- } = options;
257
-
258
- let currentSolution = Array.isArray(initialSolution)
259
- ? [...initialSolution]
260
- : initialSolution;
261
-
262
- for (let i = 0; i < maxIterations; i++) {
263
- const gradient = gradientFunction(currentSolution);
264
-
265
- if (Array.isArray(currentSolution)) {
266
- const prevSolution = [...currentSolution];
267
- for (let j = 0; j < currentSolution.length; j++) {
268
- currentSolution[j] -= learningRate * gradient[j];
269
- }
270
- const change = prevSolution.reduce(
271
- (sum, val, idx) => sum + Math.abs(val - currentSolution[idx]),
272
- 0,
273
- );
274
- if (change < tolerance) break;
275
- } else {
276
- // Cas d'une seule variable (nombre)
277
- const prevSolution = currentSolution;
278
- currentSolution -= learningRate * gradient;
279
- if (Math.abs(prevSolution - currentSolution) < tolerance) break;
280
- }
281
- }
282
- return currentSolution;
283
- },
284
-
285
- /**
286
- * Exécute un solveur stochastique plusieurs fois en parallèle en utilisant un pool de workers pour éviter de surcharger le système.
287
- * @param {string} solverName - Le nom de la fonction solveur à appeler dans `Optimization.Operators`.
288
- * @param {Array<any>} baseSolverArgs - Les arguments de base à passer au solveur (sans les données aléatoires qui seront générées par worker).
289
- * @param {number} numCycles - Le nombre total de cycles à exécuter.
290
- * @param {boolean} [logProgress=false] - Si true, affiche la progression dans la console.
291
- * @param {object} [options={}] - Options pour la parallélisation.
292
- * @param {number} [options.concurrency] - Le nombre de workers à utiliser en parallèle. Par défaut, le nombre de cœurs CPU.
293
- * @param {function(number): Array<any>} [options.workerDataGenerator] - Une fonction qui, pour chaque cycle (index), génère les arguments spécifiques à passer au solveur. Si non fournie, `baseSolverArgs` est utilisé tel quel.
294
- * @returns {Promise<{bestResult: object, stats: {scores: Array<number>, average: number, stdDev: number}}>} Le meilleur résultat et des statistiques.
295
- */
296
- async runMultipleParallel(
297
- solverName,
298
- baseSolverArgs = [], // Default to empty array if not provided
299
- numCycles,
300
- logProgress = false,
301
- options = {},
302
- ) {
303
- const concurrency = options.concurrency || os.cpus().length;
304
- const workerDataGenerator = options.workerDataGenerator;
305
-
306
- if (logProgress) {
307
- console.log(
308
- ` (Utilisation d'un pool de ${concurrency} workers pour ${numCycles} cycles avec le solveur ${solverName})`,
309
- );
310
- }
311
-
312
- const allResults = new Array(numCycles);
313
- const tasks = Array.from({ length: numCycles }, (_, i) => i);
314
- let tasksCompleted = 0;
315
-
316
- const runWorker = async (workerId) => {
317
- while (tasks.length > 0) {
318
- const taskIndex = tasks.shift();
319
- if (taskIndex === undefined) continue;
320
-
321
- const workerData = {
322
- solverName,
323
- solverArgs: workerDataGenerator
324
- ? workerDataGenerator(taskIndex)
325
- : baseSolverArgs,
326
- };
327
-
328
- const result = await new Promise(async (resolve, reject) => {
329
- // Le chemin du worker doit être absolu ou relatif au fichier appelant.
330
- // On utilise import.meta.url pour résoudre le chemin de manière fiable.
331
- const worker = new Worker(new URL('./optimization.worker.js', import.meta.url), { workerData });
332
- worker.on("message", resolve);
333
- worker.on("error", reject);
334
- worker.on("exit", (code) => {
335
- if (code !== 0)
336
- reject(
337
- new Error(`Worker ${workerId} a terminé avec le code ${code}`),
338
- );
339
- });
340
- });
341
-
342
- allResults[taskIndex] = result;
343
- tasksCompleted++;
344
- if (logProgress) {
345
- const score =
346
- result.energy !== undefined ? result.energy : result.fitness;
347
- console.log(
348
- ` -> Cycle ${tasksCompleted}/${numCycles} (Worker ${workerId}): Score = ${score.toFixed(2)}`,
349
- );
350
- }
351
- }
352
- };
353
-
354
- const workerPromises = Array.from({ length: concurrency }, (_, i) =>
355
- runWorker(i + 1),
356
- );
357
- await Promise.all(workerPromises);
358
-
359
- // Le reste de la logique est identique à `runMultiple`
360
- let bestResult = null;
361
- const allScores = [];
362
- allResults.forEach((result) => {
363
- const score =
364
- result.energy !== undefined ? result.energy : result.fitness;
365
- allScores.push(score);
366
- if (
367
- !bestResult ||
368
- score <
369
- (bestResult.energy !== undefined
370
- ? bestResult.energy
371
- : bestResult.fitness)
372
- ) {
373
- bestResult = result;
374
- }
375
- });
376
-
377
- const sum = allScores.reduce((a, b) => a + b, 0);
378
- const average = sum / numCycles;
379
- const variance =
380
- allScores.reduce((a, b) => a + Math.pow(b - average, 2), 0) / numCycles;
381
- const stdDev = Math.sqrt(variance);
382
-
383
- return {
384
- bestResult,
385
- stats: { scores: allScores, average, stdDev, concurrency },
386
- };
387
- },
388
- };
389
-
390
- /**
391
- * Détermine si la solution A domine la solution B en multi-objectifs (problème de minimisation).
392
- * @private
393
- * @param {number[]} objectivesA - Tableau des scores des objectifs pour la solution A.
394
- * @param {number[]} objectivesB - Tableau des scores des objectifs pour la solution B.
395
- * @returns {boolean} - True si A domine B.
396
- */
397
- function paretoDominates(objectivesA, objectivesB) {
398
- let aIsBetterInOne = false;
399
- for (let i = 0; i < objectivesA.length; i++) {
400
- if (objectivesA[i] > objectivesB[i]) {
401
- return false; // A est pire sur au moins un objectif, donc ne domine pas.
402
- }
403
- if (objectivesA[i] < objectivesB[i]) {
404
- aIsBetterInOne = true; // A est strictement meilleur sur au moins un objectif.
405
- }
406
- }
407
- return aIsBetterInOne; // A domine B si elle n'est jamais pire et au moins une fois meilleure.
408
- }
409
-
410
- /**
411
- * Trie une population en fronts de Pareto non-dominés (inspiré de NSGA-II).
412
- * @private
413
- * @param {Array<{individual: any, objectives: number[]}>} populationWithObjectives - La population à trier.
414
- * @returns {Array<Array<{individual: any, objectives: number[]}>>} - Un tableau de fronts, le premier est le meilleur.
415
- */
416
- function nonDominatedSort(populationWithObjectives) {
417
- const fronts = [[]];
418
- for (const p1 of populationWithObjectives) {
419
- p1.dominationCount = 0;
420
- p1.dominatedSolutions = [];
421
- for (const p2 of populationWithObjectives) {
422
- if (p1 === p2) continue;
423
- if (paretoDominates(p1.objectives, p2.objectives)) {
424
- p1.dominatedSolutions.push(p2);
425
- } else if (paretoDominates(p2.objectives, p1.objectives)) {
426
- p1.dominationCount++;
427
- }
428
- }
429
- if (p1.dominationCount === 0) {
430
- p1.rank = 0;
431
- fronts[0].push(p1);
432
- }
433
- }
434
-
435
- let i = 0;
436
- while (fronts[i] && fronts[i].length > 0) {
437
- const nextFront = [];
438
- for (const p1 of fronts[i]) {
439
- for (const p2 of p1.dominatedSolutions) {
440
- p2.dominationCount--;
441
- if (p2.dominationCount === 0) {
442
- p2.rank = i + 1;
443
- nextFront.push(p2);
444
- }
445
- }
446
- }
447
- i++;
448
- if (nextFront.length > 0) {
449
- fronts[i] = nextFront;
450
- }
451
- }
452
- return fronts;
453
- }
454
-
455
- /**
456
- * Calcule la distance de promiscuité (crowding distance) pour un front, afin de préserver la diversité.
457
- * @private
458
- * @param {Array<{individual: any, objectives: number[]}>} front - Le front de Pareto.
459
- */
460
- function calculateCrowdingDistance(front) {
461
- if (front.length === 0) return;
462
- front.forEach((p) => (p.crowdingDistance = 0));
463
- const numObjectives = front[0].objectives.length;
464
-
465
- for (let i = 0; i < numObjectives; i++) {
466
- front.sort((a, b) => a.objectives[i] - b.objectives[i]);
467
- const minObj = front[0].objectives[i];
468
- const maxObj = front[front.length - 1].objectives[i];
469
-
470
- // Les solutions aux extrémités sont cruciales, on leur donne une distance infinie.
471
- front[0].crowdingDistance = Infinity;
472
- front[front.length - 1].crowdingDistance = Infinity;
473
-
474
- if (maxObj === minObj) continue;
475
-
476
- for (let j = 1; j < front.length - 1; j++) {
477
- front[j].crowdingDistance +=
478
- (front[j + 1].objectives[i] - front[j - 1].objectives[i]) /
479
- (maxObj - minObj);
480
- }
481
- }
482
- }
483
-
484
- /**
485
- * Algorithme génétique multi-objectifs (inspiré de NSGA-II) pour trouver un front de Pareto.
486
- * @param {function(): any} createIndividual - Fonction qui crée un individu aléatoire.
487
- * @param {function(any): number[]} fitnessFunction - Fonction qui évalue un individu et retourne un tableau d'objectifs à MINIMISER.
488
- * @param {function(any, any): any} crossover - Fonction de croisement.
489
- * @param {function(any): any} mutate - Fonction de mutation.
490
- * @param {object} options - Options de l'algorithme.
491
- * @returns {Array<{solution: any, objectives: number[]}>} Le premier front de Pareto (l'ensemble des meilleures solutions de compromis).
492
- */
493
- Optimization.geneticAlgorithmMultiObjective = function (
494
- createIndividual,
495
- fitnessFunction,
496
- crossover,
497
- mutate,
498
- options = {},
499
- ) {
500
- const {
501
- generations = 150, // Augmenté pour une meilleure convergence
502
- populationSize = 60, // Augmenté pour plus de diversité
503
- mutationRate = 0.1,
504
- } = options;
505
-
506
- let population = Array.from({ length: populationSize }, () => ({
507
- individual: createIndividual(),
508
- }));
509
- population.forEach((p) => (p.objectives = fitnessFunction(p.individual)));
510
-
511
- for (let gen = 0; gen < generations; gen++) {
512
- // 1. Créer une population d'enfants
513
- const offspring = [];
514
- for (let i = 0; i < populationSize; i++) {
515
- // Sélection simple pour l'exemple
516
- const parent1 = population[Math.floor(Math.random() * population.length)];
517
- const parent2 = population[Math.floor(Math.random() * population.length)];
518
- let childIndividual = crossover(parent1.individual, parent2.individual);
519
- if (Math.random() < mutationRate) {
520
- childIndividual = mutate(childIndividual);
521
- }
522
- const child = { individual: childIndividual };
523
- child.objectives = fitnessFunction(child.individual);
524
- offspring.push(child);
525
- }
526
-
527
- // 2. Combiner parents et enfants
528
- const combinedPopulation = [...population, ...offspring];
529
-
530
- // 3. Trier la population combinée en fronts
531
- const fronts = nonDominatedSort(combinedPopulation);
532
-
533
- // 4. Construire la nouvelle population
534
- const newPopulation = [];
535
- for (const front of fronts) {
536
- if (newPopulation.length + front.length <= populationSize) {
537
- newPopulation.push(...front);
538
- } else {
539
- // Si le front est trop grand, on utilise la distance de promiscuité pour choisir les individus les plus diversifiés.
540
- calculateCrowdingDistance(front);
541
- front.sort((a, b) => b.crowdingDistance - a.crowdingDistance); // Trier par distance décroissante
542
- const remaining = populationSize - newPopulation.length;
543
- newPopulation.push(...front.slice(0, remaining));
544
- break;
545
- }
546
- }
547
- population = newPopulation;
548
- }
549
-
550
- // Retourner le premier front de la population finale
551
- const finalFronts = nonDominatedSort(population);
552
- const bestFront = finalFronts.length > 0 ? finalFronts[0] : [];
553
-
554
- // Filtrer le front pour ne garder que les solutions avec des objectifs uniques
555
- const uniqueSolutionsMap = new Map();
556
- for (const p of bestFront) {
557
- const key = JSON.stringify(p.objectives);
558
- if (!uniqueSolutionsMap.has(key)) {
559
- uniqueSolutionsMap.set(key, {
560
- solution: p.individual,
561
- objectives: p.objectives,
562
- });
563
- }
564
- }
565
- return Array.from(uniqueSolutionsMap.values());
566
- };
567
-
568
- /**
569
- * Utilitaires pour les problèmes d'optimisation.
570
- */
571
- Optimization.Utils = {
572
- /** Calcule la distance euclidienne entre deux points (villes). */
573
- distance: (city1, city2) => Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2)),
574
-
575
- /** Évalue la distance totale d'un chemin TSP donné. */
576
- evaluatePathDistance: (cities, path) => {
577
- let totalDistance = 0;
578
- for (let i = 0; i < path.length - 1; i++) {
579
- totalDistance += Optimization.Utils.distance(cities[path[i]], cities[path[i + 1]]);
580
- }
581
- totalDistance += Optimization.Utils.distance(cities[path[path.length - 1]], cities[path[0]]); // Retour au départ
582
- return totalDistance;
583
- }
584
- };
585
-
586
- /**
587
- * Algorithme d'optimisation CMA-ES (Covariance Matrix Adaptation Evolution Strategy).
588
- * C'est un algorithme de pointe pour l'optimisation en boîte noire de fonctions non-linéaires et non-convexes.
589
- * Il est particulièrement efficace pour les problèmes avec des variables continues.
590
- * @param {function(Array<number>): number} fitnessFunction - La fonction à MINIMISER.
591
- * @param {Array<number>} initialSolution - Le point de départ de la recherche (un vecteur de nombres).
592
- * @param {number} initialStepSize - La taille de pas initiale (sigma).
593
- * @param {object} [options={}] - Options de l'algorithme.
594
- * @param {number} [options.maxGenerations=100] - Nombre maximum de générations.
595
- * @param {number} [options.populationSize] - Taille de la population (lambda). Calculée par défaut si non fournie.
596
- * @param {number} [options.tolerance=1e-6] - Seuil de tolérance pour l'arrêt précoce.
597
- * @returns {{solution: Array<number>, fitness: number}} La meilleure solution trouvée.
598
- */
599
- Optimization.cmaes = function(fitnessFunction, initialSolution, initialStepSize, options = {}) {
600
- const n = initialSolution.length; // Dimension du problème
601
-
602
- // --- Paramètres de l'algorithme (stratégie) ---
603
- const { maxGenerations = 100, tolerance = 1e-6 } = options;
604
- const populationSize = options.populationSize || (4 + Math.floor(3 * Math.log(n))); // Lambda
605
- const mu = Math.floor(populationSize / 2); // Nombre de parents pour la recombinaison
606
-
607
- // Poids de recombinaison
608
- let weights = Array.from({ length: mu }, (_, i) => Math.log(mu + 0.5) - Math.log(i + 1));
609
- const sumWeights = weights.reduce((s, w) => s + w, 0);
610
- weights = weights.map(w => w / sumWeights);
611
- const muEff = 1 / weights.reduce((s, w) => s + w * w, 0);
612
-
613
- // Paramètres d'adaptation
614
- const cc = (4 + muEff / n) / (n + 4 + 2 * muEff / n);
615
- const cs = (muEff + 2) / (n + muEff + 5);
616
- const c1 = 2 / (Math.pow(n + 1.3, 2) + muEff);
617
- const cmu = Math.min(1 - c1, 2 * (muEff - 2 + 1 / muEff) / (Math.pow(n + 2, 2) + muEff));
618
- const damps = 1 + 2 * Math.max(0, Math.sqrt((muEff - 1) / (n + 1)) - 1) + cs;
619
-
620
- // --- Variables d'état dynamiques ---
621
- let mean = [...initialSolution]; // Le centre de la distribution de recherche
622
- let stepSize = initialStepSize; // Sigma
623
- let C = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => (i === j ? 1 : 0))); // Matrice de covariance
624
- let pc = Array(n).fill(0); // Chemin d'évolution pour C
625
- let ps = Array(n).fill(0); // Chemin d'évolution pour sigma
626
-
627
- let bestFitness = Infinity;
628
- let bestSolution = null;
629
-
630
- // Fonction pour la décomposition de Cholesky (simplifiée, pour matrice symétrique définie positive)
631
- function cholesky(A) {
632
- const L = Array.from({ length: n }, () => Array(n).fill(0));
633
- for (let i = 0; i < n; i++) {
634
- for (let j = 0; j <= i; j++) {
635
- let sum = 0;
636
- for (let k = 0; k < j; k++) {
637
- sum += L[i][k] * L[j][k];
638
- }
639
- if (i === j) {
640
- const val = A[i][i] - sum;
641
- if (val < 0) return null; // Non définie positive
642
- L[i][j] = Math.sqrt(val);
643
- } else {
644
- if (L[j][j] === 0) return null;
645
- L[i][j] = (A[i][j] - sum) / L[j][j];
646
- }
647
- }
648
- }
649
- return L;
650
- }
651
-
652
- for (let gen = 0; gen < maxGenerations; gen++) {
653
- // 1. Échantillonnage de la nouvelle population
654
- const population = [];
655
- const arx = []; // Vecteurs de recherche
656
- const L = cholesky(C);
657
- if (!L) {
658
- console.warn("[CMA-ES] La matrice de covariance n'est plus définie positive. Arrêt.");
659
- break;
660
- }
661
-
662
- for (let i = 0; i < populationSize; i++) {
663
- const z = Array.from({ length: n }, () => Math.random() * 2 - 1); // Vecteur normal standard
664
- const y = Array(n).fill(0); // z transformé par L
665
- for (let r = 0; r < n; r++) {
666
- for (let c = 0; c < n; c++) {
667
- y[r] += L[r][c] * z[c];
668
- }
669
- }
670
- arx.push(y);
671
- const individual = mean.map((m, j) => m + stepSize * y[j]);
672
- population.push({ individual, fitness: fitnessFunction(individual) });
673
- }
674
-
675
- // 2. Trier et sélectionner les meilleurs
676
- population.sort((a, b) => a.fitness - b.fitness);
677
- const parents = population.slice(0, mu);
678
-
679
- if (parents[0].fitness < bestFitness) {
680
- bestFitness = parents[0].fitness;
681
- bestSolution = parents[0].individual;
682
- }
683
-
684
- // 3. Mise à jour des variables d'état
685
- const oldMean = [...mean];
686
- const y_w = Array(n).fill(0);
687
- for (let j = 0; j < n; j++) {
688
- for (let i = 0; i < mu; i++) {
689
- const parentIndex = population.indexOf(parents[i]);
690
- y_w[j] += weights[i] * arx[parentIndex][j];
691
- }
692
- }
693
- mean = oldMean.map((m, i) => m + stepSize * y_w[i]);
694
-
695
- // 4. Adaptation des chemins d'évolution
696
- const C_inv_sqrt = cholesky(C); // Simplification, devrait être l'inverse de la racine
697
- const C_inv_sqrt_y_w = y_w; // Approximation
698
- ps = ps.map((p, i) => (1 - cs) * p + Math.sqrt(cs * (2 - cs) * muEff) * C_inv_sqrt_y_w[i]);
699
-
700
- const hsig = Math.sqrt(ps.reduce((s, v) => s + v*v, 0)) / (1 - Math.pow(1 - cs, 2 * (gen + 1))) / n < 1.4 + 2 / (n + 1);
701
- pc = pc.map((p, i) => (1 - cc) * p + (hsig ? Math.sqrt(cc * (2 - cc) * muEff) * y_w[i] : 0));
702
-
703
- // 5. Adaptation de la matrice de covariance C
704
- let rankOneUpdate = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => c1 * pc[i] * pc[j]));
705
- let rankMuUpdate = Array.from({ length: n }, () => Array(n).fill(0));
706
- for (let k = 0; k < mu; k++) {
707
- const y_k = arx[population.indexOf(parents[k])];
708
- for (let i = 0; i < n; i++) {
709
- for (let j = 0; j < n; j++) {
710
- rankMuUpdate[i][j] += cmu * weights[k] * y_k[i] * y_k[j];
711
- }
712
- }
713
- }
714
- C = C.map((row, i) => row.map((val, j) => (1 - c1 - cmu) * val + rankOneUpdate[i][j] + rankMuUpdate[i][j]));
715
-
716
- // 6. Adaptation de la taille de pas (sigma)
717
- stepSize *= Math.exp((cs / damps) * (Math.sqrt(ps.reduce((s, v) => s + v*v, 0)) / Math.sqrt(n) - 1));
718
- }
719
-
720
- return { solution: bestSolution, fitness: bestFitness };
721
- };
722
-
723
- /**
724
- * @namespace Optimization.Operators
725
- * @description Une bibliothèque de "fabriques d'évaluateurs" pour des problèmes d'optimisation complexes,
726
- * souvent multidimensionnels, à utiliser avec les algorithmes de `Optimization` (Recuit Simulé, Algorithmes Génétiques, etc.).
727
- */
728
- Optimization.Operators = {}; // Création du namespace
729
-
730
- /**
731
- * Crée une fonction de sélection par tournoi pour un algorithme génétique.
732
- * @param {object} [options] - Options pour le tournoi.
733
- * @param {number} [options.size=5] - Le nombre de participants par tournoi.
734
- * @returns {function(Array<{chromosome: any, fitness: number}>): {chromosome: any, fitness: number}} Une fonction de sélection.
735
- */
736
- Optimization.Operators.createTournamentSelection = (options = {}) => {
737
- const tournamentSize = options.size || 5;
738
-
739
- return function tournamentSelection(population) {
740
- let best = null;
741
-
742
- for (let i = 0; i < tournamentSize; i++) {
743
- const individual =
744
- population[Math.floor(Math.random() * population.length)];
745
- if (!best || individual.fitness < best.fitness) {
746
- best = individual;
747
- }
748
- }
749
- // Retourne le meilleur trouvé. Dans le pire des cas (tous les scores sont Infinity),
750
- // on retourne le premier candidat sélectionné au lieu de null.
751
- if (!best) {
752
- return population[Math.floor(Math.random() * population.length)];
753
- }
754
- return best;
755
- };
756
- };
757
-
758
- /**
759
- * Crée une matrice de covariance à partir de coefficients de corrélation déclarés.
760
- * C'est une manière plus intuitive de définir les relations de risque entre les actifs.
761
- * @param {object} config - L'objet de configuration.
762
- * @param {Array<{name: string, volatility: number}>} config.assets - La liste des actifs avec leur volatilité.
763
- * @param {Array<{assets: [string, string], correlation: number}>} config.correlations - Une liste de relations de corrélation.
764
- * @returns {Array<Array<number>>} La matrice de covariance calculée.
765
- */
766
- Optimization.Operators.createCovarianceMatrixFromCorrelations = ({
767
- assets,
768
- correlations,
769
- }) => {
770
- const n = assets.length;
771
- const matrix = Array.from({ length: n }, () => Array(n).fill(0));
772
-
773
- // Créer un map pour un accès rapide aux infos des actifs par leur nom.
774
- const assetInfo = new Map();
775
- assets.forEach((asset, index) => {
776
- assetInfo.set(asset.name, { index, volatility: asset.volatility });
777
- });
778
-
779
- // 1. Remplir la diagonale avec les variances (volatilité^2)
780
- for (let i = 0; i < n; i++) {
781
- const variance = Math.pow(assets[i].volatility, 2);
782
- matrix[i][i] = variance;
783
- }
784
-
785
- // 2. Remplir les autres cellules avec les covariances calculées
786
- for (const corr of correlations) {
787
- const [nameA, nameB] = corr.assets;
788
- if (!assetInfo.has(nameA) || !assetInfo.has(nameB)) {
789
- console.warn(
790
- `Avertissement: L'un des actifs [${nameA}, ${nameB}] n'a pas été trouvé. La corrélation est ignorée.`,
791
- );
792
- continue;
793
- }
794
-
795
- const infoA = assetInfo.get(nameA);
796
- const infoB = assetInfo.get(nameB);
797
-
798
- // Cov(A,B) = Corr(A,B) * Vol(A) * Vol(B)
799
- const covariance = corr.correlation * infoA.volatility * infoB.volatility;
800
-
801
- matrix[infoA.index][infoB.index] = covariance;
802
- matrix[infoB.index][infoA.index] = covariance; // La matrice est symétrique
803
- }
804
-
805
- return matrix;
806
- };
807
-
808
- // Le "déséquilibre" est la différence absolue entre l'offre et la demande. On veut le minimiser.
809
- Optimization.Operators.createMarketEquilibriumEvaluator = (
810
- demandModel,
811
- supplyModel,
812
- ) => {
813
- return function marketImbalance(price) {
814
- const d = demandModel(price);
815
- const s = supplyModel(price);
816
- return Math.abs(d - s);
817
- };
818
- };
819
-
820
- // Un "individu" est un tableau de 3 poids (ex: [0.5, 0.2, 0.3]) qui doivent sommer à 1.
821
- /**
822
- * Crée une fonction de fitness pour l'optimisation de portefeuille.
823
- * @param {object} config - L'objet de configuration.
824
- * @param {Array<{name: string, expectedReturn: number, volatility: number}>} config.assets - Les actifs disponibles.
825
- * @param {number} config.maxVolatility - La contrainte de volatilité maximale du portefeuille.
826
- * @param {Array<Array<number>>} [config.covarianceMatrix] - Matrice de covariance pour un calcul de risque précis.
827
- * @returns {function(Array<number>): number} Une fonction de fitness qui évalue un portefeuille (tableau de poids).
828
- */
829
- Optimization.Operators.createPortfolioAllocator = ({
830
- assets,
831
- maxVolatility,
832
- covarianceMatrix,
833
- }) => {
834
- // La fonction de fitness évalue un portefeuille (un tableau de poids).
835
- // L'objectif est de MINIMISER le score, donc on minimise le rendement NÉGATIF.
836
- return function portfolioFitness(weights) {
837
- // Normaliser les poids pour qu'ils somment à 1
838
- const totalWeight = weights.reduce((sum, w) => sum + w, 0);
839
- if (totalWeight === 0) return Infinity; // Éviter la division par zéro, score très mauvais
840
- const normalizedWeights = weights.map((w) => w / totalWeight);
841
-
842
- let portfolioReturn = normalizedWeights.reduce(
843
- (sum, w, i) => sum + w * assets[i].expectedReturn,
844
- 0,
845
- );
846
- let portfolioVolatility;
847
-
848
- if (covarianceMatrix) {
849
- // Calcul de la volatilité avec la matrice de covariance (plus précis)
850
- // Volatilité^2 = w' * C * w
851
- let variance = 0;
852
- for (let i = 0; i < assets.length; i++) {
853
- for (let j = 0; j < assets.length; j++) {
854
- variance +=
855
- normalizedWeights[i] *
856
- normalizedWeights[j] *
857
- covarianceMatrix[i][j];
858
- }
859
- }
860
- portfolioVolatility = Math.sqrt(variance);
861
- } else {
862
- // Calcul simplifié (moins précis) : moyenne pondérée des volatilités individuelles.
863
- portfolioVolatility = normalizedWeights.reduce(
864
- (sum, w, i) => sum + w * assets[i].volatility,
865
- 0,
866
- );
867
- }
868
-
869
- for (let i = 0; i < assets.length; i++) {}
870
-
871
- // Forte pénalité si la contrainte de risque n'est pas respectée.
872
- if (portfolioVolatility > maxVolatility) {
873
- return 1000 + (portfolioVolatility - maxVolatility) * 1000; // Pénalité proportionnelle à la violation.
874
- }
875
-
876
- // On veut maximiser le rendement, donc on minimise son opposé.
877
- return -portfolioReturn;
878
- };
879
- };
880
-
881
- /**
882
- * Crée un solveur complet pour le problème du voyageur de commerce (TSP) en utilisant le Recuit Simulé.
883
- * Cette fonction factorise la création de l'évaluateur de chemin et de la fonction de voisinage.
884
- * @param {Array<{x: number, y: number}>} cities - Un tableau d'objets représentant les coordonnées des villes.
885
- * @param {object} [options] - Options pour l'algorithme de recuit simulé.
886
- * @returns {{solution: Array<number>, energy: number}} Le chemin optimal (indices des villes) et sa distance.
887
- */
888
- Optimization.Operators.solveTSP = (cities, options = {}) => {
889
- // Fonction interne pour calculer la distance entre deux villes.
890
- const distance = (city1, city2) =>
891
- Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2));
892
-
893
- // Évaluateur : calcule la longueur totale d'un chemin donné.
894
- const pathEvaluator = (path) => {
895
- let totalDistance = 0;
896
- for (let i = 0; i < path.length - 1; i++) {
897
- totalDistance += distance(cities[path[i]], cities[path[i + 1]]);
898
- }
899
- totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); // Retour au départ
900
- return totalDistance;
901
- };
902
-
903
- // Voisinage : génère un chemin voisin en inversant une sous-séquence (heuristique 2-opt).
904
- const pathNeighbor = (path) => {
905
- const newPath = [...path];
906
- let i = Math.floor(Math.random() * newPath.length);
907
- let j = Math.floor(Math.random() * newPath.length);
908
- if (i === j) j = (j + 1) % newPath.length;
909
- const [start, end] = [Math.min(i, j), Math.max(i, j)];
910
-
911
- const segment = newPath.slice(start, end + 1).reverse();
912
- newPath.splice(start, segment.length, ...segment);
913
- return newPath;
914
- };
915
-
916
- // Solution initiale : un chemin aléatoire.
917
- const initialPath = Array.from({ length: cities.length }, (_, i) => i).sort(
918
- () => Math.random() - 0.5,
919
- );
920
-
921
- // Paramètres par défaut pour le TSP, pouvant être surchargés par `options`.
922
- const saOptions = {
923
- initialTemperature: 10000,
924
- coolingRate: 0.999,
925
- maxIterations: 100000,
926
- ...options,
927
- };
928
-
929
- return Optimization.simulatedAnnealing(
930
- initialPath,
931
- pathEvaluator,
932
- pathNeighbor,
933
- saOptions.initialTemperature,
934
- saOptions.coolingRate,
935
- saOptions.maxIterations,
936
- );
937
- };
938
-
939
- /**
940
- * Crée un solveur complet pour le problème d'optimisation de portefeuille en utilisant un Algorithme Génétique.
941
- * @param {Array<{name: string, expectedReturn: number, volatility: number}>} assets - Les actifs disponibles.
942
- * @param {number} maxVolatility - La contrainte de volatilité maximale du portefeuille.
943
- * @param {object} [options] - Options pour l'algorithme génétique.
944
- * @returns {{solution: Array<number>, fitness: number}} L'allocation de poids optimale et le score de fitness associé.
945
- */
946
- Optimization.Operators.solvePortfolio = (
947
- assets,
948
- maxVolatility,
949
- options = {},
950
- ) => {
951
- // La fonction de fitness est créée par notre opérateur existant.
952
- const fitnessFunction = Optimization.Operators.createPortfolioAllocator({
953
- assets,
954
- maxVolatility,
955
- covarianceMatrix: options.covarianceMatrix,
956
- });
957
-
958
- // Fonctions spécifiques au problème pour l'AG, maintenant encapsulées.
959
- const createIndividual = () =>
960
- Array.from({ length: assets.length }, () => Math.random());
961
-
962
- const crossover = (p1, p2) => p1.map((w1, i) => (w1 + p2[i]) / 2); // Moyenne des poids
963
-
964
- const mutate = (p) => {
965
- const newP = [...p];
966
- const i = Math.floor(Math.random() * newP.length);
967
- newP[i] += (Math.random() - 0.5) * 0.2; // Mutation douce
968
- newP[i] = Math.max(0, newP[i]); // Les poids ne peuvent être négatifs
969
- return newP;
970
- };
971
-
972
- const gaOptions = {
973
- generations: 150,
974
- populationSize: 100,
975
- ...options,
976
- };
977
-
978
- return Optimization.geneticAlgorithm(
979
- createIndividual,
980
- fitnessFunction,
981
- crossover,
982
- mutate,
983
- gaOptions,
984
- );
985
- };
986
-
987
- /**
988
- * Crée un évaluateur 2D pour trouver la commission de base et le facteur de bonus qualité qui maximisent les revenus de la plateforme.
989
- * @param {object} config - L'objet de configuration.
990
- * @param {number} config.totalAdvertiserCredits - Le total des crédits disponibles chez les annonceurs.
991
- * @param {Array<{qualityScore: number}>} config.websites - Un tableau d'objets représentant les sites monétisés, chacun avec un score de qualité.
992
- * @returns {function(Array<number>): number} Un évaluateur qui prend une solution `[baseCommission, bonusFactor]` et retourne le revenu NÉGATIF (car les solveurs minimisent).
993
- */
994
- Optimization.Operators.createAdvancedPlatformRevenueEvaluator = ({
995
- totalAdvertiserCredits,
996
- websites,
997
- }) => {
998
- // Modèle de la demande (Annonceurs)
999
- // La demande est sensible à la qualité globale de l'inventaire publicitaire.
1000
- const advertiserDemandModel = (averageSiteQuality) => {
1001
- // La demande de base est toujours liée aux crédits disponibles.
1002
- const baseDemand = (totalAdvertiserCredits || 100) * 10;
1003
- // La demande augmente avec la qualité moyenne des sites.
1004
- return baseDemand * (1 + averageSiteQuality);
1005
- };
1006
-
1007
- // Modèle de l'offre (Webmasters)
1008
- // L'offre de chaque site dépend de sa rémunération individuelle.
1009
- const webmasterSupplyModel = (baseCommission, bonusFactor) => {
1010
- let totalOfferedClicks = 0;
1011
- const baseSupplyPerSite = 500; // Clics potentiels par site
1012
-
1013
- for (const site of websites) {
1014
- // La commission effective est réduite pour les sites de haute qualité.
1015
- const effectiveCommission = Math.max(
1016
- 0,
1017
- baseCommission - site.qualityScore * bonusFactor,
1018
- );
1019
- const webmasterPayoutRate = 1 - effectiveCommission;
1020
-
1021
- // L'offre d'un site est proportionnelle à son taux de rémunération.
1022
- totalOfferedClicks += baseSupplyPerSite * webmasterPayoutRate;
1023
- }
1024
- return totalOfferedClicks;
1025
- };
1026
-
1027
- // L'évaluateur pour l'algorithme d'optimisation (Recuit Simulé, etc.)
1028
- return function revenueEvaluator(solution) {
1029
- const [baseCommission, bonusFactor] = solution;
1030
-
1031
- // Contraintes : on pénalise fortement les solutions hors des clous.
1032
- if (
1033
- baseCommission < 0.01 ||
1034
- baseCommission > 0.8 ||
1035
- bonusFactor < 0 ||
1036
- bonusFactor > baseCommission
1037
- ) {
1038
- return Infinity; // Score très mauvais
1039
- }
1040
-
1041
- const averageQuality =
1042
- websites.length > 0
1043
- ? websites.reduce((sum, site) => sum + site.qualityScore, 0) /
1044
- websites.length
1045
- : 0.5;
1046
-
1047
- const demand = advertiserDemandModel(averageQuality);
1048
- const supply = webmasterSupplyModel(baseCommission, bonusFactor);
1049
-
1050
- const clicks = Math.min(demand, supply);
1051
- const averageCommission = Math.max(
1052
- 0,
1053
- baseCommission - averageQuality * bonusFactor,
1054
- );
1055
-
1056
- // On veut MAXIMISER le revenu, donc on MINIMISE son opposé.
1057
- return -(clicks * averageCommission);
1058
- };
1059
- };
1060
-
1061
- /**
1062
- * Crée un solveur pour le problème de placement d'infrastructures (Facility Location Problem).
1063
- * @param {Array<{x: number, y: number}>} customers - Coordonnées des clients.
1064
- * @param {number} numFacilities - Le nombre d'infrastructures à placer.
1065
- * @param {{minX: number, maxX: number, minY: number, maxY: number}} bounds - Les limites de la carte où placer les infrastructures.
1066
- * @param {number} [options.fixedCostPerFacility=0] - Coût fixe pour chaque infrastructure installée.
1067
- * @param {object} [options] - Options pour le recuit simulé.
1068
- * @returns {{solution: Array<{x: number, y: number}>, energy: number}} Les coordonnées optimales des infrastructures et le coût total.
1069
- */
1070
- Optimization.Operators.solveFacilityLocation = (
1071
- customers,
1072
- numFacilities,
1073
- bounds,
1074
- options = {},
1075
- ) => {
1076
- const fixedCostPerFacility = options.fixedCostPerFacility || 0;
1077
- const distanceSq = (p1, p2) =>
1078
- Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2); // On utilise la distance au carré pour l'efficacité
1079
-
1080
- // Évaluateur : calcule la somme des distances de chaque client à son infrastructure la plus proche.
1081
- const facilityEvaluator = (facilities) => {
1082
- let totalConnectionCost = 0;
1083
- for (const customer of customers) {
1084
- let minDistanceToCustomer = Infinity;
1085
- for (const facility of facilities) {
1086
- const d = distanceSq(customer, facility);
1087
- if (d < minDistanceToCustomer) {
1088
- minDistanceToCustomer = d;
1089
- }
1090
- }
1091
- totalConnectionCost += Math.sqrt(minDistanceToCustomer); // On utilise la vraie distance pour le coût
1092
- }
1093
- // Le coût total est la somme des coûts de connexion + le coût fixe des infrastructures.
1094
- return totalConnectionCost + facilities.length * fixedCostPerFacility;
1095
- };
1096
-
1097
- // Voisinage : déplace légèrement une infrastructure au hasard.
1098
- const facilityNeighbor = (facilities) => {
1099
- const newFacilities = facilities.map((f) => ({ ...f }));
1100
- const i = Math.floor(Math.random() * numFacilities);
1101
- const moveX = (Math.random() - 0.5) * (bounds.maxX - bounds.minX) * 0.1;
1102
- const moveY = (Math.random() - 0.5) * (bounds.maxY - bounds.minY) * 0.1;
1103
-
1104
- newFacilities[i].x = Math.max(
1105
- bounds.minX,
1106
- Math.min(bounds.maxX, newFacilities[i].x + moveX),
1107
- );
1108
- newFacilities[i].y = Math.max(
1109
- bounds.minY,
1110
- Math.min(bounds.maxY, newFacilities[i].y + moveY),
1111
- );
1112
-
1113
- return newFacilities;
1114
- };
1115
-
1116
- // Solution initiale : place les infrastructures au hasard sur la carte.
1117
- const initialFacilities = Array.from({ length: numFacilities }, () => ({
1118
- x: bounds.minX + Math.random() * (bounds.maxX - bounds.minX),
1119
- y: bounds.minY + Math.random() * (bounds.maxY - bounds.minY),
1120
- }));
1121
-
1122
- const saOptions = {
1123
- initialTemperature: 100000,
1124
- coolingRate: 0.999,
1125
- maxIterations: 50000,
1126
- ...options,
1127
- };
1128
-
1129
- const result = Optimization.simulatedAnnealing(
1130
- initialFacilities,
1131
- facilityEvaluator,
1132
- facilityNeighbor,
1133
- saOptions.initialTemperature,
1134
- saOptions.coolingRate,
1135
- saOptions.maxIterations,
1136
- );
1137
-
1138
- return result;
1139
- };
1140
-
1141
- // --- SOLUTION : Définir un coût de base pour un clic ---
1142
- // 1 PRIM'S = 1 clic
1143
- const BASE_CLICK_COST = 1;
1144
- /**
1145
- * Crée un évaluateur multi-objectifs pour déterminer le Coût Par Clic (CPC) optimal.
1146
- * @param {object} config - L'objet de configuration.
1147
- * @param {object} config.advertiser - L'annonceur qui paie le clic.
1148
- * @param {object} config.ad - L'annonce qui a été cliquée.
1149
- * @param {Array<object>} config.competingAds - Les autres annonces ciblant les mêmes mots-clés.
1150
- * @param {object} config.website - Le site sur lequel le clic a eu lieu.
1151
- * @param {object} config.platformParams - Les paramètres de la plateforme (taux de commission, etc.).
1152
- * @param {number} config.estimatedImpressions - Le nombre d'impressions quotidiennes estimées pour ce contexte.
1153
- */
1154
- Optimization.Operators.createOptimalCPCEvaluator = (context) => {
1155
- const { optimalBaseCommission, optimalBonusFactor } = context.platformParams;
1156
- const websiteQualityScore = (context.website?.relevanceScore || 50) / 100;
1157
-
1158
- // Le taux de commission effectif pour ce site
1159
- const effectiveCommissionRate = Math.max(
1160
- 0,
1161
- optimalBaseCommission - websiteQualityScore * optimalBonusFactor,
1162
- );
1163
-
1164
- // Modèle de la demande : combien de clics l'annonceur peut-il s'offrir ?
1165
- const advertiserDemand = (cpc) => {
1166
- if (cpc <= 0) return Infinity;
1167
- return (context.advertiser.credits || 0) / cpc;
1168
- };
1169
-
1170
- // --- SOLUTION : Utiliser l'offre réelle et la concurrence ---
1171
- // L'offre est maintenant le nombre d'impressions estimées, un chiffre concret.
1172
- const supply = context.estimatedImpressions || 1; // Fallback à 1 pour éviter la division par zéro.
1173
-
1174
- // Le facteur de concurrence augmente le prix s'il y a plus de monde sur le même créneau.
1175
- // Formule simple : 1 + (0.1 * nombre de concurrents), avec un plafond.
1176
- const competitionFactor = Math.min(
1177
- 2.5,
1178
- 1 + context.competingAds.length * 0.1,
1179
- );
1180
-
1181
- return function cpcFitness(cpcMultiplier) {
1182
- // Le CPC final est le coût de base, ajusté par le multiplicateur de l'algo et la concurrence.
1183
- const adjustedCPC = BASE_CLICK_COST * cpcMultiplier * competitionFactor;
1184
- if (adjustedCPC < 0.1) return [Infinity, Infinity, Infinity]; // CPC minimum
1185
-
1186
- // La demande de l'annonceur est calculée avec le CPC ajusté.
1187
- const demand = advertiserDemand(adjustedCPC);
1188
-
1189
- // Le nombre de clics est le minimum de l'offre et de la demande.
1190
- const estimatedClicks = Math.min(demand, supply);
1191
-
1192
- // Objectif 1 : Maximiser le revenu de la plateforme (donc minimiser son opposé)
1193
- const platformRevenue =
1194
- estimatedClicks * adjustedCPC * effectiveCommissionRate;
1195
-
1196
- // Objectif 2 : Maximiser la valeur pour l'annonceur (nombre de clics, donc minimiser son opposé)
1197
- const advertiserValue = estimatedClicks;
1198
-
1199
- // Objectif 3 : Minimiser le déséquilibre du marché (offre vs demande)
1200
- const marketImbalance = Math.abs(demand - supply);
1201
-
1202
- return [-platformRevenue, -advertiserValue, marketImbalance];
1203
- };
1204
- };
1205
-
1206
- /**
1207
- * Applique le facteur de concurrence au CPC de base.
1208
- * @private
1209
- * @param {number} baseCpc - Le CPC issu de l'algorithme génétique.
1210
- * @param {Array<object>} competingAds - Les annonces concurrentes.
1211
- * @returns {number} Le CPC final ajusté.
1212
- */
1213
- function applyCompetitionFactor(baseCpc, competingAds) {
1214
- // Formule simple : 1 + (0.1 * nombre de concurrents), avec un plafond pour éviter l'explosion des prix.
1215
- const competitionFactor = Math.min(
1216
- 2.5,
1217
- 1 + (competingAds || []).length * 0.1,
1218
- );
1219
- const finalCpc = baseCpc * competitionFactor;
1220
- // On s'assure de ne jamais descendre sous un seuil minimal.
1221
- return Math.max(0.1, finalCpc);
1222
- }
1223
- /**
1224
- * Résout le problème du CPC optimal en utilisant un algorithme génétique multi-objectifs.
1225
- * @param {object} context - Le contexte nécessaire pour l'évaluation (advertiser, ad, etc.).
1226
- * @param {object} [options] - Options pour l'algorithme génétique.
1227
- * @returns {Array<{solution: number, objectives: number[]}>} Le front de Pareto des solutions CPC.
1228
- */
1229
- Optimization.Operators.solveOptimalCPC = (context, options = {}) => {
1230
- const fitnessFunction =
1231
- Optimization.Operators.createOptimalCPCEvaluator(context);
1232
-
1233
- // Un "individu" est simplement une valeur de CPC.
1234
- const createIndividual = () => {
1235
- // Le CPC peut varier, par exemple, entre 0.5 et 5 PRIM'S.
1236
- return 0.5 + Math.random() * 4.5;
1237
- };
1238
-
1239
- // Croisement : moyenne des CPC des parents.
1240
- const crossover = (cpc1, cpc2) => {
1241
- return (cpc1 + cpc2) / 2;
1242
- };
1243
-
1244
- // Mutation : légère variation aléatoire du CPC.
1245
- const mutate = (cpc) => {
1246
- const newCpc = cpc + (Math.random() - 0.5) * 0.5;
1247
- return Math.max(0.1, newCpc); // Assurer un CPC minimum.
1248
- };
1249
-
1250
- const gaOptions = {
1251
- generations: 50,
1252
- populationSize: 40,
1253
- ...options,
1254
- };
1255
-
1256
- // On utilise l'algorithme génétique multi-objectifs pour obtenir le front de Pareto.
1257
- const paretoFront = Optimization.geneticAlgorithmMultiObjective(
1258
- createIndividual,
1259
- fitnessFunction,
1260
- crossover,
1261
- mutate,
1262
- gaOptions,
1263
- );
1264
-
1265
- // --- SOLUTION : Appliquer le facteur de concurrence sur les solutions finales ---
1266
- return paretoFront.map((result) => {
1267
- const cpcMultiplier = result.solution;
1268
- // Le CPC final est calculé ici, en dehors de la fonction de fitness.
1269
- const finalCpc = applyCompetitionFactor(
1270
- BASE_CLICK_COST * cpcMultiplier,
1271
- context.competingAds,
1272
- );
1273
- return {
1274
- ...result,
1275
- solution: Math.max(0.1, finalCpc), // On s'assure de ne jamais descendre sous le plancher absolu.
1276
- };
1277
- });
1278
- };
1279
-
1280
- /**
1281
- * Crée un évaluateur multi-objectifs pour trouver le TTL (Time-To-Live) optimal pour un ticket de sécurité.
1282
- * @param {object} context - L'objet de configuration.
1283
- * @param {number} context.suspicionScore - Le score de suspicion de l'utilisateur (0-100).
1284
- * @returns {function(number): number[]} Une fonction de fitness qui prend un TTL (en ms) et retourne les scores des objectifs [risque, friction].
1285
- */
1286
- Optimization.Operators.createOptimalTtlEvaluator = ({ suspicionScore }) => {
1287
- // On normalise le score pour qu'il soit plus impactant dans le calcul du risque.
1288
- const normalizedScore = Math.max(1, suspicionScore);
1289
-
1290
- return function ttlFitness(ttl) {
1291
- // Contraintes : un TTL doit être dans une plage raisonnable (ex: 5min à 24h)
1292
- if (ttl < 300000 || ttl > 86400000) return [Infinity, Infinity];
1293
-
1294
- // Objectif 1 : Minimiser le Risque.
1295
- // Le risque est le produit du score et de la durée de la session.
1296
- // Pour un score élevé, l'algo doit choisir un TTL faible pour minimiser ce produit.
1297
- const risk = normalizedScore * ttl;
1298
-
1299
- // Objectif 2 : Minimiser la Friction UX.
1300
- // La friction est l'inverse du TTL. On la pénalise d'autant plus que le score est FAIBLE.
1301
- // (101 - score) assure que pour un score de 1, la pénalité d'un TTL court est maximale.
1302
- // Pour un score de 100, cette pénalité est quasi nulle.
1303
- const friction = (1 / ttl) * (101 - normalizedScore);
1304
-
1305
- // On retourne 2 objectifs avec des facteurs de mise à l'échelle pour les équilibrer.
1306
- return [risk / 1e7, friction * 1e9];
1307
- };
1308
- };
1309
-
1310
- /**
1311
- * Calcule la déviation d'une série de chiffres par rapport à la loi de Benford.
1312
- * Un score élevé indique une distribution non naturelle, potentiellement frauduleuse.
1313
- * @param {string} numberString - Une chaîne de chiffres (ex: "123456789").
1314
- * @returns {number} Un score de déviation (0 = parfait, > 0.15 = suspect).
1315
- */
1316
- Optimization.Operators.benfordTest = (numbers) => {
1317
- if (!Array.isArray(numbers)) {
1318
- // Si l'entrée n'est pas un tableau, on ne peut pas l'analyser.
1319
- return 0;
1320
- }
1321
- const leadingDigits = numbers.map(n => String(n).trim().charAt(0))
1322
- .filter(d => d >= '1' && d <= '9'); // On ne garde que les chiffres de 1 à 9.
1323
-
1324
- if (leadingDigits.length < 10) {
1325
- return 0; // Pas assez de données pour un test fiable
1326
- }
1327
-
1328
- const counts = Array(10).fill(0);
1329
- for (let i = 0; i < leadingDigits.length; i++) {
1330
- counts[parseInt(leadingDigits[i], 10)]++;
1331
- }
1332
-
1333
- // Distribution attendue selon la loi de Benford pour le premier chiffre
1334
- const benfordDistribution = [
1335
- 0, // Index 0 non utilisé
1336
- 30.1, 17.6, 12.5, 9.7, 7.9, 6.7, 5.8, 5.1, 4.6
1337
- ];
1338
-
1339
- let totalDeviation = 0;
1340
- for (let i = 1; i <= 9; i++) {
1341
- const observedFrequency = (counts[i] / leadingDigits.length) * 100;
1342
- const expectedFrequency = benfordDistribution[i];
1343
- totalDeviation += Math.pow(observedFrequency - expectedFrequency, 2);
1344
- }
1345
-
1346
- // Normalise la déviation pour obtenir un score plus interprétable.
1347
- // Cette normalisation est empirique.
1348
- return Math.sqrt(totalDeviation) / 50;
1349
- };
1350
-
1351
-
1352
-
1353
-
1354
- /**
1355
- * Crée un évaluateur multi-objectifs pour trouver les seuils de détection de fraude optimaux.
1356
- * @param {object} config - L'objet de configuration.
1357
- * @param {Array<object>} config.legitimateClicks - Un échantillon de clics considérés comme légitimes.
1358
- * @param {Array<object>} config.fraudulentClicks - Un échantillon de clics identifiés comme frauduleux (ex: honeypots).
1359
- * @returns {function(Array<number>): number[]} Une fonction de fitness qui prend une solution `[minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents]` et retourne les scores des objectifs.
1360
- */
1361
- Optimization.Operators.createFraudThresholdEvaluator = ({
1362
- legitimateClicks,
1363
- fraudulentClicks,
1364
- }) => {
1365
- // Fonction d'aide pour calculer la variance des positions de clic pour une empreinte
1366
- const calculateClickVariance = (clicks) => {
1367
- if (!clicks || clicks.length < 2) return 0;
1368
- const meanX = clicks.reduce((sum, c) => sum + c.clickX, 0) / clicks.length;
1369
- const meanY = clicks.reduce((sum, c) => sum + c.clickY, 0) / clicks.length;
1370
- const variance =
1371
- clicks.reduce(
1372
- (sum, c) =>
1373
- sum + Math.pow(c.clickX - meanX, 2) + Math.pow(c.clickY - meanY, 2),
1374
- 0,
1375
- ) / clicks.length;
1376
- return variance;
1377
- };
1378
-
1379
- // Pré-calculer la variance pour chaque empreinte dans les données
1380
- const getClicksByFingerprint = (clickData) => {
1381
- const grouped = {};
1382
- for (const click of clickData) {
1383
- if (!grouped[click.fingerprint]) grouped[click.fingerprint] = [];
1384
- grouped[click.fingerprint].push(click);
1385
- }
1386
- return grouped;
1387
- };
1388
-
1389
- const legitimateGroups = getClicksByFingerprint(legitimateClicks);
1390
- const fraudulentGroups = getClicksByFingerprint(fraudulentClicks);
1391
-
1392
- return function fraudFitness(solution) {
1393
- const [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents] =
1394
- solution;
1395
-
1396
- // Contraintes pour garder des seuils logiques
1397
- if (
1398
- minTimeToClick < 100 ||
1399
- minTimeToClick > 5000 ||
1400
- maxClickVariance < 1 ||
1401
- maxClickVariance > 10000 ||
1402
- minMouseEntropy < 0 ||
1403
- minMouseEntropy > 1 ||
1404
- minScrollEvents < 0
1405
- ) {
1406
- return [Infinity, Infinity]; // Mauvais score si hors limites
1407
- }
1408
-
1409
- let truePositives = 0; // Bots correctement identifiés
1410
- let falsePositives = 0; // Humains incorrectement bloqués
1411
-
1412
- // Évaluer les clics frauduleux
1413
- for (const fingerprint in fraudulentGroups) {
1414
- const clicks = fraudulentGroups[fingerprint];
1415
- if (!clicks) continue;
1416
- const variance = calculateClickVariance(clicks);
1417
- // Un groupe de clics est frauduleux si l'une des conditions est remplie
1418
- const isTooFast = clicks.some((c) => c.timeToClick < minTimeToClick);
1419
- const isTooUniform = variance < maxClickVariance;
1420
- const hasLowEntropy = clicks.some(
1421
- (c) => c.mouseEntropy < minMouseEntropy,
1422
- );
1423
- const hasFewScrolls = clicks.some(
1424
- (c) => c.scrollEvents < minScrollEvents,
1425
- );
1426
-
1427
- if (isTooFast || isTooUniform || hasLowEntropy || hasFewScrolls) {
1428
- truePositives++;
1429
- }
1430
- }
1431
-
1432
- // Évaluer les clics légitimes
1433
- for (const fingerprint in legitimateGroups) {
1434
- const clicks = legitimateGroups[fingerprint];
1435
- if (!clicks) continue;
1436
- const variance = calculateClickVariance(clicks);
1437
- if (
1438
- clicks.some(
1439
- (c) =>
1440
- c.timeToClick < minTimeToClick ||
1441
- c.mouseEntropy < minMouseEntropy ||
1442
- c.scrollEvents < minScrollEvents,
1443
- ) ||
1444
- variance < maxClickVariance
1445
- ) {
1446
- falsePositives++;
1447
- }
1448
- }
1449
-
1450
- const totalFraudulent = Object.keys(fraudulentGroups).length || 1;
1451
- const totalLegitimate = Object.keys(legitimateGroups).length || 1;
1452
-
1453
- // Objectif 1 : Maximiser la détection de fraude (donc minimiser 1 - taux de détection)
1454
- const objective1 = 1 - truePositives / totalFraudulent;
1455
-
1456
- // Objectif 2 : Minimiser le taux de faux positifs
1457
- const objective2 = falsePositives / totalLegitimate;
1458
-
1459
- return [objective1, objective2];
1460
- };
1461
- };
1462
-
1463
- /**
1464
- * Résout le problème de la détection de fraude en trouvant un front de Pareto de seuils optimaux.
1465
- * @param {object} context - Le contexte contenant les données de clics.
1466
- * @param {Array<object>} context.legitimateClicks - Échantillon de clics légitimes.
1467
- * @param {Array<object>} context.fraudulentClicks - Échantillon de clics frauduleux.
1468
- * @param {object} [options] - Options pour l'algorithme génétique.
1469
- * @returns {Array<{solution: Array<number>, objectives: number[]}>} Le front de Pareto des solutions [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents].
1470
- */
1471
- Optimization.Operators.solveFraudDetection = (context, options = {}) => {
1472
- const fitnessFunction =
1473
- Optimization.Operators.createFraudThresholdEvaluator(context);
1474
-
1475
- // Un "individu" est un tableau de 4 seuils : [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents]
1476
- const createIndividual = () => {
1477
- const minTimeToClick = 100 + Math.random() * 4900; // entre 100ms et 5s
1478
- const maxClickVariance = 1 + Math.random() * 9999; // entre 1 et 10000
1479
- const minMouseEntropy = Math.random() * 0.5; // entre 0 et 0.5
1480
- const minScrollEvents = Math.floor(Math.random() * 10); // entre 0 et 10
1481
- return [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents];
1482
- };
1483
-
1484
- // Croisement : moyenne des seuils des parents
1485
- const crossover = (s1, s2) => {
1486
- return [
1487
- (s1[0] + s2[0]) / 2,
1488
- (s1[1] + s2[1]) / 2,
1489
- (s1[2] + s2[2]) / 2,
1490
- (s1[3] + s2[3]) / 2,
1491
- ];
1492
- };
1493
-
1494
- // Mutation : légère variation aléatoire d'un des seuils
1495
- const mutate = (solution) => {
1496
- const newSolution = [...solution];
1497
- const i = Math.floor(Math.random() * 4);
1498
- // Amplitudes de mutation différentes pour chaque seuil
1499
- const mutationFactors = [500, 1000, 0.1, 2];
1500
- const mutationFactor = mutationFactors[i];
1501
- newSolution[i] += (Math.random() - 0.5) * mutationFactor;
1502
- return newSolution;
1503
- };
1504
-
1505
- const gaOptions = {
1506
- generations: 80,
1507
- populationSize: 60,
1508
- ...options,
1509
- };
1510
-
1511
- return Optimization.geneticAlgorithmMultiObjective(
1512
- createIndividual,
1513
- fitnessFunction,
1514
- crossover,
1515
- mutate,
1516
- gaOptions,
1517
- );
1518
- };
1519
-
1520
- /**
1521
- * Crée un évaluateur multi-objectifs pour l'auto-tuning complet de la configuration de sécurité.
1522
- * Optimise à la fois les seuils, les poids de suspicion et les paramètres de détection de patterns.
1523
- * @param {object} config - L'objet de configuration.
1524
- * @param {Array<object>} config.trafficData - Données de trafic collectées.
1525
- * @returns {function(object): number[]} Une fonction de fitness qui prend une configuration complète et retourne les scores [taux de faux positifs, taux de faux négatifs].
1526
- */
1527
- Optimization.Operators.createFullSecurityConfigEvaluator = ({ trafficData }) => {
1528
- // Un "individu" est un objet de configuration complet
1529
- // { thresholds: { low, medium, high }, weights: { historyScore, ... }, patterns: { velocityThreshold, ... } }
1530
-
1531
- return function fullConfigFitness(config) {
1532
- let falsePositives = 0; // Humains légitimes challengés ou bloqués
1533
- let falseNegatives = 0; // Bots qui sont passés
1534
- let totalHumans = 0;
1535
- let totalBots = 0;
1536
-
1537
- // Simule le calcul du score pour chaque point de données avec la configuration testée
1538
- const calculateScore = (log) => {
1539
- // Cette fonction interne devrait répliquer la logique de `getSuspicionVector`
1540
- // en utilisant les poids et les paramètres de `config`.
1541
- // Pour cet exemple, nous utilisons une version simplifiée.
1542
- let score = 0;
1543
- for (const key in config.weights) {
1544
- score += (log.vector[key] || 0) * config.weights[key];
1545
- }
1546
- return score;
1547
- };
1548
-
1549
- for (const log of trafficData) {
1550
- // On se base sur le comportement observé pour déterminer la nature "réelle" de la requête
1551
- const isLikelyBot = log.type === 'challenge_issued' || log.type === 'request_blocked';
1552
- const isLikelyHuman = log.type === 'request_passed' || log.type === 'challenge_solved';
1553
-
1554
- if (isLikelyBot) {
1555
- totalBots++;
1556
- const score = calculateScore(log);
1557
- // Faux négatif : un bot qui aurait dû être challengé mais ne l'a pas été
1558
- if (score < config.thresholds.low) {
1559
- falseNegatives++;
1560
- }
1561
- } else if (isLikelyHuman) {
1562
- totalHumans++;
1563
- const score = calculateScore(log);
1564
- // Faux positif : un humain qui a été challengé inutilement
1565
- if (score >= config.thresholds.low) {
1566
- falsePositives++;
1567
- }
1568
- }
1569
- }
1570
-
1571
- const falsePositiveRate = totalHumans > 0 ? falsePositives / totalHumans : 0;
1572
- const falseNegativeRate = totalBots > 0 ? falseNegatives / totalBots : 0;
1573
-
1574
- // L'algorithme doit minimiser ces deux objectifs
1575
- return [falsePositiveRate, falseNegativeRate];
1576
- };
1577
- };
1578
-
1579
- /**
1580
- * Résout le problème de l'auto-tuning complet de la configuration de sécurité.
1581
- * @param {object} context - Le contexte contenant les données de trafic.
1582
- * @param {object} [options] - Options pour l'algorithme génétique.
1583
- * @returns {Array<{solution: object, objectives: number[]}>} Le front de Pareto des configurations optimales.
1584
- */
1585
- Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
1586
- const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator(context);
1587
-
1588
- // Un "individu" est un objet de configuration complet
1589
- const createIndividual = () => ({
1590
- thresholds: {
1591
- low: 15 + Math.random() * 20, // 15-35
1592
- medium: 40 + Math.random() * 25, // 40-65
1593
- high: 70 + Math.random() * 20, // 70-90
1594
- },
1595
- weights: {
1596
- historyScore: Math.random(),
1597
- rotationScore: Math.random(),
1598
- headerAnomalyScore: Math.random(),
1599
- requestPatternScore: 0.5 + Math.random(), // Donner plus d'importance aux patterns
1600
- inconsistencyScore: Math.random(),
1601
- honeypotScore: 1.0, // Garder le honeypot à 1.0 est une bonne pratique
1602
- behaviorScore: Math.random(),
1603
- crossLayerInconsistencyScore: Math.random(),
1604
- timeInconsistencyScore: Math.random(),
1605
- },
1606
- patterns: {
1607
- velocityThreshold: 100 + Math.random() * 400, // 100-500ms
1608
- velocityWeight: 10 + Math.random() * 40,
1609
- burstThreshold: 300 + Math.random() * 700, // 300-1000ms
1610
- burstWeight: 20 + Math.random() * 40,
1611
- scrapeThreshold: 500 + Math.random() * 1000, // 500-1500ms
1612
- scrapeWeight: 15 + Math.random() * 35,
1613
- sequenceLength: 3 + Math.floor(Math.random() * 3), // 3-5
1614
- sequenceWeight: 20 + Math.random() * 50,
1615
- regularityThreshold: 50 + Math.random() * 200, // 50-250ms
1616
- regularityWeight: 20 + Math.random() * 40,
1617
- decayFactor: 0.85 + Math.random() * 0.14, // 0.85-0.99
1618
- inactivityReset: 15000 + Math.random() * 45000, // 15s-60s
1619
- }
1620
- });
1621
-
1622
- // Le crossover et la mutation doivent maintenant opérer sur des objets complexes
1623
- const crossover = (c1, c2) => {
1624
- const child = JSON.parse(JSON.stringify(c1)); // Deep copy
1625
- // Croisement pour chaque groupe de paramètres
1626
- for (const key in child.thresholds) {
1627
- child.thresholds[key] = (c1.thresholds[key] + c2.thresholds[key]) / 2;
1628
- }
1629
- for (const key in child.weights) {
1630
- if (key !== 'honeypotScore') { // Ne pas croiser le poids du honeypot
1631
- child.weights[key] = (c1.weights[key] + c2.weights[key]) / 2;
1632
- }
1633
- }
1634
- for (const key in child.patterns) {
1635
- child.patterns[key] = (c1.patterns[key] + c2.patterns[key]) / 2;
1636
- }
1637
- return child;
1638
- };
1639
-
1640
- const mutate = (c) => {
1641
- const newConfig = JSON.parse(JSON.stringify(c));
1642
-
1643
- // --- NOUVELLE LOGIQUE : Sélection de section pondérée ---
1644
- // On donne plus de poids à la mutation des 'patterns' et des 'weights',
1645
- // car ils ont un impact plus direct sur la détection que les seuils.
1646
- const sections = [
1647
- { name: 'patterns', weight: 0.5 }, // 50% de chance
1648
- { name: 'weights', weight: 0.35 }, // 35% de chance
1649
- { name: 'thresholds', weight: 0.15 } // 15% de chance
1650
- ];
1651
- const rand = Math.random();
1652
- let cumulativeWeight = 0;
1653
- let sectionToMutate = 'patterns'; // Fallback
1654
- for (const section of sections) {
1655
- cumulativeWeight += section.weight;
1656
- if (rand < cumulativeWeight) {
1657
- sectionToMutate = section.name;
1658
- break;
1659
- }
1660
- }
1661
-
1662
- const keys = Object.keys(newConfig[sectionToMutate]);
1663
- const keyToMutate = keys[Math.floor(Math.random() * keys.length)];
1664
-
1665
- if (keyToMutate === 'honeypotScore') return newConfig; // Ne pas muter le poids du honeypot
1666
-
1667
- // Appliquer une mutation avec une amplitude variable
1668
- const mutationAmount = (Math.random() - 0.5) * 0.4; // +/- 20%
1669
- newConfig[sectionToMutate][keyToMutate] *= (1 + mutationAmount);
1670
-
1671
- // S'assurer que les valeurs restent dans des limites raisonnables
1672
- if (sectionToMutate === 'weights') {
1673
- newConfig[sectionToMutate][keyToMutate] = Math.max(0, Math.min(1.5, newConfig[sectionToMutate][keyToMutate]));
1674
- }
1675
- if (keyToMutate === 'decayFactor') {
1676
- newConfig.patterns.decayFactor = Math.max(0.8, Math.min(0.999, newConfig.patterns.decayFactor));
1677
- }
1678
- if (keyToMutate.includes('Threshold') || keyToMutate.includes('Reset')) {
1679
- newConfig.patterns[keyToMutate] = Math.max(50, newConfig.patterns[keyToMutate]);
1680
- }
1681
-
1682
- return newConfig;
1683
- };
1684
-
1685
- return Optimization.geneticAlgorithmMultiObjective(
1686
- createIndividual,
1687
- fitnessFunction,
1688
- crossover,
1689
- mutate,
1690
- { generations: 50, populationSize: 50, ...options }
1691
- );
1692
- };
1693
-
1694
- export { Optimization };
1
+ /**
2
+ * @file @/library.js
3
+ * @description Une bibliothèque d'outils basés sur le principe fondamental de la dichotomie (division en deux).
4
+ * Inclut des algorithmes pour les tableaux triés, des structures de données et des solveurs de problèmes conceptuels.
5
+ *
6
+ * @template T
7
+ * @callback Comparator
8
+ * @param {T} element - L'élément du tableau.
9
+ * @param {any} target - La valeur cible.
10
+ * @returns {number} -1 si element < target, 0 si element == target, 1 si element > target.
11
+ *
12
+ */
13
+
14
+ import { Worker } from "node:worker_threads";
15
+ import os from "node:os";
16
+ import crypto from "node:crypto";
17
+
18
+ /**
19
+ * Génère un nombre flottant aléatoire cryptographiquement sûr entre 0 (inclus) et 1 (exclus).
20
+ * @returns {number}
21
+ */
22
+ const secureRandom = () => crypto.randomBytes(4).readUInt32LE(0) / 0x100000000;
23
+
24
+ const Optimization = {
25
+ // eslint-disable-line no-unused-vars
26
+ /**
27
+ * Trouve une bonne solution à un problème d'optimisation en utilisant le Recuit Simulé.
28
+ * Cet algorithme est efficace pour trouver un optimum global dans un grand espace de recherche
29
+ * avec de nombreux optima locaux (plusieurs "pics" ou "vallées").
30
+ * @template TSolution - Le type de la solution (peut être un nombre, un tableau, un objet...).
31
+ * @param {TSolution} initialSolution - Le point de départ de la recherche.
32
+ * @param {function(TSolution): number} evaluator - Fonction qui évalue une solution. L'objectif est de minimiser ce score.
33
+ * @param {function(TSolution): TSolution} neighbor - Fonction qui génère une solution "voisine" aléatoire.
34
+ * @param {number} [initialTemperature=1000] - La température de départ.
35
+ * @param {number} [coolingRate=0.995] - Le taux de refroidissement (proche de 1).
36
+ * @param {number} [maxIterations=10000] - Le nombre total d'itérations.
37
+ * @returns {{solution: TSolution, energy: number}} Le meilleur couple solution/score trouvé.
38
+ */
39
+ simulatedAnnealing(
40
+ initialSolution,
41
+ evaluator,
42
+ neighbor,
43
+ initialTemperature = 1000,
44
+ coolingRate = 0.995,
45
+ maxIterations = 10000,
46
+ ) {
47
+ let currentSolution = initialSolution;
48
+ let currentEnergy = evaluator(currentSolution);
49
+
50
+ let bestSolution = currentSolution;
51
+ let bestEnergy = currentEnergy;
52
+
53
+ let temperature = initialTemperature;
54
+
55
+ for (let i = 0; i < maxIterations; i++) {
56
+ const newSolution = neighbor(currentSolution);
57
+ const newEnergy = evaluator(newSolution);
58
+
59
+ // Calcule la probabilité d'accepter une moins bonne solution.
60
+ const acceptanceProbability = Math.exp(
61
+ (currentEnergy - newEnergy) / temperature,
62
+ );
63
+
64
+ // Décide si on se déplace vers la nouvelle solution.
65
+ if (newEnergy < currentEnergy || secureRandom() < acceptanceProbability) {
66
+ currentSolution = newSolution;
67
+ currentEnergy = newEnergy;
68
+ }
69
+
70
+ // Met à jour la meilleure solution trouvée jusqu'à présent.
71
+ if (currentEnergy < bestEnergy) {
72
+ bestSolution = currentSolution;
73
+ bestEnergy = currentEnergy;
74
+ }
75
+
76
+ // Refroidit la température.
77
+ temperature *= coolingRate;
78
+ }
79
+
80
+ return { solution: bestSolution, energy: bestEnergy };
81
+ },
82
+
83
+ /**
84
+ * Résout un problème d'optimisation en utilisant un Algorithme Génétique.
85
+ * Idéal pour les problèmes complexes où l'espace de recherche est vaste et non-linéaire.
86
+ * @template TChromosome - Le type de la solution (un "chromosome").
87
+ * @param {function(): TChromosome} createIndividual - Fonction pour créer un individu aléatoire.
88
+ * @param {function(TChromosome): number} fitnessFunction - Évalue un individu. L'objectif est de MINIMISER ce score.
89
+ * @param {function(TChromosome, TChromosome): TChromosome} crossover - Croise deux parents pour créer un enfant.
90
+ * @param {function(TChromosome): TChromosome} mutate - Applique une mutation aléatoire à un individu.
91
+ * @param {object} options - Options de l'algorithme.
92
+ * @param {number} [options.populationSize=100] - Taille de la population.
93
+ * @param {number} [options.generations=100] - Nombre de générations à simuler.
94
+ * @param {number} [options.crossoverRate=0.8] - Probabilité de croisement.
95
+ * @param {number} [options.mutationRate=0.1] - Probabilité de mutation.
96
+ * @param {function} [options.selectionFunction] - Fonction de sélection des parents. Par défaut, un tournoi.
97
+ * @param {boolean} [options.returnPopulation=false] - Si true, retourne la population finale au lieu du meilleur individu.
98
+ * @returns {{solution: TChromosome, fitness: number}} Le meilleur individu trouvé.
99
+ */
100
+ geneticAlgorithm(
101
+ createIndividual,
102
+ fitnessFunction,
103
+ crossover,
104
+ mutate,
105
+ options = {},
106
+ ) {
107
+ const populationSize = options.populationSize || 100;
108
+ const generations = options.generations || 100;
109
+ const crossoverRate =
110
+ options.crossoverRate !== undefined ? options.crossoverRate : 0.8;
111
+ const mutationRate =
112
+ options.mutationRate !== undefined ? options.mutationRate : 0.1;
113
+ const selectionFunction =
114
+ options.selectionFunction ||
115
+ this.Operators.createTournamentSelection({ size: 5 });
116
+ const returnPopulation = options.returnPopulation || false;
117
+
118
+ // 1. Initialisation
119
+ // La population est un tableau d'objets { chromosome, fitness }
120
+ // La fitness est calculée une seule fois par individu.
121
+ let population = Array.from({ length: populationSize }, () => {
122
+ const chromosome = createIndividual();
123
+ return { chromosome, fitness: fitnessFunction(chromosome) };
124
+ });
125
+
126
+ // Trier la population initiale pour trouver le meilleur
127
+ population.sort((a, b) => a.fitness - b.fitness);
128
+ let bestOverall = population[0];
129
+
130
+ // 2. Boucle des générations
131
+ for (let gen = 0; gen < generations; gen++) {
132
+ const newPopulation = [];
133
+
134
+ // Élitisme : le meilleur individu de la génération précédente est conservé.
135
+ // Il est déjà à l'index 0 grâce au tri à la fin de la boucle précédente.
136
+ newPopulation.push(population[0]);
137
+
138
+ while (newPopulation.length < populationSize) {
139
+ // 3. Sélection
140
+ const parent1 = selectionFunction(population);
141
+ const parent2 = selectionFunction(population);
142
+
143
+ let offspringChromosome;
144
+ // 4. Croisement
145
+ if (secureRandom() < crossoverRate) {
146
+ offspringChromosome = crossover(
147
+ parent1.chromosome,
148
+ parent2.chromosome,
149
+ );
150
+ } else {
151
+ offspringChromosome = parent1.chromosome;
152
+ }
153
+
154
+ // 5. Mutation
155
+ if (secureRandom() < mutationRate) {
156
+ offspringChromosome = mutate(offspringChromosome);
157
+ }
158
+
159
+ // S'assurer que les opérateurs ont bien retourné un individu
160
+ if (offspringChromosome) {
161
+ newPopulation.push({
162
+ chromosome: offspringChromosome,
163
+ fitness: fitnessFunction(offspringChromosome),
164
+ });
165
+ } else {
166
+ // Si le croisement/mutation échoue, on réinsère un parent pour garder la taille de la population
167
+ newPopulation.push(parent1);
168
+ }
169
+ }
170
+
171
+ population = newPopulation;
172
+
173
+ // Trier la nouvelle population pour la prochaine génération (élitisme) et la mise à jour du meilleur
174
+ population.sort((a, b) => a.fitness - b.fitness);
175
+
176
+ if (population[0].fitness < bestOverall.fitness) {
177
+ bestOverall = population[0];
178
+ }
179
+ }
180
+
181
+ if (returnPopulation) {
182
+ return population;
183
+ }
184
+
185
+ return { solution: bestOverall.chromosome, fitness: bestOverall.fitness };
186
+ },
187
+
188
+ /**
189
+ * Exécute un solveur stochastique plusieurs fois et retourne le meilleur résultat.
190
+ * C'est une méta-heuristique pour augmenter la probabilité de trouver un optimum global
191
+ * en échange d'un temps de calcul plus long.
192
+ * @param {function(): {solution: any, energy?: number, fitness?: number}} solverFunction - Une fonction qui, lorsqu'elle est appelée, exécute un algorithme d'optimisation et retourne un objet résultat.
193
+ * @param {number} numCycles - Le nombre de fois où exécuter le solveur.
194
+ * @param {boolean} [logProgress=false] - Si true, affiche le score de chaque cycle dans la console.
195
+ * @returns {{bestResult: object, stats: {scores: Array<number>, average: number, stdDev: number}}} Le meilleur résultat et des statistiques sur les exécutions.
196
+ */
197
+ runMultiple(solverFunction, numCycles, logProgress = false) {
198
+ let bestResult = null;
199
+ const allScores = [];
200
+
201
+ for (let i = 0; i < numCycles; i++) {
202
+ const currentResult = solverFunction();
203
+
204
+ // Gère les résultats du Recuit Simulé (energy) et des Algorithmes Génétiques (fitness).
205
+ // On suppose que pour les deux, un score plus bas est meilleur.
206
+ const currentScore =
207
+ currentResult.energy !== undefined
208
+ ? currentResult.energy
209
+ : currentResult.fitness;
210
+ allScores.push(currentScore);
211
+
212
+ if (logProgress) {
213
+ console.log(
214
+ ` -> Cycle ${i + 1}/${numCycles}: Score trouvé = ${currentScore.toFixed(2)}`,
215
+ );
216
+ }
217
+
218
+ if (
219
+ !bestResult ||
220
+ currentScore <
221
+ (bestResult.energy !== undefined
222
+ ? bestResult.energy
223
+ : bestResult.fitness)
224
+ ) {
225
+ bestResult = currentResult;
226
+ }
227
+ }
228
+
229
+ // Calcul des statistiques
230
+ const sum = allScores.reduce((a, b) => a + b, 0);
231
+ const average = sum / numCycles;
232
+ const variance =
233
+ allScores.reduce((a, b) => a + Math.pow(b - average, 2), 0) / numCycles;
234
+ const stdDev = Math.sqrt(variance);
235
+
236
+ return {
237
+ bestResult,
238
+ stats: {
239
+ scores: allScores,
240
+ average: average,
241
+ stdDev: stdDev,
242
+ },
243
+ };
244
+ },
245
+
246
+ /**
247
+ * Trouve un minimum local d'une fonction en utilisant l'algorithme de Descente de Gradient.
248
+ * Nécessite que la fonction soit différentiable et que son gradient soit connu.
249
+ * @template TSolution - Le type de la solution (nombre ou tableau de nombres).
250
+ * @param {TSolution} initialSolution - Le point de départ.
251
+ * @param {function(TSolution): TSolution} gradientFunction - Fonction qui calcule le gradient au point donné.
252
+ * @param {object} options - Options de l'algorithme.
253
+ * @param {number} [options.learningRate=0.01] - Le "pas" de la descente.
254
+ * @param {number} [options.maxIterations=1000] - Nombre d'itérations.
255
+ * @param {number} [options.tolerance=1e-6] - Seuil pour arrêter si la solution ne change plus beaucoup.
256
+ * @returns {TSolution} La solution (minimum local) trouvée.
257
+ */
258
+ gradientDescent(initialSolution, gradientFunction, options = {}) {
259
+ const {
260
+ learningRate = 0.01,
261
+ maxIterations = 1000,
262
+ tolerance = 1e-6,
263
+ } = options;
264
+
265
+ let currentSolution = Array.isArray(initialSolution)
266
+ ? [...initialSolution]
267
+ : initialSolution;
268
+
269
+ for (let i = 0; i < maxIterations; i++) {
270
+ const gradient = gradientFunction(currentSolution);
271
+
272
+ if (Array.isArray(currentSolution)) {
273
+ const prevSolution = [...currentSolution];
274
+ for (let j = 0; j < currentSolution.length; j++) {
275
+ currentSolution[j] -= learningRate * gradient[j];
276
+ }
277
+ const change = prevSolution.reduce(
278
+ (sum, val, idx) => sum + Math.abs(val - currentSolution[idx]),
279
+ 0,
280
+ );
281
+ if (change < tolerance) break;
282
+ } else {
283
+ // Cas d'une seule variable (nombre)
284
+ const prevSolution = currentSolution;
285
+ currentSolution -= learningRate * gradient;
286
+ if (Math.abs(prevSolution - currentSolution) < tolerance) break;
287
+ }
288
+ }
289
+ return currentSolution;
290
+ },
291
+
292
+ /**
293
+ * Exécute un solveur stochastique plusieurs fois en parallèle en utilisant un pool de workers pour éviter de surcharger le système.
294
+ * @param {string} solverName - Le nom de la fonction solveur à appeler dans `Optimization.Operators`.
295
+ * @param {Array<any>} baseSolverArgs - Les arguments de base à passer au solveur (sans les données aléatoires qui seront générées par worker).
296
+ * @param {number} numCycles - Le nombre total de cycles à exécuter.
297
+ * @param {boolean} [logProgress=false] - Si true, affiche la progression dans la console.
298
+ * @param {object} [options={}] - Options pour la parallélisation.
299
+ * @param {number} [options.concurrency] - Le nombre de workers à utiliser en parallèle. Par défaut, le nombre de cœurs CPU.
300
+ * @param {function(number): Array<any>} [options.workerDataGenerator] - Une fonction qui, pour chaque cycle (index), génère les arguments spécifiques à passer au solveur. Si non fournie, `baseSolverArgs` est utilisé tel quel.
301
+ * @returns {Promise<{bestResult: object, stats: {scores: Array<number>, average: number, stdDev: number}}>} Le meilleur résultat et des statistiques.
302
+ */
303
+ async runMultipleParallel(
304
+ solverName,
305
+ baseSolverArgs = [], // Default to empty array if not provided
306
+ numCycles,
307
+ logProgress = false,
308
+ options = {},
309
+ ) {
310
+ const concurrency = options.concurrency || os.cpus().length;
311
+ const workerDataGenerator = options.workerDataGenerator;
312
+
313
+ if (logProgress) {
314
+ console.log(
315
+ ` (Utilisation d'un pool de ${concurrency} workers pour ${numCycles} cycles avec le solveur ${solverName})`,
316
+ );
317
+ }
318
+
319
+ const allResults = new Array(numCycles);
320
+ const tasks = Array.from({ length: numCycles }, (_, i) => i);
321
+ let tasksCompleted = 0;
322
+
323
+ const runWorker = async (workerId) => {
324
+ while (tasks.length > 0) {
325
+ const taskIndex = tasks.shift();
326
+ if (taskIndex === undefined) continue;
327
+
328
+ const workerData = {
329
+ solverName,
330
+ solverArgs: workerDataGenerator
331
+ ? workerDataGenerator(taskIndex)
332
+ : baseSolverArgs,
333
+ };
334
+
335
+ const result = await new Promise(async (resolve, reject) => {
336
+ // Le chemin du worker doit être absolu ou relatif au fichier appelant.
337
+ // On utilise import.meta.url pour résoudre le chemin de manière fiable.
338
+ const worker = new Worker(new URL('./optimization.worker.js', import.meta.url), { workerData });
339
+ worker.on("message", resolve);
340
+ worker.on("error", reject);
341
+ worker.on("exit", (code) => {
342
+ if (code !== 0)
343
+ reject(
344
+ new Error(`Worker ${workerId} a terminé avec le code ${code}`),
345
+ );
346
+ });
347
+ });
348
+
349
+ allResults[taskIndex] = result;
350
+ tasksCompleted++;
351
+ if (logProgress) {
352
+ const score =
353
+ result.energy !== undefined ? result.energy : result.fitness;
354
+ console.log(
355
+ ` -> Cycle ${tasksCompleted}/${numCycles} (Worker ${workerId}): Score = ${score.toFixed(2)}`,
356
+ );
357
+ }
358
+ }
359
+ };
360
+
361
+ const workerPromises = Array.from({ length: concurrency }, (_, i) =>
362
+ runWorker(i + 1),
363
+ );
364
+ await Promise.all(workerPromises);
365
+
366
+ // Le reste de la logique est identique à `runMultiple`
367
+ let bestResult = null;
368
+ const allScores = [];
369
+ allResults.forEach((result) => {
370
+ const score =
371
+ result.energy !== undefined ? result.energy : result.fitness;
372
+ allScores.push(score);
373
+ if (
374
+ !bestResult ||
375
+ score <
376
+ (bestResult.energy !== undefined
377
+ ? bestResult.energy
378
+ : bestResult.fitness)
379
+ ) {
380
+ bestResult = result;
381
+ }
382
+ });
383
+
384
+ const sum = allScores.reduce((a, b) => a + b, 0);
385
+ const average = sum / numCycles;
386
+ const variance =
387
+ allScores.reduce((a, b) => a + Math.pow(b - average, 2), 0) / numCycles;
388
+ const stdDev = Math.sqrt(variance);
389
+
390
+ return {
391
+ bestResult,
392
+ stats: { scores: allScores, average, stdDev, concurrency },
393
+ };
394
+ },
395
+ };
396
+
397
+ /**
398
+ * Détermine si la solution A domine la solution B en multi-objectifs (problème de minimisation).
399
+ * @private
400
+ * @param {number[]} objectivesA - Tableau des scores des objectifs pour la solution A.
401
+ * @param {number[]} objectivesB - Tableau des scores des objectifs pour la solution B.
402
+ * @returns {boolean} - True si A domine B.
403
+ */
404
+ function paretoDominates(objectivesA, objectivesB) {
405
+ let aIsBetterInOne = false;
406
+ for (let i = 0; i < objectivesA.length; i++) {
407
+ if (objectivesA[i] > objectivesB[i]) {
408
+ return false; // A est pire sur au moins un objectif, donc ne domine pas.
409
+ }
410
+ if (objectivesA[i] < objectivesB[i]) {
411
+ aIsBetterInOne = true; // A est strictement meilleur sur au moins un objectif.
412
+ }
413
+ }
414
+ return aIsBetterInOne; // A domine B si elle n'est jamais pire et au moins une fois meilleure.
415
+ }
416
+
417
+ /**
418
+ * Trie une population en fronts de Pareto non-dominés (inspiré de NSGA-II).
419
+ * @private
420
+ * @param {Array<{individual: any, objectives: number[]}>} populationWithObjectives - La population à trier.
421
+ * @returns {Array<Array<{individual: any, objectives: number[]}>>} - Un tableau de fronts, où le premier est le meilleur.
422
+ */
423
+ function nonDominatedSort(populationWithObjectives) {
424
+ const fronts = [[]];
425
+ for (const p1 of populationWithObjectives) {
426
+ p1.dominationCount = 0;
427
+ p1.dominatedSolutions = [];
428
+ for (const p2 of populationWithObjectives) {
429
+ if (p1 === p2) continue;
430
+ if (paretoDominates(p1.objectives, p2.objectives)) {
431
+ p1.dominatedSolutions.push(p2);
432
+ } else if (paretoDominates(p2.objectives, p1.objectives)) {
433
+ p1.dominationCount++;
434
+ }
435
+ }
436
+ if (p1.dominationCount === 0) {
437
+ p1.rank = 0;
438
+ fronts[0].push(p1);
439
+ }
440
+ }
441
+
442
+ let i = 0;
443
+ while (fronts[i] && fronts[i].length > 0) {
444
+ const nextFront = [];
445
+ for (const p1 of fronts[i]) {
446
+ for (const p2 of p1.dominatedSolutions) {
447
+ p2.dominationCount--;
448
+ if (p2.dominationCount === 0) {
449
+ p2.rank = i + 1;
450
+ nextFront.push(p2);
451
+ }
452
+ }
453
+ }
454
+ i++;
455
+ if (nextFront.length > 0) {
456
+ fronts[i] = nextFront;
457
+ }
458
+ }
459
+ return fronts;
460
+ }
461
+
462
+ /**
463
+ * Calcule la distance de promiscuité (crowding distance) pour un front, afin de préserver la diversité.
464
+ * @private
465
+ * @param {Array<{individual: any, objectives: number[]}>} front - Le front de Pareto.
466
+ */
467
+ function calculateCrowdingDistance(front) {
468
+ if (front.length === 0) return;
469
+ front.forEach((p) => (p.crowdingDistance = 0));
470
+ const numObjectives = front[0].objectives.length;
471
+
472
+ for (let i = 0; i < numObjectives; i++) {
473
+ front.sort((a, b) => a.objectives[i] - b.objectives[i]);
474
+ const minObj = front[0].objectives[i];
475
+ const maxObj = front[front.length - 1].objectives[i];
476
+
477
+ // Les solutions aux extrémités sont cruciales, on leur donne une distance infinie.
478
+ front[0].crowdingDistance = Infinity;
479
+ front[front.length - 1].crowdingDistance = Infinity;
480
+
481
+ if (maxObj === minObj) continue;
482
+
483
+ for (let j = 1; j < front.length - 1; j++) {
484
+ front[j].crowdingDistance +=
485
+ (front[j + 1].objectives[i] - front[j - 1].objectives[i]) /
486
+ (maxObj - minObj);
487
+ }
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Algorithme génétique multi-objectifs (inspiré de NSGA-II) pour trouver un front de Pareto.
493
+ * @param {function(): any} createIndividual - Fonction qui crée un individu aléatoire.
494
+ * @param {function(any): number[]} fitnessFunction - Fonction qui évalue un individu et retourne un tableau d'objectifs à MINIMISER.
495
+ * @param {function(any, any): any} crossover - Fonction de croisement.
496
+ * @param {function(any): any} mutate - Fonction de mutation.
497
+ * @param {object} options - Options de l'algorithme.
498
+ * @returns {Array<{solution: any, objectives: number[]}>} Le premier front de Pareto (l'ensemble des meilleures solutions de compromis).
499
+ */
500
+ Optimization.geneticAlgorithmMultiObjective = function (
501
+ createIndividual,
502
+ fitnessFunction,
503
+ crossover,
504
+ mutate,
505
+ options = {},
506
+ ) {
507
+ const {
508
+ generations = 150, // Augmenté pour une meilleure convergence
509
+ populationSize = 60, // Augmenté pour plus de diversité
510
+ mutationRate = 0.1,
511
+ } = options;
512
+
513
+ let population = Array.from({ length: populationSize }, () => ({
514
+ individual: createIndividual(),
515
+ }));
516
+ population.forEach((p) => (p.objectives = fitnessFunction(p.individual)));
517
+
518
+ for (let gen = 0; gen < generations; gen++) {
519
+ // 1. Créer une population d'enfants
520
+ const offspring = [];
521
+ for (let i = 0; i < populationSize; i++) {
522
+ // Sélection simple pour l'exemple
523
+ const parent1 = population[Math.floor(secureRandom() * population.length)];
524
+ const parent2 = population[Math.floor(secureRandom() * population.length)];
525
+ let childIndividual = crossover(parent1.individual, parent2.individual);
526
+ if (secureRandom() < mutationRate) {
527
+ childIndividual = mutate(childIndividual);
528
+ }
529
+ const child = { individual: childIndividual };
530
+ child.objectives = fitnessFunction(child.individual);
531
+ offspring.push(child);
532
+ }
533
+
534
+ // 2. Combiner parents et enfants
535
+ const combinedPopulation = [...population, ...offspring];
536
+
537
+ // 3. Trier la population combinée en fronts
538
+ const fronts = nonDominatedSort(combinedPopulation);
539
+
540
+ // 4. Construire la nouvelle population
541
+ const newPopulation = [];
542
+ for (const front of fronts) {
543
+ if (newPopulation.length + front.length <= populationSize) {
544
+ newPopulation.push(...front);
545
+ } else {
546
+ // Si le front est trop grand, on utilise la distance de promiscuité pour choisir les individus les plus diversifiés.
547
+ calculateCrowdingDistance(front);
548
+ front.sort((a, b) => b.crowdingDistance - a.crowdingDistance); // Trier par distance décroissante
549
+ const remaining = populationSize - newPopulation.length;
550
+ newPopulation.push(...front.slice(0, remaining));
551
+ break;
552
+ }
553
+ }
554
+ population = newPopulation;
555
+ }
556
+
557
+ // Retourner le premier front de la population finale
558
+ const finalFronts = nonDominatedSort(population);
559
+ const bestFront = finalFronts.length > 0 ? finalFronts[0] : [];
560
+
561
+ // Filtrer le front pour ne garder que les solutions avec des objectifs uniques
562
+ const uniqueSolutionsMap = new Map();
563
+ for (const p of bestFront) {
564
+ const key = JSON.stringify(p.objectives);
565
+ if (!uniqueSolutionsMap.has(key)) {
566
+ uniqueSolutionsMap.set(key, {
567
+ solution: p.individual,
568
+ objectives: p.objectives,
569
+ });
570
+ }
571
+ }
572
+ return Array.from(uniqueSolutionsMap.values());
573
+ };
574
+
575
+ /**
576
+ * Utilitaires pour les problèmes d'optimisation.
577
+ */
578
+ Optimization.Utils = {
579
+ /** Calcule la distance euclidienne entre deux points (villes). */
580
+ distance: (city1, city2) => Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2)),
581
+
582
+ /** Évalue la distance totale d'un chemin TSP donné. */
583
+ evaluatePathDistance: (cities, path) => {
584
+ let totalDistance = 0;
585
+ for (let i = 0; i < path.length - 1; i++) {
586
+ totalDistance += Optimization.Utils.distance(cities[path[i]], cities[path[i + 1]]);
587
+ }
588
+ totalDistance += Optimization.Utils.distance(cities[path[path.length - 1]], cities[path[0]]); // Retour au départ
589
+ return totalDistance;
590
+ }
591
+ };
592
+
593
+ /**
594
+ * Algorithme d'optimisation CMA-ES (Covariance Matrix Adaptation Evolution Strategy).
595
+ * C'est un algorithme de pointe pour l'optimisation en boîte noire de fonctions non-linéaires et non-convexes.
596
+ * Il est particulièrement efficace pour les problèmes avec des variables continues.
597
+ * @param {function(Array<number>): number} fitnessFunction - La fonction à MINIMISER.
598
+ * @param {Array<number>} initialSolution - Le point de départ de la recherche (un vecteur de nombres).
599
+ * @param {number} initialStepSize - La taille de pas initiale (sigma).
600
+ * @param {object} [options={}] - Options de l'algorithme.
601
+ * @param {number} [options.maxGenerations=100] - Nombre maximum de générations.
602
+ * @param {number} [options.populationSize] - Taille de la population (lambda). Calculée par défaut si non fournie.
603
+ * @param {number} [options.tolerance=1e-6] - Seuil de tolérance pour l'arrêt précoce.
604
+ * @returns {{solution: Array<number>, fitness: number}} La meilleure solution trouvée.
605
+ */
606
+ Optimization.cmaes = function(fitnessFunction, initialSolution, initialStepSize, options = {}) {
607
+ const n = initialSolution.length; // Dimension du problème
608
+
609
+ // --- Paramètres de l'algorithme (stratégie) ---
610
+ const { maxGenerations = 100, tolerance = 1e-6 } = options;
611
+ const populationSize = options.populationSize || (4 + Math.floor(3 * Math.log(n))); // Lambda
612
+ const mu = Math.floor(populationSize / 2); // Nombre de parents pour la recombinaison
613
+
614
+ // Poids de recombinaison
615
+ let weights = Array.from({ length: mu }, (_, i) => Math.log(mu + 0.5) - Math.log(i + 1));
616
+ const sumWeights = weights.reduce((s, w) => s + w, 0);
617
+ weights = weights.map(w => w / sumWeights);
618
+ const muEff = 1 / weights.reduce((s, w) => s + w * w, 0);
619
+
620
+ // Paramètres d'adaptation
621
+ const cc = (4 + muEff / n) / (n + 4 + 2 * muEff / n);
622
+ const cs = (muEff + 2) / (n + muEff + 5);
623
+ const c1 = 2 / (Math.pow(n + 1.3, 2) + muEff);
624
+ const cmu = Math.min(1 - c1, 2 * (muEff - 2 + 1 / muEff) / (Math.pow(n + 2, 2) + muEff));
625
+ const damps = 1 + 2 * Math.max(0, Math.sqrt((muEff - 1) / (n + 1)) - 1) + cs;
626
+
627
+ // --- Variables d'état dynamiques ---
628
+ let mean = [...initialSolution]; // Le centre de la distribution de recherche
629
+ let stepSize = initialStepSize; // Sigma
630
+ let C = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => (i === j ? 1 : 0))); // Matrice de covariance
631
+ let pc = Array(n).fill(0); // Chemin d'évolution pour C
632
+ let ps = Array(n).fill(0); // Chemin d'évolution pour sigma
633
+
634
+ let bestFitness = Infinity;
635
+ let bestSolution = null;
636
+
637
+ // Fonction pour la décomposition de Cholesky (simplifiée, pour matrice symétrique définie positive)
638
+ function cholesky(A) {
639
+ const L = Array.from({ length: n }, () => Array(n).fill(0));
640
+ for (let i = 0; i < n; i++) {
641
+ for (let j = 0; j <= i; j++) {
642
+ let sum = 0;
643
+ for (let k = 0; k < j; k++) {
644
+ sum += L[i][k] * L[j][k];
645
+ }
646
+ if (i === j) {
647
+ const val = A[i][i] - sum;
648
+ if (val < 0) return null; // Non définie positive
649
+ L[i][j] = Math.sqrt(val);
650
+ } else {
651
+ if (L[j][j] === 0) return null;
652
+ L[i][j] = (A[i][j] - sum) / L[j][j];
653
+ }
654
+ }
655
+ }
656
+ return L;
657
+ }
658
+
659
+ for (let gen = 0; gen < maxGenerations; gen++) {
660
+ // 1. Échantillonnage de la nouvelle population
661
+ const population = [];
662
+ const arx = []; // Vecteurs de recherche
663
+ const L = cholesky(C);
664
+ if (!L) {
665
+ console.warn("[CMA-ES] La matrice de covariance n'est plus définie positive. Arrêt.");
666
+ break;
667
+ }
668
+
669
+ for (let i = 0; i < populationSize; i++) {
670
+ const z = Array.from({ length: n }, () => secureRandom() * 2 - 1); // Vecteur normal standard
671
+ const y = Array(n).fill(0); // z transformé par L
672
+ for (let r = 0; r < n; r++) {
673
+ for (let c = 0; c < n; c++) {
674
+ y[r] += L[r][c] * z[c];
675
+ }
676
+ }
677
+ arx.push(y);
678
+ const individual = mean.map((m, j) => m + stepSize * y[j]);
679
+ population.push({ individual, fitness: fitnessFunction(individual) });
680
+ }
681
+
682
+ // 2. Trier et sélectionner les meilleurs
683
+ population.sort((a, b) => a.fitness - b.fitness);
684
+ const parents = population.slice(0, mu);
685
+
686
+ if (parents[0].fitness < bestFitness) {
687
+ bestFitness = parents[0].fitness;
688
+ bestSolution = parents[0].individual;
689
+ }
690
+
691
+ // 3. Mise à jour des variables d'état
692
+ const oldMean = [...mean];
693
+ const y_w = Array(n).fill(0);
694
+ for (let j = 0; j < n; j++) {
695
+ for (let i = 0; i < mu; i++) {
696
+ const parentIndex = population.indexOf(parents[i]);
697
+ y_w[j] += weights[i] * arx[parentIndex][j];
698
+ }
699
+ }
700
+ mean = oldMean.map((m, i) => m + stepSize * y_w[i]);
701
+
702
+ // 4. Adaptation des chemins d'évolution
703
+ const C_inv_sqrt = cholesky(C); // Simplification, devrait être l'inverse de la racine
704
+ const C_inv_sqrt_y_w = y_w; // Approximation
705
+ ps = ps.map((p, i) => (1 - cs) * p + Math.sqrt(cs * (2 - cs) * muEff) * C_inv_sqrt_y_w[i]);
706
+
707
+ const hsig = Math.sqrt(ps.reduce((s, v) => s + v*v, 0)) / (1 - Math.pow(1 - cs, 2 * (gen + 1))) / n < 1.4 + 2 / (n + 1);
708
+ pc = pc.map((p, i) => (1 - cc) * p + (hsig ? Math.sqrt(cc * (2 - cc) * muEff) * y_w[i] : 0));
709
+
710
+ // 5. Adaptation de la matrice de covariance C
711
+ let rankOneUpdate = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => c1 * pc[i] * pc[j]));
712
+ let rankMuUpdate = Array.from({ length: n }, () => Array(n).fill(0));
713
+ for (let k = 0; k < mu; k++) {
714
+ const y_k = arx[population.indexOf(parents[k])];
715
+ for (let i = 0; i < n; i++) {
716
+ for (let j = 0; j < n; j++) {
717
+ rankMuUpdate[i][j] += cmu * weights[k] * y_k[i] * y_k[j];
718
+ }
719
+ }
720
+ }
721
+ C = C.map((row, i) => row.map((val, j) => (1 - c1 - cmu) * val + rankOneUpdate[i][j] + rankMuUpdate[i][j]));
722
+
723
+ // 6. Adaptation de la taille de pas (sigma)
724
+ stepSize *= Math.exp((cs / damps) * (Math.sqrt(ps.reduce((s, v) => s + v*v, 0)) / Math.sqrt(n) - 1));
725
+ }
726
+
727
+ return { solution: bestSolution, fitness: bestFitness };
728
+ };
729
+
730
+ /**
731
+ * @namespace Optimization.Operators
732
+ * @description Une bibliothèque de "fabriques d'évaluateurs" pour des problèmes d'optimisation complexes,
733
+ * souvent multidimensionnels, à utiliser avec les algorithmes de `Optimization` (Recuit Simulé, Algorithmes Génétiques, etc.).
734
+ */
735
+ Optimization.Operators = {}; // Création du namespace
736
+
737
+ /**
738
+ * Crée une fonction de sélection par tournoi pour un algorithme génétique.
739
+ * @param {object} [options] - Options pour le tournoi.
740
+ * @param {number} [options.size=5] - Le nombre de participants par tournoi.
741
+ * @returns {function(Array<{chromosome: any, fitness: number}>): {chromosome: any, fitness: number}} Une fonction de sélection.
742
+ */
743
+ Optimization.Operators.createTournamentSelection = (options = {}) => {
744
+ const tournamentSize = options.size || 5;
745
+
746
+ return function tournamentSelection(population) {
747
+ let best = null;
748
+
749
+ for (let i = 0; i < tournamentSize; i++) {
750
+ const individual =
751
+ population[Math.floor(secureRandom() * population.length)];
752
+ if (!best || individual.fitness < best.fitness) {
753
+ best = individual;
754
+ }
755
+ }
756
+ // Retourne le meilleur trouvé. Dans le pire des cas (tous les scores sont Infinity),
757
+ // on retourne le premier candidat sélectionné au lieu de null.
758
+ if (!best) {
759
+ return population[Math.floor(secureRandom() * population.length)];
760
+ }
761
+ return best;
762
+ };
763
+ };
764
+
765
+ /**
766
+ * Crée une matrice de covariance à partir de coefficients de corrélation déclarés.
767
+ * C'est une manière plus intuitive de définir les relations de risque entre les actifs.
768
+ * @param {object} config - L'objet de configuration.
769
+ * @param {Array<{name: string, volatility: number}>} config.assets - La liste des actifs avec leur volatilité.
770
+ * @param {Array<{assets: [string, string], correlation: number}>} config.correlations - Une liste de relations de corrélation.
771
+ * @returns {Array<Array<number>>} La matrice de covariance calculée.
772
+ */
773
+ Optimization.Operators.createCovarianceMatrixFromCorrelations = ({
774
+ assets,
775
+ correlations,
776
+ }) => {
777
+ const n = assets.length;
778
+ const matrix = Array.from({ length: n }, () => Array(n).fill(0));
779
+
780
+ // Créer un map pour un accès rapide aux infos des actifs par leur nom.
781
+ const assetInfo = new Map();
782
+ assets.forEach((asset, index) => {
783
+ assetInfo.set(asset.name, { index, volatility: asset.volatility });
784
+ });
785
+
786
+ // 1. Remplir la diagonale avec les variances (volatilité^2)
787
+ for (let i = 0; i < n; i++) {
788
+ const variance = Math.pow(assets[i].volatility, 2);
789
+ matrix[i][i] = variance;
790
+ }
791
+
792
+ // 2. Remplir les autres cellules avec les covariances calculées
793
+ for (const corr of correlations) {
794
+ const [nameA, nameB] = corr.assets;
795
+ if (!assetInfo.has(nameA) || !assetInfo.has(nameB)) {
796
+ console.warn(
797
+ `Avertissement: L'un des actifs [${nameA}, ${nameB}] n'a pas été trouvé. La corrélation est ignorée.`,
798
+ );
799
+ continue;
800
+ }
801
+
802
+ const infoA = assetInfo.get(nameA);
803
+ const infoB = assetInfo.get(nameB);
804
+
805
+ // Cov(A,B) = Corr(A,B) * Vol(A) * Vol(B)
806
+ const covariance = corr.correlation * infoA.volatility * infoB.volatility;
807
+
808
+ matrix[infoA.index][infoB.index] = covariance;
809
+ matrix[infoB.index][infoA.index] = covariance; // La matrice est symétrique
810
+ }
811
+
812
+ return matrix;
813
+ };
814
+
815
+ // Le "déséquilibre" est la différence absolue entre l'offre et la demande. On veut le minimiser.
816
+ Optimization.Operators.createMarketEquilibriumEvaluator = (
817
+ demandModel,
818
+ supplyModel,
819
+ ) => {
820
+ return function marketImbalance(price) {
821
+ const d = demandModel(price);
822
+ const s = supplyModel(price);
823
+ return Math.abs(d - s);
824
+ };
825
+ };
826
+
827
+ // Un "individu" est un tableau de 3 poids (ex: [0.5, 0.2, 0.3]) qui doivent sommer à 1.
828
+ /**
829
+ * Crée une fonction de fitness pour l'optimisation de portefeuille.
830
+ * @param {object} config - L'objet de configuration.
831
+ * @param {Array<{name: string, expectedReturn: number, volatility: number}>} config.assets - Les actifs disponibles.
832
+ * @param {number} config.maxVolatility - La contrainte de volatilité maximale du portefeuille.
833
+ * @param {Array<Array<number>>} [config.covarianceMatrix] - Matrice de covariance pour un calcul de risque précis.
834
+ * @returns {function(Array<number>): number} Une fonction de fitness qui évalue un portefeuille (tableau de poids).
835
+ */
836
+ Optimization.Operators.createPortfolioAllocator = ({
837
+ assets,
838
+ maxVolatility,
839
+ covarianceMatrix,
840
+ }) => {
841
+ // La fonction de fitness évalue un portefeuille (un tableau de poids).
842
+ // L'objectif est de MINIMISER le score, donc on minimise le rendement NÉGATIF.
843
+ return function portfolioFitness(weights) {
844
+ // Normaliser les poids pour qu'ils somment à 1
845
+ const totalWeight = weights.reduce((sum, w) => sum + w, 0);
846
+ if (totalWeight === 0) return Infinity; // Éviter la division par zéro, score très mauvais
847
+ const normalizedWeights = weights.map((w) => w / totalWeight);
848
+
849
+ let portfolioReturn = normalizedWeights.reduce(
850
+ (sum, w, i) => sum + w * assets[i].expectedReturn,
851
+ 0,
852
+ );
853
+ let portfolioVolatility;
854
+
855
+ if (covarianceMatrix) {
856
+ // Calcul de la volatilité avec la matrice de covariance (plus précis)
857
+ // Volatilité^2 = w' * C * w
858
+ let variance = 0;
859
+ for (let i = 0; i < assets.length; i++) {
860
+ for (let j = 0; j < assets.length; j++) {
861
+ variance +=
862
+ normalizedWeights[i] *
863
+ normalizedWeights[j] *
864
+ covarianceMatrix[i][j];
865
+ }
866
+ }
867
+ portfolioVolatility = Math.sqrt(variance);
868
+ } else {
869
+ // Calcul simplifié (moins précis) : moyenne pondérée des volatilités individuelles.
870
+ portfolioVolatility = normalizedWeights.reduce(
871
+ (sum, w, i) => sum + w * assets[i].volatility,
872
+ 0,
873
+ );
874
+ }
875
+
876
+ for (let i = 0; i < assets.length; i++) {}
877
+
878
+ // Forte pénalité si la contrainte de risque n'est pas respectée.
879
+ if (portfolioVolatility > maxVolatility) {
880
+ return 1000 + (portfolioVolatility - maxVolatility) * 1000; // Pénalité proportionnelle à la violation.
881
+ }
882
+
883
+ // On veut maximiser le rendement, donc on minimise son opposé.
884
+ return -portfolioReturn;
885
+ };
886
+ };
887
+
888
+ /**
889
+ * Crée un solveur complet pour le problème du voyageur de commerce (TSP) en utilisant le Recuit Simulé.
890
+ * Cette fonction factorise la création de l'évaluateur de chemin et de la fonction de voisinage.
891
+ * @param {Array<{x: number, y: number}>} cities - Un tableau d'objets représentant les coordonnées des villes.
892
+ * @param {object} [options] - Options pour l'algorithme de recuit simulé.
893
+ * @returns {{solution: Array<number>, energy: number}} Le chemin optimal (indices des villes) et sa distance.
894
+ */
895
+ Optimization.Operators.solveTSP = (cities, options = {}) => {
896
+ // Fonction interne pour calculer la distance entre deux villes.
897
+ const distance = (city1, city2) =>
898
+ Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2));
899
+
900
+ // Évaluateur : calcule la longueur totale d'un chemin donné.
901
+ const pathEvaluator = (path) => {
902
+ let totalDistance = 0;
903
+ for (let i = 0; i < path.length - 1; i++) {
904
+ totalDistance += distance(cities[path[i]], cities[path[i + 1]]);
905
+ }
906
+ totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); // Retour au départ
907
+ return totalDistance;
908
+ };
909
+
910
+ // Voisinage : génère un chemin voisin en inversant une sous-séquence (heuristique 2-opt).
911
+ const pathNeighbor = (path) => {
912
+ const newPath = [...path];
913
+ let i = Math.floor(secureRandom() * newPath.length);
914
+ let j = Math.floor(secureRandom() * newPath.length);
915
+ if (i === j) j = (j + 1) % newPath.length;
916
+ const [start, end] = [Math.min(i, j), Math.max(i, j)];
917
+
918
+ const segment = newPath.slice(start, end + 1).reverse();
919
+ newPath.splice(start, segment.length, ...segment);
920
+ return newPath;
921
+ };
922
+
923
+ // Solution initiale : un chemin aléatoire.
924
+ const initialPath = Array.from({ length: cities.length }, (_, i) => i).sort(
925
+ () => secureRandom() - 0.5,
926
+ );
927
+
928
+ // Paramètres par défaut pour le TSP, pouvant être surchargés par `options`.
929
+ const saOptions = {
930
+ initialTemperature: 10000,
931
+ coolingRate: 0.999,
932
+ maxIterations: 100000,
933
+ ...options,
934
+ };
935
+
936
+ return Optimization.simulatedAnnealing(
937
+ initialPath,
938
+ pathEvaluator,
939
+ pathNeighbor,
940
+ saOptions.initialTemperature,
941
+ saOptions.coolingRate,
942
+ saOptions.maxIterations,
943
+ );
944
+ };
945
+
946
+ /**
947
+ * Crée un solveur complet pour le problème d'optimisation de portefeuille en utilisant un Algorithme Génétique.
948
+ * @param {Array<{name: string, expectedReturn: number, volatility: number}>} assets - Les actifs disponibles.
949
+ * @param {number} maxVolatility - La contrainte de volatilité maximale du portefeuille.
950
+ * @param {object} [options] - Options pour l'algorithme génétique.
951
+ * @returns {{solution: Array<number>, fitness: number}} L'allocation de poids optimale et le score de fitness associé.
952
+ */
953
+ Optimization.Operators.solvePortfolio = (
954
+ assets,
955
+ maxVolatility,
956
+ options = {},
957
+ ) => {
958
+ // La fonction de fitness est créée par notre opérateur existant.
959
+ const fitnessFunction = Optimization.Operators.createPortfolioAllocator({
960
+ assets,
961
+ maxVolatility,
962
+ covarianceMatrix: options.covarianceMatrix,
963
+ });
964
+
965
+ // Fonctions spécifiques au problème pour l'AG, maintenant encapsulées.
966
+ const createIndividual = () =>
967
+ Array.from({ length: assets.length }, () => secureRandom());
968
+
969
+ const crossover = (p1, p2) => p1.map((w1, i) => (w1 + p2[i]) / 2); // Moyenne des poids
970
+
971
+ const mutate = (p) => {
972
+ const newP = [...p];
973
+ const i = Math.floor(secureRandom() * newP.length);
974
+ newP[i] += (secureRandom() - 0.5) * 0.2; // Mutation douce
975
+ newP[i] = Math.max(0, newP[i]); // Les poids ne peuvent être négatifs
976
+ return newP;
977
+ };
978
+
979
+ const gaOptions = {
980
+ generations: 150,
981
+ populationSize: 100,
982
+ ...options,
983
+ };
984
+
985
+ return Optimization.geneticAlgorithm(
986
+ createIndividual,
987
+ fitnessFunction,
988
+ crossover,
989
+ mutate,
990
+ gaOptions,
991
+ );
992
+ };
993
+
994
+ /**
995
+ * Crée un évaluateur 2D pour trouver la commission de base et le facteur de bonus qualité qui maximisent les revenus de la plateforme.
996
+ * @param {object} config - L'objet de configuration.
997
+ * @param {number} config.totalAdvertiserCredits - Le total des crédits disponibles chez les annonceurs.
998
+ * @param {Array<{qualityScore: number}>} config.websites - Un tableau d'objets représentant les sites monétisés, chacun avec un score de qualité.
999
+ * @returns {function(Array<number>): number} Un évaluateur qui prend une solution `[baseCommission, bonusFactor]` et retourne le revenu NÉGATIF (car les solveurs minimisent).
1000
+ */
1001
+ Optimization.Operators.createAdvancedPlatformRevenueEvaluator = ({
1002
+ totalAdvertiserCredits,
1003
+ websites,
1004
+ }) => {
1005
+ // Modèle de la demande (Annonceurs)
1006
+ // La demande est sensible à la qualité globale de l'inventaire publicitaire.
1007
+ const advertiserDemandModel = (averageSiteQuality) => {
1008
+ // La demande de base est toujours liée aux crédits disponibles.
1009
+ const baseDemand = (totalAdvertiserCredits || 100) * 10;
1010
+ // La demande augmente avec la qualité moyenne des sites.
1011
+ return baseDemand * (1 + averageSiteQuality);
1012
+ };
1013
+
1014
+ // Modèle de l'offre (Webmasters)
1015
+ // L'offre de chaque site dépend de sa rémunération individuelle.
1016
+ const webmasterSupplyModel = (baseCommission, bonusFactor) => {
1017
+ let totalOfferedClicks = 0;
1018
+ const baseSupplyPerSite = 500; // Clics potentiels par site
1019
+
1020
+ for (const site of websites) {
1021
+ // La commission effective est réduite pour les sites de haute qualité.
1022
+ const effectiveCommission = Math.max(
1023
+ 0,
1024
+ baseCommission - site.qualityScore * bonusFactor,
1025
+ );
1026
+ const webmasterPayoutRate = 1 - effectiveCommission;
1027
+
1028
+ // L'offre d'un site est proportionnelle à son taux de rémunération.
1029
+ totalOfferedClicks += baseSupplyPerSite * webmasterPayoutRate;
1030
+ }
1031
+ return totalOfferedClicks;
1032
+ };
1033
+
1034
+ // L'évaluateur pour l'algorithme d'optimisation (Recuit Simulé, etc.)
1035
+ return function revenueEvaluator(solution) {
1036
+ const [baseCommission, bonusFactor] = solution;
1037
+
1038
+ // Contraintes : on pénalise fortement les solutions hors des clous.
1039
+ if (
1040
+ baseCommission < 0.01 ||
1041
+ baseCommission > 0.8 ||
1042
+ bonusFactor < 0 ||
1043
+ bonusFactor > baseCommission
1044
+ ) {
1045
+ return Infinity; // Score très mauvais
1046
+ }
1047
+
1048
+ const averageQuality =
1049
+ websites.length > 0
1050
+ ? websites.reduce((sum, site) => sum + site.qualityScore, 0) /
1051
+ websites.length
1052
+ : 0.5;
1053
+
1054
+ const demand = advertiserDemandModel(averageQuality);
1055
+ const supply = webmasterSupplyModel(baseCommission, bonusFactor);
1056
+
1057
+ const clicks = Math.min(demand, supply);
1058
+ const averageCommission = Math.max(
1059
+ 0,
1060
+ baseCommission - averageQuality * bonusFactor,
1061
+ );
1062
+
1063
+ // On veut MAXIMISER le revenu, donc on MINIMISE son opposé.
1064
+ return -(clicks * averageCommission);
1065
+ };
1066
+ };
1067
+
1068
+ /**
1069
+ * Crée un solveur pour le problème de placement d'infrastructures (Facility Location Problem).
1070
+ * @param {Array<{x: number, y: number}>} customers - Coordonnées des clients.
1071
+ * @param {number} numFacilities - Le nombre d'infrastructures à placer.
1072
+ * @param {{minX: number, maxX: number, minY: number, maxY: number}} bounds - Les limites de la carte où placer les infrastructures.
1073
+ * @param {number} [options.fixedCostPerFacility=0] - Coût fixe pour chaque infrastructure installée.
1074
+ * @param {object} [options] - Options pour le recuit simulé.
1075
+ * @returns {{solution: Array<{x: number, y: number}>, energy: number}} Les coordonnées optimales des infrastructures et le coût total.
1076
+ */
1077
+ Optimization.Operators.solveFacilityLocation = (
1078
+ customers,
1079
+ numFacilities,
1080
+ bounds,
1081
+ options = {},
1082
+ ) => {
1083
+ const fixedCostPerFacility = options.fixedCostPerFacility || 0;
1084
+ const distanceSq = (p1, p2) =>
1085
+ Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2); // On utilise la distance au carré pour l'efficacité
1086
+
1087
+ // Évaluateur : calcule la somme des distances de chaque client à son infrastructure la plus proche.
1088
+ const facilityEvaluator = (facilities) => {
1089
+ let totalConnectionCost = 0;
1090
+ for (const customer of customers) {
1091
+ let minDistanceToCustomer = Infinity;
1092
+ for (const facility of facilities) {
1093
+ const d = distanceSq(customer, facility);
1094
+ if (d < minDistanceToCustomer) {
1095
+ minDistanceToCustomer = d;
1096
+ }
1097
+ }
1098
+ totalConnectionCost += Math.sqrt(minDistanceToCustomer); // On utilise la vraie distance pour le coût
1099
+ }
1100
+ // Le coût total est la somme des coûts de connexion + le coût fixe des infrastructures.
1101
+ return totalConnectionCost + facilities.length * fixedCostPerFacility;
1102
+ };
1103
+
1104
+ // Voisinage : déplace légèrement une infrastructure au hasard.
1105
+ const facilityNeighbor = (facilities) => {
1106
+ const newFacilities = facilities.map((f) => ({ ...f }));
1107
+ const i = Math.floor(secureRandom() * numFacilities);
1108
+ const moveX = (secureRandom() - 0.5) * (bounds.maxX - bounds.minX) * 0.1;
1109
+ const moveY = (secureRandom() - 0.5) * (bounds.maxY - bounds.minY) * 0.1;
1110
+
1111
+ newFacilities[i].x = Math.max(
1112
+ bounds.minX,
1113
+ Math.min(bounds.maxX, newFacilities[i].x + moveX),
1114
+ );
1115
+ newFacilities[i].y = Math.max(
1116
+ bounds.minY,
1117
+ Math.min(bounds.maxY, newFacilities[i].y + moveY),
1118
+ );
1119
+
1120
+ return newFacilities;
1121
+ };
1122
+
1123
+ // Solution initiale : place les infrastructures au hasard sur la carte.
1124
+ const initialFacilities = Array.from({ length: numFacilities }, () => ({
1125
+ x: bounds.minX + secureRandom() * (bounds.maxX - bounds.minX),
1126
+ y: bounds.minY + secureRandom() * (bounds.maxY - bounds.minY),
1127
+ }));
1128
+
1129
+ const saOptions = {
1130
+ initialTemperature: 100000,
1131
+ coolingRate: 0.999,
1132
+ maxIterations: 50000,
1133
+ ...options,
1134
+ };
1135
+
1136
+ const result = Optimization.simulatedAnnealing(
1137
+ initialFacilities,
1138
+ facilityEvaluator,
1139
+ facilityNeighbor,
1140
+ saOptions.initialTemperature,
1141
+ saOptions.coolingRate,
1142
+ saOptions.maxIterations,
1143
+ );
1144
+
1145
+ return result;
1146
+ };
1147
+
1148
+ // --- SOLUTION : Définir un coût de base pour un clic ---
1149
+ // 1 PRIM'S = 1 clic
1150
+ const BASE_CLICK_COST = 1;
1151
+ /**
1152
+ * Crée un évaluateur multi-objectifs pour déterminer le Coût Par Clic (CPC) optimal.
1153
+ * @param {object} config - L'objet de configuration.
1154
+ * @param {object} config.advertiser - L'annonceur qui paie le clic.
1155
+ * @param {object} config.ad - L'annonce qui a été cliquée.
1156
+ * @param {Array<object>} config.competingAds - Les autres annonces ciblant les mêmes mots-clés.
1157
+ * @param {object} config.website - Le site sur lequel le clic a eu lieu.
1158
+ * @param {object} config.platformParams - Les paramètres de la plateforme (taux de commission, etc.).
1159
+ * @param {number} config.estimatedImpressions - Le nombre d'impressions quotidiennes estimées pour ce contexte.
1160
+ */
1161
+ Optimization.Operators.createOptimalCPCEvaluator = (context) => {
1162
+ const { optimalBaseCommission, optimalBonusFactor } = context.platformParams;
1163
+ const websiteQualityScore = (context.website?.relevanceScore || 50) / 100;
1164
+
1165
+ // Le taux de commission effectif pour ce site
1166
+ const effectiveCommissionRate = Math.max(
1167
+ 0,
1168
+ optimalBaseCommission - websiteQualityScore * optimalBonusFactor,
1169
+ );
1170
+
1171
+ // Modèle de la demande : combien de clics l'annonceur peut-il s'offrir ?
1172
+ const advertiserDemand = (cpc) => {
1173
+ if (cpc <= 0) return Infinity;
1174
+ return (context.advertiser.credits || 0) / cpc;
1175
+ };
1176
+
1177
+ // --- SOLUTION : Utiliser l'offre réelle et la concurrence ---
1178
+ // L'offre est maintenant le nombre d'impressions estimées, un chiffre concret.
1179
+ const supply = context.estimatedImpressions || 1; // Fallback à 1 pour éviter la division par zéro.
1180
+
1181
+ // Le facteur de concurrence augmente le prix s'il y a plus de monde sur le même créneau.
1182
+ // Formule simple : 1 + (0.1 * nombre de concurrents), avec un plafond.
1183
+ const competitionFactor = Math.min(
1184
+ 2.5,
1185
+ 1 + context.competingAds.length * 0.1,
1186
+ );
1187
+
1188
+ return function cpcFitness(cpcMultiplier) {
1189
+ // Le CPC final est le coût de base, ajusté par le multiplicateur de l'algo et la concurrence.
1190
+ const adjustedCPC = BASE_CLICK_COST * cpcMultiplier * competitionFactor;
1191
+ if (adjustedCPC < 0.1) return [Infinity, Infinity, Infinity]; // CPC minimum
1192
+
1193
+ // La demande de l'annonceur est calculée avec le CPC ajusté.
1194
+ const demand = advertiserDemand(adjustedCPC);
1195
+
1196
+ // Le nombre de clics est le minimum de l'offre et de la demande.
1197
+ const estimatedClicks = Math.min(demand, supply);
1198
+
1199
+ // Objectif 1 : Maximiser le revenu de la plateforme (donc minimiser son opposé)
1200
+ const platformRevenue =
1201
+ estimatedClicks * adjustedCPC * effectiveCommissionRate;
1202
+
1203
+ // Objectif 2 : Maximiser la valeur pour l'annonceur (nombre de clics, donc minimiser son opposé)
1204
+ const advertiserValue = estimatedClicks;
1205
+
1206
+ // Objectif 3 : Minimiser le déséquilibre du marché (offre vs demande)
1207
+ const marketImbalance = Math.abs(demand - supply);
1208
+
1209
+ return [-platformRevenue, -advertiserValue, marketImbalance];
1210
+ };
1211
+ };
1212
+
1213
+ /**
1214
+ * Applique le facteur de concurrence au CPC de base.
1215
+ * @private
1216
+ * @param {number} baseCpc - Le CPC issu de l'algorithme génétique.
1217
+ * @param {Array<object>} competingAds - Les annonces concurrentes.
1218
+ * @returns {number} Le CPC final ajusté.
1219
+ */
1220
+ function applyCompetitionFactor(baseCpc, competingAds) {
1221
+ // Formule simple : 1 + (0.1 * nombre de concurrents), avec un plafond pour éviter l'explosion des prix.
1222
+ const competitionFactor = Math.min(
1223
+ 2.5,
1224
+ 1 + (competingAds || []).length * 0.1,
1225
+ );
1226
+ const finalCpc = baseCpc * competitionFactor;
1227
+ // On s'assure de ne jamais descendre sous un seuil minimal.
1228
+ return Math.max(0.1, finalCpc);
1229
+ }
1230
+ /**
1231
+ * Résout le problème du CPC optimal en utilisant un algorithme génétique multi-objectifs.
1232
+ * @param {object} context - Le contexte nécessaire pour l'évaluation (advertiser, ad, etc.).
1233
+ * @param {object} [options] - Options pour l'algorithme génétique.
1234
+ * @returns {Array<{solution: number, objectives: number[]}>} Le front de Pareto des solutions CPC.
1235
+ */
1236
+ Optimization.Operators.solveOptimalCPC = (context, options = {}) => {
1237
+ const fitnessFunction =
1238
+ Optimization.Operators.createOptimalCPCEvaluator(context);
1239
+
1240
+ // Un "individu" est simplement une valeur de CPC.
1241
+ const createIndividual = () => {
1242
+ // Le CPC peut varier, par exemple, entre 0.5 et 5 PRIM'S.
1243
+ return 0.5 + secureRandom() * 4.5;
1244
+ };
1245
+
1246
+ // Croisement : moyenne des CPC des parents.
1247
+ const crossover = (cpc1, cpc2) => {
1248
+ return (cpc1 + cpc2) / 2;
1249
+ };
1250
+
1251
+ // Mutation : légère variation aléatoire du CPC.
1252
+ const mutate = (cpc) => {
1253
+ const newCpc = cpc + (secureRandom() - 0.5) * 0.5;
1254
+ return Math.max(0.1, newCpc); // Assurer un CPC minimum.
1255
+ };
1256
+
1257
+ const gaOptions = {
1258
+ generations: 50,
1259
+ populationSize: 40,
1260
+ ...options,
1261
+ };
1262
+
1263
+ // On utilise l'algorithme génétique multi-objectifs pour obtenir le front de Pareto.
1264
+ const paretoFront = Optimization.geneticAlgorithmMultiObjective(
1265
+ createIndividual,
1266
+ fitnessFunction,
1267
+ crossover,
1268
+ mutate,
1269
+ gaOptions,
1270
+ );
1271
+
1272
+ // --- SOLUTION : Appliquer le facteur de concurrence sur les solutions finales ---
1273
+ return paretoFront.map((result) => {
1274
+ const cpcMultiplier = result.solution;
1275
+ // Le CPC final est calculé ici, en dehors de la fonction de fitness.
1276
+ const finalCpc = applyCompetitionFactor(
1277
+ BASE_CLICK_COST * cpcMultiplier,
1278
+ context.competingAds,
1279
+ );
1280
+ return {
1281
+ ...result,
1282
+ solution: Math.max(0.1, finalCpc), // On s'assure de ne jamais descendre sous le plancher absolu.
1283
+ };
1284
+ });
1285
+ };
1286
+
1287
+ /**
1288
+ * Crée un évaluateur multi-objectifs pour trouver le TTL (Time-To-Live) optimal pour un ticket de sécurité.
1289
+ * @param {object} context - L'objet de configuration.
1290
+ * @param {number} context.suspicionScore - Le score de suspicion de l'utilisateur (0-100).
1291
+ * @returns {function(number): number[]} Une fonction de fitness qui prend un TTL (en ms) et retourne les scores des objectifs [risque, friction].
1292
+ */
1293
+ Optimization.Operators.createOptimalTtlEvaluator = ({ suspicionScore }) => {
1294
+ // On normalise le score pour qu'il soit plus impactant dans le calcul du risque.
1295
+ const normalizedScore = Math.max(1, suspicionScore);
1296
+
1297
+ return function ttlFitness(ttl) {
1298
+ // Contraintes : un TTL doit être dans une plage raisonnable (ex: 5min à 24h)
1299
+ if (ttl < 300000 || ttl > 86400000) return [Infinity, Infinity];
1300
+
1301
+ // Objectif 1 : Minimiser le Risque.
1302
+ // Le risque est le produit du score et de la durée de la session.
1303
+ // Pour un score élevé, l'algo doit choisir un TTL faible pour minimiser ce produit.
1304
+ const risk = normalizedScore * ttl;
1305
+
1306
+ // Objectif 2 : Minimiser la Friction UX.
1307
+ // La friction est l'inverse du TTL. On la pénalise d'autant plus que le score est FAIBLE.
1308
+ // (101 - score) assure que pour un score de 1, la pénalité d'un TTL court est maximale.
1309
+ // Pour un score de 100, cette pénalité est quasi nulle.
1310
+ const friction = (1 / ttl) * (101 - normalizedScore);
1311
+
1312
+ // On retourne 2 objectifs avec des facteurs de mise à l'échelle pour les équilibrer.
1313
+ return [risk / 1e7, friction * 1e9];
1314
+ };
1315
+ };
1316
+
1317
+ /**
1318
+ * Calcule la déviation d'une série de chiffres par rapport à la loi de Benford.
1319
+ * Un score élevé indique une distribution non naturelle, potentiellement frauduleuse.
1320
+ * @param {string} numberString - Une chaîne de chiffres (ex: "123456789").
1321
+ * @returns {number} Un score de déviation (0 = parfait, > 0.15 = suspect).
1322
+ */
1323
+ Optimization.Operators.benfordTest = (numbers) => {
1324
+ if (!Array.isArray(numbers)) {
1325
+ // Si l'entrée n'est pas un tableau, on ne peut pas l'analyser.
1326
+ return 0;
1327
+ }
1328
+ const leadingDigits = numbers.map(n => String(n).trim().charAt(0))
1329
+ .filter(d => d >= '1' && d <= '9'); // On ne garde que les chiffres de 1 à 9.
1330
+
1331
+ if (leadingDigits.length < 10) {
1332
+ return 0; // Pas assez de données pour un test fiable
1333
+ }
1334
+
1335
+ const counts = Array(10).fill(0);
1336
+ for (let i = 0; i < leadingDigits.length; i++) {
1337
+ counts[parseInt(leadingDigits[i], 10)]++;
1338
+ }
1339
+
1340
+ // Distribution attendue selon la loi de Benford pour le premier chiffre
1341
+ const benfordDistribution = [
1342
+ 0, // Index 0 non utilisé
1343
+ 30.1, 17.6, 12.5, 9.7, 7.9, 6.7, 5.8, 5.1, 4.6
1344
+ ];
1345
+
1346
+ let totalDeviation = 0;
1347
+ for (let i = 1; i <= 9; i++) {
1348
+ const observedFrequency = (counts[i] / leadingDigits.length) * 100;
1349
+ const expectedFrequency = benfordDistribution[i];
1350
+ totalDeviation += Math.pow(observedFrequency - expectedFrequency, 2);
1351
+ }
1352
+
1353
+ // Normalise la déviation pour obtenir un score plus interprétable.
1354
+ // Cette normalisation est empirique.
1355
+ return Math.sqrt(totalDeviation) / 50;
1356
+ };
1357
+
1358
+
1359
+
1360
+
1361
+ /**
1362
+ * Crée un évaluateur multi-objectifs pour trouver les seuils de détection de fraude optimaux.
1363
+ * @param {object} config - L'objet de configuration.
1364
+ * @param {Array<object>} config.legitimateClicks - Un échantillon de clics considérés comme légitimes.
1365
+ * @param {Array<object>} config.fraudulentClicks - Un échantillon de clics identifiés comme frauduleux (ex: honeypots).
1366
+ * @returns {function(Array<number>): number[]} Une fonction de fitness qui prend une solution `[minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents]` et retourne les scores des objectifs.
1367
+ */
1368
+ Optimization.Operators.createFraudThresholdEvaluator = ({
1369
+ legitimateClicks,
1370
+ fraudulentClicks,
1371
+ }) => {
1372
+ // Fonction d'aide pour calculer la variance des positions de clic pour une empreinte
1373
+ const calculateClickVariance = (clicks) => {
1374
+ if (!clicks || clicks.length < 2) return 0;
1375
+ const meanX = clicks.reduce((sum, c) => sum + c.clickX, 0) / clicks.length;
1376
+ const meanY = clicks.reduce((sum, c) => sum + c.clickY, 0) / clicks.length;
1377
+ const variance =
1378
+ clicks.reduce(
1379
+ (sum, c) =>
1380
+ sum + Math.pow(c.clickX - meanX, 2) + Math.pow(c.clickY - meanY, 2),
1381
+ 0,
1382
+ ) / clicks.length;
1383
+ return variance;
1384
+ };
1385
+
1386
+ // Pré-calculer la variance pour chaque empreinte dans les données
1387
+ const getClicksByFingerprint = (clickData) => {
1388
+ const grouped = {};
1389
+ for (const click of clickData) {
1390
+ if (!grouped[click.fingerprint]) grouped[click.fingerprint] = [];
1391
+ grouped[click.fingerprint].push(click);
1392
+ }
1393
+ return grouped;
1394
+ };
1395
+
1396
+ const legitimateGroups = getClicksByFingerprint(legitimateClicks);
1397
+ const fraudulentGroups = getClicksByFingerprint(fraudulentClicks);
1398
+
1399
+ return function fraudFitness(solution) {
1400
+ const [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents] =
1401
+ solution;
1402
+
1403
+ // Contraintes pour garder des seuils logiques
1404
+ if (
1405
+ minTimeToClick < 100 ||
1406
+ minTimeToClick > 5000 ||
1407
+ maxClickVariance < 1 ||
1408
+ maxClickVariance > 10000 ||
1409
+ minMouseEntropy < 0 ||
1410
+ minMouseEntropy > 1 ||
1411
+ minScrollEvents < 0
1412
+ ) {
1413
+ return [Infinity, Infinity]; // Mauvais score si hors limites
1414
+ }
1415
+
1416
+ let truePositives = 0; // Bots correctement identifiés
1417
+ let falsePositives = 0; // Humains incorrectement bloqués
1418
+
1419
+ // Évaluer les clics frauduleux
1420
+ for (const fingerprint in fraudulentGroups) {
1421
+ const clicks = fraudulentGroups[fingerprint];
1422
+ if (!clicks) continue;
1423
+ const variance = calculateClickVariance(clicks);
1424
+ // Un groupe de clics est frauduleux si l'une des conditions est remplie
1425
+ const isTooFast = clicks.some((c) => c.timeToClick < minTimeToClick);
1426
+ const isTooUniform = variance < maxClickVariance;
1427
+ const hasLowEntropy = clicks.some(
1428
+ (c) => c.mouseEntropy < minMouseEntropy,
1429
+ );
1430
+ const hasFewScrolls = clicks.some(
1431
+ (c) => c.scrollEvents < minScrollEvents,
1432
+ );
1433
+
1434
+ if (isTooFast || isTooUniform || hasLowEntropy || hasFewScrolls) {
1435
+ truePositives++;
1436
+ }
1437
+ }
1438
+
1439
+ // Évaluer les clics légitimes
1440
+ for (const fingerprint in legitimateGroups) {
1441
+ const clicks = legitimateGroups[fingerprint];
1442
+ if (!clicks) continue;
1443
+ const variance = calculateClickVariance(clicks);
1444
+ if (
1445
+ clicks.some(
1446
+ (c) =>
1447
+ c.timeToClick < minTimeToClick ||
1448
+ c.mouseEntropy < minMouseEntropy ||
1449
+ c.scrollEvents < minScrollEvents,
1450
+ ) ||
1451
+ variance < maxClickVariance
1452
+ ) {
1453
+ falsePositives++;
1454
+ }
1455
+ }
1456
+
1457
+ const totalFraudulent = Object.keys(fraudulentGroups).length || 1;
1458
+ const totalLegitimate = Object.keys(legitimateGroups).length || 1;
1459
+
1460
+ // Objectif 1 : Maximiser la détection de fraude (donc minimiser 1 - taux de détection)
1461
+ const objective1 = 1 - truePositives / totalFraudulent;
1462
+
1463
+ // Objectif 2 : Minimiser le taux de faux positifs
1464
+ const objective2 = falsePositives / totalLegitimate;
1465
+
1466
+ return [objective1, objective2];
1467
+ };
1468
+ };
1469
+
1470
+ /**
1471
+ * Résout le problème de la détection de fraude en trouvant un front de Pareto de seuils optimaux.
1472
+ * @param {object} context - Le contexte contenant les données de clics.
1473
+ * @param {Array<object>} context.legitimateClicks - Échantillon de clics légitimes.
1474
+ * @param {Array<object>} context.fraudulentClicks - Échantillon de clics frauduleux.
1475
+ * @param {object} [options] - Options pour l'algorithme génétique.
1476
+ * @returns {Array<{solution: Array<number>, objectives: number[]}>} Le front de Pareto des solutions [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents].
1477
+ */
1478
+ Optimization.Operators.solveFraudDetection = (context, options = {}) => {
1479
+ const fitnessFunction =
1480
+ Optimization.Operators.createFraudThresholdEvaluator(context);
1481
+
1482
+ // Un "individu" est un tableau de 4 seuils : [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents]
1483
+ const createIndividual = () => {
1484
+ const minTimeToClick = 100 + secureRandom() * 4900; // entre 100ms et 5s
1485
+ const maxClickVariance = 1 + secureRandom() * 9999; // entre 1 et 10000
1486
+ const minMouseEntropy = secureRandom() * 0.5; // entre 0 et 0.5
1487
+ const minScrollEvents = Math.floor(secureRandom() * 10); // entre 0 et 10
1488
+ return [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents];
1489
+ };
1490
+
1491
+ // Croisement : moyenne des seuils des parents
1492
+ const crossover = (s1, s2) => {
1493
+ return [
1494
+ (s1[0] + s2[0]) / 2,
1495
+ (s1[1] + s2[1]) / 2,
1496
+ (s1[2] + s2[2]) / 2,
1497
+ (s1[3] + s2[3]) / 2,
1498
+ ];
1499
+ };
1500
+
1501
+ // Mutation : légère variation aléatoire d'un des seuils
1502
+ const mutate = (solution) => {
1503
+ const newSolution = [...solution];
1504
+ const i = Math.floor(secureRandom() * 4);
1505
+ // Amplitudes de mutation différentes pour chaque seuil
1506
+ const mutationFactors = [500, 1000, 0.1, 2];
1507
+ const mutationFactor = mutationFactors[i];
1508
+ newSolution[i] += (secureRandom() - 0.5) * mutationFactor;
1509
+ return newSolution;
1510
+ };
1511
+
1512
+ const gaOptions = {
1513
+ generations: 80,
1514
+ populationSize: 60,
1515
+ ...options,
1516
+ };
1517
+
1518
+ return Optimization.geneticAlgorithmMultiObjective(
1519
+ createIndividual,
1520
+ fitnessFunction,
1521
+ crossover,
1522
+ mutate,
1523
+ gaOptions,
1524
+ );
1525
+ };
1526
+
1527
+ /**
1528
+ * Crée un évaluateur multi-objectifs pour l'auto-tuning complet de la configuration de sécurité.
1529
+ * Optimise à la fois les seuils, les poids de suspicion et les paramètres de détection de patterns.
1530
+ * @param {object} config - L'objet de configuration.
1531
+ * @param {Array<object>} config.trafficData - Données de trafic collectées.
1532
+ * @returns {function(object): number[]} Une fonction de fitness qui prend une configuration complète et retourne les scores [taux de faux positifs, taux de faux négatifs].
1533
+ */
1534
+ Optimization.Operators.createFullSecurityConfigEvaluator = ({ trafficData }) => {
1535
+ // Un "individu" est un objet de configuration complet
1536
+ // { thresholds: { low, medium, high }, weights: { historyScore, ... }, patterns: { velocityThreshold, ... } }
1537
+
1538
+ return function fullConfigFitness(config) {
1539
+ let falsePositives = 0; // Humains légitimes challengés ou bloqués
1540
+ let falseNegatives = 0; // Bots qui sont passés
1541
+ let totalHumans = 0;
1542
+ let totalBots = 0;
1543
+ let totalChallenges = 0;
1544
+ let totalChallengeCost = 0; // NOUVEAU: Coût cumulé des challenges
1545
+
1546
+ // Simule le calcul du score pour chaque point de données avec la configuration testée
1547
+ const calculateScore = (log) => {
1548
+ // Cette fonction interne devrait répliquer la logique de `getSuspicionVector`
1549
+ // en utilisant les poids et les paramètres de `config`.
1550
+ // Pour cet exemple, nous utilisons une version simplifiée.
1551
+ let score = 0;
1552
+ for (const key in config.weights) {
1553
+ score += (log.vector[key] || 0) * config.weights[key];
1554
+ }
1555
+ return score;
1556
+ };
1557
+
1558
+ // Pondération de la confiance : un challenge résolu est un signal humain plus fort.
1559
+ const confidenceWeights = {
1560
+ request_passed: 0.7,
1561
+ challenge_issued: 1.0,
1562
+ request_blocked: 1.0,
1563
+ challenge_solved: 1.5, // Signal "humain" de très haute confiance
1564
+ trap_triggered: 2.0, // Signal "bot" de confiance absolue
1565
+ };
1566
+
1567
+ for (const log of trafficData) {
1568
+ const confidence = confidenceWeights[log.type] || 1.0;
1569
+
1570
+ // On se base sur le comportement observé pour déterminer la nature "réelle" de la requête
1571
+ const isLikelyBot = log.type === 'challenge_issued' || log.type === 'request_blocked';
1572
+ const isLikelyHuman = log.type === 'request_passed' || log.type === 'challenge_solved';
1573
+
1574
+ if (isLikelyBot) {
1575
+ totalBots++;
1576
+ const score = calculateScore(log);
1577
+ // Faux négatif : un bot qui aurait dû être challengé mais ne l'a pas été
1578
+ if (score < config.thresholds.low) {
1579
+ falseNegatives += confidence;
1580
+ } else {
1581
+ totalChallenges += confidence; // Un bot correctement challengé
1582
+ // Calculer le coût de ce challenge (proportionnel à la difficulté)
1583
+ const suspicionFactor = (score - config.thresholds.low) / (config.thresholds.high - config.thresholds.low);
1584
+ totalChallengeCost += Math.min(1, Math.max(0, suspicionFactor)) * confidence;
1585
+ }
1586
+ } else if (isLikelyHuman) {
1587
+ totalHumans++;
1588
+ const score = calculateScore(log);
1589
+ // Faux positif : un humain qui a été challengé inutilement
1590
+ if (score >= config.thresholds.low) {
1591
+ falsePositives += confidence;
1592
+ totalChallenges += confidence; // Un humain incorrectement challengé
1593
+ const suspicionFactor = (score - config.thresholds.low) / (config.thresholds.high - config.thresholds.low);
1594
+ totalChallengeCost += Math.min(1, Math.max(0, suspicionFactor)) * confidence;
1595
+ }
1596
+ }
1597
+ }
1598
+
1599
+ const falsePositiveRate = totalHumans > 0 ? falsePositives / totalHumans : 0;
1600
+ const falseNegativeRate = totalBots > 0 ? falseNegatives / totalBots : 0;
1601
+ // NOUVEAU: Taux de challenge global
1602
+ const challengeRate = (totalHumans + totalBots) > 0 ? totalChallenges / (totalHumans + totalBots) : 0;
1603
+ // NOUVEAU: Coût moyen par challenge émis
1604
+ const averageChallengeCost = totalChallenges > 0 ? totalChallengeCost / totalChallenges : 0;
1605
+
1606
+ // L'algorithme doit minimiser ces deux objectifs
1607
+ // On a maintenant 4 objectifs à minimiser !
1608
+ return [falsePositiveRate, falseNegativeRate, challengeRate, averageChallengeCost];
1609
+ };
1610
+ };
1611
+
1612
+ /**
1613
+ * Résout le problème de l'auto-tuning complet de la configuration de sécurité.
1614
+ * @param {object} context - Le contexte contenant les données de trafic.
1615
+ * @param {object} [options] - Options pour l'algorithme génétique.
1616
+ * @returns {Array<{solution: object, objectives: number[]}>} Le front de Pareto des configurations optimales.
1617
+ */
1618
+ Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
1619
+ const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator(context);
1620
+
1621
+ // Un "individu" est un objet de configuration complet
1622
+ const createIndividual = () => ({
1623
+ thresholds: {
1624
+ low: 15 + secureRandom() * 20, // 15-35
1625
+ medium: 40 + secureRandom() * 25, // 40-65
1626
+ high: 70 + secureRandom() * 20, // 70-90
1627
+ },
1628
+ weights: {
1629
+ historyScore: secureRandom(),
1630
+ rotationScore: secureRandom(),
1631
+ headerAnomalyScore: secureRandom(),
1632
+ requestPatternScore: 0.5 + secureRandom(), // Donner plus d'importance aux patterns
1633
+ inconsistencyScore: secureRandom(),
1634
+ honeypotScore: 1.0, // Garder le honeypot à 1.0 est une bonne pratique
1635
+ behaviorScore: secureRandom(),
1636
+ crossLayerInconsistencyScore: secureRandom(),
1637
+ timeInconsistencyScore: secureRandom(),
1638
+ },
1639
+ patterns: {
1640
+ velocityThreshold: 100 + secureRandom() * 400, // 100-500ms
1641
+ velocityWeight: 10 + secureRandom() * 40,
1642
+ burstThreshold: 300 + secureRandom() * 700, // 300-1000ms
1643
+ burstWeight: 20 + secureRandom() * 40,
1644
+ scrapeThreshold: 500 + secureRandom() * 1000, // 500-1500ms
1645
+ scrapeWeight: 15 + secureRandom() * 35,
1646
+ sequenceLength: 3 + Math.floor(secureRandom() * 3), // 3-5
1647
+ sequenceWeight: 20 + secureRandom() * 50,
1648
+ regularityThreshold: 50 + secureRandom() * 200, // 50-250ms
1649
+ regularityWeight: 20 + secureRandom() * 40,
1650
+ decayFactor: 0.85 + secureRandom() * 0.14, // 0.85-0.99
1651
+ inactivityReset: 15000 + secureRandom() * 45000, // 15s-60s
1652
+ }
1653
+ });
1654
+
1655
+ // Le crossover et la mutation doivent maintenant opérer sur des objets complexes
1656
+ const crossover = (c1, c2) => {
1657
+ const child = JSON.parse(JSON.stringify(c1)); // Deep copy
1658
+ // Croisement pour chaque groupe de paramètres
1659
+ for (const key in child.thresholds) {
1660
+ child.thresholds[key] = (c1.thresholds[key] + c2.thresholds[key]) / 2;
1661
+ }
1662
+ for (const key in child.weights) {
1663
+ if (key !== 'honeypotScore') { // Ne pas croiser le poids du honeypot
1664
+ child.weights[key] = (c1.weights[key] + c2.weights[key]) / 2;
1665
+ }
1666
+ }
1667
+ for (const key in child.patterns) {
1668
+ child.patterns[key] = (c1.patterns[key] + c2.patterns[key]) / 2;
1669
+ }
1670
+ return child;
1671
+ };
1672
+
1673
+ const mutate = (c) => {
1674
+ const newConfig = JSON.parse(JSON.stringify(c));
1675
+
1676
+ // --- NOUVELLE LOGIQUE : Sélection de section pondérée ---
1677
+ // On donne plus de poids à la mutation des 'patterns' et des 'weights',
1678
+ // car ils ont un impact plus direct sur la détection que les seuils.
1679
+ const sections = [
1680
+ { name: 'patterns', weight: 0.5 }, // 50% de chance
1681
+ { name: 'weights', weight: 0.35 }, // 35% de chance
1682
+ { name: 'thresholds', weight: 0.15 } // 15% de chance
1683
+ ];
1684
+ const rand = secureRandom();
1685
+ let cumulativeWeight = 0;
1686
+ let sectionToMutate = 'patterns'; // Fallback
1687
+ for (const section of sections) {
1688
+ cumulativeWeight += section.weight;
1689
+ if (rand < cumulativeWeight) {
1690
+ sectionToMutate = section.name;
1691
+ break;
1692
+ }
1693
+ }
1694
+
1695
+ const keys = Object.keys(newConfig[sectionToMutate]);
1696
+ const keyToMutate = keys[Math.floor(secureRandom() * keys.length)];
1697
+
1698
+ if (keyToMutate === 'honeypotScore') return newConfig; // Ne pas muter le poids du honeypot
1699
+
1700
+ // Appliquer une mutation avec une amplitude variable
1701
+ const mutationAmount = (secureRandom() - 0.5) * 0.4; // +/- 20%
1702
+ newConfig[sectionToMutate][keyToMutate] *= (1 + mutationAmount);
1703
+
1704
+ // S'assurer que les valeurs restent dans des limites raisonnables
1705
+ if (sectionToMutate === 'weights') {
1706
+ newConfig[sectionToMutate][keyToMutate] = Math.max(0, Math.min(1.5, newConfig[sectionToMutate][keyToMutate]));
1707
+ }
1708
+ if (keyToMutate === 'decayFactor') {
1709
+ newConfig.patterns.decayFactor = Math.max(0.8, Math.min(0.999, newConfig.patterns.decayFactor));
1710
+ }
1711
+ if (keyToMutate.includes('Threshold') || keyToMutate.includes('Reset')) {
1712
+ newConfig.patterns[keyToMutate] = Math.max(50, newConfig.patterns[keyToMutate]);
1713
+ }
1714
+
1715
+ return newConfig;
1716
+ };
1717
+
1718
+ return Optimization.geneticAlgorithmMultiObjective(
1719
+ createIndividual,
1720
+ fitnessFunction,
1721
+ crossover,
1722
+ mutate,
1723
+ { generations: 50, populationSize: 50, ...options }
1724
+ );
1725
+ };
1726
+
1727
+ export { Optimization };