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,174 @@
|
|
|
1
|
+
"""Convergence visualization utilities.
|
|
2
|
+
|
|
3
|
+
Author: Eshan Roy <eshanized@proton.me>
|
|
4
|
+
Organization: TONMOY INFRASTRUCTURE & VISION
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from morphml.logging_config import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def plot_convergence(
|
|
15
|
+
history: List[Dict],
|
|
16
|
+
metric: str = "best_fitness",
|
|
17
|
+
save_path: Optional[str] = None,
|
|
18
|
+
title: str = "Convergence Plot",
|
|
19
|
+
) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Plot optimization convergence over generations/iterations.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
history: List of history dictionaries with metric values
|
|
25
|
+
metric: Metric to plot (default: 'best_fitness')
|
|
26
|
+
save_path: Path to save plot (displays if None)
|
|
27
|
+
title: Plot title
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
>>> from morphml.visualization.convergence_plot import plot_convergence
|
|
31
|
+
>>> plot_convergence(optimizer.get_history())
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
import matplotlib.pyplot as plt
|
|
35
|
+
except ImportError:
|
|
36
|
+
logger.error("matplotlib required for plotting. Install with: pip install matplotlib")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# Extract metric values
|
|
40
|
+
generations = []
|
|
41
|
+
values = []
|
|
42
|
+
|
|
43
|
+
for i, entry in enumerate(history):
|
|
44
|
+
if metric in entry:
|
|
45
|
+
generations.append(i)
|
|
46
|
+
values.append(entry[metric])
|
|
47
|
+
|
|
48
|
+
if not values:
|
|
49
|
+
logger.error(f"Metric '{metric}' not found in history")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# Create plot
|
|
53
|
+
plt.figure(figsize=(10, 6))
|
|
54
|
+
plt.plot(generations, values, linewidth=2, marker="o", markersize=4)
|
|
55
|
+
plt.xlabel("Generation/Iteration", fontsize=12)
|
|
56
|
+
plt.ylabel(metric.replace("_", " ").title(), fontsize=12)
|
|
57
|
+
plt.title(title, fontsize=14, fontweight="bold")
|
|
58
|
+
plt.grid(True, alpha=0.3)
|
|
59
|
+
|
|
60
|
+
if save_path:
|
|
61
|
+
plt.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
62
|
+
logger.info(f"Convergence plot saved to {save_path}")
|
|
63
|
+
else:
|
|
64
|
+
plt.show()
|
|
65
|
+
|
|
66
|
+
plt.close()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def plot_convergence_comparison(
|
|
70
|
+
histories: Dict[str, List[Dict]],
|
|
71
|
+
metric: str = "best_fitness",
|
|
72
|
+
save_path: Optional[str] = None,
|
|
73
|
+
title: str = "Convergence Comparison",
|
|
74
|
+
) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Compare convergence of multiple optimizers.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
histories: Dict mapping optimizer names to history lists
|
|
80
|
+
metric: Metric to plot
|
|
81
|
+
save_path: Path to save plot (displays if None)
|
|
82
|
+
title: Plot title
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
>>> from morphml.visualization.convergence_plot import plot_convergence_comparison
|
|
86
|
+
>>> histories = {
|
|
87
|
+
... 'GA': ga_optimizer.get_history(),
|
|
88
|
+
... 'TPE': tpe_optimizer.get_history()
|
|
89
|
+
... }
|
|
90
|
+
>>> plot_convergence_comparison(histories)
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
import matplotlib.pyplot as plt
|
|
94
|
+
except ImportError:
|
|
95
|
+
logger.error("matplotlib required for plotting")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
plt.figure(figsize=(12, 6))
|
|
99
|
+
|
|
100
|
+
for optimizer_name, history in histories.items():
|
|
101
|
+
generations = []
|
|
102
|
+
values = []
|
|
103
|
+
|
|
104
|
+
for i, entry in enumerate(history):
|
|
105
|
+
if metric in entry:
|
|
106
|
+
generations.append(i)
|
|
107
|
+
values.append(entry[metric])
|
|
108
|
+
|
|
109
|
+
if values:
|
|
110
|
+
plt.plot(
|
|
111
|
+
generations,
|
|
112
|
+
values,
|
|
113
|
+
linewidth=2,
|
|
114
|
+
marker="o",
|
|
115
|
+
markersize=4,
|
|
116
|
+
label=optimizer_name,
|
|
117
|
+
alpha=0.8,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
plt.xlabel("Generation/Iteration", fontsize=12)
|
|
121
|
+
plt.ylabel(metric.replace("_", " ").title(), fontsize=12)
|
|
122
|
+
plt.title(title, fontsize=14, fontweight="bold")
|
|
123
|
+
plt.legend(fontsize=10)
|
|
124
|
+
plt.grid(True, alpha=0.3)
|
|
125
|
+
|
|
126
|
+
if save_path:
|
|
127
|
+
plt.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
128
|
+
logger.info(f"Convergence comparison plot saved to {save_path}")
|
|
129
|
+
else:
|
|
130
|
+
plt.show()
|
|
131
|
+
|
|
132
|
+
plt.close()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def plot_fitness_distribution(
|
|
136
|
+
population_history: List[List[float]],
|
|
137
|
+
save_path: Optional[str] = None,
|
|
138
|
+
title: str = "Fitness Distribution Over Time",
|
|
139
|
+
) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Plot fitness distribution evolution using box plots.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
population_history: List of fitness lists per generation
|
|
145
|
+
save_path: Path to save plot
|
|
146
|
+
title: Plot title
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
import matplotlib.pyplot as plt
|
|
150
|
+
except ImportError:
|
|
151
|
+
logger.error("matplotlib required for plotting")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
plt.figure(figsize=(12, 6))
|
|
155
|
+
plt.boxplot(population_history, positions=range(len(population_history)))
|
|
156
|
+
plt.xlabel("Generation", fontsize=12)
|
|
157
|
+
plt.ylabel("Fitness", fontsize=12)
|
|
158
|
+
plt.title(title, fontsize=14, fontweight="bold")
|
|
159
|
+
plt.grid(True, alpha=0.3)
|
|
160
|
+
|
|
161
|
+
if save_path:
|
|
162
|
+
plt.savefig(save_path, dpi=300, bbox_inches="tight")
|
|
163
|
+
logger.info(f"Fitness distribution plot saved to {save_path}")
|
|
164
|
+
else:
|
|
165
|
+
plt.show()
|
|
166
|
+
|
|
167
|
+
plt.close()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# Re-export from benchmarks module for convenience
|
|
171
|
+
try:
|
|
172
|
+
pass
|
|
173
|
+
except ImportError:
|
|
174
|
+
pass
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""Visualization utilities for crossover operations.
|
|
2
|
+
|
|
3
|
+
This module provides tools to visualize genetic crossover operations,
|
|
4
|
+
showing how parent architectures are combined to create offspring.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from morphml.visualization.crossover_viz import visualize_crossover
|
|
8
|
+
>>> from morphml.core.graph.mutations import crossover
|
|
9
|
+
>>>
|
|
10
|
+
>>> offspring1, offspring2 = crossover(parent1, parent2)
|
|
11
|
+
>>> visualize_crossover(parent1, parent2, offspring1, offspring2,
|
|
12
|
+
... output_file="crossover.png")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Optional, Tuple
|
|
16
|
+
|
|
17
|
+
import matplotlib.pyplot as plt
|
|
18
|
+
import networkx as nx
|
|
19
|
+
|
|
20
|
+
from morphml.core.graph import ModelGraph
|
|
21
|
+
from morphml.logging_config import get_logger
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def visualize_crossover(
|
|
27
|
+
parent1: ModelGraph,
|
|
28
|
+
parent2: ModelGraph,
|
|
29
|
+
offspring1: ModelGraph,
|
|
30
|
+
offspring2: ModelGraph,
|
|
31
|
+
output_file: Optional[str] = None,
|
|
32
|
+
figsize: Tuple[int, int] = (16, 12),
|
|
33
|
+
show_labels: bool = True,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Visualize crossover operation showing parents and offspring.
|
|
37
|
+
|
|
38
|
+
Creates a 2x2 grid showing:
|
|
39
|
+
- Top left: Parent 1
|
|
40
|
+
- Top right: Parent 2
|
|
41
|
+
- Bottom left: Offspring 1
|
|
42
|
+
- Bottom right: Offspring 2
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
parent1: First parent graph
|
|
46
|
+
parent2: Second parent graph
|
|
47
|
+
offspring1: First offspring graph
|
|
48
|
+
offspring2: Second offspring graph
|
|
49
|
+
output_file: Optional path to save figure
|
|
50
|
+
figsize: Figure size (width, height)
|
|
51
|
+
show_labels: Whether to show node labels
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> visualize_crossover(p1, p2, o1, o2, "crossover_result.png")
|
|
55
|
+
"""
|
|
56
|
+
fig, axes = plt.subplots(2, 2, figsize=figsize)
|
|
57
|
+
fig.suptitle("Crossover Operation", fontsize=16, fontweight="bold")
|
|
58
|
+
|
|
59
|
+
graphs = [
|
|
60
|
+
(parent1, "Parent 1", "lightblue"),
|
|
61
|
+
(parent2, "Parent 2", "lightgreen"),
|
|
62
|
+
(offspring1, "Offspring 1", "lightyellow"),
|
|
63
|
+
(offspring2, "Offspring 2", "lightcoral"),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
for idx, (graph, title, color) in enumerate(graphs):
|
|
67
|
+
ax = axes[idx // 2, idx % 2]
|
|
68
|
+
_plot_graph(graph, ax, title, color, show_labels)
|
|
69
|
+
|
|
70
|
+
plt.tight_layout()
|
|
71
|
+
|
|
72
|
+
if output_file:
|
|
73
|
+
plt.savefig(output_file, dpi=300, bbox_inches="tight")
|
|
74
|
+
logger.info(f"Saved crossover visualization to {output_file}")
|
|
75
|
+
else:
|
|
76
|
+
plt.show()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def visualize_crossover_comparison(
|
|
80
|
+
parent1: ModelGraph,
|
|
81
|
+
parent2: ModelGraph,
|
|
82
|
+
offspring: ModelGraph,
|
|
83
|
+
crossover_point: Optional[int] = None,
|
|
84
|
+
output_file: Optional[str] = None,
|
|
85
|
+
figsize: Tuple[int, int] = (15, 5),
|
|
86
|
+
) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Visualize single offspring with parent comparison.
|
|
89
|
+
|
|
90
|
+
Shows parents side-by-side with offspring, highlighting the
|
|
91
|
+
crossover point if provided.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
parent1: First parent graph
|
|
95
|
+
parent2: Second parent graph
|
|
96
|
+
offspring: Offspring graph
|
|
97
|
+
crossover_point: Optional crossover point to highlight
|
|
98
|
+
output_file: Optional path to save figure
|
|
99
|
+
figsize: Figure size (width, height)
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
>>> visualize_crossover_comparison(p1, p2, child, crossover_point=3)
|
|
103
|
+
"""
|
|
104
|
+
fig, axes = plt.subplots(1, 3, figsize=figsize)
|
|
105
|
+
fig.suptitle("Crossover Comparison", fontsize=14, fontweight="bold")
|
|
106
|
+
|
|
107
|
+
_plot_graph(parent1, axes[0], "Parent 1", "lightblue", True)
|
|
108
|
+
_plot_graph(parent2, axes[1], "Parent 2", "lightgreen", True)
|
|
109
|
+
_plot_graph(offspring, axes[2], "Offspring", "lightyellow", True)
|
|
110
|
+
|
|
111
|
+
# Add statistics
|
|
112
|
+
stats_text = (
|
|
113
|
+
f"Parent 1: {len(parent1.nodes)} nodes\n"
|
|
114
|
+
f"Parent 2: {len(parent2.nodes)} nodes\n"
|
|
115
|
+
f"Offspring: {len(offspring.nodes)} nodes"
|
|
116
|
+
)
|
|
117
|
+
fig.text(
|
|
118
|
+
0.5,
|
|
119
|
+
0.02,
|
|
120
|
+
stats_text,
|
|
121
|
+
ha="center",
|
|
122
|
+
fontsize=10,
|
|
123
|
+
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
plt.tight_layout()
|
|
127
|
+
|
|
128
|
+
if output_file:
|
|
129
|
+
plt.savefig(output_file, dpi=300, bbox_inches="tight")
|
|
130
|
+
logger.info(f"Saved comparison visualization to {output_file}")
|
|
131
|
+
else:
|
|
132
|
+
plt.show()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def visualize_crossover_animation(
|
|
136
|
+
parent1: ModelGraph,
|
|
137
|
+
parent2: ModelGraph,
|
|
138
|
+
offspring_sequence: list,
|
|
139
|
+
output_file: str = "crossover_animation.gif",
|
|
140
|
+
fps: int = 2,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Create animated visualization of crossover process.
|
|
144
|
+
|
|
145
|
+
Shows the step-by-step process of creating offspring from parents.
|
|
146
|
+
Requires imageio for GIF creation.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
parent1: First parent graph
|
|
150
|
+
parent2: Second parent graph
|
|
151
|
+
offspring_sequence: List of intermediate offspring graphs
|
|
152
|
+
output_file: Path to save GIF
|
|
153
|
+
fps: Frames per second
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
>>> # Create sequence of intermediate offspring
|
|
157
|
+
>>> sequence = [step1, step2, step3, final]
|
|
158
|
+
>>> visualize_crossover_animation(p1, p2, sequence)
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
import imageio
|
|
162
|
+
except ImportError:
|
|
163
|
+
logger.error("imageio required for animation. Install with: pip install imageio")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
import os
|
|
167
|
+
import tempfile
|
|
168
|
+
|
|
169
|
+
frames = []
|
|
170
|
+
temp_dir = tempfile.mkdtemp()
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
# Create frame for each step
|
|
174
|
+
for i, offspring in enumerate(offspring_sequence):
|
|
175
|
+
temp_file = os.path.join(temp_dir, f"frame_{i:03d}.png")
|
|
176
|
+
|
|
177
|
+
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
|
178
|
+
fig.suptitle(
|
|
179
|
+
f"Crossover Step {i+1}/{len(offspring_sequence)}", fontsize=14, fontweight="bold"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
_plot_graph(parent1, axes[0], "Parent 1", "lightblue", True)
|
|
183
|
+
_plot_graph(parent2, axes[1], "Parent 2", "lightgreen", True)
|
|
184
|
+
_plot_graph(offspring, axes[2], f"Offspring (Step {i+1})", "lightyellow", True)
|
|
185
|
+
|
|
186
|
+
plt.tight_layout()
|
|
187
|
+
plt.savefig(temp_file, dpi=150, bbox_inches="tight")
|
|
188
|
+
plt.close()
|
|
189
|
+
|
|
190
|
+
frames.append(imageio.imread(temp_file))
|
|
191
|
+
|
|
192
|
+
# Save as GIF
|
|
193
|
+
imageio.mimsave(output_file, frames, fps=fps)
|
|
194
|
+
logger.info(f"Saved crossover animation to {output_file}")
|
|
195
|
+
|
|
196
|
+
finally:
|
|
197
|
+
# Cleanup temp files
|
|
198
|
+
import shutil
|
|
199
|
+
|
|
200
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _plot_graph(
|
|
204
|
+
graph: ModelGraph,
|
|
205
|
+
ax: plt.Axes,
|
|
206
|
+
title: str,
|
|
207
|
+
node_color: str,
|
|
208
|
+
show_labels: bool,
|
|
209
|
+
) -> None:
|
|
210
|
+
"""
|
|
211
|
+
Plot a single graph on given axes.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
graph: Graph to plot
|
|
215
|
+
ax: Matplotlib axes
|
|
216
|
+
title: Plot title
|
|
217
|
+
node_color: Color for nodes
|
|
218
|
+
show_labels: Whether to show node labels
|
|
219
|
+
"""
|
|
220
|
+
# Convert to NetworkX for visualization
|
|
221
|
+
try:
|
|
222
|
+
nx_graph = graph.to_networkx()
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.warning(f"Failed to convert graph: {e}")
|
|
225
|
+
ax.text(
|
|
226
|
+
0.5, 0.5, "Graph visualization failed", ha="center", va="center", transform=ax.transAxes
|
|
227
|
+
)
|
|
228
|
+
ax.set_title(title)
|
|
229
|
+
ax.axis("off")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
# Use hierarchical layout for better visualization
|
|
233
|
+
try:
|
|
234
|
+
pos = nx.spring_layout(nx_graph, k=1, iterations=50)
|
|
235
|
+
except:
|
|
236
|
+
pos = nx.random_layout(nx_graph)
|
|
237
|
+
|
|
238
|
+
# Draw nodes
|
|
239
|
+
nx.draw_networkx_nodes(
|
|
240
|
+
nx_graph,
|
|
241
|
+
pos,
|
|
242
|
+
ax=ax,
|
|
243
|
+
node_color=node_color,
|
|
244
|
+
node_size=500,
|
|
245
|
+
alpha=0.9,
|
|
246
|
+
edgecolors="black",
|
|
247
|
+
linewidths=2,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Draw edges
|
|
251
|
+
nx.draw_networkx_edges(
|
|
252
|
+
nx_graph,
|
|
253
|
+
pos,
|
|
254
|
+
ax=ax,
|
|
255
|
+
edge_color="gray",
|
|
256
|
+
arrows=True,
|
|
257
|
+
arrowsize=20,
|
|
258
|
+
arrowstyle="->",
|
|
259
|
+
width=2,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Draw labels
|
|
263
|
+
if show_labels:
|
|
264
|
+
labels = {}
|
|
265
|
+
for node_id in nx_graph.nodes():
|
|
266
|
+
node = graph.nodes.get(node_id)
|
|
267
|
+
if node:
|
|
268
|
+
# Shorten operation name for display
|
|
269
|
+
op = node.operation
|
|
270
|
+
if len(op) > 8:
|
|
271
|
+
op = op[:8] + "..."
|
|
272
|
+
labels[node_id] = op
|
|
273
|
+
else:
|
|
274
|
+
labels[node_id] = node_id[:8]
|
|
275
|
+
|
|
276
|
+
nx.draw_networkx_labels(
|
|
277
|
+
nx_graph,
|
|
278
|
+
pos,
|
|
279
|
+
labels,
|
|
280
|
+
ax=ax,
|
|
281
|
+
font_size=8,
|
|
282
|
+
font_weight="bold",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
ax.set_title(title, fontsize=12, fontweight="bold")
|
|
286
|
+
ax.axis("off")
|
|
287
|
+
|
|
288
|
+
# Add statistics
|
|
289
|
+
stats = f"Nodes: {len(graph.nodes)} | Edges: {len(graph.edges)}"
|
|
290
|
+
ax.text(0.5, -0.05, stats, ha="center", transform=ax.transAxes, fontsize=9)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def compare_crossover_diversity(
|
|
294
|
+
parents: list,
|
|
295
|
+
offspring_list: list,
|
|
296
|
+
output_file: Optional[str] = None,
|
|
297
|
+
figsize: Tuple[int, int] = (12, 6),
|
|
298
|
+
) -> None:
|
|
299
|
+
"""
|
|
300
|
+
Compare diversity of offspring from multiple crossover operations.
|
|
301
|
+
|
|
302
|
+
Creates visualization showing distribution of offspring characteristics
|
|
303
|
+
compared to parents.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
parents: List of parent graphs
|
|
307
|
+
offspring_list: List of offspring graphs
|
|
308
|
+
output_file: Optional path to save figure
|
|
309
|
+
figsize: Figure size (width, height)
|
|
310
|
+
|
|
311
|
+
Example:
|
|
312
|
+
>>> parents = [p1, p2, p3, p4]
|
|
313
|
+
>>> offspring = [o1, o2, o3, o4, o5, o6]
|
|
314
|
+
>>> compare_crossover_diversity(parents, offspring)
|
|
315
|
+
"""
|
|
316
|
+
fig, axes = plt.subplots(1, 3, figsize=figsize)
|
|
317
|
+
fig.suptitle("Crossover Diversity Analysis", fontsize=14, fontweight="bold")
|
|
318
|
+
|
|
319
|
+
# Extract metrics
|
|
320
|
+
parent_nodes = [len(g.nodes) for g in parents]
|
|
321
|
+
parent_edges = [len(g.edges) for g in parents]
|
|
322
|
+
parent_depth = [g.depth() for g in parents]
|
|
323
|
+
|
|
324
|
+
offspring_nodes = [len(g.nodes) for g in offspring_list]
|
|
325
|
+
offspring_edges = [len(g.edges) for g in offspring_list]
|
|
326
|
+
offspring_depth = [g.depth() for g in offspring_list]
|
|
327
|
+
|
|
328
|
+
# Plot node count distribution
|
|
329
|
+
axes[0].hist(parent_nodes, alpha=0.5, label="Parents", bins=10, color="blue")
|
|
330
|
+
axes[0].hist(offspring_nodes, alpha=0.5, label="Offspring", bins=10, color="orange")
|
|
331
|
+
axes[0].set_xlabel("Number of Nodes")
|
|
332
|
+
axes[0].set_ylabel("Frequency")
|
|
333
|
+
axes[0].set_title("Node Count Distribution")
|
|
334
|
+
axes[0].legend()
|
|
335
|
+
axes[0].grid(True, alpha=0.3)
|
|
336
|
+
|
|
337
|
+
# Plot edge count distribution
|
|
338
|
+
axes[1].hist(parent_edges, alpha=0.5, label="Parents", bins=10, color="blue")
|
|
339
|
+
axes[1].hist(offspring_edges, alpha=0.5, label="Offspring", bins=10, color="orange")
|
|
340
|
+
axes[1].set_xlabel("Number of Edges")
|
|
341
|
+
axes[1].set_ylabel("Frequency")
|
|
342
|
+
axes[1].set_title("Edge Count Distribution")
|
|
343
|
+
axes[1].legend()
|
|
344
|
+
axes[1].grid(True, alpha=0.3)
|
|
345
|
+
|
|
346
|
+
# Plot depth distribution
|
|
347
|
+
axes[2].hist(parent_depth, alpha=0.5, label="Parents", bins=10, color="blue")
|
|
348
|
+
axes[2].hist(offspring_depth, alpha=0.5, label="Offspring", bins=10, color="orange")
|
|
349
|
+
axes[2].set_xlabel("Depth")
|
|
350
|
+
axes[2].set_ylabel("Frequency")
|
|
351
|
+
axes[2].set_title("Depth Distribution")
|
|
352
|
+
axes[2].legend()
|
|
353
|
+
axes[2].grid(True, alpha=0.3)
|
|
354
|
+
|
|
355
|
+
plt.tight_layout()
|
|
356
|
+
|
|
357
|
+
if output_file:
|
|
358
|
+
plt.savefig(output_file, dpi=300, bbox_inches="tight")
|
|
359
|
+
logger.info(f"Saved diversity analysis to {output_file}")
|
|
360
|
+
else:
|
|
361
|
+
plt.show()
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# Convenience function
|
|
365
|
+
def quick_crossover_viz(
|
|
366
|
+
parent1: ModelGraph, parent2: ModelGraph, output_file: str = "crossover.png"
|
|
367
|
+
) -> None:
|
|
368
|
+
"""
|
|
369
|
+
Quick visualization of crossover operation.
|
|
370
|
+
|
|
371
|
+
Performs crossover and visualizes the result in one call.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
parent1: First parent graph
|
|
375
|
+
parent2: Second parent graph
|
|
376
|
+
output_file: Path to save visualization
|
|
377
|
+
|
|
378
|
+
Example:
|
|
379
|
+
>>> quick_crossover_viz(parent1, parent2, "my_crossover.png")
|
|
380
|
+
"""
|
|
381
|
+
from morphml.core.graph.mutations import crossover
|
|
382
|
+
|
|
383
|
+
offspring1, offspring2 = crossover(parent1, parent2)
|
|
384
|
+
visualize_crossover(parent1, parent2, offspring1, offspring2, output_file)
|
|
385
|
+
|
|
386
|
+
logger.info(f"Crossover visualization saved to {output_file}")
|