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,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)
|