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.
- morphml/__init__.py +14 -0
- morphml/api/__init__.py +26 -0
- morphml/api/app.py +326 -0
- morphml/api/auth.py +193 -0
- morphml/api/client.py +338 -0
- morphml/api/models.py +132 -0
- morphml/api/rate_limit.py +192 -0
- morphml/benchmarking/__init__.py +36 -0
- morphml/benchmarking/comparison.py +430 -0
- morphml/benchmarks/__init__.py +56 -0
- morphml/benchmarks/comparator.py +409 -0
- morphml/benchmarks/datasets.py +280 -0
- morphml/benchmarks/metrics.py +199 -0
- morphml/benchmarks/openml_suite.py +201 -0
- morphml/benchmarks/problems.py +289 -0
- morphml/benchmarks/suite.py +318 -0
- morphml/cli/__init__.py +5 -0
- morphml/cli/commands/experiment.py +329 -0
- morphml/cli/main.py +457 -0
- morphml/cli/quickstart.py +312 -0
- morphml/config.py +278 -0
- morphml/constraints/__init__.py +19 -0
- morphml/constraints/handler.py +205 -0
- morphml/constraints/predicates.py +285 -0
- morphml/core/__init__.py +3 -0
- morphml/core/crossover.py +449 -0
- morphml/core/dsl/README.md +359 -0
- morphml/core/dsl/__init__.py +72 -0
- morphml/core/dsl/ast_nodes.py +364 -0
- morphml/core/dsl/compiler.py +318 -0
- morphml/core/dsl/layers.py +368 -0
- morphml/core/dsl/lexer.py +336 -0
- morphml/core/dsl/parser.py +455 -0
- morphml/core/dsl/search_space.py +386 -0
- morphml/core/dsl/syntax.py +199 -0
- morphml/core/dsl/type_system.py +361 -0
- morphml/core/dsl/validator.py +386 -0
- morphml/core/graph/__init__.py +40 -0
- morphml/core/graph/edge.py +124 -0
- morphml/core/graph/graph.py +507 -0
- morphml/core/graph/mutations.py +409 -0
- morphml/core/graph/node.py +196 -0
- morphml/core/graph/serialization.py +361 -0
- morphml/core/graph/visualization.py +431 -0
- morphml/core/objectives/__init__.py +20 -0
- morphml/core/search/__init__.py +33 -0
- morphml/core/search/individual.py +252 -0
- morphml/core/search/parameters.py +453 -0
- morphml/core/search/population.py +375 -0
- morphml/core/search/search_engine.py +340 -0
- morphml/distributed/__init__.py +76 -0
- morphml/distributed/fault_tolerance.py +497 -0
- morphml/distributed/health_monitor.py +348 -0
- morphml/distributed/master.py +709 -0
- morphml/distributed/proto/README.md +224 -0
- morphml/distributed/proto/__init__.py +74 -0
- morphml/distributed/proto/worker.proto +170 -0
- morphml/distributed/proto/worker_pb2.py +79 -0
- morphml/distributed/proto/worker_pb2_grpc.py +423 -0
- morphml/distributed/resource_manager.py +416 -0
- morphml/distributed/scheduler.py +567 -0
- morphml/distributed/storage/__init__.py +33 -0
- morphml/distributed/storage/artifacts.py +381 -0
- morphml/distributed/storage/cache.py +366 -0
- morphml/distributed/storage/checkpointing.py +329 -0
- morphml/distributed/storage/database.py +459 -0
- morphml/distributed/worker.py +549 -0
- morphml/evaluation/__init__.py +5 -0
- morphml/evaluation/heuristic.py +237 -0
- morphml/exceptions.py +55 -0
- morphml/execution/__init__.py +5 -0
- morphml/execution/local_executor.py +350 -0
- morphml/integrations/__init__.py +28 -0
- morphml/integrations/jax_adapter.py +206 -0
- morphml/integrations/pytorch_adapter.py +530 -0
- morphml/integrations/sklearn_adapter.py +206 -0
- morphml/integrations/tensorflow_adapter.py +230 -0
- morphml/logging_config.py +93 -0
- morphml/meta_learning/__init__.py +66 -0
- morphml/meta_learning/architecture_similarity.py +277 -0
- morphml/meta_learning/experiment_database.py +240 -0
- morphml/meta_learning/knowledge_base/__init__.py +19 -0
- morphml/meta_learning/knowledge_base/embedder.py +179 -0
- morphml/meta_learning/knowledge_base/knowledge_base.py +313 -0
- morphml/meta_learning/knowledge_base/meta_features.py +265 -0
- morphml/meta_learning/knowledge_base/vector_store.py +271 -0
- morphml/meta_learning/predictors/__init__.py +27 -0
- morphml/meta_learning/predictors/ensemble.py +221 -0
- morphml/meta_learning/predictors/gnn_predictor.py +552 -0
- morphml/meta_learning/predictors/learning_curve.py +231 -0
- morphml/meta_learning/predictors/proxy_metrics.py +261 -0
- morphml/meta_learning/strategy_evolution/__init__.py +27 -0
- morphml/meta_learning/strategy_evolution/adaptive_optimizer.py +226 -0
- morphml/meta_learning/strategy_evolution/bandit.py +276 -0
- morphml/meta_learning/strategy_evolution/portfolio.py +230 -0
- morphml/meta_learning/transfer.py +581 -0
- morphml/meta_learning/warm_start.py +286 -0
- morphml/optimizers/__init__.py +74 -0
- morphml/optimizers/adaptive_operators.py +399 -0
- morphml/optimizers/bayesian/__init__.py +52 -0
- morphml/optimizers/bayesian/acquisition.py +387 -0
- morphml/optimizers/bayesian/base.py +319 -0
- morphml/optimizers/bayesian/gaussian_process.py +635 -0
- morphml/optimizers/bayesian/smac.py +534 -0
- morphml/optimizers/bayesian/tpe.py +411 -0
- morphml/optimizers/differential_evolution.py +220 -0
- morphml/optimizers/evolutionary/__init__.py +61 -0
- morphml/optimizers/evolutionary/cma_es.py +416 -0
- morphml/optimizers/evolutionary/differential_evolution.py +556 -0
- morphml/optimizers/evolutionary/encoding.py +426 -0
- morphml/optimizers/evolutionary/particle_swarm.py +449 -0
- morphml/optimizers/genetic_algorithm.py +486 -0
- morphml/optimizers/gradient_based/__init__.py +22 -0
- morphml/optimizers/gradient_based/darts.py +550 -0
- morphml/optimizers/gradient_based/enas.py +585 -0
- morphml/optimizers/gradient_based/operations.py +474 -0
- morphml/optimizers/gradient_based/utils.py +601 -0
- morphml/optimizers/hill_climbing.py +169 -0
- morphml/optimizers/multi_objective/__init__.py +56 -0
- morphml/optimizers/multi_objective/indicators.py +504 -0
- morphml/optimizers/multi_objective/nsga2.py +647 -0
- morphml/optimizers/multi_objective/visualization.py +427 -0
- morphml/optimizers/nsga2.py +308 -0
- morphml/optimizers/random_search.py +172 -0
- morphml/optimizers/simulated_annealing.py +181 -0
- morphml/plugins/__init__.py +35 -0
- morphml/plugins/custom_evaluator_example.py +81 -0
- morphml/plugins/custom_optimizer_example.py +63 -0
- morphml/plugins/plugin_system.py +454 -0
- morphml/reports/__init__.py +30 -0
- morphml/reports/generator.py +362 -0
- morphml/tracking/__init__.py +7 -0
- morphml/tracking/experiment.py +309 -0
- morphml/tracking/logger.py +301 -0
- morphml/tracking/reporter.py +357 -0
- morphml/utils/__init__.py +6 -0
- morphml/utils/checkpoint.py +189 -0
- morphml/utils/comparison.py +390 -0
- morphml/utils/export.py +407 -0
- morphml/utils/progress.py +392 -0
- morphml/utils/validation.py +392 -0
- morphml/version.py +7 -0
- morphml/visualization/__init__.py +50 -0
- morphml/visualization/analytics.py +423 -0
- morphml/visualization/architecture_diagrams.py +353 -0
- morphml/visualization/architecture_plot.py +223 -0
- morphml/visualization/convergence_plot.py +174 -0
- morphml/visualization/crossover_viz.py +386 -0
- morphml/visualization/graph_viz.py +338 -0
- morphml/visualization/pareto_plot.py +149 -0
- morphml/visualization/plotly_dashboards.py +422 -0
- morphml/visualization/population.py +309 -0
- morphml/visualization/progress.py +260 -0
- morphml-1.0.0.dist-info/METADATA +434 -0
- morphml-1.0.0.dist-info/RECORD +158 -0
- morphml-1.0.0.dist-info/WHEEL +4 -0
- morphml-1.0.0.dist-info/entry_points.txt +3 -0
- 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,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
|