@anonympins/fingerprint 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +193 -0
- package/README.md +1080 -834
- package/composer.json +39 -0
- package/index.js +5 -0
- package/package.json +31 -23
- package/phpunit.xml +21 -0
- package/public/fp.js +2 -0
- package/src/js/build-client.js +69 -0
- package/{fingerprint.builder.js → src/js/fingerprint.builder.js} +168 -175
- package/{fingerprint.client.js → src/js/fingerprint.client.js} +634 -538
- package/src/js/fingerprint.client.obfuscated.js +1 -0
- package/{fingerprint.js → src/js/fingerprint.js} +3733 -3294
- package/{library.js → src/js/library.js} +1729 -1729
- package/{problem-manager.js → src/js/problem-manager.js} +539 -522
- package/src/php/AutoTuner.php +155 -0
- package/src/php/Challenge/ChallengeUtils.php +306 -0
- package/src/php/Config/SecurityProfiles.php +267 -0
- package/src/php/DirectFingerprint.php +81 -0
- package/src/php/FingerprintBuilder.php +186 -0
- package/src/php/FingerprintClient.php +132 -0
- package/src/php/FingerprintEngine.php +863 -0
- package/src/php/Optimization/FunctionRegistry.php +63 -0
- package/src/php/Optimization/Optimization.php +256 -0
- package/src/php/Optimization/OptimizationOperators.php +305 -0
- package/src/php/Optimization/ProblemInitializers.php +53 -0
- package/src/php/ProblemManager.php +255 -0
- package/src/php/RequestContext.php +91 -0
- package/src/php/Store/IStore.php +42 -0
- package/src/php/Store/InMemoryStore.php +67 -0
- package/src/php/Store/StoreManager.php +36 -0
- package/src/php/Tests/ChallengeUtilsTest.php +82 -0
- package/src/php/Tests/FingerprintBuilderTest.php +58 -0
- package/src/php/Tests/FingerprintEngineTest.php +300 -0
- package/src/php/Tests/IpReputationTest.php +157 -0
- package/src/php/Tests/PowTest.php +40 -0
- package/src/php/Tests/ProblemManagerTest.php +295 -0
- package/src/php/Tests/RequestUtilsTest.php +81 -0
- package/src/php/Tests/problems.config.json +9 -0
- package/src/php/Utils/BigInt.php +145 -0
- package/src/php/Utils/BlockList.php +100 -0
- package/src/php/Utils/Logger.php +30 -0
- package/src/php/Utils/MaliciousPatterns.php +59 -0
- package/src/php/Utils/RequestUtils.php +962 -0
- package/fingerprint.client.obfuscated.js +0 -1
- /package/{mongodb-store.js → src/js/mongodb-store.js} +0 -0
- /package/{optimization.worker.js → src/js/optimization.worker.js} +0 -0
- /package/{pow.solver.inline.js → src/js/pow.solver.inline.js} +0 -0
- /package/{pow.solver.js → src/js/pow.solver.js} +0 -0
- /package/{pow.worker.js → src/js/pow.worker.js} +0 -0
- /package/{redis-store.js → src/js/redis-store.js} +0 -0
- /package/{sql-store.js → src/js/sql-store.js} +0 -0
|
@@ -1,1729 +1,1729 @@
|
|
|
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
|
-
tlsSpoofingScore: secureRandom(), // NOUVEAU: Ajout du poids pour le spoofing TLS
|
|
1639
|
-
botScore: secureRandom(), // NOUVEAU: Ajout du poids pour la détection de bot
|
|
1640
|
-
},
|
|
1641
|
-
patterns: {
|
|
1642
|
-
velocityThreshold: 100 + secureRandom() * 400, // 100-500ms
|
|
1643
|
-
velocityWeight: 10 + secureRandom() * 40,
|
|
1644
|
-
burstThreshold: 300 + secureRandom() * 700, // 300-1000ms
|
|
1645
|
-
burstWeight: 20 + secureRandom() * 40,
|
|
1646
|
-
scrapeThreshold: 500 + secureRandom() * 1000, // 500-1500ms
|
|
1647
|
-
scrapeWeight: 15 + secureRandom() * 35,
|
|
1648
|
-
sequenceLength: 3 + Math.floor(secureRandom() * 3), // 3-5
|
|
1649
|
-
sequenceWeight: 20 + secureRandom() * 50,
|
|
1650
|
-
regularityThreshold: 50 + secureRandom() * 200, // 50-250ms
|
|
1651
|
-
regularityWeight: 20 + secureRandom() * 40,
|
|
1652
|
-
decayFactor: 0.85 + secureRandom() * 0.14, // 0.85-0.99
|
|
1653
|
-
inactivityReset: 15000 + secureRandom() * 45000, // 15s-60s
|
|
1654
|
-
}
|
|
1655
|
-
});
|
|
1656
|
-
|
|
1657
|
-
// Le crossover et la mutation doivent maintenant opérer sur des objets complexes
|
|
1658
|
-
const crossover = (c1, c2) => {
|
|
1659
|
-
const child = JSON.parse(JSON.stringify(c1)); // Deep copy
|
|
1660
|
-
// Croisement pour chaque groupe de paramètres
|
|
1661
|
-
for (const key in child.thresholds) {
|
|
1662
|
-
child.thresholds[key] = (c1.thresholds[key] + c2.thresholds[key]) / 2;
|
|
1663
|
-
}
|
|
1664
|
-
for (const key in child.weights) {
|
|
1665
|
-
if (key !== 'honeypotScore') { // Ne pas croiser le poids du honeypot
|
|
1666
|
-
child.weights[key] = (c1.weights[key] + c2.weights[key]) / 2;
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
for (const key in child.patterns) {
|
|
1670
|
-
child.patterns[key] = (c1.patterns[key] + c2.patterns[key]) / 2;
|
|
1671
|
-
}
|
|
1672
|
-
return child;
|
|
1673
|
-
};
|
|
1674
|
-
|
|
1675
|
-
const mutate = (c) => {
|
|
1676
|
-
const newConfig = JSON.parse(JSON.stringify(c));
|
|
1677
|
-
|
|
1678
|
-
// --- NOUVELLE LOGIQUE : Sélection de section pondérée ---
|
|
1679
|
-
// On donne plus de poids à la mutation des 'patterns' et des 'weights',
|
|
1680
|
-
// car ils ont un impact plus direct sur la détection que les seuils.
|
|
1681
|
-
const sections = [
|
|
1682
|
-
{ name: 'patterns', weight: 0.5 }, // 50% de chance
|
|
1683
|
-
{ name: 'weights', weight: 0.35 }, // 35% de chance
|
|
1684
|
-
{ name: 'thresholds', weight: 0.15 } // 15% de chance
|
|
1685
|
-
];
|
|
1686
|
-
const rand = secureRandom();
|
|
1687
|
-
let cumulativeWeight = 0;
|
|
1688
|
-
let sectionToMutate = 'patterns'; // Fallback
|
|
1689
|
-
for (const section of sections) {
|
|
1690
|
-
cumulativeWeight += section.weight;
|
|
1691
|
-
if (rand < cumulativeWeight) {
|
|
1692
|
-
sectionToMutate = section.name;
|
|
1693
|
-
break;
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
const keys = Object.keys(newConfig[sectionToMutate]);
|
|
1698
|
-
const keyToMutate = keys[Math.floor(secureRandom() * keys.length)];
|
|
1699
|
-
|
|
1700
|
-
if (keyToMutate === 'honeypotScore') return newConfig; // Ne pas muter le poids du honeypot
|
|
1701
|
-
|
|
1702
|
-
// Appliquer une mutation avec une amplitude variable
|
|
1703
|
-
const mutationAmount = (secureRandom() - 0.5) * 0.4; // +/- 20%
|
|
1704
|
-
newConfig[sectionToMutate][keyToMutate] *= (1 + mutationAmount);
|
|
1705
|
-
|
|
1706
|
-
// S'assurer que les valeurs restent dans des limites raisonnables
|
|
1707
|
-
if (sectionToMutate === 'weights') {
|
|
1708
|
-
newConfig[sectionToMutate][keyToMutate] = Math.max(0, Math.min(1.5, newConfig[sectionToMutate][keyToMutate]));
|
|
1709
|
-
}
|
|
1710
|
-
if (keyToMutate === 'decayFactor') {
|
|
1711
|
-
newConfig.patterns.decayFactor = Math.max(0.8, Math.min(0.999, newConfig.patterns.decayFactor));
|
|
1712
|
-
}
|
|
1713
|
-
if (keyToMutate.includes('Threshold') || keyToMutate.includes('Reset')) {
|
|
1714
|
-
newConfig.patterns[keyToMutate] = Math.max(50, newConfig.patterns[keyToMutate]);
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
return newConfig;
|
|
1718
|
-
};
|
|
1719
|
-
|
|
1720
|
-
return Optimization.geneticAlgorithmMultiObjective(
|
|
1721
|
-
createIndividual,
|
|
1722
|
-
fitnessFunction,
|
|
1723
|
-
crossover,
|
|
1724
|
-
mutate,
|
|
1725
|
-
{ generations: 50, populationSize: 50, ...options }
|
|
1726
|
-
);
|
|
1727
|
-
};
|
|
1728
|
-
|
|
1729
|
-
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
|
+
tlsSpoofingScore: secureRandom(), // NOUVEAU: Ajout du poids pour le spoofing TLS
|
|
1639
|
+
botScore: secureRandom(), // NOUVEAU: Ajout du poids pour la détection de bot
|
|
1640
|
+
},
|
|
1641
|
+
patterns: {
|
|
1642
|
+
velocityThreshold: 100 + secureRandom() * 400, // 100-500ms
|
|
1643
|
+
velocityWeight: 10 + secureRandom() * 40,
|
|
1644
|
+
burstThreshold: 300 + secureRandom() * 700, // 300-1000ms
|
|
1645
|
+
burstWeight: 20 + secureRandom() * 40,
|
|
1646
|
+
scrapeThreshold: 500 + secureRandom() * 1000, // 500-1500ms
|
|
1647
|
+
scrapeWeight: 15 + secureRandom() * 35,
|
|
1648
|
+
sequenceLength: 3 + Math.floor(secureRandom() * 3), // 3-5
|
|
1649
|
+
sequenceWeight: 20 + secureRandom() * 50,
|
|
1650
|
+
regularityThreshold: 50 + secureRandom() * 200, // 50-250ms
|
|
1651
|
+
regularityWeight: 20 + secureRandom() * 40,
|
|
1652
|
+
decayFactor: 0.85 + secureRandom() * 0.14, // 0.85-0.99
|
|
1653
|
+
inactivityReset: 15000 + secureRandom() * 45000, // 15s-60s
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
// Le crossover et la mutation doivent maintenant opérer sur des objets complexes
|
|
1658
|
+
const crossover = (c1, c2) => {
|
|
1659
|
+
const child = JSON.parse(JSON.stringify(c1)); // Deep copy
|
|
1660
|
+
// Croisement pour chaque groupe de paramètres
|
|
1661
|
+
for (const key in child.thresholds) {
|
|
1662
|
+
child.thresholds[key] = (c1.thresholds[key] + c2.thresholds[key]) / 2;
|
|
1663
|
+
}
|
|
1664
|
+
for (const key in child.weights) {
|
|
1665
|
+
if (key !== 'honeypotScore') { // Ne pas croiser le poids du honeypot
|
|
1666
|
+
child.weights[key] = (c1.weights[key] + c2.weights[key]) / 2;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
for (const key in child.patterns) {
|
|
1670
|
+
child.patterns[key] = (c1.patterns[key] + c2.patterns[key]) / 2;
|
|
1671
|
+
}
|
|
1672
|
+
return child;
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1675
|
+
const mutate = (c) => {
|
|
1676
|
+
const newConfig = JSON.parse(JSON.stringify(c));
|
|
1677
|
+
|
|
1678
|
+
// --- NOUVELLE LOGIQUE : Sélection de section pondérée ---
|
|
1679
|
+
// On donne plus de poids à la mutation des 'patterns' et des 'weights',
|
|
1680
|
+
// car ils ont un impact plus direct sur la détection que les seuils.
|
|
1681
|
+
const sections = [
|
|
1682
|
+
{ name: 'patterns', weight: 0.5 }, // 50% de chance
|
|
1683
|
+
{ name: 'weights', weight: 0.35 }, // 35% de chance
|
|
1684
|
+
{ name: 'thresholds', weight: 0.15 } // 15% de chance
|
|
1685
|
+
];
|
|
1686
|
+
const rand = secureRandom();
|
|
1687
|
+
let cumulativeWeight = 0;
|
|
1688
|
+
let sectionToMutate = 'patterns'; // Fallback
|
|
1689
|
+
for (const section of sections) {
|
|
1690
|
+
cumulativeWeight += section.weight;
|
|
1691
|
+
if (rand < cumulativeWeight) {
|
|
1692
|
+
sectionToMutate = section.name;
|
|
1693
|
+
break;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
const keys = Object.keys(newConfig[sectionToMutate]);
|
|
1698
|
+
const keyToMutate = keys[Math.floor(secureRandom() * keys.length)];
|
|
1699
|
+
|
|
1700
|
+
if (keyToMutate === 'honeypotScore') return newConfig; // Ne pas muter le poids du honeypot
|
|
1701
|
+
|
|
1702
|
+
// Appliquer une mutation avec une amplitude variable
|
|
1703
|
+
const mutationAmount = (secureRandom() - 0.5) * 0.4; // +/- 20%
|
|
1704
|
+
newConfig[sectionToMutate][keyToMutate] *= (1 + mutationAmount);
|
|
1705
|
+
|
|
1706
|
+
// S'assurer que les valeurs restent dans des limites raisonnables
|
|
1707
|
+
if (sectionToMutate === 'weights') {
|
|
1708
|
+
newConfig[sectionToMutate][keyToMutate] = Math.max(0, Math.min(1.5, newConfig[sectionToMutate][keyToMutate]));
|
|
1709
|
+
}
|
|
1710
|
+
if (keyToMutate === 'decayFactor') {
|
|
1711
|
+
newConfig.patterns.decayFactor = Math.max(0.8, Math.min(0.999, newConfig.patterns.decayFactor));
|
|
1712
|
+
}
|
|
1713
|
+
if (keyToMutate.includes('Threshold') || keyToMutate.includes('Reset')) {
|
|
1714
|
+
newConfig.patterns[keyToMutate] = Math.max(50, newConfig.patterns[keyToMutate]);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
return newConfig;
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
return Optimization.geneticAlgorithmMultiObjective(
|
|
1721
|
+
createIndividual,
|
|
1722
|
+
fitnessFunction,
|
|
1723
|
+
crossover,
|
|
1724
|
+
mutate,
|
|
1725
|
+
{ generations: 50, populationSize: 50, ...options }
|
|
1726
|
+
);
|
|
1727
|
+
};
|
|
1728
|
+
|
|
1729
|
+
export { Optimization };
|