mnt.designer 0.2.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.
mnt/designer/app.py ADDED
@@ -0,0 +1,1701 @@
1
+ import argparse
2
+ import io
3
+ import logging
4
+ import os
5
+ import re
6
+ import secrets
7
+ import struct
8
+ import tempfile
9
+ import uuid
10
+ import webbrowser
11
+ from collections.abc import Sequence
12
+ from contextlib import redirect_stdout
13
+ from importlib.metadata import version
14
+ from pathlib import Path
15
+
16
+ from flask import Flask, cli, jsonify, render_template, request, send_file, session
17
+ from werkzeug.exceptions import RequestEntityTooLarge
18
+
19
+ from mnt.pyfiction import (
20
+ a_star,
21
+ apply_bestagon_library,
22
+ apply_qca_one_library,
23
+ cartesian_gate_layout,
24
+ cartesian_obstruction_layout,
25
+ color_mode,
26
+ eq_type,
27
+ equivalence_checking,
28
+ equivalence_checking_stats,
29
+ gate_level_drvs,
30
+ gold_cost_objective,
31
+ gold_effort_mode,
32
+ graph_oriented_layout_design,
33
+ graph_oriented_layout_design_params,
34
+ hexagonalization,
35
+ orthogonal,
36
+ post_layout_optimization,
37
+ post_layout_optimization_params,
38
+ read_cartesian_fgl_layout,
39
+ read_technology_network,
40
+ route_path,
41
+ write_dot_layout,
42
+ write_fgl_layout,
43
+ write_qca_layout_svg,
44
+ write_qca_layout_svg_params,
45
+ write_sidb_layout_svg,
46
+ write_sidb_layout_svg_params,
47
+ )
48
+
49
+ try:
50
+ from mnt.pyfiction import exact_cartesian, exact_params
51
+ except (ImportError, AttributeError):
52
+ exact_params = None
53
+ exact_cartesian = None
54
+
55
+
56
+ app = Flask(__name__)
57
+ app.secret_key = os.environ.get("MNT_DESIGNER_SECRET_KEY") or secrets.token_hex(32)
58
+ app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
59
+ app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024 # Entire request, including multipart overhead.
60
+
61
+ # Layouts and networks are process-local; use a single worker for hosted instances.
62
+ layouts = {}
63
+
64
+ # In-memory storage for user networks
65
+ networks = {}
66
+
67
+ # In-memory storage for user verilog
68
+ verilogs = {}
69
+
70
+
71
+ @app.errorhandler(RequestEntityTooLarge)
72
+ def request_too_large(_error):
73
+ return jsonify({"success": False, "error": "Request exceeds the upload size limit."}), 413
74
+
75
+
76
+ def _internal_error_response():
77
+ app.logger.exception("Unhandled request error")
78
+ return jsonify({"success": False, "error": "An internal error occurred."})
79
+
80
+
81
+ @app.route("/")
82
+ def index():
83
+ # Assign a unique session ID if not already present
84
+ if "session_id" not in session:
85
+ session["session_id"] = str(uuid.uuid4())
86
+ return render_template("index.html")
87
+
88
+
89
+ @app.route("/create_layout", methods=["POST"])
90
+ def create_layout():
91
+ try:
92
+ data = request.json
93
+ x = int(data.get("x")) - 1
94
+ y = int(data.get("y")) - 1
95
+ z = 1 # Default Z value
96
+ if x < 0 or y < 0:
97
+ return jsonify({"success": False, "error": "Layout dimensions must be positive."}), 400
98
+
99
+ session_id = session["session_id"]
100
+ layout = layouts.get(session_id)
101
+
102
+ if layout:
103
+ _, maximum = layout.bounding_box_2d()
104
+ if maximum.x > x or maximum.y > y:
105
+ return jsonify({"success": False, "error": "Layout dimensions would hide existing gates."}), 400
106
+
107
+ if not layout:
108
+ # Create a new layout if one doesn't exist
109
+ layout = cartesian_obstruction_layout(cartesian_gate_layout((0, 0, 0), "2DDWave", "Layout"))
110
+ layouts[session_id] = layout
111
+
112
+ # Resize the existing layout
113
+ layout.resize((x, y, z))
114
+ return jsonify({"success": True})
115
+ except RequestEntityTooLarge:
116
+ raise
117
+ except Exception:
118
+ return _internal_error_response()
119
+
120
+
121
+ @app.route("/reset_layout", methods=["POST"])
122
+ def reset_layout():
123
+ try:
124
+ data = request.json
125
+ x = int(data.get("x")) - 1
126
+ y = int(data.get("y")) - 1
127
+ z = 1 # Default Z value
128
+ if x < 0 or y < 0:
129
+ return jsonify({"success": False, "error": "Layout dimensions must be positive."}), 400
130
+ layout = cartesian_obstruction_layout(cartesian_gate_layout((x, y, z), "2DDWave", "Layout"))
131
+
132
+ session_id = session["session_id"]
133
+ layouts[session_id] = layout
134
+
135
+ return jsonify({"success": True})
136
+ except RequestEntityTooLarge:
137
+ raise
138
+ except Exception:
139
+ return _internal_error_response()
140
+
141
+
142
+ @app.route("/reset_editor", methods=["POST"])
143
+ def reset_editor():
144
+ try:
145
+ session_id = session.get("session_id")
146
+ if not session_id:
147
+ return jsonify({"success": False, "error": "Session ID not found."}), 400
148
+
149
+ # Define the default Verilog code
150
+ default_verilog_code = ""
151
+
152
+ # Reset the editor's code to the default
153
+ verilogs[session_id] = default_verilog_code
154
+ networks[session_id] = None
155
+
156
+ return jsonify({"success": True, "code": default_verilog_code}), 200
157
+
158
+ except Exception:
159
+ return _internal_error_response(), 500
160
+
161
+
162
+ def _gate_layer(layout, x, y):
163
+ # The editor projects both native layers onto one tile; an isolated upper wire is still editable.
164
+ return 1 if layout.z() > 0 and layout.is_empty_tile((x, y, 0)) else 0
165
+
166
+
167
+ @app.route("/place_gate", methods=["POST"])
168
+ def place_gate():
169
+ try:
170
+ data = request.json
171
+ x = int(data["x"])
172
+ y = int(data["y"])
173
+ gate_type = data["gate_type"]
174
+ params = data["params"]
175
+ update_first = False
176
+ update_second = False
177
+
178
+ session_id = session["session_id"]
179
+ layout = layouts.get(session_id)
180
+ if not layout:
181
+ return jsonify({"success": False, "error": "Layout not found."})
182
+
183
+ node = layout.get_node((x, y, _gate_layer(layout, x, y)))
184
+ if node != 0:
185
+ return jsonify({"success": False, "error": "Tile already has a gate."})
186
+
187
+ if gate_type in ["bufc", "bufk"] and layout.z() == 0:
188
+ layout.resize((layout.x(), layout.y(), 1))
189
+
190
+ # Enforce incoming signal constraints
191
+ if gate_type == "pi":
192
+ if params:
193
+ return jsonify({"success": False, "error": "PI gate cannot have inputs."})
194
+ layout.create_pi("", (x, y))
195
+ elif gate_type in ["buf", "inv", "po"]:
196
+ if "first" not in params or "second" in params:
197
+ return jsonify(
198
+ {
199
+ "success": False,
200
+ "error": f"{gate_type.upper()} gate requires exactly one input.",
201
+ }
202
+ )
203
+ source_x = int(params["first"]["position"]["x"])
204
+ source_y = int(params["first"]["position"]["y"])
205
+ source_z = _gate_layer(layout, source_x, source_y)
206
+ source_gate_type = params["first"]["gate_type"]
207
+
208
+ if source_gate_type == "bufc":
209
+ if source_x < x:
210
+ if layout.has_southern_outgoing_signal((source_x, source_y, 0)):
211
+ source_z = 1
212
+ elif layout.has_southern_outgoing_signal((source_x, source_y, 1)):
213
+ source_z = 0
214
+ else:
215
+ if layout.has_northern_incoming_signal((source_x, source_y, 0)):
216
+ source_z = 1
217
+ elif layout.has_northern_incoming_signal((source_x, source_y, 1)):
218
+ source_z = 0
219
+ else:
220
+ return jsonify({"success": False, "error": "Something went wrong."})
221
+ elif source_y < y:
222
+ if layout.has_eastern_outgoing_signal((source_x, source_y, 0)):
223
+ source_z = 1
224
+ elif layout.has_eastern_outgoing_signal((source_x, source_y, 1)):
225
+ source_z = 0
226
+ else:
227
+ if layout.has_northern_incoming_signal((source_x, source_y, 0)):
228
+ source_z = 0
229
+ elif layout.has_northern_incoming_signal((source_x, source_y, 1)):
230
+ source_z = 1
231
+ else:
232
+ return jsonify({"success": False, "error": "Something went wrong."})
233
+ else:
234
+ return jsonify({"success": False, "error": "Something went wrong."})
235
+
236
+ if source_gate_type == "bufk":
237
+ if source_x < x:
238
+ if layout.has_southern_outgoing_signal((source_x, source_y, 0)):
239
+ source_z = 1
240
+ elif layout.has_southern_outgoing_signal((source_x, source_y, 1)):
241
+ source_z = 0
242
+ else:
243
+ if layout.has_northern_incoming_signal((source_x, source_y, 0)):
244
+ source_z = 0
245
+ elif layout.has_northern_incoming_signal((source_x, source_y, 1)):
246
+ source_z = 1
247
+ else:
248
+ return jsonify({"success": False, "error": "Something went wrong."})
249
+ elif source_y < y:
250
+ if layout.has_eastern_outgoing_signal((source_x, source_y, 0)):
251
+ source_z = 1
252
+ elif layout.has_eastern_outgoing_signal((source_x, source_y, 1)):
253
+ source_z = 0
254
+ else:
255
+ if layout.has_northern_incoming_signal((source_x, source_y, 0)):
256
+ source_z = 1
257
+ elif layout.has_northern_incoming_signal((source_x, source_y, 1)):
258
+ source_z = 0
259
+ else:
260
+ return jsonify({"success": False, "error": "Something went wrong."})
261
+ else:
262
+ return jsonify({"success": False, "error": "Something went wrong."})
263
+
264
+ source_node = layout.get_node((source_x, source_y, source_z))
265
+ if not source_node:
266
+ return jsonify({"success": False, "error": "Source gate not found."})
267
+
268
+ # Check if the gate already has inputs
269
+ existing_fanins = layout.fanins((x, y))
270
+ if existing_fanins:
271
+ return jsonify(
272
+ {
273
+ "success": False,
274
+ "error": f"Gate at ({x}, {y}) cannot have more than 1 input.",
275
+ }
276
+ )
277
+
278
+ # Determine allowed number of fanouts
279
+ existing_fanouts = layout.fanouts((source_x, source_y, source_z))
280
+ num_fanouts = len(existing_fanouts)
281
+
282
+ if layout.is_po(source_node):
283
+ max_fanouts = 0
284
+ elif layout.is_wire(source_node) and not source_z == 1:
285
+ max_fanouts = 2
286
+ else:
287
+ max_fanouts = 1
288
+
289
+ if num_fanouts >= max_fanouts:
290
+ return jsonify(
291
+ {
292
+ "success": False,
293
+ "error": f"Gate at ({source_x}, {source_y}, {source_z}) cannot have more than {max_fanouts} outgoing connections.",
294
+ }
295
+ )
296
+
297
+ if gate_type == "po":
298
+ layout.create_po(layout.make_signal(source_node), "", (x, y))
299
+ elif gate_type == "inv":
300
+ layout.create_not(layout.make_signal(source_node), (x, y))
301
+ elif gate_type == "buf":
302
+ layout.create_buf(layout.make_signal(source_node), (x, y))
303
+ if layout.fanout_size(source_node) == 2:
304
+ update_first = True
305
+ elif gate_type in ["and", "or", "nor", "xor", "xnor", "bufc", "bufk"]:
306
+ if "first" not in params or "second" not in params:
307
+ return jsonify(
308
+ {
309
+ "success": False,
310
+ "error": f"{gate_type.upper()} gate requires exactly two inputs.",
311
+ }
312
+ )
313
+ first_x = int(params["first"]["position"]["x"])
314
+ first_y = int(params["first"]["position"]["y"])
315
+ first_z = _gate_layer(layout, first_x, first_y)
316
+ first_source_gate_type = params["first"]["gate_type"]
317
+ if first_source_gate_type == "bufc":
318
+ if first_x < x:
319
+ if layout.has_southern_outgoing_signal((first_x, first_y, 0)):
320
+ first_z = 1
321
+ elif layout.has_southern_outgoing_signal((first_x, first_y, 1)):
322
+ first_z = 0
323
+ else:
324
+ if layout.has_northern_incoming_signal((first_x, first_y, 0)):
325
+ first_z = 1
326
+ elif layout.has_northern_incoming_signal((first_x, first_y, 1)):
327
+ first_z = 0
328
+ else:
329
+ return jsonify({"success": False, "error": "Something went wrong."})
330
+ elif first_y < y:
331
+ if layout.has_eastern_outgoing_signal((first_x, first_y, 0)):
332
+ first_z = 1
333
+ elif layout.has_eastern_outgoing_signal((first_x, first_y, 1)):
334
+ first_z = 0
335
+ else:
336
+ if layout.has_northern_incoming_signal((first_x, first_y, 0)):
337
+ first_z = 0
338
+ elif layout.has_northern_incoming_signal((first_x, first_y, 1)):
339
+ first_z = 1
340
+ else:
341
+ return jsonify({"success": False, "error": "Something went wrong."})
342
+ else:
343
+ return jsonify({"success": False, "error": "Something went wrong."})
344
+
345
+ if first_source_gate_type == "bufk":
346
+ if first_x < x:
347
+ if layout.has_southern_outgoing_signal((first_x, first_y, 0)):
348
+ first_z = 1
349
+ elif layout.has_southern_outgoing_signal((first_x, first_y, 1)):
350
+ first_z = 0
351
+ else:
352
+ if layout.has_northern_incoming_signal((first_x, first_y, 0)):
353
+ first_z = 0
354
+ elif layout.has_northern_incoming_signal((first_x, first_y, 1)):
355
+ first_z = 1
356
+ else:
357
+ return jsonify({"success": False, "error": "Something went wrong."})
358
+ elif first_y < y:
359
+ if layout.has_eastern_outgoing_signal((first_x, first_y, 0)):
360
+ first_z = 1
361
+ elif layout.has_eastern_outgoing_signal((first_x, first_y, 1)):
362
+ first_z = 0
363
+ else:
364
+ if layout.has_northern_incoming_signal((first_x, first_y, 0)):
365
+ first_z = 1
366
+ elif layout.has_northern_incoming_signal((first_x, first_y, 1)):
367
+ first_z = 0
368
+ else:
369
+ return jsonify({"success": False, "error": "Something went wrong."})
370
+ else:
371
+ return jsonify({"success": False, "error": "Something went wrong."})
372
+ second_x = int(params["second"]["position"]["x"])
373
+ second_y = int(params["second"]["position"]["y"])
374
+ second_z = _gate_layer(layout, second_x, second_y)
375
+ second_source_gate_type = params["second"]["gate_type"]
376
+ if second_source_gate_type == "bufc":
377
+ if second_x < x:
378
+ if layout.has_southern_outgoing_signal((second_x, second_y, 0)):
379
+ second_z = 1
380
+ elif layout.has_southern_outgoing_signal((second_x, second_y, 1)):
381
+ second_z = 0
382
+ else:
383
+ if layout.has_northern_incoming_signal((second_x, second_y, 0)):
384
+ second_z = 1
385
+ elif layout.has_northern_incoming_signal((second_x, second_y, 1)):
386
+ second_z = 0
387
+ else:
388
+ return jsonify({"success": False, "error": "Something went wrong."})
389
+ elif second_y < y:
390
+ if layout.has_eastern_outgoing_signal((second_x, second_y, 0)):
391
+ second_z = 1
392
+ elif layout.has_eastern_outgoing_signal((second_x, second_y, 1)):
393
+ second_z = 0
394
+ else:
395
+ if layout.has_northern_incoming_signal((second_x, second_y, 0)):
396
+ second_z = 0
397
+ elif layout.has_northern_incoming_signal((second_x, second_y, 1)):
398
+ second_z = 1
399
+ else:
400
+ return jsonify({"success": False, "error": "Something went wrong."})
401
+ else:
402
+ return jsonify({"success": False, "error": "Something went wrong."})
403
+
404
+ if second_source_gate_type == "bufk":
405
+ if second_x < x:
406
+ if layout.has_southern_outgoing_signal((second_x, second_y, 0)):
407
+ second_z = 1
408
+ elif layout.has_southern_outgoing_signal((second_x, second_y, 1)):
409
+ second_z = 0
410
+ else:
411
+ if layout.has_northern_incoming_signal((second_x, second_y, 0)):
412
+ second_z = 0
413
+ elif layout.has_northern_incoming_signal((second_x, second_y, 1)):
414
+ second_z = 1
415
+ else:
416
+ return jsonify({"success": False, "error": "Something went wrong."})
417
+ elif second_y < y:
418
+ if layout.has_eastern_outgoing_signal((second_x, second_y, 0)):
419
+ second_z = 1
420
+ elif layout.has_eastern_outgoing_signal((second_x, second_y, 1)):
421
+ second_z = 0
422
+ else:
423
+ if layout.has_northern_incoming_signal((second_x, second_y, 0)):
424
+ second_z = 1
425
+ elif layout.has_northern_incoming_signal((second_x, second_y, 1)):
426
+ second_z = 0
427
+ else:
428
+ return jsonify({"success": False, "error": "Something went wrong."})
429
+ else:
430
+ return jsonify({"success": False, "error": "Something went wrong."})
431
+ first_node = layout.get_node((first_x, first_y, first_z))
432
+ second_node = layout.get_node((second_x, second_y, second_z))
433
+ if not first_node or not second_node:
434
+ return jsonify({"success": False, "error": "One or both source gates not found."})
435
+
436
+ # Check if the gate already has inputs
437
+ existing_fanins = layout.fanins((x, y))
438
+ if len(existing_fanins) >= 2:
439
+ return jsonify(
440
+ {
441
+ "success": False,
442
+ "error": f"Gate at ({x}, {y}) cannot have more than 2 inputs.",
443
+ }
444
+ )
445
+
446
+ for existing_fanin in existing_fanins:
447
+ # Determine allowed number of fanouts
448
+ existing_fanouts = layout.fanouts(existing_fanin)
449
+ num_fanouts = len(existing_fanouts)
450
+
451
+ if layout.is_po(existing_fanin):
452
+ max_fanouts = 0
453
+ elif layout.is_wire(existing_fanin) and not existing_fanin.z == 1:
454
+ max_fanouts = 2
455
+ else:
456
+ max_fanouts = 1
457
+
458
+ if num_fanouts >= max_fanouts:
459
+ return jsonify(
460
+ {
461
+ "success": False,
462
+ "error": f"Gate at {existing_fanin} cannot have more than {max_fanouts} outgoing connections.",
463
+ }
464
+ )
465
+
466
+ # Determine allowed number of fanouts
467
+ existing_fanouts_first_node = layout.fanouts((first_x, first_y, first_z))
468
+ num_fanouts_first_node = len(existing_fanouts_first_node)
469
+
470
+ if layout.is_po(first_node):
471
+ max_fanouts_first_node = 0
472
+ elif layout.is_wire(first_node) and not first_z == 1:
473
+ max_fanouts_first_node = 2
474
+ else:
475
+ max_fanouts_first_node = 1
476
+
477
+ if num_fanouts_first_node >= max_fanouts_first_node:
478
+ return jsonify(
479
+ {
480
+ "success": False,
481
+ "error": f"Gate at ({first_x}, {first_y}, {first_z}) cannot have more than {max_fanouts_first_node} outgoing connections.",
482
+ }
483
+ )
484
+
485
+ # Determine allowed number of fanouts
486
+ existing_fanouts_second_node = layout.fanouts((second_x, second_y, second_z))
487
+ num_fanouts_second_node = len(existing_fanouts_second_node)
488
+
489
+ if layout.is_po(second_node):
490
+ max_fanouts_second_node = 0
491
+ elif layout.is_wire(second_node) and not second_z == 1:
492
+ max_fanouts_second_node = 2
493
+ else:
494
+ max_fanouts_second_node = 1
495
+
496
+ if num_fanouts_second_node >= max_fanouts_second_node:
497
+ return jsonify(
498
+ {
499
+ "success": False,
500
+ "error": f"Gate at ({second_x}, {second_y}, {second_z}) cannot have more than {max_fanouts_second_node} outgoing connections.",
501
+ }
502
+ )
503
+
504
+ if gate_type == "and":
505
+ layout.create_and(
506
+ layout.make_signal(first_node),
507
+ layout.make_signal(second_node),
508
+ (x, y),
509
+ )
510
+ elif gate_type == "or":
511
+ layout.create_or(
512
+ layout.make_signal(first_node),
513
+ layout.make_signal(second_node),
514
+ (x, y),
515
+ )
516
+ elif gate_type == "nor":
517
+ layout.create_nor(
518
+ layout.make_signal(first_node),
519
+ layout.make_signal(second_node),
520
+ (x, y),
521
+ )
522
+ elif gate_type == "xor":
523
+ layout.create_xor(
524
+ layout.make_signal(first_node),
525
+ layout.make_signal(second_node),
526
+ (x, y),
527
+ )
528
+ elif gate_type == "xnor":
529
+ layout.create_xnor(
530
+ layout.make_signal(first_node),
531
+ layout.make_signal(second_node),
532
+ (x, y),
533
+ )
534
+ elif gate_type in ["bufc", "bufk"]:
535
+ # The upper wire always faces east: west input for straight, north input for bent crossings.
536
+ upper_is_first = first_x < x if gate_type == "bufc" else first_y < y
537
+ lower_node, upper_node = (second_node, first_node) if upper_is_first else (first_node, second_node)
538
+ layout.create_buf(layout.make_signal(lower_node), (x, y, 0))
539
+ layout.create_buf(layout.make_signal(upper_node), (x, y, 1))
540
+ layout.obstruct_coordinate((x, y, 1))
541
+ if layout.fanout_size(first_node) == 2:
542
+ update_first = True
543
+ if layout.fanout_size(second_node) == 2:
544
+ update_second = True
545
+ else:
546
+ return jsonify({"success": False, "error": f"Unsupported gate type: {gate_type}"})
547
+
548
+ layout.obstruct_coordinate((x, y, 0))
549
+
550
+ return (
551
+ jsonify(
552
+ {
553
+ "success": True,
554
+ "updateFirstBufToFanout": update_first,
555
+ "updateSecondBufToFanout": update_second,
556
+ }
557
+ ),
558
+ 200,
559
+ )
560
+
561
+ except RequestEntityTooLarge:
562
+ raise
563
+ except Exception:
564
+ return _internal_error_response()
565
+
566
+
567
+ @app.route("/delete_gate", methods=["POST"])
568
+ def delete_gate():
569
+ try:
570
+ data = request.json
571
+ x = int(data["x"])
572
+ y = int(data["y"])
573
+
574
+ session_id = session["session_id"]
575
+ layout = layouts.get(session_id)
576
+ if not layout:
577
+ return jsonify({"success": False, "error": "Layout not found."})
578
+
579
+ found = False
580
+ for z in (1, 0) if layout.z() > 0 else (0,):
581
+ tile = (x, y, z)
582
+ if layout.is_empty_tile(tile):
583
+ continue
584
+ found = True
585
+ outgoing_tiles = layout.fanouts(tile)
586
+ layout.clear_tile(tile)
587
+ layout.clear_obstructed_coordinate(tile)
588
+ for outgoing_tile in outgoing_tiles:
589
+ incoming_tiles = layout.fanins(outgoing_tile)
590
+ incoming_signals = [layout.make_signal(layout.get_node(inp)) for inp in incoming_tiles if inp != tile]
591
+ layout.move_node(layout.get_node(outgoing_tile), outgoing_tile, incoming_signals)
592
+
593
+ if found:
594
+ return jsonify({"success": True})
595
+ else:
596
+ return jsonify({"success": False, "error": "Gate not found at the specified position."})
597
+ except RequestEntityTooLarge:
598
+ raise
599
+ except Exception:
600
+ return _internal_error_response()
601
+
602
+
603
+ @app.route("/connect_gates", methods=["POST"])
604
+ def connect_gates():
605
+ try:
606
+ data = request.json
607
+ session_id = session["session_id"]
608
+ layout = layouts.get(session_id)
609
+ if not layout:
610
+ return jsonify({"success": False, "error": "Layout not found."})
611
+
612
+ source_x = int(data["source_x"])
613
+ source_y = int(data["source_y"])
614
+ source_z = _gate_layer(layout, source_x, source_y)
615
+ source_gate_type = data["source_gate_type"]
616
+ target_x = int(data["target_x"])
617
+ target_y = int(data["target_y"])
618
+ target_z = _gate_layer(layout, target_x, target_y)
619
+ target_gate_type = data["target_gate_type"]
620
+ find_path = data["find_path"]
621
+
622
+ if source_gate_type == "bufc":
623
+ if find_path:
624
+ return jsonify(
625
+ {
626
+ "success": False,
627
+ "error": "Source gate is a crossing and the outgoing direction cannot be specified, create a connected buffer first.",
628
+ }
629
+ )
630
+ if source_x < target_x:
631
+ if layout.has_southern_outgoing_signal((source_x, source_y, 0)):
632
+ source_z = 1
633
+ elif layout.has_southern_outgoing_signal((source_x, source_y, 1)):
634
+ source_z = 0
635
+ else:
636
+ if layout.has_northern_incoming_signal((source_x, source_y, 0)):
637
+ source_z = 1
638
+ elif layout.has_northern_incoming_signal((source_x, source_y, 1)):
639
+ source_z = 0
640
+ else:
641
+ source_z = 0
642
+ elif source_y < target_y:
643
+ if layout.has_eastern_outgoing_signal((source_x, source_y, 0)):
644
+ source_z = 1
645
+ elif layout.has_eastern_outgoing_signal((source_x, source_y, 1)):
646
+ source_z = 0
647
+ else:
648
+ if layout.has_northern_incoming_signal((source_x, source_y, 0)):
649
+ source_z = 0
650
+ elif layout.has_northern_incoming_signal((source_x, source_y, 1)):
651
+ source_z = 1
652
+ else:
653
+ source_z = 0
654
+ else:
655
+ return jsonify({"success": False, "error": "Something went wrong."})
656
+
657
+ if source_gate_type == "bufk":
658
+ if find_path:
659
+ return jsonify(
660
+ {
661
+ "success": False,
662
+ "error": "Source gate is a crossing and the outgoing direction cannot be specified, create a connected buffer first.",
663
+ }
664
+ )
665
+ if source_x < target_x:
666
+ if layout.has_southern_outgoing_signal((source_x, source_y, 0)):
667
+ source_z = 1
668
+ elif layout.has_southern_outgoing_signal((source_x, source_y, 1)):
669
+ source_z = 0
670
+ else:
671
+ if layout.has_northern_incoming_signal((source_x, source_y, 0)):
672
+ source_z = 0
673
+ elif layout.has_northern_incoming_signal((source_x, source_y, 1)):
674
+ source_z = 1
675
+ else:
676
+ source_z = 0
677
+ elif source_y < target_y:
678
+ if layout.has_eastern_outgoing_signal((source_x, source_y, 0)):
679
+ source_z = 1
680
+ elif layout.has_eastern_outgoing_signal((source_x, source_y, 1)):
681
+ source_z = 0
682
+ else:
683
+ if layout.has_northern_incoming_signal((source_x, source_y, 0)):
684
+ source_z = 1
685
+ elif layout.has_northern_incoming_signal((source_x, source_y, 1)):
686
+ source_z = 0
687
+ else:
688
+ source_z = 0
689
+ else:
690
+ return jsonify({"success": False, "error": "Something went wrong."})
691
+
692
+ if target_gate_type == "bufc":
693
+ if find_path:
694
+ return jsonify(
695
+ {
696
+ "success": False,
697
+ "error": "Target gate is a crossing and the incoming direction cannot be specified, create a connected buffer first.",
698
+ }
699
+ )
700
+ if source_x < target_x:
701
+ if layout.has_southern_outgoing_signal((target_x, target_y, 0)):
702
+ target_z = 1
703
+ elif layout.has_southern_outgoing_signal((target_x, target_y, 1)):
704
+ target_z = 0
705
+ else:
706
+ if layout.has_northern_incoming_signal((target_x, target_y, 0)):
707
+ target_z = 1
708
+ elif layout.has_northern_incoming_signal((target_x, target_y, 1)):
709
+ target_z = 0
710
+ else:
711
+ target_z = 0
712
+ elif source_y < target_y:
713
+ if layout.has_eastern_outgoing_signal((target_x, target_y, 0)):
714
+ target_z = 1
715
+ elif layout.has_eastern_outgoing_signal((target_x, target_y, 1)):
716
+ target_z = 0
717
+ else:
718
+ if layout.has_western_incoming_signal((target_x, target_y, 0)):
719
+ target_z = 1
720
+ elif layout.has_western_incoming_signal((target_x, target_y, 1)):
721
+ target_z = 0
722
+ else:
723
+ target_z = 0
724
+ else:
725
+ return jsonify({"success": False, "error": "Something went wrong."})
726
+
727
+ if target_gate_type == "bufk":
728
+ if find_path:
729
+ return jsonify(
730
+ {
731
+ "success": False,
732
+ "error": "Target gate is a crossing and the incoming direction cannot be specified, create a connected buffer first.",
733
+ }
734
+ )
735
+ if source_x < target_x:
736
+ if layout.has_southern_outgoing_signal((target_x, target_y, 0)):
737
+ target_z = 0
738
+ elif layout.has_southern_outgoing_signal((target_x, target_y, 1)):
739
+ target_z = 1
740
+ else:
741
+ if layout.has_northern_incoming_signal((target_x, target_y, 0)):
742
+ target_z = 1
743
+ elif layout.has_northern_incoming_signal((target_x, target_y, 1)):
744
+ target_z = 0
745
+ else:
746
+ target_z = 0
747
+ elif source_y < target_y:
748
+ if layout.has_eastern_outgoing_signal((target_x, target_y, 0)):
749
+ target_z = 0
750
+ elif layout.has_eastern_outgoing_signal((target_x, target_y, 1)):
751
+ target_z = 1
752
+ else:
753
+ if layout.has_western_incoming_signal((target_x, target_y, 0)):
754
+ target_z = 1
755
+ elif layout.has_western_incoming_signal((target_x, target_y, 1)):
756
+ target_z = 0
757
+ else:
758
+ target_z = 0
759
+ else:
760
+ return jsonify({"success": False, "error": "Something went wrong."})
761
+
762
+ source_node = layout.get_node((source_x, source_y, source_z))
763
+ target_node = layout.get_node((target_x, target_y, target_z))
764
+
765
+ if not source_node:
766
+ return jsonify({"success": False, "error": "Source gate not found."})
767
+ if not target_node:
768
+ return jsonify({"success": False, "error": "Target gate not found."})
769
+
770
+ # Determine allowed number of fanouts
771
+ existing_fanouts = layout.fanouts((source_x, source_y, source_z))
772
+ num_fanouts = len(existing_fanouts)
773
+
774
+ if layout.is_po(source_node):
775
+ max_fanouts = 0
776
+ elif layout.is_wire(source_node) and not source_z == 1:
777
+ max_fanouts = 2
778
+ else:
779
+ max_fanouts = 1
780
+
781
+ if num_fanouts >= max_fanouts:
782
+ return jsonify(
783
+ {
784
+ "success": False,
785
+ "error": f"Gate at ({source_x}, {source_y}, {source_z}) cannot have more than {max_fanouts} outgoing connections.",
786
+ }
787
+ )
788
+
789
+ # Determine allowed number of fanins
790
+ existing_fanins = layout.fanins((target_x, target_y, target_z))
791
+ num_fanins = len(existing_fanins)
792
+
793
+ if layout.is_pi(target_node):
794
+ max_fanins = 0
795
+ elif layout.is_wire(target_node) or layout.is_inv(target_node):
796
+ max_fanins = 1
797
+ else:
798
+ max_fanins = 2
799
+
800
+ if num_fanins >= max_fanins:
801
+ return jsonify(
802
+ {
803
+ "success": False,
804
+ "error": f"Gate at ({target_x}, {target_y}, {target_z}) cannot have more than {max_fanins} incoming connections.",
805
+ }
806
+ )
807
+
808
+ if (source_x, source_y, source_z) in existing_fanins:
809
+ return jsonify(
810
+ {
811
+ "success": False,
812
+ "error": f"Gate at ({target_x}, {target_y}, {target_z}) is already connected to ({source_x}, {source_y}, {source_z}.",
813
+ }
814
+ )
815
+ else:
816
+ if not find_path:
817
+ existing_fanins.append((source_x, source_y, source_z))
818
+
819
+ incoming_signals = []
820
+ for fanin in existing_fanins:
821
+ incoming_signals.append(layout.make_signal(layout.get_node(fanin)))
822
+
823
+ if find_path:
824
+ path = a_star(layout, (source_x, source_y, source_z), (target_x, target_y, target_z))
825
+
826
+ if not path:
827
+ return jsonify(
828
+ {
829
+ "success": False,
830
+ "error": "No (crossing-free) path found between the selected gates.",
831
+ }
832
+ )
833
+ else:
834
+ path = [(source_x, source_y, source_z), (target_x, target_y, target_z)]
835
+
836
+ if find_path:
837
+ route_path(layout, path)
838
+ else:
839
+ layout.move_node(target_node, (target_x, target_y, target_z), incoming_signals)
840
+
841
+ if layout.fanout_size(source_node) == 2:
842
+ update = True
843
+ else:
844
+ update = False
845
+
846
+ if find_path:
847
+ for coord in path:
848
+ layout.obstruct_coordinate(coord)
849
+
850
+ return (
851
+ jsonify(
852
+ {
853
+ "success": True,
854
+ "updateBufToFanout": update,
855
+ "path": [(coord.x, coord.y) for coord in path]
856
+ if data["find_path"]
857
+ else [(source_x, source_y), (target_x, target_y)],
858
+ }
859
+ ),
860
+ 200,
861
+ )
862
+ except RequestEntityTooLarge:
863
+ raise
864
+ except Exception:
865
+ return _internal_error_response()
866
+
867
+
868
+ @app.route("/move_gate", methods=["POST"])
869
+ def move_gate():
870
+ try:
871
+ data = request.json
872
+ session_id = session["session_id"]
873
+ layout = layouts.get(session_id)
874
+ if not layout:
875
+ return jsonify({"success": False, "error": "Layout not found."})
876
+
877
+ source_x = int(data["source_x"])
878
+ source_y = int(data["source_y"])
879
+ source_gate_type = data["source_gate_type"]
880
+ source_z = _gate_layer(layout, source_x, source_y) if source_gate_type not in ("bufc", "bufk") else 1
881
+ source = (source_x, source_y, source_z)
882
+ target_x = int(data["target_x"])
883
+ target_y = int(data["target_y"])
884
+ target_z = source_z
885
+ target = (target_x, target_y, target_z)
886
+
887
+ if not (0 <= target_x <= layout.x() and 0 <= target_y <= layout.y()):
888
+ return jsonify({"success": False, "error": "Target tile is outside the layout."}), 400
889
+ if not layout.is_empty_tile((target_x, target_y, 0)) or not layout.is_empty_tile((target_x, target_y, 1)):
890
+ return jsonify({"success": False, "error": "Target tile already has a gate."}), 400
891
+ if source_gate_type in ("bufc", "bufk") and layout.is_empty_tile((source_x, source_y, 0)):
892
+ return jsonify({"success": False, "error": "Crossing gate is incomplete."}), 400
893
+
894
+ source_node = layout.get_node(source)
895
+
896
+ if not source_node:
897
+ return jsonify({"success": False, "error": "Source gate not found."})
898
+
899
+ # Find all gates that use this node as an input signal
900
+ outgoing_tiles = layout.fanouts(source)
901
+
902
+ layout.move_node(source_node, target, [])
903
+ layout.clear_obstructed_coordinate(source)
904
+ layout.obstruct_coordinate(target)
905
+
906
+ # Update signals for dependent nodes
907
+ for outgoing_tile in outgoing_tiles:
908
+ # Get the other input signals, if any
909
+ incoming_tiles = layout.fanins(outgoing_tile)
910
+ incoming_signals = [layout.make_signal(layout.get_node(inp)) for inp in incoming_tiles if inp != source]
911
+ layout.move_node(layout.get_node(outgoing_tile), outgoing_tile, incoming_signals)
912
+
913
+ if source_gate_type in ("bufc", "bufk"):
914
+ source_z = 0
915
+ source = (source_x, source_y, source_z)
916
+ target_z = 0
917
+ target = (target_x, target_y, target_z)
918
+
919
+ source_node = layout.get_node(source)
920
+
921
+ if not source_node:
922
+ return jsonify({"success": False, "error": "Source gate not found."})
923
+
924
+ # Find all gates that use this node as an input signal
925
+ outgoing_tiles = layout.fanouts(source)
926
+ layout.move_node(source_node, target, [])
927
+ layout.clear_tile(source)
928
+ layout.clear_obstructed_coordinate(source)
929
+ layout.obstruct_coordinate(target)
930
+
931
+ # Update signals for dependent nodes
932
+ for outgoing_tile in outgoing_tiles:
933
+ # Get the other input signals, if any
934
+ incoming_tiles = layout.fanins(outgoing_tile)
935
+ incoming_signals = [layout.make_signal(layout.get_node(inp)) for inp in incoming_tiles if inp != source]
936
+ layout.move_node(layout.get_node(outgoing_tile), outgoing_tile, incoming_signals)
937
+
938
+ return (
939
+ jsonify(
940
+ {
941
+ "success": True,
942
+ "updateGateType": source_gate_type == "fanout",
943
+ }
944
+ ),
945
+ 200,
946
+ )
947
+ except RequestEntityTooLarge:
948
+ raise
949
+ except Exception:
950
+ return _internal_error_response()
951
+
952
+
953
+ @app.route("/check_design_rules", methods=["POST"])
954
+ def check_design_rules():
955
+ try:
956
+ session_id = session["session_id"]
957
+ layout = layouts.get(session_id)
958
+ if not layout:
959
+ return jsonify({"success": False, "error": "Layout not found."})
960
+
961
+ warnings, errors, report = check_design_rules_function(layout)
962
+
963
+ return (
964
+ jsonify(
965
+ {
966
+ "success": True,
967
+ "errors": errors,
968
+ "warnings": warnings,
969
+ "report": report,
970
+ }
971
+ ),
972
+ 200,
973
+ )
974
+ except Exception:
975
+ return _internal_error_response()
976
+
977
+
978
+ def strip_ansi_codes(text):
979
+ ansi_escape = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
980
+ return ansi_escape.sub("", text)
981
+
982
+
983
+ def format_output(text):
984
+ # Split the text based on the [i] marker
985
+ sections = text.split("[i]")
986
+
987
+ # Reconstruct the output with proper line breaks
988
+ formatted = ""
989
+ for section in sections:
990
+ stripped = section.strip()
991
+ if stripped:
992
+ formatted += f"[i] {stripped}\n"
993
+ return formatted
994
+
995
+
996
+ def check_design_rules_function(layout):
997
+ # Create a StringIO object to capture the output
998
+ captured_output = io.StringIO()
999
+
1000
+ # Redirect stdout to the StringIO object
1001
+ with redirect_stdout(captured_output):
1002
+ warnings, errors = gate_level_drvs(layout, print_report=True)
1003
+
1004
+ # Retrieve the captured output
1005
+ raw_output = captured_output.getvalue()
1006
+ clean_output = strip_ansi_codes(raw_output)
1007
+ output = format_output(clean_output)
1008
+ return warnings, errors, output
1009
+
1010
+
1011
+ @app.route("/check_equivalence", methods=["POST"])
1012
+ def check_equivalence():
1013
+ try:
1014
+ session_id = session.get("session_id")
1015
+ if not session_id:
1016
+ return jsonify({"success": False, "error": "Session not found."}), 400
1017
+
1018
+ layout = layouts.get(session_id)
1019
+ if not layout:
1020
+ return jsonify({"success": False, "error": "Layout not found."}), 404
1021
+
1022
+ network = networks.get(session_id)
1023
+ if not network:
1024
+ return jsonify({"success": False, "error": "Network not found."}), 404
1025
+
1026
+ # Call your design rule checking function
1027
+ equivalence, counter_example = check_equivalence_function(layout, network)
1028
+
1029
+ return (
1030
+ jsonify(
1031
+ {
1032
+ "success": True,
1033
+ "equivalence": equivalence,
1034
+ "counter_example": counter_example,
1035
+ }
1036
+ ),
1037
+ 200,
1038
+ )
1039
+
1040
+ except Exception:
1041
+ return _internal_error_response(), 500
1042
+
1043
+
1044
+ def check_equivalence_function(layout, network):
1045
+ stats = equivalence_checking_stats()
1046
+ eq = equivalence_checking(layout, network, stats)
1047
+
1048
+ if eq == eq_type.STRONG:
1049
+ equivalence = "STRONG"
1050
+ counter_example = None # No counter example needed
1051
+ elif eq == eq_type.WEAK:
1052
+ equivalence = "WEAK"
1053
+ counter_example = None # No counter example needed
1054
+ else:
1055
+ equivalence = "NO"
1056
+ counter_example = stats.counter_example
1057
+ print(counter_example)
1058
+
1059
+ return equivalence, counter_example
1060
+
1061
+
1062
+ def _send_layout_file(layout, writer, filename, mimetype, *params):
1063
+ # Each request owns its files; read the result before the temporary directory is removed.
1064
+ with tempfile.TemporaryDirectory(prefix="mnt-designer-") as directory:
1065
+ path = Path(directory) / filename
1066
+ writer(layout, str(path), *params)
1067
+ content = io.BytesIO(path.read_bytes())
1068
+ return send_file(content, as_attachment=True, mimetype=mimetype, download_name=filename)
1069
+
1070
+
1071
+ @app.route("/export_layout", methods=["GET"])
1072
+ def export_layout():
1073
+ try:
1074
+ session_id = session.get("session_id")
1075
+ layout = layouts.get(session_id)
1076
+
1077
+ if not layout:
1078
+ return jsonify({"success": False, "error": "Layout not found."})
1079
+
1080
+ return _send_layout_file(layout, write_fgl_layout, "layout.fgl", "application/fgl")
1081
+
1082
+ except Exception:
1083
+ return _internal_error_response()
1084
+
1085
+
1086
+ @app.route("/export_dot_layout", methods=["GET"])
1087
+ def export_dot_layout():
1088
+ try:
1089
+ session_id = session.get("session_id")
1090
+ layout = layouts.get(session_id)
1091
+
1092
+ if not layout:
1093
+ return jsonify({"success": False, "error": "Layout not found."})
1094
+
1095
+ return _send_layout_file(layout, write_dot_layout, "layout.dot", "application/dot")
1096
+
1097
+ except Exception:
1098
+ return _internal_error_response()
1099
+
1100
+
1101
+ @app.route("/export_qca_layout", methods=["GET"])
1102
+ def export_qca_layout():
1103
+ try:
1104
+ session_id = session.get("session_id")
1105
+ layout = layouts.get(session_id)
1106
+
1107
+ if not layout:
1108
+ return jsonify({"success": False, "error": "Layout not found."})
1109
+
1110
+ cell_level_layout = apply_qca_one_library(layout)
1111
+ params = write_qca_layout_svg_params()
1112
+ return _send_layout_file(cell_level_layout, write_qca_layout_svg, "layout_qca.svg", "image/svg+xml", params)
1113
+
1114
+ except Exception:
1115
+ return _internal_error_response()
1116
+
1117
+
1118
+ @app.route("/export_sidb_layout", methods=["GET"])
1119
+ def export_sidb_layout():
1120
+ try:
1121
+ session_id = session.get("session_id")
1122
+ layout = layouts.get(session_id)
1123
+
1124
+ if not layout:
1125
+ return jsonify({"success": False, "error": "Layout not found."})
1126
+
1127
+ hex_layout = hexagonalization(layout)
1128
+ cell_level_layout = apply_bestagon_library(hex_layout)
1129
+
1130
+ params = write_sidb_layout_svg_params()
1131
+ params.color_background = color_mode.DARK
1132
+ return _send_layout_file(cell_level_layout, write_sidb_layout_svg, "layout_sidb.svg", "image/svg+xml", params)
1133
+
1134
+ except Exception:
1135
+ return _internal_error_response()
1136
+
1137
+
1138
+ @app.route("/import_layout", methods=["POST"])
1139
+ def import_layout():
1140
+ try:
1141
+ # Get the uploaded file with the key 'file'
1142
+ file = request.files.get("file")
1143
+ if not file:
1144
+ return jsonify({"success": False, "error": "No file provided."})
1145
+
1146
+ with tempfile.TemporaryDirectory(prefix="mnt-designer-") as directory:
1147
+ path = Path(directory) / "layout.fgl"
1148
+ file.save(path)
1149
+ layout = read_cartesian_fgl_layout(str(path))
1150
+
1151
+ # Override the current layout with the imported layout
1152
+ session_id = session["session_id"]
1153
+ layouts[session_id] = cartesian_obstruction_layout(layout)
1154
+
1155
+ return jsonify({"success": True})
1156
+
1157
+ except RequestEntityTooLarge:
1158
+ raise
1159
+ except Exception:
1160
+ return _internal_error_response()
1161
+
1162
+
1163
+ @app.route("/get_layout", methods=["GET"])
1164
+ def get_layout():
1165
+ try:
1166
+ session_id = session["session_id"]
1167
+ layout = layouts.get(session_id)
1168
+ if not layout:
1169
+ return jsonify({"success": False, "error": "Layout not found."})
1170
+
1171
+ # Extract layout data
1172
+ layout_dimensions, gates = get_layout_information(layout)
1173
+ return jsonify({"success": True, "layoutDimensions": layout_dimensions, "gates": gates})
1174
+ except Exception:
1175
+ return _internal_error_response()
1176
+
1177
+
1178
+ @app.route("/get_bounding_box", methods=["GET"])
1179
+ def get_bounding_box():
1180
+ try:
1181
+ session_id = session["session_id"]
1182
+ layout = layouts.get(session_id)
1183
+ if not layout:
1184
+ return jsonify({"success": True, "max_x": 0, "max_y": 0})
1185
+
1186
+ # Extract layout data
1187
+ _, max_coord = layout.bounding_box_2d()
1188
+ return jsonify({"success": True, "max_x": max_coord.x, "max_y": max_coord.y})
1189
+ except Exception:
1190
+ return _internal_error_response()
1191
+
1192
+
1193
+ @app.route("/get_verilog_code", methods=["GET"])
1194
+ def get_verilog_code():
1195
+ try:
1196
+ # Ensure the user has a session_id
1197
+ if "session_id" not in session:
1198
+ return jsonify({"success": False, "error": "Session not found."}), 400
1199
+
1200
+ session_id = session["session_id"]
1201
+
1202
+ # Retrieve the Verilog code for the current session
1203
+ code = verilogs.get(session_id)
1204
+
1205
+ if code is None:
1206
+ return jsonify({"success": False, "error": "Verilog code not found."})
1207
+
1208
+ # Return the Verilog code
1209
+ return jsonify({"success": True, "code": code}), 200
1210
+
1211
+ except Exception:
1212
+ return _internal_error_response(), 500
1213
+
1214
+
1215
+ def _read_verilog(code):
1216
+ with tempfile.TemporaryDirectory(prefix="mnt-designer-") as directory:
1217
+ path = Path(directory) / "network.v"
1218
+ path.write_text(code, encoding="utf-8")
1219
+ return read_technology_network(str(path))
1220
+
1221
+
1222
+ @app.route("/save_verilog_code", methods=["POST"])
1223
+ def save_verilog_code():
1224
+ try:
1225
+ data = request.json
1226
+ code = data.get("code", "")
1227
+
1228
+ network = _read_verilog(code)
1229
+
1230
+ session_id = session["session_id"]
1231
+ networks[session_id] = network
1232
+ verilogs[session_id] = code
1233
+
1234
+ return jsonify({"success": True})
1235
+ except RequestEntityTooLarge:
1236
+ raise
1237
+ except Exception:
1238
+ return _internal_error_response()
1239
+
1240
+
1241
+ @app.route("/import_verilog_code", methods=["POST"])
1242
+ def import_verilog_code():
1243
+ try:
1244
+ # Get the uploaded file with the key 'file'
1245
+ uploaded_file = request.files.get("file")
1246
+ if not uploaded_file:
1247
+ return jsonify({"success": False, "error": "No file provided."})
1248
+
1249
+ # Read the file content
1250
+ code = uploaded_file.read().decode("utf-8")
1251
+
1252
+ network = _read_verilog(code)
1253
+
1254
+ # Store the network in the session
1255
+ session_id = session["session_id"]
1256
+ networks[session_id] = network
1257
+ verilogs[session_id] = code
1258
+
1259
+ # Return the code to be displayed in the editor
1260
+ return jsonify({"success": True, "code": code})
1261
+
1262
+ except RequestEntityTooLarge:
1263
+ raise
1264
+ except Exception:
1265
+ return _internal_error_response()
1266
+
1267
+
1268
+ @app.route("/apply_orthogonal", methods=["POST"])
1269
+ def apply_orthogonal():
1270
+ try:
1271
+ session_id = session["session_id"]
1272
+ network = networks.get(session_id)
1273
+ if not network:
1274
+ return jsonify(
1275
+ {
1276
+ "success": False,
1277
+ "error": "Network not found. Please save or import Verilog code first.",
1278
+ }
1279
+ )
1280
+
1281
+ if network.size() < 3:
1282
+ return jsonify(
1283
+ {
1284
+ "success": False,
1285
+ "error": "Network size is less than 3, indicating that not gates are present.",
1286
+ }
1287
+ )
1288
+
1289
+ if network.size() > 500:
1290
+ return jsonify(
1291
+ {
1292
+ "success": False,
1293
+ "error": "Network size exceeds 500 nodes and the resulting layout can not be rendered.",
1294
+ }
1295
+ )
1296
+
1297
+ for po in network.pos():
1298
+ for fanin in network.fanins(po):
1299
+ if fanin in (0, 1):
1300
+ return jsonify(
1301
+ {
1302
+ "success": False,
1303
+ "error": f"Network has an unconnected PO: {network.get_output_name(network.po_index(po))}.",
1304
+ }
1305
+ )
1306
+
1307
+ layout = orthogonal(network)
1308
+
1309
+ layouts[session_id] = cartesian_obstruction_layout(layout) # Update the layout in the session
1310
+
1311
+ layout_dimensions, gates = get_layout_information(layout)
1312
+
1313
+ return jsonify({"success": True, "layoutDimensions": layout_dimensions, "gates": gates})
1314
+ except Exception:
1315
+ return _internal_error_response()
1316
+
1317
+
1318
+ @app.route("/apply_iosdn", methods=["POST"])
1319
+ def apply_iosdn():
1320
+ try:
1321
+ session_id = session["session_id"]
1322
+ network = networks.get(session_id)
1323
+ if not network:
1324
+ return jsonify(
1325
+ {
1326
+ "success": False,
1327
+ "error": "Network not found. Please save or import Verilog code first.",
1328
+ }
1329
+ )
1330
+
1331
+ if network.size() < 3:
1332
+ return jsonify(
1333
+ {
1334
+ "success": False,
1335
+ "error": "Network size is less than 3, indicating that not gates are present.",
1336
+ }
1337
+ )
1338
+
1339
+ if network.size() > 500:
1340
+ return jsonify(
1341
+ {
1342
+ "success": False,
1343
+ "error": "Network size exceeds 500 nodes and the resulting layout can not be rendered.",
1344
+ }
1345
+ )
1346
+
1347
+ for po in network.pos():
1348
+ for fanin in network.fanins(po):
1349
+ if fanin in (0, 1):
1350
+ return jsonify(
1351
+ {
1352
+ "success": False,
1353
+ "error": f"Network has an unconnected PO: {network.get_output_name(network.po_index(po))}.",
1354
+ }
1355
+ )
1356
+
1357
+ return jsonify({"success": False, "error": "Input-ordering SDN not available in pyfiction yet."})
1358
+ except Exception:
1359
+ return _internal_error_response()
1360
+
1361
+
1362
+ @app.route("/apply_gold", methods=["POST"])
1363
+ def apply_gold():
1364
+ try:
1365
+ session_id = session["session_id"]
1366
+ network = networks.get(session_id)
1367
+ if not network:
1368
+ return jsonify(
1369
+ {
1370
+ "success": False,
1371
+ "error": "Network not found. Please save or import Verilog code first.",
1372
+ }
1373
+ )
1374
+
1375
+ if network.size() < 3:
1376
+ return jsonify(
1377
+ {
1378
+ "success": False,
1379
+ "error": "Network is empty.",
1380
+ }
1381
+ )
1382
+
1383
+ if network.size() > 150:
1384
+ return jsonify({"success": False, "error": "Network size exceeds 150 nodes."})
1385
+
1386
+ for po in network.pos():
1387
+ for fanin in network.fanins(po):
1388
+ if fanin in (0, 1):
1389
+ return jsonify(
1390
+ {
1391
+ "success": False,
1392
+ "error": f"Network has an unconnected PO: {network.get_output_name(network.po_index(po))}.",
1393
+ }
1394
+ )
1395
+
1396
+ # Parameters
1397
+ data = request.json
1398
+ params = graph_oriented_layout_design_params()
1399
+ params.return_first = bool(data.get("return_first"))
1400
+
1401
+ mode = data.get("mode")
1402
+ if mode == "HIGH_EFFICIENCY":
1403
+ params.mode = gold_effort_mode.HIGH_EFFICIENCY
1404
+ elif mode == "HIGH_EFFORT":
1405
+ params.mode = gold_effort_mode.HIGH_EFFORT
1406
+ elif mode == "HIGHEST_EFFORT":
1407
+ params.mode = gold_effort_mode.HIGHEST_EFFORT
1408
+ else:
1409
+ return jsonify({"success": False, "error": f"Unknown mode: {mode}."})
1410
+
1411
+ params.timeout = int(data.get("timeout"))
1412
+ params.num_vertex_expansions = int(data.get("num_vertex_expansions"))
1413
+ params.planar = bool(data.get("planar"))
1414
+ params.enable_multithreading = bool(data.get("enable_multithreading", params.enable_multithreading))
1415
+
1416
+ cost = data.get("cost")
1417
+ if cost == "AREA":
1418
+ params.cost = gold_cost_objective.AREA
1419
+ elif cost == "WIRES":
1420
+ params.cost = gold_cost_objective.WIRES
1421
+ elif cost == "CROSSINGS":
1422
+ params.cost = gold_cost_objective.CROSSINGS
1423
+ elif cost == "ACP":
1424
+ params.cost = gold_cost_objective.ACP
1425
+ else:
1426
+ return jsonify({"success": False, "error": f"Unknown cost objective: {cost}."})
1427
+
1428
+ layout = graph_oriented_layout_design(network, params)
1429
+ if layout:
1430
+ layouts[session_id] = cartesian_obstruction_layout(layout) # Update the layout in the session
1431
+ layout_dimensions, gates = get_layout_information(layout)
1432
+
1433
+ return jsonify({"success": True, "layoutDimensions": layout_dimensions, "gates": gates})
1434
+ else:
1435
+ return jsonify(
1436
+ {
1437
+ "success": False,
1438
+ "error": "No layout found with the specified parameters.",
1439
+ }
1440
+ )
1441
+ except RequestEntityTooLarge:
1442
+ raise
1443
+ except Exception:
1444
+ return _internal_error_response()
1445
+
1446
+
1447
+ @app.route("/apply_exact", methods=["POST"])
1448
+ def apply_exact():
1449
+ try:
1450
+ session_id = session["session_id"]
1451
+ network = networks.get(session_id)
1452
+ if not network:
1453
+ return jsonify(
1454
+ {
1455
+ "success": False,
1456
+ "error": "Network not found. Please save or import Verilog code first.",
1457
+ }
1458
+ )
1459
+
1460
+ if network.size() < 3:
1461
+ return jsonify(
1462
+ {
1463
+ "success": False,
1464
+ "error": "Network is empty.",
1465
+ }
1466
+ )
1467
+
1468
+ if network.size() > 30:
1469
+ return jsonify({"success": False, "error": "Network size exceeds 30 nodes."})
1470
+
1471
+ for po in network.pos():
1472
+ for fanin in network.fanins(po):
1473
+ if fanin in (0, 1):
1474
+ return jsonify(
1475
+ {
1476
+ "success": False,
1477
+ "error": f"Network has an unconnected PO: {network.get_output_name(network.po_index(po))}.",
1478
+ }
1479
+ )
1480
+
1481
+ if exact_params is None or exact_cartesian is None:
1482
+ return jsonify(
1483
+ {
1484
+ "success": False,
1485
+ "error": "Pyfiction was installed without Z3 enabled.",
1486
+ }
1487
+ )
1488
+ # Parameters
1489
+ data = request.json
1490
+ if not isinstance(data, dict):
1491
+ return jsonify({"success": False, "error": "Exact parameters must be a JSON object."}), 400
1492
+ params = exact_params()
1493
+ params.scheme = "2DDWave"
1494
+ # Match the native uint16_t bounds, size_t thread count, and unsigned timeout.
1495
+ limits = {
1496
+ "upper_bound_x": 65535,
1497
+ "upper_bound_y": 65535,
1498
+ "num_threads": (1 << (8 * struct.calcsize("P"))) - 1,
1499
+ "timeout": 4294967295,
1500
+ }
1501
+ for name, maximum in limits.items():
1502
+ value = data.get(name, getattr(params, name))
1503
+ if type(value) is not int or not 1 <= value <= maximum:
1504
+ return jsonify({"success": False, "error": f"{name} must be an integer between 1 and {maximum}."}), 400
1505
+ setattr(params, name, value)
1506
+ params.fixed_size = bool(data.get("fixed_size", False))
1507
+ params.crossings = bool(data.get("crossings", True))
1508
+ params.border_io = bool(data.get("border_io", True))
1509
+ params.straight_inverters = bool(data.get("straight_inverters", False))
1510
+ params.desynchronize = bool(data.get("desynchronize", True))
1511
+ params.minimize_wires = bool(data.get("minimize_wires", False))
1512
+ params.minimize_crossings = bool(data.get("minimize_crossings", False))
1513
+ # Now run the exact algorithm
1514
+ layout = exact_cartesian(network, params)
1515
+ if layout:
1516
+ layouts[session_id] = cartesian_obstruction_layout(layout)
1517
+ layout_dimensions, gates = get_layout_information(layout)
1518
+
1519
+ return jsonify({"success": True, "layoutDimensions": layout_dimensions, "gates": gates})
1520
+ else:
1521
+ return jsonify(
1522
+ {
1523
+ "success": False,
1524
+ "error": "No layout found with the specified parameters.",
1525
+ }
1526
+ )
1527
+ except RequestEntityTooLarge:
1528
+ raise
1529
+ except Exception:
1530
+ return _internal_error_response()
1531
+
1532
+
1533
+ @app.route("/apply_optimization", methods=["POST"])
1534
+ def apply_optimization():
1535
+ try:
1536
+ session_id = session["session_id"]
1537
+ layout = layouts.get(session_id)
1538
+ params = post_layout_optimization_params()
1539
+
1540
+ if not layout:
1541
+ return jsonify(
1542
+ {
1543
+ "success": False,
1544
+ "error": "Layout not found. Please create a layout first",
1545
+ }
1546
+ )
1547
+
1548
+ warnings, errors, report = check_design_rules_function(layout)
1549
+ if errors != 0:
1550
+ return jsonify(
1551
+ {
1552
+ "success": False,
1553
+ "error": f"Layout has {errors} errors. Fix them first before optimizing.",
1554
+ }
1555
+ )
1556
+ if warnings != 0:
1557
+ for x in range(layout.x() + 1):
1558
+ for y in range(layout.y() + 1):
1559
+ for z in range(layout.z() + 1):
1560
+ if not layout.is_empty_tile((x, y, z)) and layout.is_dead(layout.get_node((x, y, z))):
1561
+ return jsonify(
1562
+ {
1563
+ "success": False,
1564
+ "error": f"Layout has a dead node: ({x}, {y}, {z}). Fix it first before optimizing.",
1565
+ }
1566
+ )
1567
+
1568
+ data = request.json
1569
+ max_gate_relocations = data.get("max_gate_relocations")
1570
+ if max_gate_relocations is not None:
1571
+ params.max_gate_relocations = int(max_gate_relocations)
1572
+
1573
+ params.optimize_pos_only = bool(data.get("optimize_pos_only"))
1574
+ params.planar_optimization = bool(data.get("planar_optimization"))
1575
+
1576
+ params.timeout = int(data.get("timeout"))
1577
+
1578
+ post_layout_optimization(layout, params)
1579
+ layouts[session_id] = layout # Update the layout in the session
1580
+
1581
+ layout_dimensions, gates = get_layout_information(layout)
1582
+
1583
+ return jsonify({"success": True, "layoutDimensions": layout_dimensions, "gates": gates})
1584
+ except RequestEntityTooLarge:
1585
+ raise
1586
+ except Exception:
1587
+ return _internal_error_response()
1588
+
1589
+
1590
+ def _crossing_type(layout, x, y):
1591
+ # Connected native/FGL layouts may use either layer order; actual wiring takes precedence.
1592
+ for z in (0, 1):
1593
+ incoming = layout.fanins((x, y, z))
1594
+ outgoing = layout.fanouts((x, y, z))
1595
+ if incoming and outgoing:
1596
+ return "bufc" if incoming[0].x == outgoing[0].x or incoming[0].y == outgoing[0].y else "bufk"
1597
+
1598
+ # Input-only crossings created here encode intent in their east-facing upper wire.
1599
+ for z in (1, 0):
1600
+ incoming = layout.fanins((x, y, z))
1601
+ if incoming:
1602
+ straight = incoming[0].x < x if z == 1 else incoming[0].y < y
1603
+ return "bufc" if straight else "bufk"
1604
+ # Fully disconnected crossings have no routing intent in native/FGL data. Foreign partial
1605
+ # layouts may also use a different layer convention; their original intent cannot be recovered.
1606
+ return "bufk"
1607
+
1608
+
1609
+ def get_layout_information(layout):
1610
+ layout_dimensions = {"x": layout.x() + 1, "y": layout.y() + 1}
1611
+ gates = []
1612
+
1613
+ for x in range(layout.x() + 1):
1614
+ for y in range(layout.y() + 1):
1615
+ z = _gate_layer(layout, x, y)
1616
+ node = layout.get_node((x, y, z))
1617
+ name = ""
1618
+ if node:
1619
+ if layout.is_pi(node):
1620
+ gate_type = "pi"
1621
+ try:
1622
+ name = layout.get_name(layout.make_signal(node))
1623
+ except Exception:
1624
+ name = ""
1625
+ elif layout.is_po(node):
1626
+ gate_type = "po"
1627
+ try:
1628
+ name = layout.get_name(layout.make_signal(node))
1629
+ except Exception:
1630
+ name = ""
1631
+ elif layout.is_wire(node):
1632
+ gate_type = "buf"
1633
+ above_gate = layout.above(layout.get_tile(node))
1634
+ if z == 0 and layout.z() == 1 and not layout.is_empty_tile(above_gate):
1635
+ gate_type = _crossing_type(layout, x, y)
1636
+ if layout.fanout_size(node) == 2:
1637
+ gate_type = "fanout"
1638
+ elif layout.is_inv(node):
1639
+ gate_type = "inv"
1640
+ elif layout.is_and(node):
1641
+ gate_type = "and"
1642
+ elif layout.is_nand(node):
1643
+ gate_type = "nand"
1644
+ elif layout.is_or(node):
1645
+ gate_type = "or"
1646
+ elif layout.is_nor(node):
1647
+ gate_type = "nor"
1648
+ elif layout.is_xor(node):
1649
+ gate_type = "xor"
1650
+ elif layout.is_xnor(node):
1651
+ gate_type = "xnor"
1652
+ else:
1653
+ raise Exception("Unsupported gate type")
1654
+
1655
+ gate_info = {
1656
+ "x": x,
1657
+ "y": y,
1658
+ "type": gate_type,
1659
+ "connections": [],
1660
+ "name": name,
1661
+ }
1662
+ # Get fanins (source nodes)
1663
+ fanins = layout.fanins((x, y, z))
1664
+ for fin in fanins:
1665
+ gate_info["connections"].append({"sourceX": fin.x, "sourceY": fin.y})
1666
+ if gate_type in ("bufc", "bufk"):
1667
+ fanins = layout.fanins((x, y, 1))
1668
+ for fin in fanins:
1669
+ gate_info["connections"].append({"sourceX": fin.x, "sourceY": fin.y})
1670
+ gates.append(gate_info)
1671
+ return layout_dimensions, gates
1672
+
1673
+
1674
+ def start_server(host: str = "127.0.0.1", port: int = 5001, open_browser: bool = True) -> None:
1675
+ logging.getLogger("werkzeug").setLevel(logging.ERROR)
1676
+ url_host = f"[{host}]" if ":" in host else host
1677
+ url = f"http://{url_host}:{port}"
1678
+ print(
1679
+ f"Server is hosted at: {url}.",
1680
+ "To stop it, interrupt the process (e.g., via CTRL+C). \n",
1681
+ )
1682
+ cli.show_server_banner = lambda *_args: None
1683
+ if open_browser:
1684
+ webbrowser.open(url)
1685
+ app.run(debug=False, host=host, port=port)
1686
+
1687
+
1688
+ def main(argv: Sequence[str] | None = None) -> None:
1689
+ parser = argparse.ArgumentParser(description="Start the MNT Designer web interface.")
1690
+ parser.add_argument("--host", default="127.0.0.1", help="Address to bind to (default: 127.0.0.1).")
1691
+ parser.add_argument("--port", type=int, default=5001, help="Port to listen on (default: 5001).")
1692
+ parser.add_argument("--no-browser", action="store_true", help="Do not open a browser automatically.")
1693
+ parser.add_argument("--version", action="version", version=f"%(prog)s {version('mnt.designer')}")
1694
+ args = parser.parse_args(argv)
1695
+ if not 1 <= args.port <= 65535:
1696
+ parser.error("--port must be between 1 and 65535")
1697
+ start_server(host=args.host, port=args.port, open_browser=not args.no_browser)
1698
+
1699
+
1700
+ if __name__ == "__main__":
1701
+ main()