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,301 @@
1
+ """Metric logging utilities."""
2
+
3
+ import json
4
+ from collections import defaultdict
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from morphml.logging_config import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class MetricLogger:
13
+ """
14
+ Log metrics during optimization.
15
+
16
+ Example:
17
+ >>> logger = MetricLogger()
18
+ >>> logger.log("fitness", 0.85, step=10)
19
+ >>> logger.log("diversity", 0.45, step=10)
20
+ >>> logger.save("metrics.json")
21
+ """
22
+
23
+ def __init__(self):
24
+ """Initialize metric logger."""
25
+ self.metrics: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
26
+ self.step_counter = 0
27
+
28
+ def log(self, metric_name: str, value: Any, step: Optional[int] = None) -> None:
29
+ """
30
+ Log a metric value.
31
+
32
+ Args:
33
+ metric_name: Name of metric
34
+ value: Metric value
35
+ step: Optional step/generation number
36
+ """
37
+ if step is None:
38
+ step = self.step_counter
39
+ self.step_counter += 1
40
+
41
+ self.metrics[metric_name].append({"step": step, "value": value})
42
+
43
+ def log_dict(self, metrics: Dict[str, Any], step: Optional[int] = None) -> None:
44
+ """
45
+ Log multiple metrics at once.
46
+
47
+ Args:
48
+ metrics: Dictionary of metric name to value
49
+ step: Optional step number
50
+ """
51
+ for name, value in metrics.items():
52
+ self.log(name, value, step)
53
+
54
+ def get_metric(self, metric_name: str) -> List[Dict[str, Any]]:
55
+ """Get all values for a metric."""
56
+ return self.metrics.get(metric_name, [])
57
+
58
+ def get_last_value(self, metric_name: str) -> Optional[Any]:
59
+ """Get last logged value for a metric."""
60
+ values = self.metrics.get(metric_name, [])
61
+ return values[-1]["value"] if values else None
62
+
63
+ def get_values(self, metric_name: str) -> List[Any]:
64
+ """Get all values for a metric (without step info)."""
65
+ return [entry["value"] for entry in self.metrics.get(metric_name, [])]
66
+
67
+ def get_steps(self, metric_name: str) -> List[int]:
68
+ """Get all steps for a metric."""
69
+ return [entry["step"] for entry in self.metrics.get(metric_name, [])]
70
+
71
+ def clear(self) -> None:
72
+ """Clear all metrics."""
73
+ self.metrics.clear()
74
+ self.step_counter = 0
75
+
76
+ def save(self, filepath: str) -> None:
77
+ """Save metrics to JSON file."""
78
+ with open(filepath, "w") as f:
79
+ json.dump(dict(self.metrics), f, indent=2)
80
+
81
+ logger.info(f"Metrics saved to {filepath}")
82
+
83
+ def load(self, filepath: str) -> None:
84
+ """Load metrics from JSON file."""
85
+ with open(filepath, "r") as f:
86
+ data = json.load(f)
87
+
88
+ self.metrics = defaultdict(list, data)
89
+
90
+ # Update step counter
91
+ max_step = 0
92
+ for entries in self.metrics.values():
93
+ for entry in entries:
94
+ max_step = max(max_step, entry.get("step", 0))
95
+
96
+ self.step_counter = max_step + 1
97
+
98
+ logger.info(f"Metrics loaded from {filepath}")
99
+
100
+ def get_summary(self) -> Dict[str, Dict[str, Any]]:
101
+ """Get summary statistics for all metrics."""
102
+ import statistics
103
+
104
+ summary = {}
105
+
106
+ for metric_name, entries in self.metrics.items():
107
+ values = [e["value"] for e in entries if isinstance(e["value"], (int, float))]
108
+
109
+ if values:
110
+ summary[metric_name] = {
111
+ "count": len(values),
112
+ "mean": statistics.mean(values),
113
+ "median": statistics.median(values),
114
+ "std": statistics.stdev(values) if len(values) > 1 else 0.0,
115
+ "min": min(values),
116
+ "max": max(values),
117
+ "last": values[-1],
118
+ }
119
+
120
+ return summary
121
+
122
+ def print_summary(self) -> None:
123
+ """Print summary of all metrics."""
124
+ summary = self.get_summary()
125
+
126
+ print("\n" + "=" * 60)
127
+ print("METRIC SUMMARY")
128
+ print("=" * 60)
129
+
130
+ for metric_name, stats in sorted(summary.items()):
131
+ print(f"\n{metric_name}:")
132
+ print(f" Count: {stats['count']}")
133
+ print(f" Mean: {stats['mean']:.4f}")
134
+ print(f" Median: {stats['median']:.4f}")
135
+ print(f" Std: {stats['std']:.4f}")
136
+ print(f" Range: [{stats['min']:.4f}, {stats['max']:.4f}]")
137
+ print(f" Last: {stats['last']:.4f}")
138
+
139
+ print("=" * 60)
140
+
141
+
142
+ class CSVLogger(MetricLogger):
143
+ """Log metrics to CSV file."""
144
+
145
+ def __init__(self, filepath: str):
146
+ """
147
+ Initialize CSV logger.
148
+
149
+ Args:
150
+ filepath: Path to CSV file
151
+ """
152
+ super().__init__()
153
+ self.filepath = filepath
154
+ self.file = None
155
+ self.writer = None
156
+ self.headers_written = False
157
+
158
+ def __enter__(self):
159
+ """Enter context."""
160
+ self.file = open(self.filepath, "w", newline="")
161
+ return self
162
+
163
+ def __exit__(self, exc_type, exc_val, exc_tb):
164
+ """Exit context."""
165
+ if self.file:
166
+ self.file.close()
167
+ return False
168
+
169
+ def log_dict(self, metrics: Dict[str, Any], step: Optional[int] = None) -> None:
170
+ """Log metrics to CSV."""
171
+ super().log_dict(metrics, step)
172
+
173
+ if not self.file:
174
+ return
175
+
176
+ # Write headers if needed
177
+ if not self.headers_written:
178
+ import csv
179
+
180
+ self.writer = csv.DictWriter(self.file, fieldnames=["step"] + list(metrics.keys()))
181
+ self.writer.writeheader()
182
+ self.headers_written = True
183
+
184
+ # Write row
185
+ if self.writer:
186
+ row = {"step": step if step is not None else self.step_counter - 1}
187
+ row.update(metrics)
188
+ self.writer.writerow(row)
189
+ self.file.flush()
190
+
191
+
192
+ class TensorBoardLogger(MetricLogger):
193
+ """Log metrics to TensorBoard."""
194
+
195
+ def __init__(self, log_dir: str):
196
+ """
197
+ Initialize TensorBoard logger.
198
+
199
+ Args:
200
+ log_dir: TensorBoard log directory
201
+ """
202
+ super().__init__()
203
+ self.log_dir = log_dir
204
+ self.writer = None
205
+
206
+ try:
207
+ from torch.utils.tensorboard import SummaryWriter
208
+
209
+ self.writer = SummaryWriter(log_dir)
210
+ logger.info(f"TensorBoard logging to {log_dir}")
211
+ except ImportError:
212
+ logger.warning("tensorboard not available")
213
+
214
+ def log(self, metric_name: str, value: Any, step: Optional[int] = None) -> None:
215
+ """Log metric to TensorBoard."""
216
+ super().log(metric_name, value, step)
217
+
218
+ if self.writer and isinstance(value, (int, float)):
219
+ step = step if step is not None else self.step_counter - 1
220
+ self.writer.add_scalar(metric_name, value, step)
221
+
222
+ def log_histogram(self, name: str, values: List[float], step: int) -> None:
223
+ """Log histogram to TensorBoard."""
224
+ if self.writer:
225
+ import numpy as np
226
+
227
+ self.writer.add_histogram(name, np.array(values), step)
228
+
229
+ def close(self) -> None:
230
+ """Close TensorBoard writer."""
231
+ if self.writer:
232
+ self.writer.close()
233
+
234
+
235
+ class WandbLogger(MetricLogger):
236
+ """Log metrics to Weights & Biases."""
237
+
238
+ def __init__(self, project: str, name: Optional[str] = None, config: Optional[Dict] = None):
239
+ """
240
+ Initialize W&B logger.
241
+
242
+ Args:
243
+ project: W&B project name
244
+ name: Run name
245
+ config: Configuration dict
246
+ """
247
+ super().__init__()
248
+ self.project = project
249
+ self.run = None
250
+
251
+ try:
252
+ import wandb
253
+
254
+ self.run = wandb.init(project=project, name=name, config=config)
255
+ logger.info(f"W&B logging to project {project}")
256
+ except ImportError:
257
+ logger.warning("wandb not available")
258
+
259
+ def log_dict(self, metrics: Dict[str, Any], step: Optional[int] = None) -> None:
260
+ """Log metrics to W&B."""
261
+ super().log_dict(metrics, step)
262
+
263
+ if self.run:
264
+ import wandb
265
+
266
+ wandb.log(metrics, step=step)
267
+
268
+ def finish(self) -> None:
269
+ """Finish W&B run."""
270
+ if self.run:
271
+ import wandb
272
+
273
+ wandb.finish()
274
+
275
+
276
+ class MultiLogger(MetricLogger):
277
+ """Log to multiple backends simultaneously."""
278
+
279
+ def __init__(self, loggers: List[MetricLogger]):
280
+ """
281
+ Initialize multi-logger.
282
+
283
+ Args:
284
+ loggers: List of logger instances
285
+ """
286
+ super().__init__()
287
+ self.loggers = loggers
288
+
289
+ def log(self, metric_name: str, value: Any, step: Optional[int] = None) -> None:
290
+ """Log to all loggers."""
291
+ super().log(metric_name, value, step)
292
+
293
+ for logger_instance in self.loggers:
294
+ logger_instance.log(metric_name, value, step)
295
+
296
+ def log_dict(self, metrics: Dict[str, Any], step: Optional[int] = None) -> None:
297
+ """Log dict to all loggers."""
298
+ super().log_dict(metrics, step)
299
+
300
+ for logger_instance in self.loggers:
301
+ logger_instance.log_dict(metrics, step)
@@ -0,0 +1,357 @@
1
+ """Report generation for experiments."""
2
+
3
+ import json
4
+ from datetime import datetime
5
+ from typing import Any, Dict, List
6
+
7
+ from morphml.logging_config import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class Reporter:
13
+ """
14
+ Generate reports for experiments.
15
+
16
+ Example:
17
+ >>> reporter = Reporter()
18
+ >>> report = reporter.generate_markdown_report(experiment)
19
+ >>> reporter.save_report(report, "report.md")
20
+ """
21
+
22
+ def __init__(self):
23
+ """Initialize reporter."""
24
+ pass
25
+
26
+ def generate_markdown_report(self, experiment: Any) -> str:
27
+ """
28
+ Generate Markdown report.
29
+
30
+ Args:
31
+ experiment: Experiment instance
32
+
33
+ Returns:
34
+ Markdown string
35
+ """
36
+ lines = []
37
+
38
+ # Header
39
+ lines.append(f"# Experiment Report: {experiment.name}")
40
+ lines.append(f"\n**ID:** `{experiment.id}`\n")
41
+ lines.append(f"**Status:** {experiment.status}")
42
+ lines.append(f"**Duration:** {experiment.get_duration():.2f} seconds\n")
43
+
44
+ # Configuration
45
+ lines.append("## Configuration\n")
46
+ lines.append("```json")
47
+ lines.append(json.dumps(experiment.config, indent=2))
48
+ lines.append("```\n")
49
+
50
+ # Best Result
51
+ if experiment.best_result:
52
+ lines.append("## Best Result\n")
53
+ for key, value in experiment.best_result.items():
54
+ lines.append(f"- **{key}:** {value}")
55
+ lines.append("")
56
+
57
+ # Metrics Summary
58
+ if experiment.metrics:
59
+ lines.append("## Metrics Summary\n")
60
+ lines.append("| Metric | Count | Final Value |")
61
+ lines.append("|--------|-------|-------------|")
62
+
63
+ for metric_name, entries in experiment.metrics.items():
64
+ count = len(entries)
65
+ final_value = entries[-1]["value"] if entries else "N/A"
66
+ lines.append(f"| {metric_name} | {count} | {final_value} |")
67
+
68
+ lines.append("")
69
+
70
+ # Artifacts
71
+ if experiment.artifacts:
72
+ lines.append("## Artifacts\n")
73
+ for name, path in experiment.artifacts.items():
74
+ lines.append(f"- **{name}:** `{path}`")
75
+ lines.append("")
76
+
77
+ # Footer
78
+ lines.append("---")
79
+ lines.append(f"\n*Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")
80
+
81
+ return "\n".join(lines)
82
+
83
+ def generate_html_report(self, experiment: Any) -> str:
84
+ """
85
+ Generate HTML report.
86
+
87
+ Args:
88
+ experiment: Experiment instance
89
+
90
+ Returns:
91
+ HTML string
92
+ """
93
+ html = [
94
+ "<!DOCTYPE html>",
95
+ "<html>",
96
+ "<head>",
97
+ f"<title>Experiment Report: {experiment.name}</title>",
98
+ "<style>",
99
+ "body { font-family: Arial, sans-serif; margin: 40px; }",
100
+ "h1 { color: #333; }",
101
+ "table { border-collapse: collapse; width: 100%; margin: 20px 0; }",
102
+ "th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }",
103
+ "th { background-color: #4CAF50; color: white; }",
104
+ "pre { background: #f4f4f4; padding: 15px; border-radius: 5px; }",
105
+ (
106
+ ".metric-card { background: #fff; border: 1px solid #ddd; "
107
+ "padding: 15px; margin: 10px 0; border-radius: 5px; }"
108
+ ),
109
+ "</style>",
110
+ "</head>",
111
+ "<body>",
112
+ f"<h1>Experiment Report: {experiment.name}</h1>",
113
+ f"<p><strong>ID:</strong> {experiment.id}</p>",
114
+ f"<p><strong>Status:</strong> {experiment.status}</p>",
115
+ f"<p><strong>Duration:</strong> {experiment.get_duration():.2f} seconds</p>",
116
+ ]
117
+
118
+ # Configuration
119
+ html.append("<h2>Configuration</h2>")
120
+ html.append("<pre>")
121
+ html.append(json.dumps(experiment.config, indent=2))
122
+ html.append("</pre>")
123
+
124
+ # Best Result
125
+ if experiment.best_result:
126
+ html.append("<h2>Best Result</h2>")
127
+ html.append("<table>")
128
+ html.append("<tr><th>Metric</th><th>Value</th></tr>")
129
+ for key, value in experiment.best_result.items():
130
+ html.append(f"<tr><td>{key}</td><td>{value}</td></tr>")
131
+ html.append("</table>")
132
+
133
+ # Metrics
134
+ if experiment.metrics:
135
+ html.append("<h2>Metrics</h2>")
136
+ for metric_name, entries in experiment.metrics.items():
137
+ html.append("<div class='metric-card'>")
138
+ html.append(f"<h3>{metric_name}</h3>")
139
+ html.append(f"<p>Total entries: {len(entries)}</p>")
140
+ if entries:
141
+ html.append(f"<p>Final value: {entries[-1]['value']}</p>")
142
+ html.append("</div>")
143
+
144
+ # Footer
145
+ html.append("<hr>")
146
+ html.append(f"<p><em>Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</em></p>")
147
+ html.append("</body>")
148
+ html.append("</html>")
149
+
150
+ return "\n".join(html)
151
+
152
+ def generate_comparison_report(
153
+ self, experiments: List[Any], metric: str = "best_fitness"
154
+ ) -> str:
155
+ """
156
+ Generate comparison report for multiple experiments.
157
+
158
+ Args:
159
+ experiments: List of experiments
160
+ metric: Metric to compare
161
+
162
+ Returns:
163
+ Markdown string
164
+ """
165
+ lines = []
166
+
167
+ lines.append("# Experiment Comparison\n")
168
+ lines.append(f"**Metric:** {metric}\n")
169
+ lines.append(f"**Experiments:** {len(experiments)}\n")
170
+
171
+ # Table
172
+ lines.append("| Experiment | Status | Duration (s) | Best Value |")
173
+ lines.append("|------------|--------|--------------|------------|")
174
+
175
+ for exp in experiments:
176
+ best_value = "N/A"
177
+ if exp.best_result and metric in exp.best_result:
178
+ best_value = f"{exp.best_result[metric]:.4f}"
179
+
180
+ lines.append(
181
+ f"| {exp.name} | {exp.status} | " f"{exp.get_duration():.2f} | {best_value} |"
182
+ )
183
+
184
+ lines.append("")
185
+
186
+ # Rankings
187
+ ranked = sorted(
188
+ [e for e in experiments if e.best_result and metric in e.best_result],
189
+ key=lambda x: x.best_result[metric],
190
+ reverse=True,
191
+ )
192
+
193
+ if ranked:
194
+ lines.append("## Rankings\n")
195
+ for i, exp in enumerate(ranked, 1):
196
+ value = exp.best_result[metric]
197
+ lines.append(f"{i}. **{exp.name}** - {value:.4f}")
198
+ lines.append("")
199
+
200
+ return "\n".join(lines)
201
+
202
+ def save_report(self, content: str, filepath: str) -> None:
203
+ """Save report to file."""
204
+ with open(filepath, "w") as f:
205
+ f.write(content)
206
+
207
+ logger.info(f"Report saved to {filepath}")
208
+
209
+ def generate_latex_report(self, experiment: Any) -> str:
210
+ """
211
+ Generate LaTeX report.
212
+
213
+ Args:
214
+ experiment: Experiment instance
215
+
216
+ Returns:
217
+ LaTeX string
218
+ """
219
+ lines = [
220
+ "\\documentclass{article}",
221
+ "\\usepackage{booktabs}",
222
+ "\\usepackage{hyperref}",
223
+ "\\begin{document}",
224
+ "",
225
+ f"\\title{{Experiment Report: {experiment.name}}}",
226
+ "\\maketitle",
227
+ "",
228
+ "\\section{Overview}",
229
+ "",
230
+ f"\\textbf{{Experiment ID:}} \\texttt{{{experiment.id}}}",
231
+ "",
232
+ f"\\textbf{{Status:}} {experiment.status}",
233
+ "",
234
+ f"\\textbf{{Duration:}} {experiment.get_duration():.2f} seconds",
235
+ "",
236
+ ]
237
+
238
+ # Best Result
239
+ if experiment.best_result:
240
+ lines.append("\\section{Best Result}")
241
+ lines.append("\\begin{table}[h]")
242
+ lines.append("\\centering")
243
+ lines.append("\\begin{tabular}{ll}")
244
+ lines.append("\\toprule")
245
+ lines.append("Metric & Value \\\\")
246
+ lines.append("\\midrule")
247
+
248
+ for key, value in experiment.best_result.items():
249
+ lines.append(f"{key} & {value} \\\\")
250
+
251
+ lines.append("\\bottomrule")
252
+ lines.append("\\end{tabular}")
253
+ lines.append("\\end{table}")
254
+ lines.append("")
255
+
256
+ lines.append("\\end{document}")
257
+
258
+ return "\n".join(lines)
259
+
260
+
261
+ class ProgressReporter:
262
+ """Report progress during optimization."""
263
+
264
+ def __init__(self, report_every: int = 10):
265
+ """
266
+ Initialize progress reporter.
267
+
268
+ Args:
269
+ report_every: Report every N steps
270
+ """
271
+ self.report_every = report_every
272
+ self.step = 0
273
+
274
+ def report(self, metrics: Dict[str, Any]) -> None:
275
+ """Report progress."""
276
+ self.step += 1
277
+
278
+ if self.step % self.report_every == 0:
279
+ self._print_progress(metrics)
280
+
281
+ def _print_progress(self, metrics: Dict[str, Any]) -> None:
282
+ """Print progress to console."""
283
+ print(f"\n[Step {self.step}]")
284
+ for key, value in metrics.items():
285
+ if isinstance(value, float):
286
+ print(f" {key}: {value:.4f}")
287
+ else:
288
+ print(f" {key}: {value}")
289
+
290
+
291
+ class EmailReporter:
292
+ """Send email reports (requires SMTP configuration)."""
293
+
294
+ def __init__(self, smtp_server: str, smtp_port: int, sender: str, password: str):
295
+ """Initialize email reporter."""
296
+ self.smtp_server = smtp_server
297
+ self.smtp_port = smtp_port
298
+ self.sender = sender
299
+ self.password = password
300
+
301
+ def send_report(self, recipient: str, subject: str, body: str) -> None:
302
+ """Send email report."""
303
+ try:
304
+ import smtplib
305
+ from email.mime.text import MIMEText
306
+
307
+ msg = MIMEText(body)
308
+ msg["Subject"] = subject
309
+ msg["From"] = self.sender
310
+ msg["To"] = recipient
311
+
312
+ with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
313
+ server.starttls()
314
+ server.login(self.sender, self.password)
315
+ server.send_message(msg)
316
+
317
+ logger.info(f"Email report sent to {recipient}")
318
+
319
+ except Exception as e:
320
+ logger.error(f"Failed to send email: {e}")
321
+
322
+
323
+ class SlackReporter:
324
+ """Send reports to Slack."""
325
+
326
+ def __init__(self, webhook_url: str):
327
+ """Initialize Slack reporter."""
328
+ self.webhook_url = webhook_url
329
+
330
+ def send_message(self, text: str) -> None:
331
+ """Send message to Slack."""
332
+ try:
333
+ import requests
334
+
335
+ payload = {"text": text}
336
+ response = requests.post(self.webhook_url, json=payload)
337
+
338
+ if response.status_code == 200:
339
+ logger.info("Slack message sent")
340
+ else:
341
+ logger.error(f"Slack message failed: {response.status_code}")
342
+
343
+ except Exception as e:
344
+ logger.error(f"Failed to send Slack message: {e}")
345
+
346
+ def send_experiment_report(self, experiment: Any) -> None:
347
+ """Send experiment report to Slack."""
348
+ text = f"*Experiment Complete:* {experiment.name}\n"
349
+ text += f"Status: {experiment.status}\n"
350
+ text += f"Duration: {experiment.get_duration():.2f}s\n"
351
+
352
+ if experiment.best_result:
353
+ text += "\n*Best Result:*\n"
354
+ for key, value in list(experiment.best_result.items())[:5]:
355
+ text += f"• {key}: {value}\n"
356
+
357
+ self.send_message(text)
@@ -0,0 +1,6 @@
1
+ """Utility functions and helpers."""
2
+
3
+ from morphml.utils.checkpoint import Checkpoint
4
+ from morphml.utils.export import ArchitectureExporter
5
+
6
+ __all__ = ["Checkpoint", "ArchitectureExporter"]