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