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,407 @@
1
+ """Export architectures to framework-specific code.
2
+
3
+ Example:
4
+ >>> from morphml.utils import ArchitectureExporter
5
+ >>>
6
+ >>> exporter = ArchitectureExporter()
7
+ >>> pytorch_code = exporter.to_pytorch(graph)
8
+ >>> print(pytorch_code)
9
+ """
10
+
11
+ import numpy as np
12
+
13
+ from morphml.core.graph import ModelGraph
14
+ from morphml.logging_config import get_logger
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ class ArchitectureExporter:
20
+ """
21
+ Export neural architectures to executable code.
22
+
23
+ Generates framework-specific code (PyTorch, Keras) from
24
+ ModelGraph representations. Supports custom layer handlers.
25
+
26
+ Example:
27
+ >>> exporter = ArchitectureExporter()
28
+ >>>
29
+ >>> # Add custom layer handler
30
+ >>> def custom_handler(node, shapes):
31
+ ... return f"nn.CustomLayer({node.params})"
32
+ >>> exporter.add_custom_layer_handler("custom_op", custom_handler)
33
+ >>>
34
+ >>> # PyTorch
35
+ >>> pytorch_code = exporter.to_pytorch(graph)
36
+ >>> with open('model.py', 'w') as f:
37
+ ... f.write(pytorch_code)
38
+ >>>
39
+ >>> # Keras
40
+ >>> keras_code = exporter.to_keras(graph)
41
+ """
42
+
43
+ def __init__(self):
44
+ """Initialize exporter with custom layer handlers."""
45
+ self.custom_pytorch_handlers = {}
46
+ self.custom_keras_handlers = {}
47
+ logger.debug("Initialized ArchitectureExporter")
48
+
49
+ def add_custom_layer_handler(
50
+ self,
51
+ operation_name: str,
52
+ pytorch_handler=None,
53
+ keras_handler=None,
54
+ ):
55
+ """
56
+ Add custom handler for a layer type.
57
+
58
+ Args:
59
+ operation_name: Name of the operation (e.g., "custom_conv")
60
+ pytorch_handler: Function(node, shapes) -> str for PyTorch code
61
+ keras_handler: Function(node, shapes) -> str for Keras code
62
+
63
+ Example:
64
+ >>> def my_pytorch_handler(node, shapes):
65
+ ... return f"nn.MyCustomLayer({node.params['size']})"
66
+ >>> exporter.add_custom_layer_handler("my_op", pytorch_handler=my_pytorch_handler)
67
+ """
68
+ if pytorch_handler:
69
+ self.custom_pytorch_handlers[operation_name] = pytorch_handler
70
+ logger.info(f"Added custom PyTorch handler for '{operation_name}'")
71
+
72
+ if keras_handler:
73
+ self.custom_keras_handlers[operation_name] = keras_handler
74
+ logger.info(f"Added custom Keras handler for '{operation_name}'")
75
+
76
+ def remove_custom_layer_handler(self, operation_name: str):
77
+ """Remove custom handler for a layer type."""
78
+ self.custom_pytorch_handlers.pop(operation_name, None)
79
+ self.custom_keras_handlers.pop(operation_name, None)
80
+ logger.info(f"Removed custom handlers for '{operation_name}'")
81
+
82
+ def _infer_shape(self, node, shapes):
83
+ """Infer shape of node based on predecessors."""
84
+ if node.operation == "input":
85
+ return node.get_param("shape", (32, 32, 3))
86
+
87
+ pred_node = list(node.predecessors)[0]
88
+ pred_shape = shapes.get(pred_node.id)
89
+
90
+ if pred_shape is None:
91
+ return None
92
+
93
+ op = node.operation
94
+ params = node.params
95
+
96
+ if op == "conv2d":
97
+ filters = params.get("filters", 64)
98
+ kernel_size = params.get("kernel_size", 3)
99
+ padding = params.get("padding", "same")
100
+ kernel_size // 2 if padding == "same" else 0
101
+
102
+ if len(pred_shape) == 3:
103
+ return (filters, pred_shape[1], pred_shape[2])
104
+ elif len(pred_shape) == 1:
105
+ return (filters, pred_shape[0])
106
+ else:
107
+ return None
108
+
109
+ elif op == "dense":
110
+ units = params.get("units", 128)
111
+ return (units,)
112
+
113
+ elif op == "maxpool":
114
+ pool_size = params.get("pool_size", 2)
115
+ if len(pred_shape) == 3:
116
+ return (pred_shape[0], pred_shape[1] // pool_size, pred_shape[2] // pool_size)
117
+ elif len(pred_shape) == 1:
118
+ return (pred_shape[0],)
119
+ else:
120
+ return None
121
+
122
+ elif op == "avgpool":
123
+ pool_size = params.get("pool_size", 2)
124
+ if len(pred_shape) == 3:
125
+ return (pred_shape[0], pred_shape[1] // pool_size, pred_shape[2] // pool_size)
126
+ elif len(pred_shape) == 1:
127
+ return (pred_shape[0],)
128
+ else:
129
+ return None
130
+
131
+ elif op == "flatten":
132
+ return (int(np.prod(pred_shape)),)
133
+
134
+ elif op in ["relu", "sigmoid", "tanh", "softmax"]:
135
+ return pred_shape
136
+
137
+ elif op == "dropout":
138
+ return pred_shape
139
+
140
+ elif op == "batchnorm":
141
+ return pred_shape
142
+
143
+ else:
144
+ return None
145
+
146
+ def to_pytorch(self, graph: ModelGraph, class_name: str = "GeneratedModel") -> str:
147
+ """
148
+ Generate PyTorch code.
149
+
150
+ Args:
151
+ graph: Architecture to export
152
+ class_name: Name for generated class
153
+
154
+ Returns:
155
+ PyTorch code as string
156
+
157
+ Example:
158
+ >>> code = exporter.to_pytorch(graph, 'MyModel')
159
+ >>> exec(code) # Defines MyModel class
160
+ """
161
+ code = []
162
+ code.append("import torch")
163
+ code.append("import torch.nn as nn")
164
+ code.append("import torch.nn.functional as F")
165
+ code.append("")
166
+ code.append("")
167
+ code.append(f"class {class_name}(nn.Module):")
168
+ code.append(" def __init__(self):")
169
+ code.append(" super().__init__()")
170
+ code.append("")
171
+
172
+ # Get topological order
173
+ try:
174
+ sorted_nodes = graph.topological_sort()
175
+ except Exception as e:
176
+ logger.error(f"Failed to sort graph: {e}")
177
+ sorted_nodes = list(graph.nodes.values())
178
+
179
+ shapes = {}
180
+ for node in sorted_nodes:
181
+ shapes[node.id] = self._infer_shape(node, shapes)
182
+
183
+ # Generate layers
184
+ layer_names = {}
185
+ for i, node in enumerate(sorted_nodes):
186
+ layer_name = f"layer_{i}"
187
+ layer_names[node.id] = layer_name
188
+
189
+ op = node.operation
190
+ params = node.params
191
+
192
+ if op == "conv2d":
193
+ filters = params.get("filters", 64)
194
+ kernel_size = params.get("kernel_size", 3)
195
+ padding = params.get("padding", "same")
196
+ padding_val = kernel_size // 2 if padding == "same" else 0
197
+
198
+ # Try to infer input channels
199
+ in_channels = "?"
200
+ if node.predecessors:
201
+ pred_node = list(node.predecessors)[0]
202
+ pred_shape = shapes.get(pred_node.id)
203
+ if pred_shape and len(pred_shape) >= 3:
204
+ in_channels = pred_shape[0]
205
+
206
+ code.append(
207
+ f" self.{layer_name} = nn.Conv2d("
208
+ f"in_channels={in_channels}, out_channels={filters}, "
209
+ f"kernel_size={kernel_size}, padding={padding_val})"
210
+ )
211
+
212
+ elif op == "dense":
213
+ units = params.get("units", 128)
214
+
215
+ # Try to infer input features
216
+ in_features = "?"
217
+ if node.predecessors:
218
+ pred_node = list(node.predecessors)[0]
219
+ pred_shape = shapes.get(pred_node.id)
220
+ if pred_shape:
221
+ if len(pred_shape) == 1:
222
+ in_features = pred_shape[0]
223
+ else:
224
+ # Need flattening
225
+ in_features = int(np.prod(pred_shape))
226
+
227
+ code.append(
228
+ f" self.{layer_name} = nn.Linear(in_features={in_features}, out_features={units})"
229
+ )
230
+
231
+ elif op == "maxpool":
232
+ pool_size = params.get("pool_size", 2)
233
+ code.append(f" self.{layer_name} = nn.MaxPool2d(kernel_size={pool_size})")
234
+
235
+ elif op == "avgpool":
236
+ pool_size = params.get("pool_size", 2)
237
+ code.append(f" self.{layer_name} = nn.AvgPool2d(kernel_size={pool_size})")
238
+
239
+ elif op == "dropout":
240
+ rate = params.get("rate", 0.5)
241
+ code.append(f" self.{layer_name} = nn.Dropout(p={rate})")
242
+
243
+ elif op == "batchnorm":
244
+ # Try to infer number of features
245
+ num_features = "?"
246
+ if node.predecessors:
247
+ pred_node = list(node.predecessors)[0]
248
+ pred_shape = shapes.get(pred_node.id)
249
+ if pred_shape and len(pred_shape) >= 1:
250
+ num_features = pred_shape[0] # First dimension is channels
251
+
252
+ code.append(
253
+ f" self.{layer_name} = nn.BatchNorm2d(num_features={num_features})"
254
+ )
255
+
256
+ elif op == "flatten":
257
+ code.append(f" self.{layer_name} = nn.Flatten()")
258
+
259
+ elif op in ["relu", "sigmoid", "tanh", "softmax"]:
260
+ # Functional, no layer definition needed
261
+ pass
262
+
263
+ elif op == "input":
264
+ # No layer needed
265
+ pass
266
+
267
+ else:
268
+ # Check for custom handler
269
+ if op in self.custom_pytorch_handlers:
270
+ handler = self.custom_pytorch_handlers[op]
271
+ custom_code = handler(node, shapes)
272
+ code.append(f" self.{layer_name} = {custom_code}")
273
+ else:
274
+ code.append(f" # {layer_name}: {op} (custom - no handler defined)")
275
+
276
+ code.append("")
277
+ code.append(" def forward(self, x):")
278
+
279
+ # Generate forward pass
280
+ for i, node in enumerate(sorted_nodes):
281
+ layer_name = layer_names[node.id]
282
+ op = node.operation
283
+
284
+ if op == "input":
285
+ code.append(" # x = input")
286
+ elif op == "relu":
287
+ code.append(" x = F.relu(x)")
288
+ elif op == "sigmoid":
289
+ code.append(" x = torch.sigmoid(x)")
290
+ elif op == "tanh":
291
+ code.append(" x = torch.tanh(x)")
292
+ elif op == "softmax":
293
+ code.append(" x = F.softmax(x, dim=1)")
294
+ else:
295
+ code.append(f" x = self.{layer_name}(x)")
296
+
297
+ code.append(" return x")
298
+ code.append("")
299
+ code.append("")
300
+ code.append("# Usage:")
301
+ code.append(f"# model = {class_name}()")
302
+ code.append("# output = model(input_tensor)")
303
+
304
+ return "\n".join(code)
305
+
306
+ def to_keras(self, graph: ModelGraph, model_name: str = "generated_model") -> str:
307
+ """
308
+ Generate Keras/TensorFlow code.
309
+
310
+ Args:
311
+ graph: Architecture to export
312
+ model_name: Name for generated model
313
+
314
+ Returns:
315
+ Keras code as string
316
+ """
317
+ code = []
318
+ code.append("import tensorflow as tf")
319
+ code.append("from tensorflow import keras")
320
+ code.append("from tensorflow.keras import layers")
321
+ code.append("")
322
+ code.append("")
323
+ code.append(f"def {model_name}():")
324
+ code.append(' """Generated Keras model."""')
325
+
326
+ # Get topological order
327
+ try:
328
+ sorted_nodes = graph.topological_sort()
329
+ except Exception:
330
+ sorted_nodes = list(graph.nodes.values())
331
+
332
+ # Find input shape
333
+ input_shape = None
334
+ for node in sorted_nodes:
335
+ if node.operation == "input":
336
+ input_shape = node.get_param("shape", (32, 32, 3))
337
+ break
338
+
339
+ if input_shape is None:
340
+ input_shape = (32, 32, 3)
341
+
342
+ code.append(f" inputs = keras.Input(shape={input_shape})")
343
+ code.append(" x = inputs")
344
+ code.append("")
345
+
346
+ # Generate layers
347
+ for node in sorted_nodes:
348
+ op = node.operation
349
+ params = node.params
350
+
351
+ if op == "input":
352
+ continue
353
+ elif op == "conv2d":
354
+ filters = params.get("filters", 64)
355
+ kernel_size = params.get("kernel_size", 3)
356
+ padding = params.get("padding", "same")
357
+ code.append(
358
+ f" x = layers.Conv2D({filters}, {kernel_size}, " f"padding='{padding}')(x)"
359
+ )
360
+ elif op == "dense":
361
+ units = params.get("units", 128)
362
+ code.append(f" x = layers.Dense({units})(x)")
363
+ elif op == "maxpool":
364
+ pool_size = params.get("pool_size", 2)
365
+ code.append(f" x = layers.MaxPooling2D({pool_size})(x)")
366
+ elif op == "avgpool":
367
+ pool_size = params.get("pool_size", 2)
368
+ code.append(f" x = layers.AveragePooling2D({pool_size})(x)")
369
+ elif op == "dropout":
370
+ rate = params.get("rate", 0.5)
371
+ code.append(f" x = layers.Dropout({rate})(x)")
372
+ elif op == "batchnorm":
373
+ code.append(" x = layers.BatchNormalization()(x)")
374
+ elif op == "flatten":
375
+ code.append(" x = layers.Flatten()(x)")
376
+ elif op == "relu":
377
+ code.append(" x = layers.Activation('relu')(x)")
378
+ elif op == "sigmoid":
379
+ code.append(" x = layers.Activation('sigmoid')(x)")
380
+ elif op == "tanh":
381
+ code.append(" x = layers.Activation('tanh')(x)")
382
+ elif op == "softmax":
383
+ code.append(" x = layers.Activation('softmax')(x)")
384
+
385
+ code.append("")
386
+ code.append(" model = keras.Model(inputs=inputs, outputs=x)")
387
+ code.append(" return model")
388
+ code.append("")
389
+ code.append("")
390
+ code.append("# Usage:")
391
+ code.append(f"# model = {model_name}()")
392
+ code.append("# model.compile(optimizer='adam', loss='categorical_crossentropy')")
393
+ code.append("# model.fit(x_train, y_train, epochs=10)")
394
+
395
+ return "\n".join(code)
396
+
397
+ def to_json(self, graph: ModelGraph) -> str:
398
+ """
399
+ Export architecture as JSON.
400
+
401
+ Args:
402
+ graph: Architecture to export
403
+
404
+ Returns:
405
+ JSON string
406
+ """
407
+ return graph.to_json()