@anonympins/fingerprint 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +367 -331
- package/README.md +1 -3
- package/package.json +1 -1
- package/src/js/fingerprint.js +4521 -4429
- package/src/js/problem-manager.js +590 -539
- package/src/php/AutoTuner.php +197 -154
- package/src/php/FingerprintEngine.php +1010 -1006
- package/src/php/ProblemManager.php +258 -254
- package/src/php/Tests/problems.config.json +8 -8
- package/src/php/Utils/RequestUtils.php +1189 -1169
|
@@ -1,255 +1,259 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint;
|
|
6
|
-
|
|
7
|
-
use Anonympins\Fingerprint\Optimization\FunctionRegistry;
|
|
8
|
-
use Anonympins\Fingerprint\Optimization\ProblemInitializers;
|
|
9
|
-
use Anonympins\Fingerprint\Store\IStore;
|
|
10
|
-
|
|
11
|
-
class ProblemManager
|
|
12
|
-
{
|
|
13
|
-
private static ?ProblemManager $instance = null;
|
|
14
|
-
private string $configPath;
|
|
15
|
-
private IStore $store;
|
|
16
|
-
/** @var array<int, array<string, mixed>> */
|
|
17
|
-
private array $problems = [];
|
|
18
|
-
private int $currentProblemIndex = 0;
|
|
19
|
-
private bool $initialized = false;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Le constructeur est privé pour forcer l'utilisation du singleton.
|
|
23
|
-
*/
|
|
24
|
-
private function __construct(string $configPath, IStore $store)
|
|
25
|
-
{
|
|
26
|
-
$this->configPath = $configPath;
|
|
27
|
-
$this->store = $store;
|
|
28
|
-
$this->loadProblems();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Obtient l'instance singleton du ProblemManager.
|
|
33
|
-
* Doit être initialisé une fois avec `init`.
|
|
34
|
-
*/
|
|
35
|
-
public static function getInstance(?string $configPath = null, ?IStore $store = null): self
|
|
36
|
-
{
|
|
37
|
-
if (self::$instance === null) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
$
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
$
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
$
|
|
134
|
-
$task['
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
$
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
$
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint;
|
|
6
|
+
|
|
7
|
+
use Anonympins\Fingerprint\Optimization\FunctionRegistry;
|
|
8
|
+
use Anonympins\Fingerprint\Optimization\ProblemInitializers;
|
|
9
|
+
use Anonympins\Fingerprint\Store\IStore;
|
|
10
|
+
|
|
11
|
+
class ProblemManager
|
|
12
|
+
{
|
|
13
|
+
private static ?ProblemManager $instance = null;
|
|
14
|
+
private string $configPath;
|
|
15
|
+
private IStore $store;
|
|
16
|
+
/** @var array<int, array<string, mixed>> */
|
|
17
|
+
private array $problems = [];
|
|
18
|
+
private int $currentProblemIndex = 0;
|
|
19
|
+
private bool $initialized = false;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Le constructeur est privé pour forcer l'utilisation du singleton.
|
|
23
|
+
*/
|
|
24
|
+
private function __construct(string $configPath, IStore $store)
|
|
25
|
+
{
|
|
26
|
+
$this->configPath = $configPath;
|
|
27
|
+
$this->store = $store;
|
|
28
|
+
$this->loadProblems();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Obtient l'instance singleton du ProblemManager.
|
|
33
|
+
* Doit être initialisé une fois avec `init`.
|
|
34
|
+
*/
|
|
35
|
+
public static function getInstance(?string $configPath = null, ?IStore $store = null): self
|
|
36
|
+
{
|
|
37
|
+
if (self::$instance === null) {
|
|
38
|
+
if ($configPath === null) {
|
|
39
|
+
$defaultPath = dirname(__DIR__, 2) . '/problems.config.json';
|
|
40
|
+
$configPath = file_exists($defaultPath) ? $defaultPath : null;
|
|
41
|
+
}
|
|
42
|
+
// Si on essaie d'obtenir l'instance sans l'initialiser d'abord, c'est une erreur.
|
|
43
|
+
if ($configPath === null || $store === null) {
|
|
44
|
+
throw new \RuntimeException("ProblemManager must be initialized with configPath and store.");
|
|
45
|
+
}
|
|
46
|
+
self::$instance = new self($configPath, $store);
|
|
47
|
+
}
|
|
48
|
+
return self::$instance;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public static function isInitialized(): bool
|
|
52
|
+
{
|
|
53
|
+
return self::$instance !== null && self::$instance->initialized;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Charge et parse les problèmes depuis le fichier de configuration.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
private function loadProblems(): void
|
|
60
|
+
{
|
|
61
|
+
if (!file_exists($this->configPath)) {
|
|
62
|
+
error_log("[ProblemManager] Problem config file not found: {$this->configPath}");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
$data = file_get_contents($this->configPath);
|
|
66
|
+
if ($data === false) {
|
|
67
|
+
error_log("[ProblemManager] Failed to read problem config file: {$this->configPath}");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
$problemsFromFile = json_decode($data, true);
|
|
71
|
+
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
72
|
+
error_log("[ProblemManager] Failed to parse problem config JSON: " . json_last_error_msg());
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
foreach ($problemsFromFile as $problem) {
|
|
77
|
+
$storeKey = "problem-state:{$problem['id']}";
|
|
78
|
+
$storedState = $this->store->get($storeKey);
|
|
79
|
+
|
|
80
|
+
if ($storedState === null) {
|
|
81
|
+
$storedState = $problem['state'] ?? [];
|
|
82
|
+
$this->store->set($storeKey, $storedState); // Persist initial state
|
|
83
|
+
}
|
|
84
|
+
$problem['state'] = $storedState;
|
|
85
|
+
|
|
86
|
+
// Résolution dynamique des fonctions et des données
|
|
87
|
+
if (isset($problem['workUnit']['scoreFunction'])) {
|
|
88
|
+
$problem['workUnit']['scoreFunction'] = FunctionRegistry::get($problem['workUnit']['scoreFunction']);
|
|
89
|
+
if ($problem['workUnit']['scoreFunction'] === null) {
|
|
90
|
+
// @codeCoverageIgnoreStart
|
|
91
|
+
error_log("[ProblemManager] Warning: scoreFunction '{$problem['workUnit']['scoreFunction']}' not found in registry for problem '{$problem['id']}'.");
|
|
92
|
+
// @codeCoverageIgnoreEnd
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (isset($problem['payload']) && is_array($problem['payload'])) {
|
|
97
|
+
foreach ($problem['payload'] as $key => &$value) {
|
|
98
|
+
if (is_array($value) && isset($value['$init'])) {
|
|
99
|
+
$initializer = ProblemInitializers::get($value['$init']);
|
|
100
|
+
if ($initializer) {
|
|
101
|
+
$value = $initializer($value['params'] ?? []);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
$this->problems[] = $problem;
|
|
107
|
+
}
|
|
108
|
+
$this->initialized = true; // Marquer comme initialisé seulement après un chargement réussi
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public function dispatchWork(float $suspicionFactor): ?array
|
|
112
|
+
{
|
|
113
|
+
if (empty($this->problems)) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
$problem = $this->problems[$this->currentProblemIndex];
|
|
118
|
+
$this->currentProblemIndex = ($this->currentProblemIndex + 1) % count($this->problems);
|
|
119
|
+
|
|
120
|
+
$task = ['type' => $problem['workUnit']['type']];
|
|
121
|
+
$scalingFactor = $problem['workUnit']['scalingFactor'] ?? null;
|
|
122
|
+
|
|
123
|
+
switch ($problem['workUnit']['type']) {
|
|
124
|
+
case 'simulated_annealing_iterations':
|
|
125
|
+
$baseIterations = $problem['workUnit']['baseIterations'] ?? 15000;
|
|
126
|
+
$task['iterations'] = $scalingFactor
|
|
127
|
+
? (int)floor($baseIterations * pow($scalingFactor, $suspicionFactor))
|
|
128
|
+
: (int)floor($baseIterations * (0.5 + $suspicionFactor));
|
|
129
|
+
if (isset($problem['payload'])) {
|
|
130
|
+
$task['payload'] = $problem['payload'];
|
|
131
|
+
}
|
|
132
|
+
// Ensure payload is always an array to prevent errors when accessing it.
|
|
133
|
+
$task['payload'] = $task['payload'] ?? [];
|
|
134
|
+
$task['initialSolution'] = $problem['state']['bestSolution'] ?? null;
|
|
135
|
+
break;
|
|
136
|
+
case 'multi_objective_genetic_algorithm':
|
|
137
|
+
$baseGenerationsMulti = max(30, $problem['workUnit']['baseGenerations'] ?? 0);
|
|
138
|
+
$task['generations'] = $scalingFactor
|
|
139
|
+
? (int)floor($baseGenerationsMulti * pow($scalingFactor, $suspicionFactor))
|
|
140
|
+
: (int)floor($baseGenerationsMulti * (0.5 + $suspicionFactor));
|
|
141
|
+
if (isset($problem['payload'])) {
|
|
142
|
+
$task['payload'] = $problem['payload'];
|
|
143
|
+
}
|
|
144
|
+
$task['initialFront'] = $problem['state']['paretoFront'] ?? null;
|
|
145
|
+
$task['solverName'] = $problem['workUnit']['solverName'];
|
|
146
|
+
break;
|
|
147
|
+
default:
|
|
148
|
+
error_log("[ProblemManager] Unknown useful work type: {$problem['workUnit']['type']}");
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return ['problemId' => $problem['id'], 'task' => $task];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Intègre la solution d'un client dans l'état du problème.
|
|
157
|
+
*
|
|
158
|
+
* @param string $problemId L'ID du problème.
|
|
159
|
+
* @param array $solutionData La solution renvoyée par le client.
|
|
160
|
+
*/
|
|
161
|
+
public function integrateSolution(string $problemId, array $solutionData): void
|
|
162
|
+
{
|
|
163
|
+
$problemIndex = array_search($problemId, array_column($this->problems, 'id'));
|
|
164
|
+
if ($problemIndex === false) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
$problem = &$this->problems[$problemIndex]; // Use reference to modify in place
|
|
168
|
+
|
|
169
|
+
$stateChanged = false;
|
|
170
|
+
|
|
171
|
+
$storeKey = "problem-state:{$problem['id']}";
|
|
172
|
+
|
|
173
|
+
// La logique d'intégration dépend du type de problème.
|
|
174
|
+
switch ($problem['workUnit']['type']) {
|
|
175
|
+
case 'simulated_annealing_iterations':
|
|
176
|
+
if (isset($solutionData['solution']) && isset($solutionData['energy'])) {
|
|
177
|
+
$scoreFunction = $problem['workUnit']['scoreFunction'] ?? null;
|
|
178
|
+
if (!$scoreFunction) {
|
|
179
|
+
error_log("[ProblemManager] Aucune fonction de score définie pour {$problemId}.");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// 1. Ne JAMAIS faire confiance au score du client. Recalculer systématiquement.
|
|
183
|
+
$recalculatedEnergy = $scoreFunction($solutionData['solution'], $problem['payload'] ?? []);
|
|
184
|
+
|
|
185
|
+
$currentBest = (float)($problem['state']['bestEnergy'] ?? INF);
|
|
186
|
+
|
|
187
|
+
// 2. Comparer le score recalculé, pas celui du client.
|
|
188
|
+
if ($recalculatedEnergy < $currentBest) { // @phpstan-ignore-line
|
|
189
|
+
$problem['state']['bestSolution'] = $solutionData['solution'];
|
|
190
|
+
$problem['state']['bestEnergy'] = $recalculatedEnergy; // 3. Stocker le score vérifié.
|
|
191
|
+
$problem['state']['lastUpdate'] = (new \DateTime())->format(\DateTime::ATOM);
|
|
192
|
+
$stateChanged = true;
|
|
193
|
+
error_log("[ProblemManager] New best solution for {$problemId}: {$recalculatedEnergy}"); // @phpstan-ignore-line
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
case 'multi_objective_genetic_algorithm':
|
|
198
|
+
// Pour les algorithmes génétiques, on intègre le nouveau front de Pareto.
|
|
199
|
+
if (isset($solutionData['paretoFront']) && is_array($solutionData['paretoFront'])) {
|
|
200
|
+
$stateChanged = $this->_integrateParetoFront($problem, $solutionData['paretoFront']);
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
default:
|
|
204
|
+
error_log("[ProblemManager] Integration not implemented for useful work type: {$problem['workUnit']['type']}");
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
// Sauvegarder l'état mis à jour dans le store.
|
|
208
|
+
if ($stateChanged) {
|
|
209
|
+
$this->store->set($storeKey, $problem['state']);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Intègre un nouveau front de Pareto dans l'état du problème.
|
|
215
|
+
*
|
|
216
|
+
* @param array &$problem Le problème à mettre à jour (passé par référence).
|
|
217
|
+
* @param array $newFront Le nouveau front de Pareto soumis par le client.
|
|
218
|
+
*/
|
|
219
|
+
private function _integrateParetoFront(array &$problem, array $newFront): bool
|
|
220
|
+
{
|
|
221
|
+
// Logique de fusion et de tri non-dominé pour mettre à jour le front de Pareto.
|
|
222
|
+
// Pour cet exemple, nous remplaçons simplement le front, mais une vraie implémentation
|
|
223
|
+
// fusionnerait les deux fronts et recalculerait le meilleur.
|
|
224
|
+
// On vérifie si le nouveau front est différent de l'actuel pour éviter des écritures inutiles.
|
|
225
|
+
$currentFront = $problem['state']['paretoFront'] ?? [];
|
|
226
|
+
if (!empty($newFront) && json_encode($newFront) !== json_encode($currentFront)) {
|
|
227
|
+
$problem['state']['paretoFront'] = $newFront;
|
|
228
|
+
$problem['state']['lastUpdate'] = (new \DateTime())->format(\DateTime::ATOM);
|
|
229
|
+
error_log("[ProblemManager] New Pareto front for {$problem['id']} with " . count($newFront) . " solutions."); // @phpstan-ignore-line
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Réinitialise l'instance singleton.
|
|
237
|
+
* @internal Uniquement pour les tests.
|
|
238
|
+
*/
|
|
239
|
+
public static function resetInstanceForTests(): void
|
|
240
|
+
{
|
|
241
|
+
self::$instance = null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Réinitialise l'instance singleton.
|
|
246
|
+
* @internal Uniquement pour les tests.
|
|
247
|
+
*/
|
|
248
|
+
public static function __internal_resetInstance(): void {
|
|
249
|
+
self::$instance = null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* @internal For testing purposes only.
|
|
254
|
+
*/
|
|
255
|
+
public function getProblems(): array
|
|
256
|
+
{
|
|
257
|
+
return $this->problems;
|
|
258
|
+
}
|
|
255
259
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"id": "problem-1",
|
|
4
|
-
"workUnit": {
|
|
5
|
-
"type": "simulated_annealing_iterations",
|
|
6
|
-
"baseIterations": 10000
|
|
7
|
-
}
|
|
8
|
-
}
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "problem-1",
|
|
4
|
+
"workUnit": {
|
|
5
|
+
"type": "simulated_annealing_iterations",
|
|
6
|
+
"baseIterations": 10000
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
9
|
]
|