morphml 1.0.0__py3-none-any.whl

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.

Potentially problematic release.


This version of morphml might be problematic. Click here for more details.

Files changed (158) hide show
  1. morphml/__init__.py +14 -0
  2. morphml/api/__init__.py +26 -0
  3. morphml/api/app.py +326 -0
  4. morphml/api/auth.py +193 -0
  5. morphml/api/client.py +338 -0
  6. morphml/api/models.py +132 -0
  7. morphml/api/rate_limit.py +192 -0
  8. morphml/benchmarking/__init__.py +36 -0
  9. morphml/benchmarking/comparison.py +430 -0
  10. morphml/benchmarks/__init__.py +56 -0
  11. morphml/benchmarks/comparator.py +409 -0
  12. morphml/benchmarks/datasets.py +280 -0
  13. morphml/benchmarks/metrics.py +199 -0
  14. morphml/benchmarks/openml_suite.py +201 -0
  15. morphml/benchmarks/problems.py +289 -0
  16. morphml/benchmarks/suite.py +318 -0
  17. morphml/cli/__init__.py +5 -0
  18. morphml/cli/commands/experiment.py +329 -0
  19. morphml/cli/main.py +457 -0
  20. morphml/cli/quickstart.py +312 -0
  21. morphml/config.py +278 -0
  22. morphml/constraints/__init__.py +19 -0
  23. morphml/constraints/handler.py +205 -0
  24. morphml/constraints/predicates.py +285 -0
  25. morphml/core/__init__.py +3 -0
  26. morphml/core/crossover.py +449 -0
  27. morphml/core/dsl/README.md +359 -0
  28. morphml/core/dsl/__init__.py +72 -0
  29. morphml/core/dsl/ast_nodes.py +364 -0
  30. morphml/core/dsl/compiler.py +318 -0
  31. morphml/core/dsl/layers.py +368 -0
  32. morphml/core/dsl/lexer.py +336 -0
  33. morphml/core/dsl/parser.py +455 -0
  34. morphml/core/dsl/search_space.py +386 -0
  35. morphml/core/dsl/syntax.py +199 -0
  36. morphml/core/dsl/type_system.py +361 -0
  37. morphml/core/dsl/validator.py +386 -0
  38. morphml/core/graph/__init__.py +40 -0
  39. morphml/core/graph/edge.py +124 -0
  40. morphml/core/graph/graph.py +507 -0
  41. morphml/core/graph/mutations.py +409 -0
  42. morphml/core/graph/node.py +196 -0
  43. morphml/core/graph/serialization.py +361 -0
  44. morphml/core/graph/visualization.py +431 -0
  45. morphml/core/objectives/__init__.py +20 -0
  46. morphml/core/search/__init__.py +33 -0
  47. morphml/core/search/individual.py +252 -0
  48. morphml/core/search/parameters.py +453 -0
  49. morphml/core/search/population.py +375 -0
  50. morphml/core/search/search_engine.py +340 -0
  51. morphml/distributed/__init__.py +76 -0
  52. morphml/distributed/fault_tolerance.py +497 -0
  53. morphml/distributed/health_monitor.py +348 -0
  54. morphml/distributed/master.py +709 -0
  55. morphml/distributed/proto/README.md +224 -0
  56. morphml/distributed/proto/__init__.py +74 -0
  57. morphml/distributed/proto/worker.proto +170 -0
  58. morphml/distributed/proto/worker_pb2.py +79 -0
  59. morphml/distributed/proto/worker_pb2_grpc.py +423 -0
  60. morphml/distributed/resource_manager.py +416 -0
  61. morphml/distributed/scheduler.py +567 -0
  62. morphml/distributed/storage/__init__.py +33 -0
  63. morphml/distributed/storage/artifacts.py +381 -0
  64. morphml/distributed/storage/cache.py +366 -0
  65. morphml/distributed/storage/checkpointing.py +329 -0
  66. morphml/distributed/storage/database.py +459 -0
  67. morphml/distributed/worker.py +549 -0
  68. morphml/evaluation/__init__.py +5 -0
  69. morphml/evaluation/heuristic.py +237 -0
  70. morphml/exceptions.py +55 -0
  71. morphml/execution/__init__.py +5 -0
  72. morphml/execution/local_executor.py +350 -0
  73. morphml/integrations/__init__.py +28 -0
  74. morphml/integrations/jax_adapter.py +206 -0
  75. morphml/integrations/pytorch_adapter.py +530 -0
  76. morphml/integrations/sklearn_adapter.py +206 -0
  77. morphml/integrations/tensorflow_adapter.py +230 -0
  78. morphml/logging_config.py +93 -0
  79. morphml/meta_learning/__init__.py +66 -0
  80. morphml/meta_learning/architecture_similarity.py +277 -0
  81. morphml/meta_learning/experiment_database.py +240 -0
  82. morphml/meta_learning/knowledge_base/__init__.py +19 -0
  83. morphml/meta_learning/knowledge_base/embedder.py +179 -0
  84. morphml/meta_learning/knowledge_base/knowledge_base.py +313 -0
  85. morphml/meta_learning/knowledge_base/meta_features.py +265 -0
  86. morphml/meta_learning/knowledge_base/vector_store.py +271 -0
  87. morphml/meta_learning/predictors/__init__.py +27 -0
  88. morphml/meta_learning/predictors/ensemble.py +221 -0
  89. morphml/meta_learning/predictors/gnn_predictor.py +552 -0
  90. morphml/meta_learning/predictors/learning_curve.py +231 -0
  91. morphml/meta_learning/predictors/proxy_metrics.py +261 -0
  92. morphml/meta_learning/strategy_evolution/__init__.py +27 -0
  93. morphml/meta_learning/strategy_evolution/adaptive_optimizer.py +226 -0
  94. morphml/meta_learning/strategy_evolution/bandit.py +276 -0
  95. morphml/meta_learning/strategy_evolution/portfolio.py +230 -0
  96. morphml/meta_learning/transfer.py +581 -0
  97. morphml/meta_learning/warm_start.py +286 -0
  98. morphml/optimizers/__init__.py +74 -0
  99. morphml/optimizers/adaptive_operators.py +399 -0
  100. morphml/optimizers/bayesian/__init__.py +52 -0
  101. morphml/optimizers/bayesian/acquisition.py +387 -0
  102. morphml/optimizers/bayesian/base.py +319 -0
  103. morphml/optimizers/bayesian/gaussian_process.py +635 -0
  104. morphml/optimizers/bayesian/smac.py +534 -0
  105. morphml/optimizers/bayesian/tpe.py +411 -0
  106. morphml/optimizers/differential_evolution.py +220 -0
  107. morphml/optimizers/evolutionary/__init__.py +61 -0
  108. morphml/optimizers/evolutionary/cma_es.py +416 -0
  109. morphml/optimizers/evolutionary/differential_evolution.py +556 -0
  110. morphml/optimizers/evolutionary/encoding.py +426 -0
  111. morphml/optimizers/evolutionary/particle_swarm.py +449 -0
  112. morphml/optimizers/genetic_algorithm.py +486 -0
  113. morphml/optimizers/gradient_based/__init__.py +22 -0
  114. morphml/optimizers/gradient_based/darts.py +550 -0
  115. morphml/optimizers/gradient_based/enas.py +585 -0
  116. morphml/optimizers/gradient_based/operations.py +474 -0
  117. morphml/optimizers/gradient_based/utils.py +601 -0
  118. morphml/optimizers/hill_climbing.py +169 -0
  119. morphml/optimizers/multi_objective/__init__.py +56 -0
  120. morphml/optimizers/multi_objective/indicators.py +504 -0
  121. morphml/optimizers/multi_objective/nsga2.py +647 -0
  122. morphml/optimizers/multi_objective/visualization.py +427 -0
  123. morphml/optimizers/nsga2.py +308 -0
  124. morphml/optimizers/random_search.py +172 -0
  125. morphml/optimizers/simulated_annealing.py +181 -0
  126. morphml/plugins/__init__.py +35 -0
  127. morphml/plugins/custom_evaluator_example.py +81 -0
  128. morphml/plugins/custom_optimizer_example.py +63 -0
  129. morphml/plugins/plugin_system.py +454 -0
  130. morphml/reports/__init__.py +30 -0
  131. morphml/reports/generator.py +362 -0
  132. morphml/tracking/__init__.py +7 -0
  133. morphml/tracking/experiment.py +309 -0
  134. morphml/tracking/logger.py +301 -0
  135. morphml/tracking/reporter.py +357 -0
  136. morphml/utils/__init__.py +6 -0
  137. morphml/utils/checkpoint.py +189 -0
  138. morphml/utils/comparison.py +390 -0
  139. morphml/utils/export.py +407 -0
  140. morphml/utils/progress.py +392 -0
  141. morphml/utils/validation.py +392 -0
  142. morphml/version.py +7 -0
  143. morphml/visualization/__init__.py +50 -0
  144. morphml/visualization/analytics.py +423 -0
  145. morphml/visualization/architecture_diagrams.py +353 -0
  146. morphml/visualization/architecture_plot.py +223 -0
  147. morphml/visualization/convergence_plot.py +174 -0
  148. morphml/visualization/crossover_viz.py +386 -0
  149. morphml/visualization/graph_viz.py +338 -0
  150. morphml/visualization/pareto_plot.py +149 -0
  151. morphml/visualization/plotly_dashboards.py +422 -0
  152. morphml/visualization/population.py +309 -0
  153. morphml/visualization/progress.py +260 -0
  154. morphml-1.0.0.dist-info/METADATA +434 -0
  155. morphml-1.0.0.dist-info/RECORD +158 -0
  156. morphml-1.0.0.dist-info/WHEEL +4 -0
  157. morphml-1.0.0.dist-info/entry_points.txt +3 -0
  158. morphml-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,362 @@
1
+ """Report generation for NAS experiments.
2
+
3
+ This module provides tools to generate comprehensive HTML reports
4
+ summarizing optimization results, comparisons, and architectures.
5
+
6
+ Author: Eshan Roy <eshanized@proton.me>
7
+ Organization: TONMOY INFRASTRUCTURE & VISION
8
+ """
9
+
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from morphml.logging_config import get_logger
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ class ReportGenerator:
20
+ """
21
+ Generate HTML reports for NAS experiments.
22
+
23
+ Creates comprehensive reports including:
24
+ - Experiment summary
25
+ - Optimizer comparisons
26
+ - Performance metrics
27
+ - Best architectures
28
+ - Convergence plots
29
+
30
+ Example:
31
+ >>> from morphml.reports import ReportGenerator
32
+ >>>
33
+ >>> generator = ReportGenerator("My NAS Experiment")
34
+ >>> generator.add_section("Overview", "Experiment description...")
35
+ >>> generator.add_optimizer_results("GP", results)
36
+ >>> generator.generate("report.html")
37
+ """
38
+
39
+ def __init__(self, experiment_name: str):
40
+ """
41
+ Initialize report generator.
42
+
43
+ Args:
44
+ experiment_name: Name of the experiment
45
+ """
46
+ self.experiment_name = experiment_name
47
+ self.sections: List[Dict[str, Any]] = []
48
+ self.metadata: Dict[str, Any] = {
49
+ "generated_at": datetime.now().isoformat(),
50
+ "experiment_name": experiment_name,
51
+ }
52
+
53
+ logger.debug(f"Initialized ReportGenerator for '{experiment_name}'")
54
+
55
+ def add_section(self, title: str, content: str, section_type: str = "text") -> None:
56
+ """
57
+ Add a section to the report.
58
+
59
+ Args:
60
+ title: Section title
61
+ content: Section content (HTML or text)
62
+ section_type: Type of section ('text', 'html', 'table', 'image')
63
+ """
64
+ self.sections.append({"title": title, "content": content, "type": section_type})
65
+ logger.debug(f"Added section: {title}")
66
+
67
+ def add_optimizer_results(self, optimizer_name: str, results: Dict[str, Any]) -> None:
68
+ """
69
+ Add optimizer results section.
70
+
71
+ Args:
72
+ optimizer_name: Name of the optimizer
73
+ results: Results dictionary with statistics
74
+ """
75
+ content = self._format_optimizer_results(optimizer_name, results)
76
+ self.add_section(f"{optimizer_name} Results", content, "html")
77
+
78
+ def add_comparison_table(self, comparison_data: Dict[str, Dict]) -> None:
79
+ """
80
+ Add optimizer comparison table.
81
+
82
+ Args:
83
+ comparison_data: Dictionary of optimizer results
84
+ """
85
+ content = self._create_comparison_table(comparison_data)
86
+ self.add_section("Optimizer Comparison", content, "html")
87
+
88
+ def generate(self, output_path: str) -> None:
89
+ """
90
+ Generate HTML report.
91
+
92
+ Args:
93
+ output_path: Path to save the report
94
+ """
95
+ html = self._generate_html()
96
+
97
+ Path(output_path).parent.mkdir(parents=True, exist_ok=True)
98
+ with open(output_path, "w") as f:
99
+ f.write(html)
100
+
101
+ logger.info(f"Report generated: {output_path}")
102
+
103
+ def _format_optimizer_results(self, name: str, results: Dict[str, Any]) -> str:
104
+ """Format optimizer results as HTML."""
105
+ html = "<div class='optimizer-results'>\n"
106
+ html += f"<h3>{name}</h3>\n"
107
+
108
+ if "mean_best" in results:
109
+ html += "<table class='results-table'>\n"
110
+ html += "<tr><th>Metric</th><th>Value</th></tr>\n"
111
+ html += f"<tr><td>Mean Best</td><td>{results['mean_best']:.4f}</td></tr>\n"
112
+ html += f"<tr><td>Std Dev</td><td>{results['std_best']:.4f}</td></tr>\n"
113
+ html += f"<tr><td>Min</td><td>{results['min_best']:.4f}</td></tr>\n"
114
+ html += f"<tr><td>Max</td><td>{results['max_best']:.4f}</td></tr>\n"
115
+ html += f"<tr><td>Median</td><td>{results['median_best']:.4f}</td></tr>\n"
116
+
117
+ if "mean_time" in results:
118
+ html += f"<tr><td>Mean Time (s)</td><td>{results['mean_time']:.2f}</td></tr>\n"
119
+
120
+ html += "</table>\n"
121
+
122
+ html += "</div>\n"
123
+ return html
124
+
125
+ def _create_comparison_table(self, comparison_data: Dict[str, Dict]) -> str:
126
+ """Create comparison table HTML."""
127
+ html = "<table class='comparison-table'>\n"
128
+ html += "<tr><th>Optimizer</th><th>Mean Best</th><th>Std</th><th>Min</th><th>Max</th><th>Time (s)</th></tr>\n"
129
+
130
+ # Sort by mean_best descending
131
+ sorted_data = sorted(
132
+ comparison_data.items(), key=lambda x: x[1].get("mean_best", 0), reverse=True
133
+ )
134
+
135
+ for i, (name, results) in enumerate(sorted_data):
136
+ row_class = "best-row" if i == 0 else ""
137
+ html += f"<tr class='{row_class}'>\n"
138
+ html += f"<td><strong>{name}</strong></td>\n"
139
+ html += f"<td>{results.get('mean_best', 0):.4f}</td>\n"
140
+ html += f"<td>{results.get('std_best', 0):.4f}</td>\n"
141
+ html += f"<td>{results.get('min_best', 0):.4f}</td>\n"
142
+ html += f"<td>{results.get('max_best', 0):.4f}</td>\n"
143
+ html += f"<td>{results.get('mean_time', 0):.2f}</td>\n"
144
+ html += "</tr>\n"
145
+
146
+ html += "</table>\n"
147
+ return html
148
+
149
+ def _generate_html(self) -> str:
150
+ """Generate complete HTML document."""
151
+ html = f"""<!DOCTYPE html>
152
+ <html lang="en">
153
+ <head>
154
+ <meta charset="UTF-8">
155
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
156
+ <title>{self.experiment_name} - Report</title>
157
+ <style>
158
+ {self._get_css()}
159
+ </style>
160
+ </head>
161
+ <body>
162
+ <div class="container">
163
+ <header>
164
+ <h1>{self.experiment_name}</h1>
165
+ <p class="subtitle">Neural Architecture Search Report</p>
166
+ <p class="meta">Generated: {self.metadata['generated_at']}</p>
167
+ </header>
168
+
169
+ <main>
170
+ """
171
+
172
+ # Add all sections
173
+ for section in self.sections:
174
+ html += self._render_section(section)
175
+
176
+ html += """
177
+ </main>
178
+
179
+ <footer>
180
+ <p>Generated by MorphML - Morphological Machine Learning</p>
181
+ <p>Organization: TONMOY INFRASTRUCTURE & VISION</p>
182
+ </footer>
183
+ </div>
184
+ </body>
185
+ </html>
186
+ """
187
+ return html
188
+
189
+ def _render_section(self, section: Dict[str, Any]) -> str:
190
+ """Render a single section."""
191
+ html = "<section>\n"
192
+ html += f"<h2>{section['title']}</h2>\n"
193
+
194
+ if section["type"] == "text":
195
+ html += f"<p>{section['content']}</p>\n"
196
+ elif section["type"] == "html":
197
+ html += section["content"]
198
+ elif section["type"] == "table":
199
+ html += section["content"]
200
+
201
+ html += "</section>\n"
202
+ return html
203
+
204
+ def _get_css(self) -> str:
205
+ """Get CSS styles."""
206
+ return """
207
+ * {
208
+ margin: 0;
209
+ padding: 0;
210
+ box-sizing: border-box;
211
+ }
212
+
213
+ body {
214
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
215
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
216
+ padding: 20px;
217
+ color: #333;
218
+ }
219
+
220
+ .container {
221
+ max-width: 1200px;
222
+ margin: 0 auto;
223
+ background: white;
224
+ border-radius: 10px;
225
+ box-shadow: 0 10px 40px rgba(0,0,0,0.2);
226
+ overflow: hidden;
227
+ }
228
+
229
+ header {
230
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
231
+ color: white;
232
+ padding: 40px;
233
+ text-align: center;
234
+ }
235
+
236
+ header h1 {
237
+ font-size: 2.5em;
238
+ margin-bottom: 10px;
239
+ }
240
+
241
+ .subtitle {
242
+ font-size: 1.2em;
243
+ opacity: 0.9;
244
+ margin-bottom: 10px;
245
+ }
246
+
247
+ .meta {
248
+ font-size: 0.9em;
249
+ opacity: 0.8;
250
+ }
251
+
252
+ main {
253
+ padding: 40px;
254
+ }
255
+
256
+ section {
257
+ margin-bottom: 40px;
258
+ }
259
+
260
+ h2 {
261
+ color: #667eea;
262
+ border-bottom: 3px solid #667eea;
263
+ padding-bottom: 10px;
264
+ margin-bottom: 20px;
265
+ font-size: 1.8em;
266
+ }
267
+
268
+ table {
269
+ width: 100%;
270
+ border-collapse: collapse;
271
+ margin: 20px 0;
272
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
273
+ }
274
+
275
+ table th, table td {
276
+ padding: 12px;
277
+ text-align: left;
278
+ border-bottom: 1px solid #ddd;
279
+ }
280
+
281
+ table th {
282
+ background: #667eea;
283
+ color: white;
284
+ font-weight: 600;
285
+ }
286
+
287
+ table tr:hover {
288
+ background: #f5f5f5;
289
+ }
290
+
291
+ .best-row {
292
+ background: #d4edda !important;
293
+ font-weight: 600;
294
+ }
295
+
296
+ .optimizer-results {
297
+ background: #f8f9fa;
298
+ padding: 20px;
299
+ border-radius: 8px;
300
+ margin: 20px 0;
301
+ }
302
+
303
+ .results-table {
304
+ background: white;
305
+ }
306
+
307
+ footer {
308
+ background: #333;
309
+ color: white;
310
+ padding: 20px;
311
+ text-align: center;
312
+ }
313
+
314
+ footer p {
315
+ margin: 5px 0;
316
+ font-size: 0.9em;
317
+ }
318
+ """
319
+
320
+
321
+ # Convenience function
322
+ def create_comparison_report(
323
+ experiment_name: str,
324
+ comparison_results: Dict[str, Dict],
325
+ output_path: str,
326
+ description: Optional[str] = None,
327
+ ) -> None:
328
+ """
329
+ Quick function to create a comparison report.
330
+
331
+ Args:
332
+ experiment_name: Name of the experiment
333
+ comparison_results: Results from OptimizerComparison
334
+ output_path: Where to save the report
335
+ description: Optional experiment description
336
+
337
+ Example:
338
+ >>> from morphml.reports import create_comparison_report
339
+ >>>
340
+ >>> create_comparison_report(
341
+ ... "NAS Comparison",
342
+ ... results,
343
+ ... "comparison_report.html",
344
+ ... "Comparing 4 optimizers on CIFAR-10"
345
+ ... )
346
+ """
347
+ generator = ReportGenerator(experiment_name)
348
+
349
+ if description:
350
+ generator.add_section("Experiment Description", description, "text")
351
+
352
+ # Add comparison table
353
+ generator.add_comparison_table(comparison_results)
354
+
355
+ # Add individual results
356
+ for name, results in comparison_results.items():
357
+ generator.add_optimizer_results(name, results)
358
+
359
+ # Generate report
360
+ generator.generate(output_path)
361
+
362
+ logger.info(f"Comparison report created: {output_path}")
@@ -0,0 +1,7 @@
1
+ """Experiment tracking and logging."""
2
+
3
+ from morphml.tracking.experiment import ExperimentTracker
4
+ from morphml.tracking.logger import MetricLogger
5
+ from morphml.tracking.reporter import Reporter
6
+
7
+ __all__ = ["ExperimentTracker", "MetricLogger", "Reporter"]
@@ -0,0 +1,309 @@
1
+ """Experiment tracking for reproducibility and analysis."""
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ from datetime import datetime
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from morphml.logging_config import get_logger
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ class Experiment:
15
+ """Single experiment run."""
16
+
17
+ def __init__(self, name: str, config: Dict[str, Any], experiment_id: Optional[str] = None):
18
+ """Initialize experiment."""
19
+ self.name = name
20
+ self.config = config
21
+ self.id = experiment_id or self._generate_id()
22
+ self.start_time = datetime.now()
23
+ self.end_time: Optional[datetime] = None
24
+ self.metrics: Dict[str, List[Any]] = {}
25
+ self.artifacts: Dict[str, str] = {}
26
+ self.best_result: Optional[Dict[str, Any]] = None
27
+ self.status = "running"
28
+
29
+ def _generate_id(self) -> str:
30
+ """Generate unique experiment ID."""
31
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
32
+ return f"{self.name}_{timestamp}"
33
+
34
+ def log_metric(self, key: str, value: Any, step: Optional[int] = None) -> None:
35
+ """Log a metric value."""
36
+ if key not in self.metrics:
37
+ self.metrics[key] = []
38
+
39
+ entry = {"value": value}
40
+ if step is not None:
41
+ entry["step"] = step
42
+ entry["timestamp"] = time.time()
43
+
44
+ self.metrics[key].append(entry)
45
+
46
+ def log_artifact(self, key: str, path: str) -> None:
47
+ """Log an artifact path."""
48
+ self.artifacts[key] = path
49
+
50
+ def set_best_result(self, result: Dict[str, Any]) -> None:
51
+ """Set best result."""
52
+ self.best_result = result
53
+
54
+ def finish(self, status: str = "completed") -> None:
55
+ """Mark experiment as finished."""
56
+ self.end_time = datetime.now()
57
+ self.status = status
58
+ logger.info(f"Experiment {self.id} finished with status: {status}")
59
+
60
+ def get_duration(self) -> float:
61
+ """Get experiment duration in seconds."""
62
+ end = self.end_time or datetime.now()
63
+ return (end - self.start_time).total_seconds()
64
+
65
+ def to_dict(self) -> Dict[str, Any]:
66
+ """Convert to dictionary."""
67
+ return {
68
+ "id": self.id,
69
+ "name": self.name,
70
+ "config": self.config,
71
+ "start_time": self.start_time.isoformat(),
72
+ "end_time": self.end_time.isoformat() if self.end_time else None,
73
+ "duration_seconds": self.get_duration(),
74
+ "status": self.status,
75
+ "metrics": self.metrics,
76
+ "artifacts": self.artifacts,
77
+ "best_result": self.best_result,
78
+ }
79
+
80
+
81
+ class ExperimentTracker:
82
+ """
83
+ Track multiple experiments.
84
+
85
+ Example:
86
+ >>> tracker = ExperimentTracker("./experiments")
87
+ >>> exp = tracker.create_experiment("GA_CIFAR10", config={...})
88
+ >>> exp.log_metric("fitness", 0.85, step=10)
89
+ >>> exp.finish()
90
+ >>> tracker.save_experiment(exp)
91
+ """
92
+
93
+ def __init__(self, base_dir: str = "./experiments"):
94
+ """
95
+ Initialize tracker.
96
+
97
+ Args:
98
+ base_dir: Base directory for experiments
99
+ """
100
+ self.base_dir = base_dir
101
+ self.experiments: Dict[str, Experiment] = {}
102
+
103
+ os.makedirs(base_dir, exist_ok=True)
104
+ logger.info(f"ExperimentTracker initialized at {base_dir}")
105
+
106
+ def create_experiment(
107
+ self, name: str, config: Dict[str, Any], experiment_id: Optional[str] = None
108
+ ) -> Experiment:
109
+ """
110
+ Create new experiment.
111
+
112
+ Args:
113
+ name: Experiment name
114
+ config: Configuration dict
115
+ experiment_id: Optional custom ID
116
+
117
+ Returns:
118
+ Experiment instance
119
+ """
120
+ exp = Experiment(name, config, experiment_id)
121
+ self.experiments[exp.id] = exp
122
+
123
+ # Create experiment directory
124
+ exp_dir = os.path.join(self.base_dir, exp.id)
125
+ os.makedirs(exp_dir, exist_ok=True)
126
+
127
+ logger.info(f"Created experiment: {exp.id}")
128
+ return exp
129
+
130
+ def get_experiment(self, experiment_id: str) -> Optional[Experiment]:
131
+ """Get experiment by ID."""
132
+ return self.experiments.get(experiment_id)
133
+
134
+ def save_experiment(self, experiment: Experiment) -> None:
135
+ """Save experiment to disk."""
136
+ exp_dir = os.path.join(self.base_dir, experiment.id)
137
+ os.makedirs(exp_dir, exist_ok=True)
138
+
139
+ # Save metadata
140
+ metadata_path = os.path.join(exp_dir, "metadata.json")
141
+ with open(metadata_path, "w") as f:
142
+ json.dump(experiment.to_dict(), f, indent=2)
143
+
144
+ logger.info(f"Saved experiment {experiment.id} to {exp_dir}")
145
+
146
+ def load_experiment(self, experiment_id: str) -> Optional[Experiment]:
147
+ """Load experiment from disk."""
148
+ exp_dir = os.path.join(self.base_dir, experiment_id)
149
+ metadata_path = os.path.join(exp_dir, "metadata.json")
150
+
151
+ if not os.path.exists(metadata_path):
152
+ logger.warning(f"Experiment {experiment_id} not found")
153
+ return None
154
+
155
+ with open(metadata_path, "r") as f:
156
+ data = json.load(f)
157
+
158
+ exp = Experiment(data["name"], data["config"], data["id"])
159
+ exp.start_time = datetime.fromisoformat(data["start_time"])
160
+ if data["end_time"]:
161
+ exp.end_time = datetime.fromisoformat(data["end_time"])
162
+ exp.status = data["status"]
163
+ exp.metrics = data["metrics"]
164
+ exp.artifacts = data["artifacts"]
165
+ exp.best_result = data["best_result"]
166
+
167
+ self.experiments[exp.id] = exp
168
+ logger.info(f"Loaded experiment {experiment_id}")
169
+
170
+ return exp
171
+
172
+ def list_experiments(self) -> List[str]:
173
+ """List all experiment IDs."""
174
+ experiments = []
175
+
176
+ if not os.path.exists(self.base_dir):
177
+ return experiments
178
+
179
+ for item in os.listdir(self.base_dir):
180
+ exp_dir = os.path.join(self.base_dir, item)
181
+ if os.path.isdir(exp_dir):
182
+ metadata_path = os.path.join(exp_dir, "metadata.json")
183
+ if os.path.exists(metadata_path):
184
+ experiments.append(item)
185
+
186
+ return experiments
187
+
188
+ def compare_experiments(self, experiment_ids: List[str], metric: str) -> Dict[str, List[float]]:
189
+ """
190
+ Compare experiments on a metric.
191
+
192
+ Args:
193
+ experiment_ids: List of experiment IDs
194
+ metric: Metric name to compare
195
+
196
+ Returns:
197
+ Dictionary mapping experiment IDs to metric values
198
+ """
199
+ results = {}
200
+
201
+ for exp_id in experiment_ids:
202
+ exp = self.get_experiment(exp_id)
203
+ if not exp:
204
+ exp = self.load_experiment(exp_id)
205
+
206
+ if exp and metric in exp.metrics:
207
+ values = [entry["value"] for entry in exp.metrics[metric]]
208
+ results[exp_id] = values
209
+
210
+ return results
211
+
212
+ def get_best_experiment(self, metric: str = "best_fitness") -> Optional[Experiment]:
213
+ """Get experiment with best result."""
214
+ best_exp = None
215
+ best_value = float("-inf")
216
+
217
+ for exp_id in self.list_experiments():
218
+ exp = self.get_experiment(exp_id)
219
+ if not exp:
220
+ exp = self.load_experiment(exp_id)
221
+
222
+ if exp and exp.best_result and metric in exp.best_result:
223
+ value = exp.best_result[metric]
224
+ if value > best_value:
225
+ best_value = value
226
+ best_exp = exp
227
+
228
+ return best_exp
229
+
230
+ def export_summary(self, output_path: str) -> None:
231
+ """Export summary of all experiments."""
232
+ summary = {
233
+ "base_dir": self.base_dir,
234
+ "total_experiments": len(self.list_experiments()),
235
+ "experiments": [],
236
+ }
237
+
238
+ for exp_id in self.list_experiments():
239
+ exp = self.get_experiment(exp_id)
240
+ if not exp:
241
+ exp = self.load_experiment(exp_id)
242
+
243
+ if exp:
244
+ summary["experiments"].append(
245
+ {
246
+ "id": exp.id,
247
+ "name": exp.name,
248
+ "status": exp.status,
249
+ "duration": exp.get_duration(),
250
+ "best_result": exp.best_result,
251
+ }
252
+ )
253
+
254
+ with open(output_path, "w") as f:
255
+ json.dump(summary, f, indent=2)
256
+
257
+ logger.info(f"Experiment summary exported to {output_path}")
258
+
259
+ def clean_failed_experiments(self) -> int:
260
+ """Remove failed experiments."""
261
+ removed = 0
262
+
263
+ for exp_id in self.list_experiments():
264
+ exp = self.get_experiment(exp_id)
265
+ if not exp:
266
+ exp = self.load_experiment(exp_id)
267
+
268
+ if exp and exp.status == "failed":
269
+ exp_dir = os.path.join(self.base_dir, exp_id)
270
+ import shutil
271
+
272
+ shutil.rmtree(exp_dir)
273
+
274
+ if exp_id in self.experiments:
275
+ del self.experiments[exp_id]
276
+
277
+ removed += 1
278
+
279
+ logger.info(f"Removed {removed} failed experiments")
280
+ return removed
281
+
282
+
283
+ class RunContext:
284
+ """Context manager for experiment runs."""
285
+
286
+ def __init__(self, tracker: ExperimentTracker, name: str, config: Dict[str, Any]):
287
+ """Initialize run context."""
288
+ self.tracker = tracker
289
+ self.name = name
290
+ self.config = config
291
+ self.experiment: Optional[Experiment] = None
292
+
293
+ def __enter__(self) -> Experiment:
294
+ """Enter context."""
295
+ self.experiment = self.tracker.create_experiment(self.name, self.config)
296
+ return self.experiment
297
+
298
+ def __exit__(self, exc_type, exc_val, exc_tb):
299
+ """Exit context."""
300
+ if self.experiment:
301
+ if exc_type is None:
302
+ self.experiment.finish("completed")
303
+ else:
304
+ self.experiment.finish("failed")
305
+ logger.error(f"Experiment failed: {exc_val}")
306
+
307
+ self.tracker.save_experiment(self.experiment)
308
+
309
+ return False