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