@anonympins/fingerprint 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/fingerprint.js +1102 -0
- package/library.js +2270 -0
- package/package.json +46 -0
package/library.js
ADDED
|
@@ -0,0 +1,2270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file @/library.js
|
|
3
|
+
* @description Une bibliothèque d'outils basés sur le principe fondamental de la dichotomie (division en deux).
|
|
4
|
+
* Inclut des algorithmes pour les tableaux triés, des structures de données et des solveurs de problèmes conceptuels.
|
|
5
|
+
*
|
|
6
|
+
* @template T
|
|
7
|
+
* @callback Comparator
|
|
8
|
+
* @param {T} element - L'élément du tableau.
|
|
9
|
+
* @param {any} target - La valeur cible.
|
|
10
|
+
* @returns {number} -1 si element < target, 0 si element == target, 1 si element > target.
|
|
11
|
+
*
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Worker } from "worker_threads";
|
|
15
|
+
import os from "os";
|
|
16
|
+
/**
|
|
17
|
+
* @namespace Dichotomy
|
|
18
|
+
* @description Un ensemble d'outils de recherche puissants pour les tableaux triés.
|
|
19
|
+
*/
|
|
20
|
+
const Dichotomy = {
|
|
21
|
+
/**
|
|
22
|
+
* Comparateur par défaut pour les types primitifs (nombres, chaînes).
|
|
23
|
+
* @private
|
|
24
|
+
*/
|
|
25
|
+
_defaultComparator(a, b) {
|
|
26
|
+
if (a < b) return -1;
|
|
27
|
+
if (a > b) return 1;
|
|
28
|
+
return 0;
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* L'algorithme de base de la recherche dichotomique.
|
|
33
|
+
* Trouve l'index d'une valeur cible dans un tableau trié.
|
|
34
|
+
* Si plusieurs occurrences existent, l'index retourné n'est pas garanti (peut être n'importe laquelle des occurrences).
|
|
35
|
+
* @template T
|
|
36
|
+
* @param {Array<T>} arr - Le tableau trié dans lequel chercher.
|
|
37
|
+
* @param {any} target - La valeur à trouver.
|
|
38
|
+
* @param {Comparator<T>} [comparator] - Fonction de comparaison.
|
|
39
|
+
* @returns {number} L'index de la cible si elle est trouvée, sinon -1.
|
|
40
|
+
*/
|
|
41
|
+
search(arr, target, comparator = this._defaultComparator) {
|
|
42
|
+
let low = 0;
|
|
43
|
+
let high = arr.length - 1;
|
|
44
|
+
|
|
45
|
+
while (low <= high) {
|
|
46
|
+
// Utiliser Math.floor pour éviter les problèmes avec les nombres flottants
|
|
47
|
+
// et pour s'assurer que mid est un entier.
|
|
48
|
+
const mid = Math.floor(low + (high - low) / 2);
|
|
49
|
+
const comparison = comparator(arr[mid], target);
|
|
50
|
+
|
|
51
|
+
if (comparison === 0) {
|
|
52
|
+
return mid; // Cible trouvée
|
|
53
|
+
} else if (comparison < 0) {
|
|
54
|
+
low = mid + 1; // La cible est dans la moitié droite
|
|
55
|
+
} else {
|
|
56
|
+
high = mid - 1; // La cible est dans la moitié gauche
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return -1; // Cible non trouvée
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Trouve l'index de la PREMIÈRE occurrence d'une valeur cible.
|
|
65
|
+
* Utile quand le tableau contient des doublons.
|
|
66
|
+
* @template T
|
|
67
|
+
* @param {Array<T>} arr - Le tableau trié.
|
|
68
|
+
* @param {any} target - La valeur à trouver.
|
|
69
|
+
* @param {Comparator<T>} [comparator] - Fonction de comparaison.
|
|
70
|
+
* @returns {number} L'index de la première occurrence, sinon -1.
|
|
71
|
+
*/
|
|
72
|
+
findFirst(arr, target, comparator = this._defaultComparator) {
|
|
73
|
+
let low = 0;
|
|
74
|
+
let high = arr.length - 1;
|
|
75
|
+
let result = -1;
|
|
76
|
+
|
|
77
|
+
while (low <= high) {
|
|
78
|
+
const mid = Math.floor(low + (high - low) / 2);
|
|
79
|
+
const comparison = comparator(arr[mid], target);
|
|
80
|
+
|
|
81
|
+
if (comparison === 0) {
|
|
82
|
+
result = mid;
|
|
83
|
+
high = mid - 1; // Continuer à chercher à gauche
|
|
84
|
+
} else if (comparison < 0) {
|
|
85
|
+
low = mid + 1;
|
|
86
|
+
} else {
|
|
87
|
+
high = mid - 1; // On continue de chercher à gauche
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Trouve l'index de la DERNIÈRE occurrence d'une valeur cible.
|
|
96
|
+
* Utile quand le tableau contient des doublons.
|
|
97
|
+
* @template T
|
|
98
|
+
* @param {Array<T>} arr - Le tableau trié.
|
|
99
|
+
* @param {any} target - La valeur à trouver.
|
|
100
|
+
* @param {Comparator<T>} [comparator] - Fonction de comparaison.
|
|
101
|
+
* @returns {number} L'index de la dernière occurrence, sinon -1.
|
|
102
|
+
*/
|
|
103
|
+
findLast(arr, target, comparator = this._defaultComparator) {
|
|
104
|
+
let low = 0;
|
|
105
|
+
let high = arr.length - 1;
|
|
106
|
+
let result = -1;
|
|
107
|
+
|
|
108
|
+
while (low <= high) {
|
|
109
|
+
const mid = Math.floor(low + (high - low) / 2);
|
|
110
|
+
const comparison = comparator(arr[mid], target);
|
|
111
|
+
|
|
112
|
+
if (comparison === 0) {
|
|
113
|
+
result = mid;
|
|
114
|
+
low = mid + 1; // Continuer à chercher à droite
|
|
115
|
+
} else if (comparison < 0) {
|
|
116
|
+
low = mid + 1; // On continue de chercher à droite
|
|
117
|
+
} else {
|
|
118
|
+
high = mid - 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Compte le nombre total d'occurrences d'une valeur cible.
|
|
127
|
+
* Combine findFirst et findLast pour une efficacité maximale.
|
|
128
|
+
* @template T
|
|
129
|
+
* @param {Array<T>} arr - Le tableau trié.
|
|
130
|
+
* @param {any} target - La valeur à compter.
|
|
131
|
+
* @param {Comparator<T>} [comparator] - Fonction de comparaison.
|
|
132
|
+
* @returns {number} Le nombre d'occurrences de la cible.
|
|
133
|
+
*/
|
|
134
|
+
countOccurrences(arr, target, comparator = this._defaultComparator) {
|
|
135
|
+
const firstIndex = this.findFirst(arr, target, comparator);
|
|
136
|
+
|
|
137
|
+
if (firstIndex === -1) {
|
|
138
|
+
return 0; // Si la première n'existe pas, il n'y en a aucune.
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// On peut passer `firstIndex` comme point de départ à `findLast` pour optimiser, mais restons simple.
|
|
142
|
+
const lastIndex = this.findLast(arr, target, comparator);
|
|
143
|
+
return lastIndex - firstIndex + 1;
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Trouve l'index où la cible devrait être insérée pour maintenir l'ordre du tableau.
|
|
148
|
+
* C'est l'équivalent de la fonction `lower_bound` en C++.
|
|
149
|
+
* @template T
|
|
150
|
+
* @param {Array<T>} arr - Le tableau trié.
|
|
151
|
+
* @param {any} target - La valeur à insérer.
|
|
152
|
+
* @param {Comparator<T>} [comparator] - Fonction de comparaison.
|
|
153
|
+
* @returns {number} L'index d'insertion approprié.
|
|
154
|
+
*/
|
|
155
|
+
findInsertionPoint(arr, target, comparator = this._defaultComparator) {
|
|
156
|
+
let low = 0;
|
|
157
|
+
let high = arr.length; // Note: high est la longueur du tableau, pas length - 1
|
|
158
|
+
|
|
159
|
+
while (low < high) {
|
|
160
|
+
const mid = Math.floor(low + (high - low) / 2);
|
|
161
|
+
if (comparator(arr[mid], target) < 0) {
|
|
162
|
+
low = mid + 1;
|
|
163
|
+
} else {
|
|
164
|
+
high = mid;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return low; // ou high, ils sont égaux à la fin de la boucle
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Vérifie si une valeur cible est présente dans le tableau.
|
|
173
|
+
* Plus sémantique que `search(arr, target) !== -1`.
|
|
174
|
+
* @template T
|
|
175
|
+
* @param {Array<T>} arr - Le tableau trié.
|
|
176
|
+
* @param {any} target - La valeur à vérifier.
|
|
177
|
+
* @param {Comparator<T>} [comparator] - Fonction de comparaison.
|
|
178
|
+
* @returns {boolean} `true` si la cible est présente, sinon `false`.
|
|
179
|
+
*/
|
|
180
|
+
contains(arr, target, comparator = this._defaultComparator) {
|
|
181
|
+
return this.search(arr, target, comparator) !== -1;
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// --- NOUVEAUX ALGORITHMES PUISSANTS ---
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Trouve tous les éléments dans un intervalle [minTarget, maxTarget].
|
|
188
|
+
* @template T
|
|
189
|
+
* @param {Array<T>} arr - Le tableau trié.
|
|
190
|
+
* @param {any} minTarget - La borne inférieure de l'intervalle (inclusive).
|
|
191
|
+
* @param {any} maxTarget - La borne supérieure de l'intervalle (inclusive).
|
|
192
|
+
* @param {Comparator<T>} [comparator] - Fonction de comparaison.
|
|
193
|
+
* @returns {Array<T>} Un sous-tableau contenant les éléments de l'intervalle.
|
|
194
|
+
*/
|
|
195
|
+
findRange(arr, minTarget, maxTarget, comparator = this._defaultComparator) {
|
|
196
|
+
// Trouve l'index du premier élément >= minTarget
|
|
197
|
+
const startIndex = this.findInsertionPoint(arr, minTarget, comparator);
|
|
198
|
+
|
|
199
|
+
// Trouve l'index du premier élément > maxTarget
|
|
200
|
+
let endIndex = this.findInsertionPoint(arr, maxTarget, comparator);
|
|
201
|
+
|
|
202
|
+
// Il faut aussi trouver le dernier élément ÉGAL à maxTarget
|
|
203
|
+
const lastElement = this.findLast(arr, maxTarget, comparator);
|
|
204
|
+
if (lastElement !== -1) {
|
|
205
|
+
endIndex = lastElement + 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return arr.slice(startIndex, endIndex);
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Trouve l'index de l'élément le plus proche de la cible dans le tableau.
|
|
213
|
+
* @template T
|
|
214
|
+
* @param {Array<T>} arr - Le tableau trié.
|
|
215
|
+
* @param {any} target - La valeur numérique cible.
|
|
216
|
+
* @param {function(T, any): number} distanceMetric - Une fonction qui retourne la "distance" (un nombre) entre un élément et la cible.
|
|
217
|
+
* @returns {number} L'index de l'élément le plus proche, ou -1 si le tableau est vide.
|
|
218
|
+
*/
|
|
219
|
+
findClosest(arr, target, distanceMetric) {
|
|
220
|
+
if (arr.length === 0) {
|
|
221
|
+
return -1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let low = 0;
|
|
225
|
+
let high = arr.length - 1;
|
|
226
|
+
|
|
227
|
+
while (low <= high) {
|
|
228
|
+
const mid = Math.floor(low + (high - low) / 2);
|
|
229
|
+
const midValue = arr[mid];
|
|
230
|
+
|
|
231
|
+
if (distanceMetric(midValue, target) === 0) {
|
|
232
|
+
return mid;
|
|
233
|
+
} else if (distanceMetric(midValue, target) < 0) {
|
|
234
|
+
low = mid + 1;
|
|
235
|
+
} else {
|
|
236
|
+
high = mid - 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// À la fin de la boucle, `low` et `high` encadrent la position où `target` serait insérée.
|
|
241
|
+
// L'élément le plus proche est soit à `high`, soit à `low`.
|
|
242
|
+
if (high < 0) high = 0; // S'assurer que high est un index valide
|
|
243
|
+
if (low >= arr.length) low = arr.length - 1; // S'assurer que low est un index valide
|
|
244
|
+
const diffHigh = Math.abs(distanceMetric(arr[high], target));
|
|
245
|
+
const diffLow = Math.abs(distanceMetric(arr[low], target));
|
|
246
|
+
|
|
247
|
+
return diffLow <= diffHigh ? low : high;
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Trouve la "frontière" dans un tableau où une condition (prédicat) passe de false à true.
|
|
252
|
+
* C'est une généralisation de findInsertionPoint et findFirst.
|
|
253
|
+
* @template T
|
|
254
|
+
* @param {Array<T>} arr - Le tableau où chaque élément peut être évalué par le prédicat.
|
|
255
|
+
* @param {function(T): boolean} predicate - Une fonction qui retourne true ou false pour un élément.
|
|
256
|
+
* Le tableau doit être "trié" selon ce prédicat (tous les false avant tous les true).
|
|
257
|
+
* @returns {number} L'index du premier élément pour lequel le prédicat est true. Retourne arr.length si non trouvé.
|
|
258
|
+
*/
|
|
259
|
+
findBoundary(arr, predicate) {
|
|
260
|
+
let low = 0;
|
|
261
|
+
let high = arr.length;
|
|
262
|
+
while (low < high) {
|
|
263
|
+
const mid = Math.floor(low + (high - low) / 2);
|
|
264
|
+
if (predicate(arr[mid])) {
|
|
265
|
+
high = mid; // On a trouvé un 'true', la frontière est peut-être avant
|
|
266
|
+
} else {
|
|
267
|
+
low = mid + 1; // C'est 'false', la frontière est forcément après
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return low;
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Effectue une recherche dichotomique sur une fonction pour trouver la plus petite valeur d'entrée
|
|
275
|
+
* qui satisfait une condition. (Recherche sur la réponse).
|
|
276
|
+
* @param {number} low - La borne inférieure de l'espace de recherche.
|
|
277
|
+
* @param {number} high - La borne supérieure de l'espace de recherche.
|
|
278
|
+
* @param {function(number): boolean} predicate - La fonction de test. Doit être monotone (false...false, true...true).
|
|
279
|
+
* @param {number} [precision=1e-9] - La précision requise pour la réponse (pour les nombres flottants).
|
|
280
|
+
* @returns {number} La plus petite valeur (approximative) qui rend le prédicat 'true'.
|
|
281
|
+
*/
|
|
282
|
+
searchOnAnswer(low, high, predicate, precision = 1e-9) {
|
|
283
|
+
// Si on travaille avec des entiers, la boucle s'arrête quand low == high
|
|
284
|
+
const isIntegerSearch =
|
|
285
|
+
Number.isInteger(low) && Number.isInteger(high) && precision === 1;
|
|
286
|
+
|
|
287
|
+
while (high - low > precision) {
|
|
288
|
+
const mid = isIntegerSearch
|
|
289
|
+
? Math.floor(low + (high - low) / 2)
|
|
290
|
+
: low + (high - low) / 2;
|
|
291
|
+
if (predicate(mid)) {
|
|
292
|
+
high = mid; // Cette réponse est possible, essayons plus petit
|
|
293
|
+
} else {
|
|
294
|
+
low = mid; // Cette réponse n'est pas possible, il faut viser plus haut
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Pour les entiers, 'high' est la première bonne réponse. Pour les flottants, c'est une approximation.
|
|
298
|
+
return high;
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Trouve la valeur d'entrée qui maximise le résultat d'une fonction unimodale (avec un seul pic).
|
|
303
|
+
* C'est l'outil adapté pour les problèmes d'optimisation non-monotones.
|
|
304
|
+
* @param {number} low - La borne inférieure de l'espace de recherche.
|
|
305
|
+
* @param {number} high - La borne supérieure de l'espace de recherche.
|
|
306
|
+
* @param {function(number): number} evaluator - Une fonction qui prend une valeur et retourne son "score" ou "altitude".
|
|
307
|
+
* @param {number} [precision=1e-9] - La précision requise pour la réponse.
|
|
308
|
+
* @returns {number} La valeur (approximative) qui maximise le résultat de l'évaluateur.
|
|
309
|
+
*/
|
|
310
|
+
findPeak(low, high, evaluator, precision = 1e-9) {
|
|
311
|
+
while (high - low > precision) {
|
|
312
|
+
// On divise l'intervalle restant en trois.
|
|
313
|
+
const mid1 = low + (high - low) / 3;
|
|
314
|
+
const mid2 = high - (high - low) / 3;
|
|
315
|
+
|
|
316
|
+
const eval1 = evaluator(mid1);
|
|
317
|
+
const eval2 = evaluator(mid2);
|
|
318
|
+
|
|
319
|
+
if (eval1 < eval2) {
|
|
320
|
+
// Le pic est dans la partie droite (incluant mid2).
|
|
321
|
+
// On peut éliminer le premier tiers.
|
|
322
|
+
low = mid1;
|
|
323
|
+
} else {
|
|
324
|
+
// Le pic est dans la partie gauche (incluant mid1).
|
|
325
|
+
// On peut éliminer le troisième tiers.
|
|
326
|
+
high = mid2;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// low et high sont maintenant très proches. Le pic est entre les deux.
|
|
331
|
+
// On retourne le milieu de l'intervalle final pour une meilleure approximation.
|
|
332
|
+
return (low + high) / 2;
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @namespace Dichotomy.Operators
|
|
338
|
+
* @description Une bibliothèque de "fabriques de prédicats" pour des problèmes complexes,
|
|
339
|
+
* à utiliser avec `Dichotomy.searchOnAnswer`.
|
|
340
|
+
* Ces opérateurs sont conçus pour des problèmes où la réponse peut être trouvée en testant
|
|
341
|
+
* une seule variable (monodimensionnel).
|
|
342
|
+
* ---
|
|
343
|
+
* @namespace Optimization
|
|
344
|
+
* @description Une bibliothèque d'algorithmes d'optimisation pour des problèmes complexes (multimodaux, multidimensionnels).
|
|
345
|
+
*/
|
|
346
|
+
Dichotomy.Operators = {
|
|
347
|
+
/**
|
|
348
|
+
* Crée un prédicat pour trouver le niveau de financement de base le plus élevé possible.
|
|
349
|
+
* @param {object} config - L'objet de configuration.
|
|
350
|
+
* @param {Array<number>} config.baseNeeds - Tableau des besoins de base de chaque entité.
|
|
351
|
+
* @param {number} config.totalBudget - Le budget total disponible.
|
|
352
|
+
* @returns {function(number): boolean} Un prédicat qui prend un `guaranteedLevel` et retourne `true` si le budget est suffisant.
|
|
353
|
+
*/
|
|
354
|
+
createFairResourceAllocator({ baseNeeds, totalBudget }) {
|
|
355
|
+
return function isFeasible(guaranteedLevel) {
|
|
356
|
+
let requiredBudget = 0;
|
|
357
|
+
for (const need of baseNeeds) {
|
|
358
|
+
// Chaque entité reçoit AU MOINS le niveau garanti, ou son besoin de base si celui-ci est plus élevé.
|
|
359
|
+
requiredBudget += Math.max(need, guaranteedLevel);
|
|
360
|
+
}
|
|
361
|
+
return requiredBudget <= totalBudget;
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Crée un prédicat pour déterminer le taux de vaccination nécessaire pour l'immunité collective.
|
|
367
|
+
* @param {object} config - L'objet de configuration.
|
|
368
|
+
* @param {number} config.r0 - Le taux de reproduction de base du virus.
|
|
369
|
+
* @returns {function(number): boolean} Un prédicat qui prend un `vaccinationRate` (entre 0 et 1) et retourne `true` si l'immunité est atteinte.
|
|
370
|
+
*/
|
|
371
|
+
createHerdImmunityValidator({ r0 }) {
|
|
372
|
+
// La formule de base est R_effectif = R0 * (1 - taux_immunité)
|
|
373
|
+
// On veut R_effectif < 1
|
|
374
|
+
return function achievesHerdImmunity(immunityRate) {
|
|
375
|
+
const rEffective = r0 * (1 - immunityRate);
|
|
376
|
+
return rEffective < 1;
|
|
377
|
+
};
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Crée un prédicat pour trouver le bonus de score minimal pour atteindre un objectif d'équité.
|
|
382
|
+
* @param {object} config - L'objet de configuration.
|
|
383
|
+
* @param {Array<number>} config.scoresGroupA - Les scores du groupe de référence.
|
|
384
|
+
* @param {Array<number>} config.scoresGroupB - Les scores du groupe désavantagé.
|
|
385
|
+
* @param {number} config.maxAllowedDisparity - L'écart de score moyen maximal toléré (ex: 0.05 pour 5%).
|
|
386
|
+
* @returns {function(number): boolean} Un prédicat qui prend un `bonus` et retourne `true` si l'équité est atteinte.
|
|
387
|
+
*/
|
|
388
|
+
createFairnessThresholdValidator({
|
|
389
|
+
scoresGroupA,
|
|
390
|
+
scoresGroupB,
|
|
391
|
+
maxAllowedDisparity,
|
|
392
|
+
}) {
|
|
393
|
+
const avgA = scoresGroupA.reduce((a, b) => a + b, 0) / scoresGroupA.length;
|
|
394
|
+
|
|
395
|
+
return function isFairEnough(bonus) {
|
|
396
|
+
const avgB_corrected =
|
|
397
|
+
scoresGroupB.reduce((a, b) => a + b, 0) / scoresGroupB.length + bonus;
|
|
398
|
+
const newDisparity = Math.abs(avgA - avgB_corrected) / avgA;
|
|
399
|
+
return newDisparity <= maxAllowedDisparity;
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Crée un prédicat pour résoudre le "problème du chargeur de paquets" (Packer Problem).
|
|
405
|
+
* Le prédicat généré vérifie s'il est possible de transporter une liste d'objets
|
|
406
|
+
* en un nombre de voyages donné, avec une certaine capacité par voyage.
|
|
407
|
+
*
|
|
408
|
+
* @param {object} config - L'objet de configuration.
|
|
409
|
+
* @param {Array<number>} config.itemSizes - Un tableau des "tailles" de chaque objet (poids, volume, etc.).
|
|
410
|
+
* @param {number} config.maxTrips - Le nombre maximum de "conteneurs" ou de voyages disponibles.
|
|
411
|
+
* @returns {function(number): boolean} Un prédicat qui prend une `capacity` et retourne `true` si l'opération est possible.
|
|
412
|
+
*/
|
|
413
|
+
createShippingValidator({ itemSizes, maxTrips }) {
|
|
414
|
+
// On pré-calcule le plus gros objet pour une optimisation.
|
|
415
|
+
const maxItemSize = Math.max(0, ...itemSizes);
|
|
416
|
+
|
|
417
|
+
return function isSufficient(capacity) {
|
|
418
|
+
if (capacity < maxItemSize) {
|
|
419
|
+
return false; // Impossible si un objet est plus gros que la capacité.
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let trips = 1;
|
|
423
|
+
let currentLoad = 0;
|
|
424
|
+
for (const size of itemSizes) {
|
|
425
|
+
if (currentLoad + size <= capacity) {
|
|
426
|
+
currentLoad += size;
|
|
427
|
+
} else {
|
|
428
|
+
trips++;
|
|
429
|
+
currentLoad = size;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return trips <= maxTrips;
|
|
433
|
+
};
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
// On pourrait ajouter d'autres opérateurs ici :
|
|
437
|
+
// - createLoadBalancingValidator(...)
|
|
438
|
+
// - createRateLimiterValidator(...)
|
|
439
|
+
// - etc.
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Crée un prédicat pour modéliser l'impact d'une taxe carbone.
|
|
443
|
+
* @param {object} config - L'objet de configuration.
|
|
444
|
+
* @param {function(number): number} config.emissionModel - Une fonction qui prend un prix de taxe et retourne les émissions totales (ex: en millions de tonnes).
|
|
445
|
+
* @param {number} config.targetEmissions - Le niveau d'émission à ne pas dépasser.
|
|
446
|
+
* @returns {function(number): boolean} Un prédicat qui prend un `taxPrice` et retourne `true` si l'objectif est atteint.
|
|
447
|
+
*/
|
|
448
|
+
createCarbonTaxValidator({ emissionModel, targetEmissions }) {
|
|
449
|
+
return function isEffective(taxPrice) {
|
|
450
|
+
const currentEmissions = emissionModel(taxPrice);
|
|
451
|
+
return currentEmissions <= targetEmissions;
|
|
452
|
+
};
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Crée un prédicat pour évaluer l'impact d'une augmentation du salaire minimum.
|
|
457
|
+
* @param {object} config - L'objet de configuration.
|
|
458
|
+
* @param {function(number): number} config.jobLossModel - Une fonction qui prend un salaire horaire et retourne le % de perte d'emploi estimé.
|
|
459
|
+
* @param {number} config.maxAcceptableJobLoss - Le seuil de perte d'emploi à ne pas dépasser (ex: 0.01 pour 1%).
|
|
460
|
+
* @returns {function(number): boolean} Un prédicat qui prend un `hourlyWage` et retourne `true` si l'impact est jugé acceptable.
|
|
461
|
+
*/
|
|
462
|
+
createMinimumWageValidator({ jobLossModel, maxAcceptableJobLoss }) {
|
|
463
|
+
// Attention : ici, un salaire plus élevé est "moins bon" pour le prédicat.
|
|
464
|
+
// La fonction `searchOnAnswer` cherche la première valeur qui retourne `true`.
|
|
465
|
+
// On doit donc inverser la logique. On cherche le premier salaire qui est "inacceptable".
|
|
466
|
+
return function isUnacceptable(hourlyWage) {
|
|
467
|
+
const estimatedJobLoss = jobLossModel(hourlyWage);
|
|
468
|
+
return estimatedJobLoss > maxAcceptableJobLoss;
|
|
469
|
+
};
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Crée un prédicat pour un système de quota d'admission universitaire.
|
|
474
|
+
* @param {object} config - L'objet de configuration.
|
|
475
|
+
* @param {Array<number>} config.highSchoolSizes - Un tableau avec le nombre de diplômés par lycée.
|
|
476
|
+
* @param {number} config.universityCapacity - Le nombre total de places disponibles.
|
|
477
|
+
* @returns {function(number): boolean} Un prédicat qui prend un `quotaPercentage` (0 à 1) et retourne `true` si la capacité n'est pas dépassée.
|
|
478
|
+
*/
|
|
479
|
+
createAdmissionQuotaValidator({ highSchoolSizes, universityCapacity }) {
|
|
480
|
+
return function isWithinCapacity(quotaPercentage) {
|
|
481
|
+
let admittedStudents = 0;
|
|
482
|
+
for (const size of highSchoolSizes) {
|
|
483
|
+
admittedStudents += Math.ceil(size * quotaPercentage);
|
|
484
|
+
}
|
|
485
|
+
return admittedStudents <= universityCapacity;
|
|
486
|
+
};
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Crée un prédicat pour trouver un quota de récolte durable.
|
|
491
|
+
* @param {object} config - L'objet de configuration.
|
|
492
|
+
* @param {function(number): number} config.populationModel - Fonction qui prend le quota de l'année N et retourne la population de l'année N+1.
|
|
493
|
+
* @param {number} config.initialPopulation - La population actuelle.
|
|
494
|
+
* @returns {function(number): boolean} Un prédicat qui prend un `harvestQuota` et retourne `true` si la population reste stable ou augmente.
|
|
495
|
+
*/
|
|
496
|
+
createSustainableHarvestValidator({ populationModel, initialPopulation }) {
|
|
497
|
+
return function isSustainable(harvestQuota) {
|
|
498
|
+
const nextYearPopulation = populationModel(harvestQuota);
|
|
499
|
+
return nextYearPopulation >= initialPopulation;
|
|
500
|
+
};
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Crée un prédicat pour une politique de libération anticipée.
|
|
505
|
+
* @param {object} config - L'objet de configuration.
|
|
506
|
+
* @param {Array<{sentenceLength: number, timeServed: number, riskScore: number}>} config.inmates - Données sur les détenus.
|
|
507
|
+
* @param {number} config.targetPopulation - La population carcérale cible.
|
|
508
|
+
* @param {number} config.maxAverageRisk - Le score de risque moyen acceptable pour le groupe libéré.
|
|
509
|
+
* @param {number} config.currentPopulation - La population carcérale actuelle.
|
|
510
|
+
* @returns {function(number): boolean} Un prédicat qui prend un `minSentenceServedPercentage` et retourne `true` si les conditions sont remplies.
|
|
511
|
+
*/
|
|
512
|
+
createEarlyReleaseValidator({
|
|
513
|
+
inmates,
|
|
514
|
+
targetPopulation,
|
|
515
|
+
maxAverageRisk,
|
|
516
|
+
currentPopulation,
|
|
517
|
+
}) {
|
|
518
|
+
return function isViable(minSentenceServedPercentage) {
|
|
519
|
+
const eligibleInmates = inmates.filter(
|
|
520
|
+
(p) => p.timeServed / p.sentenceLength >= minSentenceServedPercentage,
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
if (eligibleInmates.length === 0) {
|
|
524
|
+
// Si personne n'est éligible, la population ne baisse pas.
|
|
525
|
+
return currentPopulation <= targetPopulation;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const finalPopulation = currentPopulation - eligibleInmates.length;
|
|
529
|
+
if (finalPopulation > targetPopulation) {
|
|
530
|
+
return false; // Pas assez de libérations pour atteindre l'objectif.
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const totalRisk = eligibleInmates.reduce(
|
|
534
|
+
(sum, p) => sum + p.riskScore,
|
|
535
|
+
0,
|
|
536
|
+
);
|
|
537
|
+
const averageRisk = totalRisk / eligibleInmates.length;
|
|
538
|
+
|
|
539
|
+
return averageRisk <= maxAverageRisk;
|
|
540
|
+
};
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Crée un prédicat pour équilibrer la modération de contenu.
|
|
545
|
+
* @param {object} config - L'objet de configuration.
|
|
546
|
+
* @param {function(number): {workloadReduction: number, falsePositiveRate: number}} config.moderationModel - Modèle qui simule l'impact d'un seuil.
|
|
547
|
+
* @param {number} config.minWorkloadReduction - L'objectif de réduction de charge de travail (ex: 0.9).
|
|
548
|
+
* @param {number} config.maxFalsePositiveRate - Le taux de faux positifs à ne pas dépasser (ex: 0.001).
|
|
549
|
+
* @returns {function(number): boolean} Un prédicat qui prend un `toxicityThreshold` et retourne `true` si les objectifs sont atteints.
|
|
550
|
+
*/
|
|
551
|
+
createContentModerationValidator({
|
|
552
|
+
moderationModel,
|
|
553
|
+
minWorkloadReduction,
|
|
554
|
+
maxFalsePositiveRate,
|
|
555
|
+
}) {
|
|
556
|
+
return function isBalanced(toxicityThreshold) {
|
|
557
|
+
const { workloadReduction, falsePositiveRate } =
|
|
558
|
+
moderationModel(toxicityThreshold);
|
|
559
|
+
return (
|
|
560
|
+
workloadReduction >= minWorkloadReduction &&
|
|
561
|
+
falsePositiveRate <= maxFalsePositiveRate
|
|
562
|
+
);
|
|
563
|
+
};
|
|
564
|
+
},
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Crée un évaluateur pour trouver le prix qui maximise le revenu.
|
|
568
|
+
* @param {object} config - L'objet de configuration.
|
|
569
|
+
* @param {function(number): number} config.demandModel - Fonction qui prend un prix et retourne le nombre d'unités vendues.
|
|
570
|
+
* @returns {function(number): number} Un évaluateur qui prend un `price` et retourne le revenu total.
|
|
571
|
+
*/
|
|
572
|
+
createOptimalPricingModel({ demandModel }) {
|
|
573
|
+
return function revenueEvaluator(price) {
|
|
574
|
+
if (price < 0) return 0;
|
|
575
|
+
const unitsSold = demandModel(price);
|
|
576
|
+
return price * unitsSold;
|
|
577
|
+
};
|
|
578
|
+
},
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Crée un évaluateur pour trouver le taux de commission qui maximise le revenu de la plateforme.
|
|
582
|
+
* @param {object} config - L'objet de configuration.
|
|
583
|
+
* @param {number} config.totalAdvertiserCredits - Le total des crédits disponibles chez les annonceurs.
|
|
584
|
+
* @param {number} config.totalWebsites - Le nombre total de sites monétisés.
|
|
585
|
+
* @returns {function(number): number} Un évaluateur qui prend un `commissionRate` et retourne le revenu estimé.
|
|
586
|
+
*/
|
|
587
|
+
createPlatformRevenueEvaluator({ totalAdvertiserCredits, totalWebsites }) {
|
|
588
|
+
// Modèle de la demande (Annonceurs)
|
|
589
|
+
// Plus la commission est basse, plus ils sont prêts à acheter de clics.
|
|
590
|
+
const advertiserDemandModel = (commissionRate) => {
|
|
591
|
+
// Le nombre de clics "demandés" diminue à mesure que la commission augmente.
|
|
592
|
+
// La demande est aussi proportionnelle au total des crédits disponibles.
|
|
593
|
+
// --- SOLUTION : Ajout d'une demande de base pour éviter un taux de 0% ---
|
|
594
|
+
// Représente l'intérêt potentiel même sans crédits existants.
|
|
595
|
+
const baseDemand = (totalAdvertiserCredits || 100) * 10; // On simule 100 crédits de base.
|
|
596
|
+
// La demande diminue avec la racine carrée de la commission, ce qui la rend moins sensible au début.
|
|
597
|
+
return baseDemand * (1 - Math.pow(commissionRate, 0.5));
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// Modèle de l'offre (Webmasters)
|
|
601
|
+
// Plus leur rémunération est haute (commission basse), plus ils offrent d'espaces.
|
|
602
|
+
const webmasterSupplyModel = (commissionRate) => {
|
|
603
|
+
// L'offre de clics augmente quand la commission baisse (et donc que leur gain augmente).
|
|
604
|
+
const baseSupply = totalWebsites * 500; // Ex: 500 clics potentiels par site
|
|
605
|
+
// L'offre est un peu moins sensible à la commission que la demande.
|
|
606
|
+
return baseSupply * (1 - commissionRate * 0.8);
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
return function platformRevenueEvaluator(commissionRate) {
|
|
610
|
+
if (commissionRate < 0 || commissionRate > 1) return 0;
|
|
611
|
+
const demand = advertiserDemandModel(commissionRate);
|
|
612
|
+
const supply = webmasterSupplyModel(commissionRate);
|
|
613
|
+
const clicks = Math.min(demand, supply);
|
|
614
|
+
return clicks * commissionRate; // Le revenu est le nombre de clics * la commission
|
|
615
|
+
};
|
|
616
|
+
},
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Crée un évaluateur N-Dimensionnel pour le rendement d'une culture en fonction de multiples facteurs.
|
|
620
|
+
* @param {object} config - L'objet de configuration.
|
|
621
|
+
* @param {Array<{name: string, optimalAmount: number, weight: number, sensitivity: number}>} config.factors - Chaque facteur a une quantité optimale, un poids, et une sensibilité.
|
|
622
|
+
* @param {number} config.baseYield - Le rendement de base sans aucun apport.
|
|
623
|
+
* @returns {function(Array<number>): number} Un évaluateur qui prend un tableau de quantités (`amounts`) et retourne le rendement total. Le but est de MAXIMISER ce rendement.
|
|
624
|
+
*/
|
|
625
|
+
createMultiFactorCropYieldModel({ factors, baseYield }) {
|
|
626
|
+
return function multiFactorYieldEvaluator(amounts) {
|
|
627
|
+
if (amounts.length !== factors.length) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
"Le nombre de quantités doit correspondre au nombre de facteurs.",
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
let totalYield = baseYield;
|
|
634
|
+
for (let i = 0; i < factors.length; i++) {
|
|
635
|
+
const factor = factors[i];
|
|
636
|
+
const amount = amounts[i];
|
|
637
|
+
if (amount < 0) return -Infinity; // Pénalité forte pour les valeurs impossibles
|
|
638
|
+
|
|
639
|
+
const deviation = amount - factor.optimalAmount;
|
|
640
|
+
// La sensibilité contrôle la "largeur" de la parabole. Une sensibilité élevée rend le rendement plus sensible aux écarts.
|
|
641
|
+
totalYield +=
|
|
642
|
+
factor.weight -
|
|
643
|
+
(factor.sensitivity * deviation * deviation) /
|
|
644
|
+
(factor.optimalAmount + 1e-6);
|
|
645
|
+
}
|
|
646
|
+
return totalYield;
|
|
647
|
+
};
|
|
648
|
+
},
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Crée un évaluateur pour le rendement d'une culture en fonction de la quantité d'engrais.
|
|
652
|
+
* @param {object} config - L'objet de configuration.
|
|
653
|
+
* @param {number} config.optimalAmount - La quantité d'engrais qui produit le rendement maximal.
|
|
654
|
+
* @param {number} config.maxYield - Le rendement maximal possible.
|
|
655
|
+
* @returns {function(number): number} Un évaluateur qui prend une `fertilizerAmount` et retourne le rendement.
|
|
656
|
+
*/
|
|
657
|
+
createCropYieldModel({ optimalAmount, maxYield }) {
|
|
658
|
+
// Modèle quadratique simple : le rendement diminue symétriquement autour de l'optimum.
|
|
659
|
+
return function yieldEvaluator(fertilizerAmount) {
|
|
660
|
+
if (fertilizerAmount < 0) return 0;
|
|
661
|
+
const deviation = fertilizerAmount - optimalAmount;
|
|
662
|
+
// La formule est une parabole inversée.
|
|
663
|
+
const yieldValue = maxYield - (deviation * deviation) / optimalAmount;
|
|
664
|
+
return Math.max(0, yieldValue); // Le rendement ne peut pas être négatif.
|
|
665
|
+
};
|
|
666
|
+
},
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Crée un évaluateur pour la satisfaction utilisateur d'une durée d'animation.
|
|
670
|
+
* @param {object} config - L'objet de configuration.
|
|
671
|
+
* @param {number} config.idealDuration - La durée perçue comme parfaite (ex: 250ms).
|
|
672
|
+
* @returns {function(number): number} Un évaluateur qui prend une `duration` et retourne un score de satisfaction.
|
|
673
|
+
*/
|
|
674
|
+
createAnimationTimingModel({ idealDuration }) {
|
|
675
|
+
// Modèle basé sur une fonction de Gauss. Le score est maximal à la durée idéale et chute rapidement.
|
|
676
|
+
return function satisfactionEvaluator(duration) {
|
|
677
|
+
if (duration <= 0) return 0;
|
|
678
|
+
const deviation = duration - idealDuration;
|
|
679
|
+
const sigma = idealDuration / 2; // L'écart-type contrôle la "tolérance" autour de l'idéal.
|
|
680
|
+
return Math.exp(-(deviation * deviation) / (2 * sigma * sigma));
|
|
681
|
+
};
|
|
682
|
+
},
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Crée un évaluateur pour le problème de la brachistochrone (toboggan le plus rapide).
|
|
686
|
+
* @param {object} config - L'objet de configuration.
|
|
687
|
+
* @param {{x: number, y: number}} config.startPoint - Le point de départ.
|
|
688
|
+
* @param {{x: number, y: number}} config.endPoint - Le point d'arrivée.
|
|
689
|
+
* @param {number} config.numPoints - Le nombre de points intermédiaires pour discrétiser la courbe.
|
|
690
|
+
* @returns {function(Array<number>): number} Un évaluateur qui prend un tableau de hauteurs `y` et retourne le temps de descente.
|
|
691
|
+
*/
|
|
692
|
+
createBrachistochroneEvaluator({ startPoint, endPoint, numPoints }) {
|
|
693
|
+
const g = 9.81; // Accélération gravitationnelle
|
|
694
|
+
const segmentLengthX = endPoint.x / (numPoints + 1);
|
|
695
|
+
const epsilon = 1e-6; // Pour éviter la division par zéro au départ
|
|
696
|
+
|
|
697
|
+
return function evaluateSlideTime(slideShape) {
|
|
698
|
+
const points = [
|
|
699
|
+
startPoint,
|
|
700
|
+
...slideShape.map((y, i) => ({ x: (i + 1) * segmentLengthX, y })),
|
|
701
|
+
endPoint,
|
|
702
|
+
];
|
|
703
|
+
|
|
704
|
+
let totalTime = 0;
|
|
705
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
706
|
+
const p1 = points[i];
|
|
707
|
+
const p2 = points[i + 1];
|
|
708
|
+
|
|
709
|
+
const deltaX = p2.x - p1.x;
|
|
710
|
+
const deltaY = p2.y - p1.y;
|
|
711
|
+
const segmentDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
712
|
+
|
|
713
|
+
// Vitesse basée sur la conservation de l'énergie: v = sqrt(2*g*h)
|
|
714
|
+
const v1 = Math.sqrt(2 * g * (startPoint.y - p1.y + epsilon));
|
|
715
|
+
const v2 = Math.sqrt(2 * g * (startPoint.y - p2.y + epsilon));
|
|
716
|
+
const avgVelocity = (v1 + v2) / 2;
|
|
717
|
+
|
|
718
|
+
if (avgVelocity > 0) {
|
|
719
|
+
totalTime += segmentDistance / avgVelocity;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return totalTime;
|
|
723
|
+
};
|
|
724
|
+
},
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Crée une fonction qui calcule le gradient pour le problème de la brachistochrone.
|
|
728
|
+
* @param {object} config - L'objet de configuration.
|
|
729
|
+
* @param {{x: number, y: number}} config.startPoint - Le point de départ.
|
|
730
|
+
* @param {{x: number, y: number}} config.endPoint - Le point d'arrivée.
|
|
731
|
+
* @param {number} config.numPoints - Le nombre de points intermédiaires.
|
|
732
|
+
* @returns {function(Array<number>): Array<number>} Une fonction qui retourne le gradient du temps de descente par rapport aux hauteurs `y`.
|
|
733
|
+
*/
|
|
734
|
+
createBrachistochroneGradient({ startPoint, endPoint, numPoints }) {
|
|
735
|
+
const evaluator = this.createBrachistochroneEvaluator({
|
|
736
|
+
startPoint,
|
|
737
|
+
endPoint,
|
|
738
|
+
numPoints,
|
|
739
|
+
});
|
|
740
|
+
const h = 1e-5; // Petit pas pour la différenciation numérique
|
|
741
|
+
|
|
742
|
+
return function calculateGradient(slideShape) {
|
|
743
|
+
const gradient = [];
|
|
744
|
+
const baseTime = evaluator(slideShape);
|
|
745
|
+
|
|
746
|
+
for (let i = 0; i < slideShape.length; i++) {
|
|
747
|
+
const newShape = [...slideShape];
|
|
748
|
+
newShape[i] += h;
|
|
749
|
+
const newTime = evaluator(newShape);
|
|
750
|
+
|
|
751
|
+
// Dérivée partielle par différence finie
|
|
752
|
+
const derivative = (newTime - baseTime) / h;
|
|
753
|
+
gradient.push(derivative);
|
|
754
|
+
}
|
|
755
|
+
return gradient;
|
|
756
|
+
};
|
|
757
|
+
},
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const Optimization = {
|
|
761
|
+
// eslint-disable-line no-unused-vars
|
|
762
|
+
/**
|
|
763
|
+
* Trouve une bonne solution à un problème d'optimisation en utilisant le Recuit Simulé.
|
|
764
|
+
* Cet algorithme est efficace pour trouver un optimum global dans un grand espace de recherche
|
|
765
|
+
* avec de nombreux optima locaux (plusieurs "pics" ou "vallées").
|
|
766
|
+
* @template TSolution - Le type de la solution (peut être un nombre, un tableau, un objet...).
|
|
767
|
+
* @param {TSolution} initialSolution - Le point de départ de la recherche.
|
|
768
|
+
* @param {function(TSolution): number} evaluator - Fonction qui évalue une solution. L'objectif est de minimiser ce score.
|
|
769
|
+
* @param {function(TSolution): TSolution} neighbor - Fonction qui génère une solution "voisine" aléatoire.
|
|
770
|
+
* @param {number} [initialTemperature=1000] - La température de départ.
|
|
771
|
+
* @param {number} [coolingRate=0.995] - Le taux de refroidissement (proche de 1).
|
|
772
|
+
* @param {number} [maxIterations=10000] - Le nombre total d'itérations.
|
|
773
|
+
* @returns {{solution: TSolution, energy: number}} Le meilleur couple solution/score trouvé.
|
|
774
|
+
*/
|
|
775
|
+
simulatedAnnealing(
|
|
776
|
+
initialSolution,
|
|
777
|
+
evaluator,
|
|
778
|
+
neighbor,
|
|
779
|
+
initialTemperature = 1000,
|
|
780
|
+
coolingRate = 0.995,
|
|
781
|
+
maxIterations = 10000,
|
|
782
|
+
) {
|
|
783
|
+
let currentSolution = initialSolution;
|
|
784
|
+
let currentEnergy = evaluator(currentSolution);
|
|
785
|
+
|
|
786
|
+
let bestSolution = currentSolution;
|
|
787
|
+
let bestEnergy = currentEnergy;
|
|
788
|
+
|
|
789
|
+
let temperature = initialTemperature;
|
|
790
|
+
|
|
791
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
792
|
+
const newSolution = neighbor(currentSolution);
|
|
793
|
+
const newEnergy = evaluator(newSolution);
|
|
794
|
+
|
|
795
|
+
// Calcule la probabilité d'accepter une moins bonne solution.
|
|
796
|
+
const acceptanceProbability = Math.exp(
|
|
797
|
+
(currentEnergy - newEnergy) / temperature,
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
// Décide si on se déplace vers la nouvelle solution.
|
|
801
|
+
if (newEnergy < currentEnergy || Math.random() < acceptanceProbability) {
|
|
802
|
+
currentSolution = newSolution;
|
|
803
|
+
currentEnergy = newEnergy;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Met à jour la meilleure solution trouvée jusqu'à présent.
|
|
807
|
+
if (currentEnergy < bestEnergy) {
|
|
808
|
+
bestSolution = currentSolution;
|
|
809
|
+
bestEnergy = currentEnergy;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Refroidit la température.
|
|
813
|
+
temperature *= coolingRate;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return { solution: bestSolution, energy: bestEnergy };
|
|
817
|
+
},
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Résout un problème d'optimisation en utilisant un Algorithme Génétique.
|
|
821
|
+
* Idéal pour les problèmes complexes où l'espace de recherche est vaste et non-linéaire.
|
|
822
|
+
* @template TChromosome - Le type de la solution (un "chromosome").
|
|
823
|
+
* @param {function(): TChromosome} createIndividual - Fonction pour créer un individu aléatoire.
|
|
824
|
+
* @param {function(TChromosome): number} fitnessFunction - Évalue un individu. L'objectif est de MINIMISER ce score.
|
|
825
|
+
* @param {function(TChromosome, TChromosome): TChromosome} crossover - Croise deux parents pour créer un enfant.
|
|
826
|
+
* @param {function(TChromosome): TChromosome} mutate - Applique une mutation aléatoire à un individu.
|
|
827
|
+
* @param {object} options - Options de l'algorithme.
|
|
828
|
+
* @param {number} [options.populationSize=100] - Taille de la population.
|
|
829
|
+
* @param {number} [options.generations=100] - Nombre de générations à simuler.
|
|
830
|
+
* @param {number} [options.crossoverRate=0.8] - Probabilité de croisement.
|
|
831
|
+
* @param {number} [options.mutationRate=0.1] - Probabilité de mutation.
|
|
832
|
+
* @param {function} [options.selectionFunction] - Fonction de sélection des parents. Par défaut, un tournoi.
|
|
833
|
+
* @param {boolean} [options.returnPopulation=false] - Si true, retourne la population finale au lieu du meilleur individu.
|
|
834
|
+
* @returns {{solution: TChromosome, fitness: number}} Le meilleur individu trouvé.
|
|
835
|
+
*/
|
|
836
|
+
geneticAlgorithm(
|
|
837
|
+
createIndividual,
|
|
838
|
+
fitnessFunction,
|
|
839
|
+
crossover,
|
|
840
|
+
mutate,
|
|
841
|
+
options = {},
|
|
842
|
+
) {
|
|
843
|
+
const populationSize = options.populationSize || 100;
|
|
844
|
+
const generations = options.generations || 100;
|
|
845
|
+
const crossoverRate =
|
|
846
|
+
options.crossoverRate !== undefined ? options.crossoverRate : 0.8;
|
|
847
|
+
const mutationRate =
|
|
848
|
+
options.mutationRate !== undefined ? options.mutationRate : 0.1;
|
|
849
|
+
const selectionFunction =
|
|
850
|
+
options.selectionFunction ||
|
|
851
|
+
this.Operators.createTournamentSelection({ size: 5 });
|
|
852
|
+
const returnPopulation = options.returnPopulation || false;
|
|
853
|
+
|
|
854
|
+
// 1. Initialisation
|
|
855
|
+
// La population est un tableau d'objets { chromosome, fitness }
|
|
856
|
+
// La fitness est calculée une seule fois par individu.
|
|
857
|
+
let population = Array.from({ length: populationSize }, () => {
|
|
858
|
+
const chromosome = createIndividual();
|
|
859
|
+
return { chromosome, fitness: fitnessFunction(chromosome) };
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
// Trier la population initiale pour trouver le meilleur
|
|
863
|
+
population.sort((a, b) => a.fitness - b.fitness);
|
|
864
|
+
let bestOverall = population[0];
|
|
865
|
+
|
|
866
|
+
// 2. Boucle des générations
|
|
867
|
+
for (let gen = 0; gen < generations; gen++) {
|
|
868
|
+
const newPopulation = [];
|
|
869
|
+
|
|
870
|
+
// Élitisme : le meilleur individu de la génération précédente est conservé.
|
|
871
|
+
// Il est déjà à l'index 0 grâce au tri à la fin de la boucle précédente.
|
|
872
|
+
newPopulation.push(population[0]);
|
|
873
|
+
|
|
874
|
+
while (newPopulation.length < populationSize) {
|
|
875
|
+
// 3. Sélection
|
|
876
|
+
const parent1 = selectionFunction(population);
|
|
877
|
+
const parent2 = selectionFunction(population);
|
|
878
|
+
|
|
879
|
+
let offspringChromosome;
|
|
880
|
+
// 4. Croisement
|
|
881
|
+
if (Math.random() < crossoverRate) {
|
|
882
|
+
offspringChromosome = crossover(
|
|
883
|
+
parent1.chromosome,
|
|
884
|
+
parent2.chromosome,
|
|
885
|
+
);
|
|
886
|
+
} else {
|
|
887
|
+
offspringChromosome = parent1.chromosome;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// 5. Mutation
|
|
891
|
+
if (Math.random() < mutationRate) {
|
|
892
|
+
offspringChromosome = mutate(offspringChromosome);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// S'assurer que les opérateurs ont bien retourné un individu
|
|
896
|
+
if (offspringChromosome) {
|
|
897
|
+
newPopulation.push({
|
|
898
|
+
chromosome: offspringChromosome,
|
|
899
|
+
fitness: fitnessFunction(offspringChromosome),
|
|
900
|
+
});
|
|
901
|
+
} else {
|
|
902
|
+
// Si le croisement/mutation échoue, on réinsère un parent pour garder la taille de la population
|
|
903
|
+
newPopulation.push(parent1);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
population = newPopulation;
|
|
908
|
+
|
|
909
|
+
// Trier la nouvelle population pour la prochaine génération (élitisme) et la mise à jour du meilleur
|
|
910
|
+
population.sort((a, b) => a.fitness - b.fitness);
|
|
911
|
+
|
|
912
|
+
if (population[0].fitness < bestOverall.fitness) {
|
|
913
|
+
bestOverall = population[0];
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (returnPopulation) {
|
|
918
|
+
return population;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
return { solution: bestOverall.chromosome, fitness: bestOverall.fitness };
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Exécute un solveur stochastique plusieurs fois et retourne le meilleur résultat.
|
|
926
|
+
* C'est une méta-heuristique pour augmenter la probabilité de trouver un optimum global
|
|
927
|
+
* en échange d'un temps de calcul plus long.
|
|
928
|
+
* @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.
|
|
929
|
+
* @param {number} numCycles - Le nombre de fois où exécuter le solveur.
|
|
930
|
+
* @param {boolean} [logProgress=false] - Si true, affiche le score de chaque cycle dans la console.
|
|
931
|
+
* @returns {{bestResult: object, stats: {scores: Array<number>, average: number, stdDev: number}}} Le meilleur résultat et des statistiques sur les exécutions.
|
|
932
|
+
*/
|
|
933
|
+
runMultiple(solverFunction, numCycles, logProgress = false) {
|
|
934
|
+
let bestResult = null;
|
|
935
|
+
const allScores = [];
|
|
936
|
+
|
|
937
|
+
for (let i = 0; i < numCycles; i++) {
|
|
938
|
+
const currentResult = solverFunction();
|
|
939
|
+
|
|
940
|
+
// Gère les résultats du Recuit Simulé (energy) et des Algorithmes Génétiques (fitness).
|
|
941
|
+
// On suppose que pour les deux, un score plus bas est meilleur.
|
|
942
|
+
const currentScore =
|
|
943
|
+
currentResult.energy !== undefined
|
|
944
|
+
? currentResult.energy
|
|
945
|
+
: currentResult.fitness;
|
|
946
|
+
allScores.push(currentScore);
|
|
947
|
+
|
|
948
|
+
if (logProgress) {
|
|
949
|
+
console.log(
|
|
950
|
+
` -> Cycle ${i + 1}/${numCycles}: Score trouvé = ${currentScore.toFixed(2)}`,
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (
|
|
955
|
+
!bestResult ||
|
|
956
|
+
currentScore <
|
|
957
|
+
(bestResult.energy !== undefined
|
|
958
|
+
? bestResult.energy
|
|
959
|
+
: bestResult.fitness)
|
|
960
|
+
) {
|
|
961
|
+
bestResult = currentResult;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Calcul des statistiques
|
|
966
|
+
const sum = allScores.reduce((a, b) => a + b, 0);
|
|
967
|
+
const average = sum / numCycles;
|
|
968
|
+
const variance =
|
|
969
|
+
allScores.reduce((a, b) => a + Math.pow(b - average, 2), 0) / numCycles;
|
|
970
|
+
const stdDev = Math.sqrt(variance);
|
|
971
|
+
|
|
972
|
+
return {
|
|
973
|
+
bestResult,
|
|
974
|
+
stats: {
|
|
975
|
+
scores: allScores,
|
|
976
|
+
average: average,
|
|
977
|
+
stdDev: stdDev,
|
|
978
|
+
},
|
|
979
|
+
};
|
|
980
|
+
},
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Trouve un minimum local d'une fonction en utilisant l'algorithme de Descente de Gradient.
|
|
984
|
+
* Nécessite que la fonction soit différentiable et que son gradient soit connu.
|
|
985
|
+
* @template TSolution - Le type de la solution (nombre ou tableau de nombres).
|
|
986
|
+
* @param {TSolution} initialSolution - Le point de départ.
|
|
987
|
+
* @param {function(TSolution): TSolution} gradientFunction - Fonction qui calcule le gradient au point donné.
|
|
988
|
+
* @param {object} options - Options de l'algorithme.
|
|
989
|
+
* @param {number} [options.learningRate=0.01] - Le "pas" de la descente.
|
|
990
|
+
* @param {number} [options.maxIterations=1000] - Nombre d'itérations.
|
|
991
|
+
* @param {number} [options.tolerance=1e-6] - Seuil pour arrêter si la solution ne change plus beaucoup.
|
|
992
|
+
* @returns {TSolution} La solution (minimum local) trouvée.
|
|
993
|
+
*/
|
|
994
|
+
gradientDescent(initialSolution, gradientFunction, options = {}) {
|
|
995
|
+
const {
|
|
996
|
+
learningRate = 0.01,
|
|
997
|
+
maxIterations = 1000,
|
|
998
|
+
tolerance = 1e-6,
|
|
999
|
+
} = options;
|
|
1000
|
+
|
|
1001
|
+
let currentSolution = Array.isArray(initialSolution)
|
|
1002
|
+
? [...initialSolution]
|
|
1003
|
+
: initialSolution;
|
|
1004
|
+
|
|
1005
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
1006
|
+
const gradient = gradientFunction(currentSolution);
|
|
1007
|
+
|
|
1008
|
+
if (Array.isArray(currentSolution)) {
|
|
1009
|
+
const prevSolution = [...currentSolution];
|
|
1010
|
+
for (let j = 0; j < currentSolution.length; j++) {
|
|
1011
|
+
currentSolution[j] -= learningRate * gradient[j];
|
|
1012
|
+
}
|
|
1013
|
+
const change = prevSolution.reduce(
|
|
1014
|
+
(sum, val, idx) => sum + Math.abs(val - currentSolution[idx]),
|
|
1015
|
+
0,
|
|
1016
|
+
);
|
|
1017
|
+
if (change < tolerance) break;
|
|
1018
|
+
} else {
|
|
1019
|
+
// Cas d'une seule variable (nombre)
|
|
1020
|
+
const prevSolution = currentSolution;
|
|
1021
|
+
currentSolution -= learningRate * gradient;
|
|
1022
|
+
if (Math.abs(prevSolution - currentSolution) < tolerance) break;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return currentSolution;
|
|
1026
|
+
},
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Exécute un solveur stochastique plusieurs fois en parallèle en utilisant un pool de workers pour éviter de surcharger le système.
|
|
1030
|
+
* @param {string} solverName - Le nom de la fonction solveur à appeler dans `Optimization.Operators`.
|
|
1031
|
+
* @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).
|
|
1032
|
+
* @param {number} numCycles - Le nombre total de cycles à exécuter.
|
|
1033
|
+
* @param {boolean} [logProgress=false] - Si true, affiche la progression dans la console.
|
|
1034
|
+
* @param {object} [options={}] - Options pour la parallélisation.
|
|
1035
|
+
* @param {number} [options.concurrency] - Le nombre de workers à utiliser en parallèle. Par défaut, le nombre de cœurs CPU.
|
|
1036
|
+
* @returns {Promise<{bestResult: object, stats: {scores: Array<number>, average: number, stdDev: number}}>} Le meilleur résultat et des statistiques.
|
|
1037
|
+
*/
|
|
1038
|
+
async runMultipleParallel(
|
|
1039
|
+
solverName,
|
|
1040
|
+
baseSolverArgs,
|
|
1041
|
+
numCycles,
|
|
1042
|
+
logProgress = false,
|
|
1043
|
+
options = {},
|
|
1044
|
+
) {
|
|
1045
|
+
const concurrency = options.concurrency || os.cpus().length;
|
|
1046
|
+
if (logProgress) {
|
|
1047
|
+
console.log(
|
|
1048
|
+
` (Utilisation d'un pool de ${concurrency} workers pour ${numCycles} cycles)`,
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const allResults = new Array(numCycles);
|
|
1053
|
+
const tasks = Array.from({ length: numCycles }, (_, i) => i);
|
|
1054
|
+
let tasksCompleted = 0;
|
|
1055
|
+
|
|
1056
|
+
const runWorker = async (workerId) => {
|
|
1057
|
+
while (tasks.length > 0) {
|
|
1058
|
+
const taskIndex = tasks.shift();
|
|
1059
|
+
if (taskIndex === undefined) continue;
|
|
1060
|
+
|
|
1061
|
+
const workerData = {
|
|
1062
|
+
solverName,
|
|
1063
|
+
solverArgs: [
|
|
1064
|
+
// Chaque worker génère son propre jeu de données aléatoires pour garantir l'indépendance des cycles.
|
|
1065
|
+
Array.from({ length: 50 }, () => ({
|
|
1066
|
+
x: Math.random() * 100,
|
|
1067
|
+
y: Math.random() * 100,
|
|
1068
|
+
})),
|
|
1069
|
+
...baseSolverArgs,
|
|
1070
|
+
],
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
const result = await new Promise((resolve, reject) => {
|
|
1074
|
+
const worker = new Worker("./worker.js", { workerData });
|
|
1075
|
+
worker.on("message", resolve);
|
|
1076
|
+
worker.on("error", reject);
|
|
1077
|
+
worker.on("exit", (code) => {
|
|
1078
|
+
if (code !== 0)
|
|
1079
|
+
reject(
|
|
1080
|
+
new Error(`Worker ${workerId} a terminé avec le code ${code}`),
|
|
1081
|
+
);
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
allResults[taskIndex] = result;
|
|
1086
|
+
tasksCompleted++;
|
|
1087
|
+
if (logProgress) {
|
|
1088
|
+
const score =
|
|
1089
|
+
result.energy !== undefined ? result.energy : result.fitness;
|
|
1090
|
+
console.log(
|
|
1091
|
+
` -> Cycle ${tasksCompleted}/${numCycles} (Worker ${workerId}): Score = ${score.toFixed(2)}`,
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
const workerPromises = Array.from({ length: concurrency }, (_, i) =>
|
|
1098
|
+
runWorker(i + 1),
|
|
1099
|
+
);
|
|
1100
|
+
await Promise.all(workerPromises);
|
|
1101
|
+
|
|
1102
|
+
// Le reste de la logique est identique à `runMultiple`
|
|
1103
|
+
let bestResult = null;
|
|
1104
|
+
const allScores = [];
|
|
1105
|
+
allResults.forEach((result) => {
|
|
1106
|
+
const score =
|
|
1107
|
+
result.energy !== undefined ? result.energy : result.fitness;
|
|
1108
|
+
allScores.push(score);
|
|
1109
|
+
if (
|
|
1110
|
+
!bestResult ||
|
|
1111
|
+
score <
|
|
1112
|
+
(bestResult.energy !== undefined
|
|
1113
|
+
? bestResult.energy
|
|
1114
|
+
: bestResult.fitness)
|
|
1115
|
+
) {
|
|
1116
|
+
bestResult = result;
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
const sum = allScores.reduce((a, b) => a + b, 0);
|
|
1121
|
+
const average = sum / numCycles;
|
|
1122
|
+
const variance =
|
|
1123
|
+
allScores.reduce((a, b) => a + Math.pow(b - average, 2), 0) / numCycles;
|
|
1124
|
+
const stdDev = Math.sqrt(variance);
|
|
1125
|
+
|
|
1126
|
+
return {
|
|
1127
|
+
bestResult,
|
|
1128
|
+
stats: { scores: allScores, average, stdDev, concurrency },
|
|
1129
|
+
};
|
|
1130
|
+
},
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Détermine si la solution A domine la solution B en multi-objectifs (problème de minimisation).
|
|
1135
|
+
* @private
|
|
1136
|
+
* @param {number[]} objectivesA - Tableau des scores des objectifs pour la solution A.
|
|
1137
|
+
* @param {number[]} objectivesB - Tableau des scores des objectifs pour la solution B.
|
|
1138
|
+
* @returns {boolean} - True si A domine B.
|
|
1139
|
+
*/
|
|
1140
|
+
function paretoDominates(objectivesA, objectivesB) {
|
|
1141
|
+
let aIsBetterInOne = false;
|
|
1142
|
+
for (let i = 0; i < objectivesA.length; i++) {
|
|
1143
|
+
if (objectivesA[i] > objectivesB[i]) {
|
|
1144
|
+
return false; // A est pire sur au moins un objectif, donc ne domine pas.
|
|
1145
|
+
}
|
|
1146
|
+
if (objectivesA[i] < objectivesB[i]) {
|
|
1147
|
+
aIsBetterInOne = true; // A est strictement meilleur sur au moins un objectif.
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return aIsBetterInOne; // A domine B si elle n'est jamais pire et au moins une fois meilleure.
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Trie une population en fronts de Pareto non-dominés (inspiré de NSGA-II).
|
|
1155
|
+
* @private
|
|
1156
|
+
* @param {Array<{individual: any, objectives: number[]}>} populationWithObjectives - La population à trier.
|
|
1157
|
+
* @returns {Array<Array<{individual: any, objectives: number[]}>>} - Un tableau de fronts, où le premier est le meilleur.
|
|
1158
|
+
*/
|
|
1159
|
+
function nonDominatedSort(populationWithObjectives) {
|
|
1160
|
+
const fronts = [[]];
|
|
1161
|
+
for (const p1 of populationWithObjectives) {
|
|
1162
|
+
p1.dominationCount = 0;
|
|
1163
|
+
p1.dominatedSolutions = [];
|
|
1164
|
+
for (const p2 of populationWithObjectives) {
|
|
1165
|
+
if (p1 === p2) continue;
|
|
1166
|
+
if (paretoDominates(p1.objectives, p2.objectives)) {
|
|
1167
|
+
p1.dominatedSolutions.push(p2);
|
|
1168
|
+
} else if (paretoDominates(p2.objectives, p1.objectives)) {
|
|
1169
|
+
p1.dominationCount++;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (p1.dominationCount === 0) {
|
|
1173
|
+
p1.rank = 0;
|
|
1174
|
+
fronts[0].push(p1);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
let i = 0;
|
|
1179
|
+
while (fronts[i] && fronts[i].length > 0) {
|
|
1180
|
+
const nextFront = [];
|
|
1181
|
+
for (const p1 of fronts[i]) {
|
|
1182
|
+
for (const p2 of p1.dominatedSolutions) {
|
|
1183
|
+
p2.dominationCount--;
|
|
1184
|
+
if (p2.dominationCount === 0) {
|
|
1185
|
+
p2.rank = i + 1;
|
|
1186
|
+
nextFront.push(p2);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
i++;
|
|
1191
|
+
if (nextFront.length > 0) {
|
|
1192
|
+
fronts[i] = nextFront;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return fronts;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Calcule la distance de promiscuité (crowding distance) pour un front, afin de préserver la diversité.
|
|
1200
|
+
* @private
|
|
1201
|
+
* @param {Array<{individual: any, objectives: number[]}>} front - Le front de Pareto.
|
|
1202
|
+
*/
|
|
1203
|
+
function calculateCrowdingDistance(front) {
|
|
1204
|
+
if (front.length === 0) return;
|
|
1205
|
+
front.forEach((p) => (p.crowdingDistance = 0));
|
|
1206
|
+
const numObjectives = front[0].objectives.length;
|
|
1207
|
+
|
|
1208
|
+
for (let i = 0; i < numObjectives; i++) {
|
|
1209
|
+
front.sort((a, b) => a.objectives[i] - b.objectives[i]);
|
|
1210
|
+
const minObj = front[0].objectives[i];
|
|
1211
|
+
const maxObj = front[front.length - 1].objectives[i];
|
|
1212
|
+
|
|
1213
|
+
// Les solutions aux extrémités sont cruciales, on leur donne une distance infinie.
|
|
1214
|
+
front[0].crowdingDistance = Infinity;
|
|
1215
|
+
front[front.length - 1].crowdingDistance = Infinity;
|
|
1216
|
+
|
|
1217
|
+
if (maxObj === minObj) continue;
|
|
1218
|
+
|
|
1219
|
+
for (let j = 1; j < front.length - 1; j++) {
|
|
1220
|
+
front[j].crowdingDistance +=
|
|
1221
|
+
(front[j + 1].objectives[i] - front[j - 1].objectives[i]) /
|
|
1222
|
+
(maxObj - minObj);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Algorithme génétique multi-objectifs (inspiré de NSGA-II) pour trouver un front de Pareto.
|
|
1229
|
+
* @param {function(): any} createIndividual - Fonction qui crée un individu aléatoire.
|
|
1230
|
+
* @param {function(any): number[]} fitnessFunction - Fonction qui évalue un individu et retourne un tableau d'objectifs à MINIMISER.
|
|
1231
|
+
* @param {function(any, any): any} crossover - Fonction de croisement.
|
|
1232
|
+
* @param {function(any): any} mutate - Fonction de mutation.
|
|
1233
|
+
* @param {object} options - Options de l'algorithme.
|
|
1234
|
+
* @returns {Array<{solution: any, objectives: number[]}>} Le premier front de Pareto (l'ensemble des meilleures solutions de compromis).
|
|
1235
|
+
*/
|
|
1236
|
+
Optimization.geneticAlgorithmMultiObjective = function (
|
|
1237
|
+
createIndividual,
|
|
1238
|
+
fitnessFunction,
|
|
1239
|
+
crossover,
|
|
1240
|
+
mutate,
|
|
1241
|
+
options = {},
|
|
1242
|
+
) {
|
|
1243
|
+
const {
|
|
1244
|
+
generations = 100,
|
|
1245
|
+
populationSize = 50,
|
|
1246
|
+
mutationRate = 0.1,
|
|
1247
|
+
} = options;
|
|
1248
|
+
|
|
1249
|
+
let population = Array.from({ length: populationSize }, () => ({
|
|
1250
|
+
individual: createIndividual(),
|
|
1251
|
+
}));
|
|
1252
|
+
population.forEach((p) => (p.objectives = fitnessFunction(p.individual)));
|
|
1253
|
+
|
|
1254
|
+
for (let gen = 0; gen < generations; gen++) {
|
|
1255
|
+
// 1. Créer une population d'enfants
|
|
1256
|
+
const offspring = [];
|
|
1257
|
+
for (let i = 0; i < populationSize; i++) {
|
|
1258
|
+
// Sélection simple pour l'exemple
|
|
1259
|
+
const parent1 = population[Math.floor(Math.random() * population.length)];
|
|
1260
|
+
const parent2 = population[Math.floor(Math.random() * population.length)];
|
|
1261
|
+
let childIndividual = crossover(parent1.individual, parent2.individual);
|
|
1262
|
+
if (Math.random() < mutationRate) {
|
|
1263
|
+
childIndividual = mutate(childIndividual);
|
|
1264
|
+
}
|
|
1265
|
+
const child = { individual: childIndividual };
|
|
1266
|
+
child.objectives = fitnessFunction(child.individual);
|
|
1267
|
+
offspring.push(child);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// 2. Combiner parents et enfants
|
|
1271
|
+
const combinedPopulation = [...population, ...offspring];
|
|
1272
|
+
|
|
1273
|
+
// 3. Trier la population combinée en fronts
|
|
1274
|
+
const fronts = nonDominatedSort(combinedPopulation);
|
|
1275
|
+
|
|
1276
|
+
// 4. Construire la nouvelle population
|
|
1277
|
+
const newPopulation = [];
|
|
1278
|
+
for (const front of fronts) {
|
|
1279
|
+
if (newPopulation.length + front.length <= populationSize) {
|
|
1280
|
+
newPopulation.push(...front);
|
|
1281
|
+
} else {
|
|
1282
|
+
// Si le front est trop grand, on utilise la distance de promiscuité pour choisir les individus les plus diversifiés.
|
|
1283
|
+
calculateCrowdingDistance(front);
|
|
1284
|
+
front.sort((a, b) => b.crowdingDistance - a.crowdingDistance); // Trier par distance décroissante
|
|
1285
|
+
const remaining = populationSize - newPopulation.length;
|
|
1286
|
+
newPopulation.push(...front.slice(0, remaining));
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
population = newPopulation;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Retourner le premier front de la population finale
|
|
1294
|
+
const finalFronts = nonDominatedSort(population);
|
|
1295
|
+
const bestFront = finalFronts.length > 0 ? finalFronts[0] : [];
|
|
1296
|
+
|
|
1297
|
+
// Filtrer le front pour ne garder que les solutions avec des objectifs uniques
|
|
1298
|
+
const uniqueSolutionsMap = new Map();
|
|
1299
|
+
for (const p of bestFront) {
|
|
1300
|
+
const key = JSON.stringify(p.objectives);
|
|
1301
|
+
if (!uniqueSolutionsMap.has(key)) {
|
|
1302
|
+
uniqueSolutionsMap.set(key, {
|
|
1303
|
+
solution: p.individual,
|
|
1304
|
+
objectives: p.objectives,
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return Array.from(uniqueSolutionsMap.values());
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Utilitaires pour les problèmes d'optimisation.
|
|
1313
|
+
*/
|
|
1314
|
+
Optimization.Utils = {
|
|
1315
|
+
/** Calcule la distance euclidienne entre deux points (villes). */
|
|
1316
|
+
distance: (city1, city2) => Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2)),
|
|
1317
|
+
|
|
1318
|
+
/** Évalue la distance totale d'un chemin TSP donné. */
|
|
1319
|
+
evaluatePathDistance: (cities, path) => {
|
|
1320
|
+
let totalDistance = 0;
|
|
1321
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
1322
|
+
totalDistance += Optimization.Utils.distance(cities[path[i]], cities[path[i + 1]]);
|
|
1323
|
+
}
|
|
1324
|
+
totalDistance += Optimization.Utils.distance(cities[path[path.length - 1]], cities[path[0]]); // Retour au départ
|
|
1325
|
+
return totalDistance;
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Algorithme d'optimisation CMA-ES (Covariance Matrix Adaptation Evolution Strategy).
|
|
1331
|
+
* C'est un algorithme de pointe pour l'optimisation en boîte noire de fonctions non-linéaires et non-convexes.
|
|
1332
|
+
* Il est particulièrement efficace pour les problèmes avec des variables continues.
|
|
1333
|
+
* @param {function(Array<number>): number} fitnessFunction - La fonction à MINIMISER.
|
|
1334
|
+
* @param {Array<number>} initialSolution - Le point de départ de la recherche (un vecteur de nombres).
|
|
1335
|
+
* @param {number} initialStepSize - La taille de pas initiale (sigma).
|
|
1336
|
+
* @param {object} [options={}] - Options de l'algorithme.
|
|
1337
|
+
* @param {number} [options.maxGenerations=100] - Nombre maximum de générations.
|
|
1338
|
+
* @param {number} [options.populationSize] - Taille de la population (lambda). Calculée par défaut si non fournie.
|
|
1339
|
+
* @param {number} [options.tolerance=1e-6] - Seuil de tolérance pour l'arrêt précoce.
|
|
1340
|
+
* @returns {{solution: Array<number>, fitness: number}} La meilleure solution trouvée.
|
|
1341
|
+
*/
|
|
1342
|
+
Optimization.cmaes = function(fitnessFunction, initialSolution, initialStepSize, options = {}) {
|
|
1343
|
+
const n = initialSolution.length; // Dimension du problème
|
|
1344
|
+
|
|
1345
|
+
// --- Paramètres de l'algorithme (stratégie) ---
|
|
1346
|
+
const { maxGenerations = 100, tolerance = 1e-6 } = options;
|
|
1347
|
+
const populationSize = options.populationSize || (4 + Math.floor(3 * Math.log(n))); // Lambda
|
|
1348
|
+
const mu = Math.floor(populationSize / 2); // Nombre de parents pour la recombinaison
|
|
1349
|
+
|
|
1350
|
+
// Poids de recombinaison
|
|
1351
|
+
let weights = Array.from({ length: mu }, (_, i) => Math.log(mu + 0.5) - Math.log(i + 1));
|
|
1352
|
+
const sumWeights = weights.reduce((s, w) => s + w, 0);
|
|
1353
|
+
weights = weights.map(w => w / sumWeights);
|
|
1354
|
+
const muEff = 1 / weights.reduce((s, w) => s + w * w, 0);
|
|
1355
|
+
|
|
1356
|
+
// Paramètres d'adaptation
|
|
1357
|
+
const cc = (4 + muEff / n) / (n + 4 + 2 * muEff / n);
|
|
1358
|
+
const cs = (muEff + 2) / (n + muEff + 5);
|
|
1359
|
+
const c1 = 2 / (Math.pow(n + 1.3, 2) + muEff);
|
|
1360
|
+
const cmu = Math.min(1 - c1, 2 * (muEff - 2 + 1 / muEff) / (Math.pow(n + 2, 2) + muEff));
|
|
1361
|
+
const damps = 1 + 2 * Math.max(0, Math.sqrt((muEff - 1) / (n + 1)) - 1) + cs;
|
|
1362
|
+
|
|
1363
|
+
// --- Variables d'état dynamiques ---
|
|
1364
|
+
let mean = [...initialSolution]; // Le centre de la distribution de recherche
|
|
1365
|
+
let stepSize = initialStepSize; // Sigma
|
|
1366
|
+
let C = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => (i === j ? 1 : 0))); // Matrice de covariance
|
|
1367
|
+
let pc = Array(n).fill(0); // Chemin d'évolution pour C
|
|
1368
|
+
let ps = Array(n).fill(0); // Chemin d'évolution pour sigma
|
|
1369
|
+
|
|
1370
|
+
let bestFitness = Infinity;
|
|
1371
|
+
let bestSolution = null;
|
|
1372
|
+
|
|
1373
|
+
// Fonction pour la décomposition de Cholesky (simplifiée, pour matrice symétrique définie positive)
|
|
1374
|
+
function cholesky(A) {
|
|
1375
|
+
const L = Array.from({ length: n }, () => Array(n).fill(0));
|
|
1376
|
+
for (let i = 0; i < n; i++) {
|
|
1377
|
+
for (let j = 0; j <= i; j++) {
|
|
1378
|
+
let sum = 0;
|
|
1379
|
+
for (let k = 0; k < j; k++) {
|
|
1380
|
+
sum += L[i][k] * L[j][k];
|
|
1381
|
+
}
|
|
1382
|
+
if (i === j) {
|
|
1383
|
+
const val = A[i][i] - sum;
|
|
1384
|
+
if (val < 0) return null; // Non définie positive
|
|
1385
|
+
L[i][j] = Math.sqrt(val);
|
|
1386
|
+
} else {
|
|
1387
|
+
if (L[j][j] === 0) return null;
|
|
1388
|
+
L[i][j] = (A[i][j] - sum) / L[j][j];
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
return L;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
for (let gen = 0; gen < maxGenerations; gen++) {
|
|
1396
|
+
// 1. Échantillonnage de la nouvelle population
|
|
1397
|
+
const population = [];
|
|
1398
|
+
const arx = []; // Vecteurs de recherche
|
|
1399
|
+
const L = cholesky(C);
|
|
1400
|
+
if (!L) {
|
|
1401
|
+
console.warn("[CMA-ES] La matrice de covariance n'est plus définie positive. Arrêt.");
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
for (let i = 0; i < populationSize; i++) {
|
|
1406
|
+
const z = Array.from({ length: n }, () => Math.random() * 2 - 1); // Vecteur normal standard
|
|
1407
|
+
const y = Array(n).fill(0); // z transformé par L
|
|
1408
|
+
for (let r = 0; r < n; r++) {
|
|
1409
|
+
for (let c = 0; c < n; c++) {
|
|
1410
|
+
y[r] += L[r][c] * z[c];
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
arx.push(y);
|
|
1414
|
+
const individual = mean.map((m, j) => m + stepSize * y[j]);
|
|
1415
|
+
population.push({ individual, fitness: fitnessFunction(individual) });
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// 2. Trier et sélectionner les meilleurs
|
|
1419
|
+
population.sort((a, b) => a.fitness - b.fitness);
|
|
1420
|
+
const parents = population.slice(0, mu);
|
|
1421
|
+
|
|
1422
|
+
if (parents[0].fitness < bestFitness) {
|
|
1423
|
+
bestFitness = parents[0].fitness;
|
|
1424
|
+
bestSolution = parents[0].individual;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// 3. Mise à jour des variables d'état
|
|
1428
|
+
const oldMean = [...mean];
|
|
1429
|
+
const y_w = Array(n).fill(0);
|
|
1430
|
+
for (let j = 0; j < n; j++) {
|
|
1431
|
+
for (let i = 0; i < mu; i++) {
|
|
1432
|
+
const parentIndex = population.indexOf(parents[i]);
|
|
1433
|
+
y_w[j] += weights[i] * arx[parentIndex][j];
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
mean = oldMean.map((m, i) => m + stepSize * y_w[i]);
|
|
1437
|
+
|
|
1438
|
+
// 4. Adaptation des chemins d'évolution
|
|
1439
|
+
const C_inv_sqrt = cholesky(C); // Simplification, devrait être l'inverse de la racine
|
|
1440
|
+
const C_inv_sqrt_y_w = y_w; // Approximation
|
|
1441
|
+
ps = ps.map((p, i) => (1 - cs) * p + Math.sqrt(cs * (2 - cs) * muEff) * C_inv_sqrt_y_w[i]);
|
|
1442
|
+
|
|
1443
|
+
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);
|
|
1444
|
+
pc = pc.map((p, i) => (1 - cc) * p + (hsig ? Math.sqrt(cc * (2 - cc) * muEff) * y_w[i] : 0));
|
|
1445
|
+
|
|
1446
|
+
// 5. Adaptation de la matrice de covariance C
|
|
1447
|
+
let rankOneUpdate = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => c1 * pc[i] * pc[j]));
|
|
1448
|
+
let rankMuUpdate = Array.from({ length: n }, () => Array(n).fill(0));
|
|
1449
|
+
for (let k = 0; k < mu; k++) {
|
|
1450
|
+
const y_k = arx[population.indexOf(parents[k])];
|
|
1451
|
+
for (let i = 0; i < n; i++) {
|
|
1452
|
+
for (let j = 0; j < n; j++) {
|
|
1453
|
+
rankMuUpdate[i][j] += cmu * weights[k] * y_k[i] * y_k[j];
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
C = C.map((row, i) => row.map((val, j) => (1 - c1 - cmu) * val + rankOneUpdate[i][j] + rankMuUpdate[i][j]));
|
|
1458
|
+
|
|
1459
|
+
// 6. Adaptation de la taille de pas (sigma)
|
|
1460
|
+
stepSize *= Math.exp((cs / damps) * (Math.sqrt(ps.reduce((s, v) => s + v*v, 0)) / Math.sqrt(n) - 1));
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
return { solution: bestSolution, fitness: bestFitness };
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
/**
|
|
1467
|
+
* @class BinarySearchTree
|
|
1468
|
+
* @description Implémentation d'un Arbre Binaire de Recherche.
|
|
1469
|
+
* Chaque noeud a une valeur, et des sous-arbres gauche/droit.
|
|
1470
|
+
*/
|
|
1471
|
+
Dichotomy.BinarySearchTree = class {
|
|
1472
|
+
constructor(comparator = Dichotomy._defaultComparator) {
|
|
1473
|
+
this.root = null;
|
|
1474
|
+
this.comparator = comparator;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* Insère une nouvelle valeur dans l'arbre.
|
|
1479
|
+
* @param {*} value
|
|
1480
|
+
*/
|
|
1481
|
+
insert(value) {
|
|
1482
|
+
const newNode = { value, left: null, right: null };
|
|
1483
|
+
if (!this.root) {
|
|
1484
|
+
this.root = newNode;
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
let current = this.root;
|
|
1489
|
+
while (true) {
|
|
1490
|
+
const comparison = this.comparator(value, current.value);
|
|
1491
|
+
if (comparison < 0) {
|
|
1492
|
+
// Aller à gauche
|
|
1493
|
+
if (!current.left) {
|
|
1494
|
+
current.left = newNode;
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
current = current.left;
|
|
1498
|
+
} else {
|
|
1499
|
+
// Aller à droite (ou si égal, pour permettre les doublons)
|
|
1500
|
+
if (!current.right) {
|
|
1501
|
+
current.right = newNode;
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
current = current.right;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* Vérifie si une valeur est présente dans l'arbre.
|
|
1511
|
+
* @param {*} value
|
|
1512
|
+
* @returns {boolean}
|
|
1513
|
+
*/
|
|
1514
|
+
contains(value) {
|
|
1515
|
+
let current = this.root;
|
|
1516
|
+
while (current) {
|
|
1517
|
+
const comparison = this.comparator(value, current.value);
|
|
1518
|
+
if (comparison === 0) {
|
|
1519
|
+
return true;
|
|
1520
|
+
} else if (comparison < 0) {
|
|
1521
|
+
current = current.left;
|
|
1522
|
+
} else {
|
|
1523
|
+
current = current.right;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
return false;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Effectue un parcours In-Order (gauche, racine, droite) qui retourne les éléments triés.
|
|
1531
|
+
* @returns {Array<*>}
|
|
1532
|
+
*/
|
|
1533
|
+
inOrderTraversal() {
|
|
1534
|
+
const result = [];
|
|
1535
|
+
function traverse(node) {
|
|
1536
|
+
if (node) {
|
|
1537
|
+
traverse(node.left);
|
|
1538
|
+
result.push(node.value);
|
|
1539
|
+
traverse(node.right);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
traverse(this.root);
|
|
1543
|
+
return result;
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* @namespace Optimization.Operators
|
|
1549
|
+
* @description Une bibliothèque de "fabriques d'évaluateurs" pour des problèmes d'optimisation complexes,
|
|
1550
|
+
* souvent multidimensionnels, à utiliser avec les algorithmes de `Optimization` (Recuit Simulé, Algorithmes Génétiques, etc.).
|
|
1551
|
+
*/
|
|
1552
|
+
Optimization.Operators = {}; // Création du namespace
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Crée une fonction de sélection par tournoi pour un algorithme génétique.
|
|
1556
|
+
* @param {object} [options] - Options pour le tournoi.
|
|
1557
|
+
* @param {number} [options.size=5] - Le nombre de participants par tournoi.
|
|
1558
|
+
* @returns {function(Array<{chromosome: any, fitness: number}>): {chromosome: any, fitness: number}} Une fonction de sélection.
|
|
1559
|
+
*/
|
|
1560
|
+
Optimization.Operators.createTournamentSelection = (options = {}) => {
|
|
1561
|
+
const tournamentSize = options.size || 5;
|
|
1562
|
+
|
|
1563
|
+
return function tournamentSelection(population) {
|
|
1564
|
+
let best = null;
|
|
1565
|
+
|
|
1566
|
+
for (let i = 0; i < tournamentSize; i++) {
|
|
1567
|
+
const individual =
|
|
1568
|
+
population[Math.floor(Math.random() * population.length)];
|
|
1569
|
+
if (!best || individual.fitness < best.fitness) {
|
|
1570
|
+
best = individual;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
// Retourne le meilleur trouvé. Dans le pire des cas (tous les scores sont Infinity),
|
|
1574
|
+
// on retourne le premier candidat sélectionné au lieu de null.
|
|
1575
|
+
if (!best) {
|
|
1576
|
+
return population[Math.floor(Math.random() * population.length)];
|
|
1577
|
+
}
|
|
1578
|
+
return best;
|
|
1579
|
+
};
|
|
1580
|
+
};
|
|
1581
|
+
|
|
1582
|
+
/**
|
|
1583
|
+
* Crée une matrice de covariance à partir de coefficients de corrélation déclarés.
|
|
1584
|
+
* C'est une manière plus intuitive de définir les relations de risque entre les actifs.
|
|
1585
|
+
* @param {object} config - L'objet de configuration.
|
|
1586
|
+
* @param {Array<{name: string, volatility: number}>} config.assets - La liste des actifs avec leur volatilité.
|
|
1587
|
+
* @param {Array<{assets: [string, string], correlation: number}>} config.correlations - Une liste de relations de corrélation.
|
|
1588
|
+
* @returns {Array<Array<number>>} La matrice de covariance calculée.
|
|
1589
|
+
*/
|
|
1590
|
+
Optimization.Operators.createCovarianceMatrixFromCorrelations = ({
|
|
1591
|
+
assets,
|
|
1592
|
+
correlations,
|
|
1593
|
+
}) => {
|
|
1594
|
+
const n = assets.length;
|
|
1595
|
+
const matrix = Array.from({ length: n }, () => Array(n).fill(0));
|
|
1596
|
+
|
|
1597
|
+
// Créer un map pour un accès rapide aux infos des actifs par leur nom.
|
|
1598
|
+
const assetInfo = new Map();
|
|
1599
|
+
assets.forEach((asset, index) => {
|
|
1600
|
+
assetInfo.set(asset.name, { index, volatility: asset.volatility });
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
// 1. Remplir la diagonale avec les variances (volatilité^2)
|
|
1604
|
+
for (let i = 0; i < n; i++) {
|
|
1605
|
+
const variance = Math.pow(assets[i].volatility, 2);
|
|
1606
|
+
matrix[i][i] = variance;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// 2. Remplir les autres cellules avec les covariances calculées
|
|
1610
|
+
for (const corr of correlations) {
|
|
1611
|
+
const [nameA, nameB] = corr.assets;
|
|
1612
|
+
if (!assetInfo.has(nameA) || !assetInfo.has(nameB)) {
|
|
1613
|
+
console.warn(
|
|
1614
|
+
`Avertissement: L'un des actifs [${nameA}, ${nameB}] n'a pas été trouvé. La corrélation est ignorée.`,
|
|
1615
|
+
);
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const infoA = assetInfo.get(nameA);
|
|
1620
|
+
const infoB = assetInfo.get(nameB);
|
|
1621
|
+
|
|
1622
|
+
// Cov(A,B) = Corr(A,B) * Vol(A) * Vol(B)
|
|
1623
|
+
const covariance = corr.correlation * infoA.volatility * infoB.volatility;
|
|
1624
|
+
|
|
1625
|
+
matrix[infoA.index][infoB.index] = covariance;
|
|
1626
|
+
matrix[infoB.index][infoA.index] = covariance; // La matrice est symétrique
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
return matrix;
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
// Le "déséquilibre" est la différence absolue entre l'offre et la demande. On veut le minimiser.
|
|
1633
|
+
Optimization.Operators.createMarketEquilibriumEvaluator = (
|
|
1634
|
+
demandModel,
|
|
1635
|
+
supplyModel,
|
|
1636
|
+
) => {
|
|
1637
|
+
return function marketImbalance(price) {
|
|
1638
|
+
const d = demandModel(price);
|
|
1639
|
+
const s = supplyModel(price);
|
|
1640
|
+
return Math.abs(d - s);
|
|
1641
|
+
};
|
|
1642
|
+
};
|
|
1643
|
+
|
|
1644
|
+
// Un "individu" est un tableau de 3 poids (ex: [0.5, 0.2, 0.3]) qui doivent sommer à 1.
|
|
1645
|
+
/**
|
|
1646
|
+
* Crée une fonction de fitness pour l'optimisation de portefeuille.
|
|
1647
|
+
* @param {object} config - L'objet de configuration.
|
|
1648
|
+
* @param {Array<{name: string, expectedReturn: number, volatility: number}>} config.assets - Les actifs disponibles.
|
|
1649
|
+
* @param {number} config.maxVolatility - La contrainte de volatilité maximale du portefeuille.
|
|
1650
|
+
* @param {Array<Array<number>>} [config.covarianceMatrix] - Matrice de covariance pour un calcul de risque précis.
|
|
1651
|
+
* @returns {function(Array<number>): number} Une fonction de fitness qui évalue un portefeuille (tableau de poids).
|
|
1652
|
+
*/
|
|
1653
|
+
Optimization.Operators.createPortfolioAllocator = ({
|
|
1654
|
+
assets,
|
|
1655
|
+
maxVolatility,
|
|
1656
|
+
covarianceMatrix,
|
|
1657
|
+
}) => {
|
|
1658
|
+
// La fonction de fitness évalue un portefeuille (un tableau de poids).
|
|
1659
|
+
// L'objectif est de MINIMISER le score, donc on minimise le rendement NÉGATIF.
|
|
1660
|
+
return function portfolioFitness(weights) {
|
|
1661
|
+
// Normaliser les poids pour qu'ils somment à 1
|
|
1662
|
+
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
|
|
1663
|
+
if (totalWeight === 0) return Infinity; // Éviter la division par zéro, score très mauvais
|
|
1664
|
+
const normalizedWeights = weights.map((w) => w / totalWeight);
|
|
1665
|
+
|
|
1666
|
+
let portfolioReturn = normalizedWeights.reduce(
|
|
1667
|
+
(sum, w, i) => sum + w * assets[i].expectedReturn,
|
|
1668
|
+
0,
|
|
1669
|
+
);
|
|
1670
|
+
let portfolioVolatility;
|
|
1671
|
+
|
|
1672
|
+
if (covarianceMatrix) {
|
|
1673
|
+
// Calcul de la volatilité avec la matrice de covariance (plus précis)
|
|
1674
|
+
// Volatilité^2 = w' * C * w
|
|
1675
|
+
let variance = 0;
|
|
1676
|
+
for (let i = 0; i < assets.length; i++) {
|
|
1677
|
+
for (let j = 0; j < assets.length; j++) {
|
|
1678
|
+
variance +=
|
|
1679
|
+
normalizedWeights[i] *
|
|
1680
|
+
normalizedWeights[j] *
|
|
1681
|
+
covarianceMatrix[i][j];
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
portfolioVolatility = Math.sqrt(variance);
|
|
1685
|
+
} else {
|
|
1686
|
+
// Calcul simplifié (moins précis) : moyenne pondérée des volatilités individuelles.
|
|
1687
|
+
portfolioVolatility = normalizedWeights.reduce(
|
|
1688
|
+
(sum, w, i) => sum + w * assets[i].volatility,
|
|
1689
|
+
0,
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
for (let i = 0; i < assets.length; i++) {}
|
|
1694
|
+
|
|
1695
|
+
// Forte pénalité si la contrainte de risque n'est pas respectée.
|
|
1696
|
+
if (portfolioVolatility > maxVolatility) {
|
|
1697
|
+
return 1000 + (portfolioVolatility - maxVolatility) * 1000; // Pénalité proportionnelle à la violation.
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// On veut maximiser le rendement, donc on minimise son opposé.
|
|
1701
|
+
return -portfolioReturn;
|
|
1702
|
+
};
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* Crée un solveur complet pour le problème du voyageur de commerce (TSP) en utilisant le Recuit Simulé.
|
|
1707
|
+
* Cette fonction factorise la création de l'évaluateur de chemin et de la fonction de voisinage.
|
|
1708
|
+
* @param {Array<{x: number, y: number}>} cities - Un tableau d'objets représentant les coordonnées des villes.
|
|
1709
|
+
* @param {object} [options] - Options pour l'algorithme de recuit simulé.
|
|
1710
|
+
* @returns {{solution: Array<number>, energy: number}} Le chemin optimal (indices des villes) et sa distance.
|
|
1711
|
+
*/
|
|
1712
|
+
Optimization.Operators.solveTSP = (cities, options = {}) => {
|
|
1713
|
+
// Fonction interne pour calculer la distance entre deux villes.
|
|
1714
|
+
const distance = (city1, city2) =>
|
|
1715
|
+
Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2));
|
|
1716
|
+
|
|
1717
|
+
// Évaluateur : calcule la longueur totale d'un chemin donné.
|
|
1718
|
+
const pathEvaluator = (path) => {
|
|
1719
|
+
let totalDistance = 0;
|
|
1720
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
1721
|
+
totalDistance += distance(cities[path[i]], cities[path[i + 1]]);
|
|
1722
|
+
}
|
|
1723
|
+
totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); // Retour au départ
|
|
1724
|
+
return totalDistance;
|
|
1725
|
+
};
|
|
1726
|
+
|
|
1727
|
+
// Voisinage : génère un chemin voisin en inversant une sous-séquence (heuristique 2-opt).
|
|
1728
|
+
const pathNeighbor = (path) => {
|
|
1729
|
+
const newPath = [...path];
|
|
1730
|
+
let i = Math.floor(Math.random() * newPath.length);
|
|
1731
|
+
let j = Math.floor(Math.random() * newPath.length);
|
|
1732
|
+
if (i === j) j = (j + 1) % newPath.length;
|
|
1733
|
+
const [start, end] = [Math.min(i, j), Math.max(i, j)];
|
|
1734
|
+
|
|
1735
|
+
const segment = newPath.slice(start, end + 1).reverse();
|
|
1736
|
+
newPath.splice(start, segment.length, ...segment);
|
|
1737
|
+
return newPath;
|
|
1738
|
+
};
|
|
1739
|
+
|
|
1740
|
+
// Solution initiale : un chemin aléatoire.
|
|
1741
|
+
const initialPath = Array.from({ length: cities.length }, (_, i) => i).sort(
|
|
1742
|
+
() => Math.random() - 0.5,
|
|
1743
|
+
);
|
|
1744
|
+
|
|
1745
|
+
// Paramètres par défaut pour le TSP, pouvant être surchargés par `options`.
|
|
1746
|
+
const saOptions = {
|
|
1747
|
+
initialTemperature: 10000,
|
|
1748
|
+
coolingRate: 0.999,
|
|
1749
|
+
maxIterations: 100000,
|
|
1750
|
+
...options,
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1753
|
+
return Optimization.simulatedAnnealing(
|
|
1754
|
+
initialPath,
|
|
1755
|
+
pathEvaluator,
|
|
1756
|
+
pathNeighbor,
|
|
1757
|
+
saOptions.initialTemperature,
|
|
1758
|
+
saOptions.coolingRate,
|
|
1759
|
+
saOptions.maxIterations,
|
|
1760
|
+
);
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* Crée un solveur complet pour le problème d'optimisation de portefeuille en utilisant un Algorithme Génétique.
|
|
1765
|
+
* @param {Array<{name: string, expectedReturn: number, volatility: number}>} assets - Les actifs disponibles.
|
|
1766
|
+
* @param {number} maxVolatility - La contrainte de volatilité maximale du portefeuille.
|
|
1767
|
+
* @param {object} [options] - Options pour l'algorithme génétique.
|
|
1768
|
+
* @returns {{solution: Array<number>, fitness: number}} L'allocation de poids optimale et le score de fitness associé.
|
|
1769
|
+
*/
|
|
1770
|
+
Optimization.Operators.solvePortfolio = (
|
|
1771
|
+
assets,
|
|
1772
|
+
maxVolatility,
|
|
1773
|
+
options = {},
|
|
1774
|
+
) => {
|
|
1775
|
+
// La fonction de fitness est créée par notre opérateur existant.
|
|
1776
|
+
const fitnessFunction = Optimization.Operators.createPortfolioAllocator({
|
|
1777
|
+
assets,
|
|
1778
|
+
maxVolatility,
|
|
1779
|
+
covarianceMatrix: options.covarianceMatrix,
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
// Fonctions spécifiques au problème pour l'AG, maintenant encapsulées.
|
|
1783
|
+
const createIndividual = () =>
|
|
1784
|
+
Array.from({ length: assets.length }, () => Math.random());
|
|
1785
|
+
|
|
1786
|
+
const crossover = (p1, p2) => p1.map((w1, i) => (w1 + p2[i]) / 2); // Moyenne des poids
|
|
1787
|
+
|
|
1788
|
+
const mutate = (p) => {
|
|
1789
|
+
const newP = [...p];
|
|
1790
|
+
const i = Math.floor(Math.random() * newP.length);
|
|
1791
|
+
newP[i] += (Math.random() - 0.5) * 0.2; // Mutation douce
|
|
1792
|
+
newP[i] = Math.max(0, newP[i]); // Les poids ne peuvent être négatifs
|
|
1793
|
+
return newP;
|
|
1794
|
+
};
|
|
1795
|
+
|
|
1796
|
+
const gaOptions = {
|
|
1797
|
+
generations: 150,
|
|
1798
|
+
populationSize: 100,
|
|
1799
|
+
...options,
|
|
1800
|
+
};
|
|
1801
|
+
|
|
1802
|
+
return Optimization.geneticAlgorithm(
|
|
1803
|
+
createIndividual,
|
|
1804
|
+
fitnessFunction,
|
|
1805
|
+
crossover,
|
|
1806
|
+
mutate,
|
|
1807
|
+
gaOptions,
|
|
1808
|
+
);
|
|
1809
|
+
};
|
|
1810
|
+
|
|
1811
|
+
/**
|
|
1812
|
+
* Crée un évaluateur 2D pour trouver la commission de base et le facteur de bonus qualité qui maximisent les revenus de la plateforme.
|
|
1813
|
+
* @param {object} config - L'objet de configuration.
|
|
1814
|
+
* @param {number} config.totalAdvertiserCredits - Le total des crédits disponibles chez les annonceurs.
|
|
1815
|
+
* @param {Array<{qualityScore: number}>} config.websites - Un tableau d'objets représentant les sites monétisés, chacun avec un score de qualité.
|
|
1816
|
+
* @returns {function(Array<number>): number} Un évaluateur qui prend une solution `[baseCommission, bonusFactor]` et retourne le revenu NÉGATIF (car les solveurs minimisent).
|
|
1817
|
+
*/
|
|
1818
|
+
Optimization.Operators.createAdvancedPlatformRevenueEvaluator = ({
|
|
1819
|
+
totalAdvertiserCredits,
|
|
1820
|
+
websites,
|
|
1821
|
+
}) => {
|
|
1822
|
+
// Modèle de la demande (Annonceurs)
|
|
1823
|
+
// La demande est sensible à la qualité globale de l'inventaire publicitaire.
|
|
1824
|
+
const advertiserDemandModel = (averageSiteQuality) => {
|
|
1825
|
+
// La demande de base est toujours liée aux crédits disponibles.
|
|
1826
|
+
const baseDemand = (totalAdvertiserCredits || 100) * 10;
|
|
1827
|
+
// La demande augmente avec la qualité moyenne des sites.
|
|
1828
|
+
return baseDemand * (1 + averageSiteQuality);
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
// Modèle de l'offre (Webmasters)
|
|
1832
|
+
// L'offre de chaque site dépend de sa rémunération individuelle.
|
|
1833
|
+
const webmasterSupplyModel = (baseCommission, bonusFactor) => {
|
|
1834
|
+
let totalOfferedClicks = 0;
|
|
1835
|
+
const baseSupplyPerSite = 500; // Clics potentiels par site
|
|
1836
|
+
|
|
1837
|
+
for (const site of websites) {
|
|
1838
|
+
// La commission effective est réduite pour les sites de haute qualité.
|
|
1839
|
+
const effectiveCommission = Math.max(
|
|
1840
|
+
0,
|
|
1841
|
+
baseCommission - site.qualityScore * bonusFactor,
|
|
1842
|
+
);
|
|
1843
|
+
const webmasterPayoutRate = 1 - effectiveCommission;
|
|
1844
|
+
|
|
1845
|
+
// L'offre d'un site est proportionnelle à son taux de rémunération.
|
|
1846
|
+
totalOfferedClicks += baseSupplyPerSite * webmasterPayoutRate;
|
|
1847
|
+
}
|
|
1848
|
+
return totalOfferedClicks;
|
|
1849
|
+
};
|
|
1850
|
+
|
|
1851
|
+
// L'évaluateur pour l'algorithme d'optimisation (Recuit Simulé, etc.)
|
|
1852
|
+
return function revenueEvaluator(solution) {
|
|
1853
|
+
const [baseCommission, bonusFactor] = solution;
|
|
1854
|
+
|
|
1855
|
+
// Contraintes : on pénalise fortement les solutions hors des clous.
|
|
1856
|
+
if (
|
|
1857
|
+
baseCommission < 0.01 ||
|
|
1858
|
+
baseCommission > 0.8 ||
|
|
1859
|
+
bonusFactor < 0 ||
|
|
1860
|
+
bonusFactor > baseCommission
|
|
1861
|
+
) {
|
|
1862
|
+
return Infinity; // Score très mauvais
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
const averageQuality =
|
|
1866
|
+
websites.length > 0
|
|
1867
|
+
? websites.reduce((sum, site) => sum + site.qualityScore, 0) /
|
|
1868
|
+
websites.length
|
|
1869
|
+
: 0.5;
|
|
1870
|
+
|
|
1871
|
+
const demand = advertiserDemandModel(averageQuality);
|
|
1872
|
+
const supply = webmasterSupplyModel(baseCommission, bonusFactor);
|
|
1873
|
+
|
|
1874
|
+
const clicks = Math.min(demand, supply);
|
|
1875
|
+
const averageCommission = Math.max(
|
|
1876
|
+
0,
|
|
1877
|
+
baseCommission - averageQuality * bonusFactor,
|
|
1878
|
+
);
|
|
1879
|
+
|
|
1880
|
+
// On veut MAXIMISER le revenu, donc on MINIMISE son opposé.
|
|
1881
|
+
return -(clicks * averageCommission);
|
|
1882
|
+
};
|
|
1883
|
+
};
|
|
1884
|
+
|
|
1885
|
+
/**
|
|
1886
|
+
* Crée un solveur pour le problème de placement d'infrastructures (Facility Location Problem).
|
|
1887
|
+
* @param {Array<{x: number, y: number}>} customers - Coordonnées des clients.
|
|
1888
|
+
* @param {number} numFacilities - Le nombre d'infrastructures à placer.
|
|
1889
|
+
* @param {{minX: number, maxX: number, minY: number, maxY: number}} bounds - Les limites de la carte où placer les infrastructures.
|
|
1890
|
+
* @param {number} [options.fixedCostPerFacility=0] - Coût fixe pour chaque infrastructure installée.
|
|
1891
|
+
* @param {object} [options] - Options pour le recuit simulé.
|
|
1892
|
+
* @returns {{solution: Array<{x: number, y: number}>, energy: number}} Les coordonnées optimales des infrastructures et le coût total.
|
|
1893
|
+
*/
|
|
1894
|
+
Optimization.Operators.solveFacilityLocation = (
|
|
1895
|
+
customers,
|
|
1896
|
+
numFacilities,
|
|
1897
|
+
bounds,
|
|
1898
|
+
options = {},
|
|
1899
|
+
) => {
|
|
1900
|
+
const fixedCostPerFacility = options.fixedCostPerFacility || 0;
|
|
1901
|
+
const distanceSq = (p1, p2) =>
|
|
1902
|
+
Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2); // On utilise la distance au carré pour l'efficacité
|
|
1903
|
+
|
|
1904
|
+
// Évaluateur : calcule la somme des distances de chaque client à son infrastructure la plus proche.
|
|
1905
|
+
const facilityEvaluator = (facilities) => {
|
|
1906
|
+
let totalConnectionCost = 0;
|
|
1907
|
+
for (const customer of customers) {
|
|
1908
|
+
let minDistanceToCustomer = Infinity;
|
|
1909
|
+
for (const facility of facilities) {
|
|
1910
|
+
const d = distanceSq(customer, facility);
|
|
1911
|
+
if (d < minDistanceToCustomer) {
|
|
1912
|
+
minDistanceToCustomer = d;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
totalConnectionCost += Math.sqrt(minDistanceToCustomer); // On utilise la vraie distance pour le coût
|
|
1916
|
+
}
|
|
1917
|
+
// Le coût total est la somme des coûts de connexion + le coût fixe des infrastructures.
|
|
1918
|
+
return totalConnectionCost + facilities.length * fixedCostPerFacility;
|
|
1919
|
+
};
|
|
1920
|
+
|
|
1921
|
+
// Voisinage : déplace légèrement une infrastructure au hasard.
|
|
1922
|
+
const facilityNeighbor = (facilities) => {
|
|
1923
|
+
const newFacilities = facilities.map((f) => ({ ...f }));
|
|
1924
|
+
const i = Math.floor(Math.random() * numFacilities);
|
|
1925
|
+
const moveX = (Math.random() - 0.5) * (bounds.maxX - bounds.minX) * 0.1;
|
|
1926
|
+
const moveY = (Math.random() - 0.5) * (bounds.maxY - bounds.minY) * 0.1;
|
|
1927
|
+
|
|
1928
|
+
newFacilities[i].x = Math.max(
|
|
1929
|
+
bounds.minX,
|
|
1930
|
+
Math.min(bounds.maxX, newFacilities[i].x + moveX),
|
|
1931
|
+
);
|
|
1932
|
+
newFacilities[i].y = Math.max(
|
|
1933
|
+
bounds.minY,
|
|
1934
|
+
Math.min(bounds.maxY, newFacilities[i].y + moveY),
|
|
1935
|
+
);
|
|
1936
|
+
|
|
1937
|
+
return newFacilities;
|
|
1938
|
+
};
|
|
1939
|
+
|
|
1940
|
+
// Solution initiale : place les infrastructures au hasard sur la carte.
|
|
1941
|
+
const initialFacilities = Array.from({ length: numFacilities }, () => ({
|
|
1942
|
+
x: bounds.minX + Math.random() * (bounds.maxX - bounds.minX),
|
|
1943
|
+
y: bounds.minY + Math.random() * (bounds.maxY - bounds.minY),
|
|
1944
|
+
}));
|
|
1945
|
+
|
|
1946
|
+
const saOptions = {
|
|
1947
|
+
initialTemperature: 100000,
|
|
1948
|
+
coolingRate: 0.999,
|
|
1949
|
+
maxIterations: 50000,
|
|
1950
|
+
...options,
|
|
1951
|
+
};
|
|
1952
|
+
|
|
1953
|
+
const result = Optimization.simulatedAnnealing(
|
|
1954
|
+
initialFacilities,
|
|
1955
|
+
facilityEvaluator,
|
|
1956
|
+
facilityNeighbor,
|
|
1957
|
+
saOptions.initialTemperature,
|
|
1958
|
+
saOptions.coolingRate,
|
|
1959
|
+
saOptions.maxIterations,
|
|
1960
|
+
);
|
|
1961
|
+
|
|
1962
|
+
return result;
|
|
1963
|
+
};
|
|
1964
|
+
|
|
1965
|
+
// --- SOLUTION : Définir un coût de base pour un clic ---
|
|
1966
|
+
// 1 PRIM'S = 1 clic
|
|
1967
|
+
const BASE_CLICK_COST = 1;
|
|
1968
|
+
/**
|
|
1969
|
+
* Crée un évaluateur multi-objectifs pour déterminer le Coût Par Clic (CPC) optimal.
|
|
1970
|
+
* @param {object} config - L'objet de configuration.
|
|
1971
|
+
* @param {object} config.advertiser - L'annonceur qui paie le clic.
|
|
1972
|
+
* @param {object} config.ad - L'annonce qui a été cliquée.
|
|
1973
|
+
* @param {Array<object>} config.competingAds - Les autres annonces ciblant les mêmes mots-clés.
|
|
1974
|
+
* @param {object} config.website - Le site sur lequel le clic a eu lieu.
|
|
1975
|
+
* @param {object} config.platformParams - Les paramètres de la plateforme (taux de commission, etc.).
|
|
1976
|
+
* @param {number} config.estimatedImpressions - Le nombre d'impressions quotidiennes estimées pour ce contexte.
|
|
1977
|
+
*/
|
|
1978
|
+
Optimization.Operators.createOptimalCPCEvaluator = (context) => {
|
|
1979
|
+
const { optimalBaseCommission, optimalBonusFactor } = platformParams;
|
|
1980
|
+
const websiteQualityScore = (website?.relevanceScore || 50) / 100;
|
|
1981
|
+
|
|
1982
|
+
// Le taux de commission effectif pour ce site
|
|
1983
|
+
const effectiveCommissionRate = Math.max(
|
|
1984
|
+
0,
|
|
1985
|
+
optimalBaseCommission - websiteQualityScore * optimalBonusFactor,
|
|
1986
|
+
);
|
|
1987
|
+
|
|
1988
|
+
// Modèle de la demande : combien de clics l'annonceur peut-il s'offrir ?
|
|
1989
|
+
const advertiserDemand = (cpc) => {
|
|
1990
|
+
if (cpc <= 0) return Infinity;
|
|
1991
|
+
return (advertiser.credits || 0) / cpc;
|
|
1992
|
+
};
|
|
1993
|
+
|
|
1994
|
+
// --- SOLUTION : Utiliser l'offre réelle et la concurrence ---
|
|
1995
|
+
// L'offre est maintenant le nombre d'impressions estimées, un chiffre concret.
|
|
1996
|
+
const supply = context.estimatedImpressions || 1; // Fallback à 1 pour éviter la division par zéro.
|
|
1997
|
+
|
|
1998
|
+
// Le facteur de concurrence augmente le prix s'il y a plus de monde sur le même créneau.
|
|
1999
|
+
// Formule simple : 1 + (0.1 * nombre de concurrents), avec un plafond.
|
|
2000
|
+
const competitionFactor = Math.min(
|
|
2001
|
+
2.5,
|
|
2002
|
+
1 + context.competingAds.length * 0.1,
|
|
2003
|
+
);
|
|
2004
|
+
|
|
2005
|
+
return function cpcFitness(cpcMultiplier) {
|
|
2006
|
+
// Le CPC final est le coût de base, ajusté par le multiplicateur de l'algo et la concurrence.
|
|
2007
|
+
const adjustedCPC = BASE_CLICK_COST * cpcMultiplier * competitionFactor;
|
|
2008
|
+
if (adjustedCPC < 0.1) return [Infinity, Infinity, Infinity]; // CPC minimum
|
|
2009
|
+
|
|
2010
|
+
// La demande de l'annonceur est calculée avec le CPC ajusté.
|
|
2011
|
+
const demand = advertiserDemand(adjustedCPC);
|
|
2012
|
+
|
|
2013
|
+
// Le nombre de clics est le minimum de l'offre et de la demande.
|
|
2014
|
+
const estimatedClicks = Math.min(demand, supply);
|
|
2015
|
+
|
|
2016
|
+
// Objectif 1 : Maximiser le revenu de la plateforme (donc minimiser son opposé)
|
|
2017
|
+
const platformRevenue =
|
|
2018
|
+
estimatedClicks * adjustedCPC * effectiveCommissionRate;
|
|
2019
|
+
|
|
2020
|
+
// Objectif 2 : Maximiser la valeur pour l'annonceur (nombre de clics, donc minimiser son opposé)
|
|
2021
|
+
const advertiserValue = estimatedClicks;
|
|
2022
|
+
|
|
2023
|
+
// Objectif 3 : Minimiser le déséquilibre du marché (offre vs demande)
|
|
2024
|
+
const marketImbalance = Math.abs(demand - supply);
|
|
2025
|
+
|
|
2026
|
+
return [-platformRevenue, -advertiserValue, marketImbalance];
|
|
2027
|
+
};
|
|
2028
|
+
};
|
|
2029
|
+
|
|
2030
|
+
/**
|
|
2031
|
+
* Applique le facteur de concurrence au CPC de base.
|
|
2032
|
+
* @private
|
|
2033
|
+
* @param {number} baseCpc - Le CPC issu de l'algorithme génétique.
|
|
2034
|
+
* @param {Array<object>} competingAds - Les annonces concurrentes.
|
|
2035
|
+
* @returns {number} Le CPC final ajusté.
|
|
2036
|
+
*/
|
|
2037
|
+
function applyCompetitionFactor(baseCpc, competingAds) {
|
|
2038
|
+
// Formule simple : 1 + (0.1 * nombre de concurrents), avec un plafond pour éviter l'explosion des prix.
|
|
2039
|
+
const competitionFactor = Math.min(
|
|
2040
|
+
2.5,
|
|
2041
|
+
1 + (competingAds || []).length * 0.1,
|
|
2042
|
+
);
|
|
2043
|
+
const finalCpc = baseCpc * competitionFactor;
|
|
2044
|
+
// On s'assure de ne jamais descendre sous un seuil minimal.
|
|
2045
|
+
return Math.max(0.1, finalCpc);
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Résout le problème du CPC optimal en utilisant un algorithme génétique multi-objectifs.
|
|
2049
|
+
* @param {object} context - Le contexte nécessaire pour l'évaluation (advertiser, ad, etc.).
|
|
2050
|
+
* @param {object} [options] - Options pour l'algorithme génétique.
|
|
2051
|
+
* @returns {Array<{solution: number, objectives: number[]}>} Le front de Pareto des solutions CPC.
|
|
2052
|
+
*/
|
|
2053
|
+
Optimization.Operators.solveOptimalCPC = (context, options = {}) => {
|
|
2054
|
+
const fitnessFunction =
|
|
2055
|
+
Optimization.Operators.createOptimalCPCEvaluator(context);
|
|
2056
|
+
|
|
2057
|
+
// Un "individu" est simplement une valeur de CPC.
|
|
2058
|
+
const createIndividual = () => {
|
|
2059
|
+
// Le CPC peut varier, par exemple, entre 0.5 et 5 PRIM'S.
|
|
2060
|
+
return 0.5 + Math.random() * 4.5;
|
|
2061
|
+
};
|
|
2062
|
+
|
|
2063
|
+
// Croisement : moyenne des CPC des parents.
|
|
2064
|
+
const crossover = (cpc1, cpc2) => {
|
|
2065
|
+
return (cpc1 + cpc2) / 2;
|
|
2066
|
+
};
|
|
2067
|
+
|
|
2068
|
+
// Mutation : légère variation aléatoire du CPC.
|
|
2069
|
+
const mutate = (cpc) => {
|
|
2070
|
+
const newCpc = cpc + (Math.random() - 0.5) * 0.5;
|
|
2071
|
+
return Math.max(0.1, newCpc); // Assurer un CPC minimum.
|
|
2072
|
+
};
|
|
2073
|
+
|
|
2074
|
+
const gaOptions = {
|
|
2075
|
+
generations: 50,
|
|
2076
|
+
populationSize: 40,
|
|
2077
|
+
...options,
|
|
2078
|
+
};
|
|
2079
|
+
|
|
2080
|
+
// On utilise l'algorithme génétique multi-objectifs pour obtenir le front de Pareto.
|
|
2081
|
+
const paretoFront = Optimization.geneticAlgorithmMultiObjective(
|
|
2082
|
+
createIndividual,
|
|
2083
|
+
fitnessFunction,
|
|
2084
|
+
crossover,
|
|
2085
|
+
mutate,
|
|
2086
|
+
gaOptions,
|
|
2087
|
+
);
|
|
2088
|
+
|
|
2089
|
+
// --- SOLUTION : Appliquer le facteur de concurrence sur les solutions finales ---
|
|
2090
|
+
return paretoFront.map((result) => {
|
|
2091
|
+
const cpcMultiplier = result.solution;
|
|
2092
|
+
// Le CPC final est calculé ici, en dehors de la fonction de fitness.
|
|
2093
|
+
const finalCpc = applyCompetitionFactor(
|
|
2094
|
+
BASE_CLICK_COST * cpcMultiplier,
|
|
2095
|
+
context.competingAds,
|
|
2096
|
+
);
|
|
2097
|
+
return {
|
|
2098
|
+
...result,
|
|
2099
|
+
solution: Math.max(0.1, finalCpc), // On s'assure de ne jamais descendre sous le plancher absolu.
|
|
2100
|
+
};
|
|
2101
|
+
});
|
|
2102
|
+
};
|
|
2103
|
+
|
|
2104
|
+
/**
|
|
2105
|
+
* Crée un évaluateur multi-objectifs pour trouver les seuils de détection de fraude optimaux.
|
|
2106
|
+
* @param {object} config - L'objet de configuration.
|
|
2107
|
+
* @param {Array<object>} config.legitimateClicks - Un échantillon de clics considérés comme légitimes.
|
|
2108
|
+
* @param {Array<object>} config.fraudulentClicks - Un échantillon de clics identifiés comme frauduleux (ex: honeypots).
|
|
2109
|
+
* @returns {function(Array<number>): number[]} Une fonction de fitness qui prend une solution `[minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents]` et retourne les scores des objectifs.
|
|
2110
|
+
*/
|
|
2111
|
+
Optimization.Operators.createFraudThresholdEvaluator = ({
|
|
2112
|
+
legitimateClicks,
|
|
2113
|
+
fraudulentClicks,
|
|
2114
|
+
}) => {
|
|
2115
|
+
// Fonction d'aide pour calculer la variance des positions de clic pour une empreinte
|
|
2116
|
+
const calculateClickVariance = (clicks) => {
|
|
2117
|
+
if (!clicks || clicks.length < 2) return 0;
|
|
2118
|
+
const meanX = clicks.reduce((sum, c) => sum + c.clickX, 0) / clicks.length;
|
|
2119
|
+
const meanY = clicks.reduce((sum, c) => sum + c.clickY, 0) / clicks.length;
|
|
2120
|
+
const variance =
|
|
2121
|
+
clicks.reduce(
|
|
2122
|
+
(sum, c) =>
|
|
2123
|
+
sum + Math.pow(c.clickX - meanX, 2) + Math.pow(c.clickY - meanY, 2),
|
|
2124
|
+
0,
|
|
2125
|
+
) / clicks.length;
|
|
2126
|
+
return variance;
|
|
2127
|
+
};
|
|
2128
|
+
|
|
2129
|
+
// Pré-calculer la variance pour chaque empreinte dans les données
|
|
2130
|
+
const getClicksByFingerprint = (clickData) => {
|
|
2131
|
+
const grouped = {};
|
|
2132
|
+
for (const click of clickData) {
|
|
2133
|
+
if (!grouped[click.fingerprint]) grouped[click.fingerprint] = [];
|
|
2134
|
+
grouped[click.fingerprint].push(click);
|
|
2135
|
+
}
|
|
2136
|
+
return grouped;
|
|
2137
|
+
};
|
|
2138
|
+
|
|
2139
|
+
const legitimateGroups = getClicksByFingerprint(legitimateClicks);
|
|
2140
|
+
const fraudulentGroups = getClicksByFingerprint(fraudulentClicks);
|
|
2141
|
+
|
|
2142
|
+
return function fraudFitness(solution) {
|
|
2143
|
+
const [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents] =
|
|
2144
|
+
solution;
|
|
2145
|
+
|
|
2146
|
+
// Contraintes pour garder des seuils logiques
|
|
2147
|
+
if (
|
|
2148
|
+
minTimeToClick < 100 ||
|
|
2149
|
+
minTimeToClick > 5000 ||
|
|
2150
|
+
maxClickVariance < 1 ||
|
|
2151
|
+
maxClickVariance > 10000 ||
|
|
2152
|
+
minMouseEntropy < 0 ||
|
|
2153
|
+
minMouseEntropy > 1 ||
|
|
2154
|
+
minScrollEvents < 0
|
|
2155
|
+
) {
|
|
2156
|
+
return [Infinity, Infinity]; // Mauvais score si hors limites
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
let truePositives = 0; // Bots correctement identifiés
|
|
2160
|
+
let falsePositives = 0; // Humains incorrectement bloqués
|
|
2161
|
+
|
|
2162
|
+
// Évaluer les clics frauduleux
|
|
2163
|
+
for (const fingerprint in fraudulentGroups) {
|
|
2164
|
+
const clicks = fraudulentGroups[fingerprint];
|
|
2165
|
+
if (!clicks) continue;
|
|
2166
|
+
const variance = calculateClickVariance(clicks);
|
|
2167
|
+
// Un groupe de clics est frauduleux si l'une des conditions est remplie
|
|
2168
|
+
const isTooFast = clicks.some((c) => c.timeToClick < minTimeToClick);
|
|
2169
|
+
const isTooUniform = variance < maxClickVariance;
|
|
2170
|
+
const hasLowEntropy = clicks.some(
|
|
2171
|
+
(c) => c.mouseEntropy < minMouseEntropy,
|
|
2172
|
+
);
|
|
2173
|
+
const hasFewScrolls = clicks.some(
|
|
2174
|
+
(c) => c.scrollEvents < minScrollEvents,
|
|
2175
|
+
);
|
|
2176
|
+
|
|
2177
|
+
if (isTooFast || isTooUniform || hasLowEntropy || hasFewScrolls) {
|
|
2178
|
+
truePositives++;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// Évaluer les clics légitimes
|
|
2183
|
+
for (const fingerprint in legitimateGroups) {
|
|
2184
|
+
const clicks = legitimateGroups[fingerprint];
|
|
2185
|
+
if (!clicks) continue;
|
|
2186
|
+
const variance = calculateClickVariance(clicks);
|
|
2187
|
+
if (
|
|
2188
|
+
clicks.some(
|
|
2189
|
+
(c) =>
|
|
2190
|
+
c.timeToClick < minTimeToClick ||
|
|
2191
|
+
c.mouseEntropy < minMouseEntropy ||
|
|
2192
|
+
c.scrollEvents < minScrollEvents,
|
|
2193
|
+
) ||
|
|
2194
|
+
variance < maxClickVariance
|
|
2195
|
+
) {
|
|
2196
|
+
falsePositives++;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
const totalFraudulent = Object.keys(fraudulentGroups).length || 1;
|
|
2201
|
+
const totalLegitimate = Object.keys(legitimateGroups).length || 1;
|
|
2202
|
+
|
|
2203
|
+
// Objectif 1 : Maximiser la détection de fraude (donc minimiser 1 - taux de détection)
|
|
2204
|
+
const objective1 = 1 - truePositives / totalFraudulent;
|
|
2205
|
+
|
|
2206
|
+
// Objectif 2 : Minimiser le taux de faux positifs
|
|
2207
|
+
const objective2 = falsePositives / totalLegitimate;
|
|
2208
|
+
|
|
2209
|
+
return [objective1, objective2];
|
|
2210
|
+
};
|
|
2211
|
+
};
|
|
2212
|
+
|
|
2213
|
+
/**
|
|
2214
|
+
* Résout le problème de la détection de fraude en trouvant un front de Pareto de seuils optimaux.
|
|
2215
|
+
* @param {object} context - Le contexte contenant les données de clics.
|
|
2216
|
+
* @param {Array<object>} context.legitimateClicks - Échantillon de clics légitimes.
|
|
2217
|
+
* @param {Array<object>} context.fraudulentClicks - Échantillon de clics frauduleux.
|
|
2218
|
+
* @param {object} [options] - Options pour l'algorithme génétique.
|
|
2219
|
+
* @returns {Array<{solution: Array<number>, objectives: number[]}>} Le front de Pareto des solutions [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents].
|
|
2220
|
+
*/
|
|
2221
|
+
Optimization.Operators.solveFraudDetection = (context, options = {}) => {
|
|
2222
|
+
const fitnessFunction =
|
|
2223
|
+
Optimization.Operators.createFraudThresholdEvaluator(context);
|
|
2224
|
+
|
|
2225
|
+
// Un "individu" est un tableau de 4 seuils : [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents]
|
|
2226
|
+
const createIndividual = () => {
|
|
2227
|
+
const minTimeToClick = 100 + Math.random() * 4900; // entre 100ms et 5s
|
|
2228
|
+
const maxClickVariance = 1 + Math.random() * 9999; // entre 1 et 10000
|
|
2229
|
+
const minMouseEntropy = Math.random() * 0.5; // entre 0 et 0.5
|
|
2230
|
+
const minScrollEvents = Math.floor(Math.random() * 10); // entre 0 et 10
|
|
2231
|
+
return [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents];
|
|
2232
|
+
};
|
|
2233
|
+
|
|
2234
|
+
// Croisement : moyenne des seuils des parents
|
|
2235
|
+
const crossover = (s1, s2) => {
|
|
2236
|
+
return [
|
|
2237
|
+
(s1[0] + s2[0]) / 2,
|
|
2238
|
+
(s1[1] + s2[1]) / 2,
|
|
2239
|
+
(s1[2] + s2[2]) / 2,
|
|
2240
|
+
(s1[3] + s2[3]) / 2,
|
|
2241
|
+
];
|
|
2242
|
+
};
|
|
2243
|
+
|
|
2244
|
+
// Mutation : légère variation aléatoire d'un des seuils
|
|
2245
|
+
const mutate = (solution) => {
|
|
2246
|
+
const newSolution = [...solution];
|
|
2247
|
+
const i = Math.floor(Math.random() * 4);
|
|
2248
|
+
// Amplitudes de mutation différentes pour chaque seuil
|
|
2249
|
+
const mutationFactors = [500, 1000, 0.1, 2];
|
|
2250
|
+
const mutationFactor = mutationFactors[i];
|
|
2251
|
+
newSolution[i] += (Math.random() - 0.5) * mutationFactor;
|
|
2252
|
+
return newSolution;
|
|
2253
|
+
};
|
|
2254
|
+
|
|
2255
|
+
const gaOptions = {
|
|
2256
|
+
generations: 80,
|
|
2257
|
+
populationSize: 60,
|
|
2258
|
+
...options,
|
|
2259
|
+
};
|
|
2260
|
+
|
|
2261
|
+
return Optimization.geneticAlgorithmMultiObjective(
|
|
2262
|
+
createIndividual,
|
|
2263
|
+
fitnessFunction,
|
|
2264
|
+
crossover,
|
|
2265
|
+
mutate,
|
|
2266
|
+
gaOptions,
|
|
2267
|
+
);
|
|
2268
|
+
};
|
|
2269
|
+
|
|
2270
|
+
export { Dichotomy, Optimization };
|