Source code for qns.network.graphalg.draw

import pandas as pd
import webbrowser
import os
import json
import copy
import math


[docs] def draw(nl, ll, filename): """ draw network topology based on provided Node list and Link list Args: nl: list of Qnodes ll: list of QuantumChannel filename: path of the output file Return: HTML file generated and opened """ node_list = [] node_ref_to_id = {} node_name_to_id = {} for i, node_obj in enumerate(nl): new_id = i + 1 name = getattr(node_obj, 'name', f"Node_{new_id}") raw_apps = getattr(node_obj, 'apps', []) processed_apps_labels = [] if raw_apps: if not isinstance(raw_apps, list): raw_apps = [raw_apps] for app_obj in raw_apps: app_index = 0 app_label = getattr(app_obj, 'name', f"Node_app{app_index+1}") processed_apps_labels.append(str(app_label)) app_index = app_index + 1 node_ref_to_id[node_obj] = new_id node_name_to_id[name] = new_id node_name_to_id[str(node_obj)] = new_id node_list.append({ "id": new_id, "name": name, "apps": processed_apps_labels, "original_obj": str(node_obj) }) edge_list = [] for i, link_obj in enumerate(ll): src_raw, dest_raw = getattr(link_obj, "node_list", None) if src_raw is None or dest_raw is None: print(f"Warning: Skipping link {link_obj}, cannot find src/dest attributes.") continue src_id = None dest_id = None if src_raw in node_ref_to_id: src_id = node_ref_to_id[src_raw] elif dest_raw in node_ref_to_id: dest_id = node_ref_to_id[dest_raw] if src_id is None: src_str = str(src_raw) if hasattr(src_raw, 'name'): src_str = src_raw.name if src_str in node_name_to_id: src_id = node_name_to_id[src_str] if dest_id is None: dest_str = str(dest_raw) if hasattr(dest_raw, 'name'): dest_str = dest_raw.name if dest_str in node_name_to_id: dest_id = node_name_to_id[dest_str] if src_id is None or dest_id is None: print(f"Warning: Skipping link, could not map nodes: {src_raw} -> {dest_raw}") continue link_name = getattr(link_obj, 'name', f"Link_{src_id}_{dest_id}") bw = getattr(link_obj, 'bandwidth', 1.0) fid = getattr(link_obj, 'fidelity', 1.0) edge_list.append({ "id": f"e{i}", "src": src_id, "dest": dest_id, "bandwidth": bw, "name": link_name, "fidelity": fid }) # --- REPLACED NETWORKX WITH SIMPLE CIRCULAR LAYOUT --- vis_nodes = copy.deepcopy(node_list) vis_edges = copy.deepcopy(edge_list) node_count = len(vis_nodes) radius = 1000 # Visual scale for initial circle for i, n in enumerate(vis_nodes): # Calculate angle for circular distribution angle = 2 * math.pi * i / max(1, node_count) # Assign initial coordinates n['x'] = radius * math.cos(angle) n['y'] = radius * math.sin(angle) n['label'] = " " n['_realLabel'] = n['name'] apps_str = ", ".join(n['apps']) if n['apps'] else "None" n['title'] = f"ID:{n['id']} Name:{n['name']}\nApps: {apps_str}" for e in vis_edges: e['from'] = e['src'] e['to'] = e['dest'] e['label'] = " " e['_realLabel'] = e['name'] e['title'] = ( f"Name:{e['name']}\n" f"Bandwidth:{e['bandwidth']}\n" f"Fidelity:{e['fidelity']}" ) # Python List Comprehension: Split into multiple lines table_nodes_data = [ {"id": n['id'], "name": n['name'], "apps": ", ".join(n['apps'])} for n in node_list ] table_edges_data = [ { "name": e['name'], "src": e['src'], "dest": e['dest'], "bandwidth": e['bandwidth'], "fidelity": e['fidelity'] } for e in edge_list ] df_nodes = pd.DataFrame(table_nodes_data) df_edges = pd.DataFrame(table_edges_data) # Pandas to_html: Split arguments node_table_html = df_nodes.to_html( classes='custom-table node-table-row', index=False, table_id="nodeTable", border=0 ) edge_table_html = df_edges.to_html( classes='custom-table edge-table-row', index=False, table_id="edgeTable", border=0 ) physics_options = { "physics": { "enabled": True, "solver": "barnesHut", "barnesHut": { "gravitationalConstant": -8000, "centralGravity": 0.3, "springLength": 95, "springConstant": 0.04, "damping": 0.09, "avoidOverlap": 0.1 }, "stabilization": {"enabled": True, "iterations": 100} } } js_nodes_data = json.dumps(vis_nodes) js_edges_data = json.dumps(vis_edges) js_physics_options = json.dumps(physics_options["physics"]) # CSS Styles css_styles = """ <style> * { box-sizing: border-box; } body, html { margin: 0; padding: 0; height: 100vh; width: 100vw; overflow: hidden; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } #app-container { display: flex; width: 100%; height: 100%; } #left-panel { width: 300px; min-width: 250px; background: #2c3e50; color: #ecf0f1; display: flex; flex-direction: column; border-right: 1px solid #1a252f; z-index: 20; } .panel-content { padding: 15px; overflow-y: auto; flex: 1; } #center-panel { flex: 1; min-width: 300px; position: relative; background: #fdfdfd; } #mynetwork { width: 100%; height: 100%; outline: none; } #right-panel { width: 450px; min-width: 200px; background: #f4f6f7; border-left: 1px solid #bdc3c7; display: flex; flex-direction: column; } .resizer { width: 5px; background: #95a5a6; cursor: col-resize; z-index: 10; transition: 0.2s; } .resizer:hover { background: #3498db; } /* Controls */ h3 { border-bottom: 2px solid #34495e; padding-bottom: 8px; margin-top: 0; font-size: 16px; color: #f1c40f; } h4 { color: #1abc9c; margin: 15px 0 5px 0; border-bottom: 1px dashed #7f8c8d; padding-bottom: 3px; font-size: 14px; } label { font-size: 12px; color: #bdc3c7; display: block; margin-top: 8px; } input, select, button { width: 100%; padding: 8px; margin-top: 4px; border-radius: 4px; border: 1px solid #34495e; font-size: 13px; box-sizing: border-box } button { background: #27ae60; color: white; border: none; cursor: pointer; margin-top: 12px; font-weight: bold; transition: 0.2s; } button:hover { background: #2ecc71; } .btn-blue { background: #2980b9; } .btn-blue:hover { background: #3498db; } .btn-orange { background: #e67e22; } .btn-orange:hover { background: #d35400; } .checkbox-container { display: flex; align-items: center; margin-top: 10px; font-size: 13px; background: rgba(0,0,0,0.2); padding: 5px; border-radius: 4px; } .checkbox-container input { width: auto; margin-right: 8px; cursor: pointer;} /* Path Results */ #path-results-container { margin-top: 15px; border-top: 1px solid #34495e; padding-top: 10px; } .path-item { background: #34495e; padding: 10px; margin-top: 8px; border-radius: 4px; cursor: pointer; font-size: 12px; border-left: 4px solid transparent; transition: 0.2s; } .path-item:hover { background: #3d566e; } .path-active { border-left: 4px solid #e74c3c; background: #2c3e50; box-shadow: 0 0 5px rgba(0,0,0,0.5); } .path-detail { color: #bdc3c7; margin-top: 4px; font-family: monospace; font-size: 11px; word-break: break-all; } /* Tables */ .table-wrapper { flex: 1; overflow: auto; padding: 0; display: flex; flex-direction: column; } .table-header-title { padding: 10px; background: #e9ecef; font-weight: bold; border-bottom: 1px solid #ddd; flex-shrink: 0; } table.custom-table { width: 100%; border-collapse: collapse; background: white; font-size: 12px; table-layout: fixed; } table.custom-table th { background: #009879; color: white; padding: 8px; text-align: left; position: sticky; top: 0; z-index: 2; } table.custom-table td { padding: 8px; border-bottom: 1px solid #eee; color: #333; word-wrap: break-word; cursor: pointer; } table.custom-table tr:hover { background: #fffde7; } table.custom-table tr.selected { background: #fff3cd !important; border-left: 4px solid #FFD700 !important; } /* Modals */ .modal { display: none; position: fixed; z-index: 100; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); } .modal-content { background: white; margin: 10% auto; padding: 25px; width: 320px; border-radius: 8px; position: relative; box-shadow: 0 5px 15px rgba(0,0,0,0.3); } .close-btn { position: absolute; right: 15px; top: 10px; font-size: 24px; cursor: pointer; color: #aaa; } .close-btn:hover { color: #333; } </style> """ html_structure = f""" <div id="app-container"> <div id="left-panel"> <div class="panel-content"> <h3> Control Panel</h3> <div style="font-size:11px; color:#aaa; margin-bottom:10px;"> Nodes: {len(nl)} | Edges: {len(ll)} </div> <h4> Display</h4> <div class="checkbox-container" onclick="document.getElementById('label-toggle').click()"> <input type="checkbox" id="label-toggle" onchange="toggleLabels(this)"> <span>Show Labels</span> </div> <div class="checkbox-container" onclick="document.getElementById('physics-toggle').click()"> <input type="checkbox" id="physics-toggle" onchange="togglePhysics(this)" checked> <span>Live Physics</span> </div> <h4> Visual Style</h4> <input id="style-name" placeholder="Target Name (e.g. Node_01)"> <input type="color" id="style-color" value="#ff0000" style="height:35px; padding:2px; margin-top:5px;"> <button onclick="applyColorByName()">Apply Color</button> <h4> Pathfinding (Edge Disjoint)</h4> <div style="font-size:11px; color:#aaa; margin-bottom:5px;"> Algorithm: Find Path -> Remove Edges -> Repeat </div> <div style="display:flex; gap:5px;"> <input id="path-start" placeholder="Start Node Name/ID"> <input id="path-end" placeholder="End Node Name/ID"> </div> <label>Metric:</label> <select id="path-weight-metric"> <option value="bandwidth">Bandwidth (Minimize Cost)</option> <option value="hops">Hop Count</option> <option value="fidelity">Max Log-Fidelity</option> </select> <label>Max K Paths (Disjoint):</label> <input type="number" id="path-k" value="3" min="1" max="100"> <button class="btn-orange" onclick="calculateKShortestPaths()"> Calculate Path </button> <button class="btn-blue" onclick="resetGraph()"> Reset View</button> <div id="path-results-container"> <div style="font-size:12px; color:#aaa; text-align:center; padding:10px;"> Results will appear here </div> </div> <br> <div style="display:flex; gap:5px;"> <button onclick="openModal('nodeModal')"> Node</button> <button onclick="openModal('edgeModal')"> Edge</button> </div> </div> </div> <div class="resizer" id="resizer-left"></div> <div id="center-panel"><div id="mynetwork"></div></div> <div class="resizer" id="resizer-right"></div> <div id="right-panel"> <div class="table-wrapper" style="border-bottom: 4px solid #ddd;"> <div class="table-header-title"> Node List (Click to Toggle)</div> {node_table_html} </div> <div class="table-wrapper"> <div class="table-header-title"> Edge List (Click to Toggle)</div> {edge_table_html} </div> </div> </div> <div id="nodeModal" class="modal"> <div class="modal-content"> <span class="close-btn" onclick="closeModal('nodeModal')">&times;</span> <h3>Add Node (Visual Only)</h3> <input id="new-node-id" placeholder="ID (Integer)"> <input id="new-node-name" placeholder="Name"> <input id="new-node-apps" placeholder="Apps"> <button onclick="addNode()">Confirm Add</button> </div> </div> <div id="edgeModal" class="modal"> <div class="modal-content"> <span class="close-btn" onclick="closeModal('edgeModal')">&times;</span> <h3>Add Connection (Visual Only)</h3> <input id="new-edge-src" placeholder="Src ID"> <input id="new-edge-dest" placeholder="Dest ID"> <input id="new-edge-bandwidth" type="number" placeholder="Bandwidth" value="10"> <input id="new-edge-fidelity" type="number" step="0.01" min="0" max="1" placeholder="Fidelity" value="0.99"> <button onclick="addEdge()">Confirm Connect</button> </div> </div> """ # JS Logic js_logic = f""" <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"> </script> <script type="text/javascript"> var nodes = new vis.DataSet({js_nodes_data}); var edges = new vis.DataSet({js_edges_data}); var container = document.getElementById('mynetwork'); var HIGHLIGHT_COLOR = '#FFD700'; var HIGHLIGHT_BORDER = '#E67E22'; var PATH_COLOR = '#e74c3c'; var options = {{ nodes: {{ shape: 'dot', size: 20, font: {{ size: 14 }}, color: {{ background: '#97C2FC', border: '#2B7CE9', highlight: {{ background: HIGHLIGHT_COLOR, border: HIGHLIGHT_BORDER }} }} }}, edges: {{ font: {{ size: 12, align: 'middle' }}, color: {{ color: '#2B7CE9', hover: '#848484', highlight: HIGHLIGHT_COLOR }}, smooth: {{ enabled: true, type: 'dynamic' }} }}, physics: {js_physics_options}, interaction: {{ hover: true, tooltipDelay: 200, zoomView: true, dragView: true, dragNodes: true }} }}; var network = new vis.Network(container, {{ nodes: nodes, edges: edges }}, options); // --- Helper: Find Node ID by Name or ID --- function resolveNodeId(input) {{ if(!input) return null; input = input.toString().trim(); // Try explicit Integer ID match first if(!isNaN(input)) {{ let id = parseInt(input); if(nodes.get(id)) return id; }} // Try Name match let node = nodes.get().find(n => n.name === input); if(node) return node.id; return null; }} // --- Build Graph Helper --- function buildGraphFromData(nodeData, edgeData, metric) {{ var graph = {{}}; nodeData.getIds().forEach(id => graph[id] = {{}}); edgeData.get().forEach(e => {{ let w = 1; if(metric === 'bandwidth') w = (e.bandwidth ? 1000/e.bandwidth : 1); else if (metric === 'fidelity') {{ let f = e.fidelity || 1.0; if(f <= 0) f = 0.0001; if(f > 1) f = 1.0; w = -Math.log(f); }} // Undirected Graph Logic if(!graph[e.from]) graph[e.from]={{}}; if(!graph[e.to]) graph[e.to]={{}}; graph[e.from][e.to] = {{weight: w, id: e.id}}; graph[e.to][e.from] = {{weight: w, id: e.id}}; }}); return graph; }} // --- Pathfinding: Edge Disjoint Algorithm (Greedy) --- function calculateKShortestPaths() {{ var startInput = document.getElementById('path-start').value; var endInput = document.getElementById('path-end').value; var K = parseInt(document.getElementById('path-k').value) || 1; var metric = document.getElementById('path-weight-metric').value; var start = resolveNodeId(startInput); var end = resolveNodeId(endInput); if(!start || !end) {{ alert("Start or End node not found!"); return; }} if(start === end) {{ alert("Start and End are the same!"); return; }} // 1. Build initial local graph var localGraph = buildGraphFromData(nodes, edges, metric); var foundPaths = []; for(let k=0; k < K; k++) {{ // 2. Run Dijkstra on current graph var result = dijkstra(localGraph, start, end); if(!result) break; // No more paths possible (graph disconnected) foundPaths.push(result); // 3. REMOVE EDGES used in this path from localGraph let pathNodes = result.nodes; for(let i=0; i < pathNodes.length - 1; i++) {{ let u = pathNodes[i]; let v = pathNodes[i+1]; // Delete forward edge if(localGraph[u]) delete localGraph[u][v]; // Delete reverse edge (undirected) if(localGraph[v]) delete localGraph[v][u]; }} }} displayPathResults(foundPaths, metric); }} function dijkstra(g, s, e) {{ var dist = {{}}, prev = {{}}, pq = new Set(Object.keys(g).map(Number)); for(let k in g) dist[k] = Infinity; dist[s] = 0; while(pq.size) {{ let u = null; for(let n of pq) if(u===null || dist[n] < dist[u]) u = n; if(u === e || dist[u] === Infinity) break; pq.delete(u); if(g[u]) {{ for(let v in g[u]) {{ let v_int = parseInt(v); if(pq.has(v_int)) {{ let alt = dist[u] + g[u][v].weight; if(alt < dist[v_int]) {{ dist[v_int] = alt; prev[v_int] = u; }} }} }} }} }} if(dist[e] === Infinity) return null; var path = [], u = e; while(u) {{ path.unshift(u); u = prev[u]; }} return {{nodes: path, cost: dist[e]}}; }} // --- Display Results & Highlight --- function displayPathResults(paths, metric) {{ var container = document.getElementById('path-results-container'); container.innerHTML = ""; if(paths.length === 0) {{ container.innerHTML = "<div style='color:#e74c3c; padding:10px; text-align:center'>" + "❌ No Path Found</div>"; return; }} paths.forEach((p, i) => {{ var div = document.createElement('div'); div.className = 'path-item'; let costText = ""; if (metric === 'fidelity') {{ costText = "Fidelity: " + (Math.exp(-p.cost)*100).toFixed(4) + "%"; }} else {{ costText = "Cost: " + p.cost.toFixed(2); }} let namePath = p.nodes.map(nid => nodes.get(nid).name).join(" ➝ "); // Multiline Template Literal for readability in Python div.innerHTML = ` <strong>Path ${{i+1}}</strong> <span style="float:right">${{costText}}</span> <div class="path-detail">${{namePath}}</div> `; div.onclick = function() {{ document.querySelectorAll('.path-item').forEach(el => el.classList.remove('path-active')); div.classList.add('path-active'); highlightPath(p.nodes); }}; container.appendChild(div); }}); // Auto-select first path if(paths.length > 0) container.children[0].click(); }} function highlightPath(nodesIds) {{ resetGraphColorOnly(); // Highlight Edges for(let i=0; i<nodesIds.length-1; i++) {{ let u = nodesIds[i], v = nodesIds[i+1]; let es = edges.get().filter(e => (e.from===u && e.to===v) || (e.from===v && e.to===u) ); es.forEach(e => edges.update({{ id:e.id, color:{{color: PATH_COLOR}}, width: 3 }})); }} // Highlight Nodes nodesIds.forEach(id => nodes.update({{ id:id, color:{{background: '#ff7675'}} }})); }} function resetGraph() {{ resetGraphColorOnly(); network.fit(); var emptyMsg = "<div style='font-size:12px; color:#aaa; " + "text-align:center; padding:10px;'>" + "Results will appear here</div>"; document.getElementById('path-results-container').innerHTML = emptyMsg; document.querySelectorAll('tr.selected').forEach(r => r.classList.remove('selected')); }} function resetGraphColorOnly() {{ edges.update(edges.getIds().map(id => ({{ id:id, color:{{color:'#2B7CE9', highlight: HIGHLIGHT_COLOR}}, width:1 }}))); nodes.update(nodes.getIds().map(id => ({{ id:id, color: {{ background:'#97C2FC', border:'#2B7CE9', highlight: {{ background: HIGHLIGHT_COLOR, border: HIGHLIGHT_BORDER }} }} }}))); }} // --- Table Interactions (Toggle) --- function attachTableEvents(tableId, isEdge) {{ var table = document.getElementById(tableId); if(!table) return; var rows = table.getElementsByTagName('tr'); for (var i = 1; i < rows.length; i++) {{ rows[i].onclick = function() {{ var wasSelected = this.classList.contains('selected'); document.querySelectorAll('tr.selected').forEach(r => r.classList.remove('selected')); if (wasSelected) {{ network.unselectAll(); resetGraphColorOnly(); }} else {{ this.classList.add('selected'); if(isEdge) {{ var name = this.cells[0].innerText; var target = edges.get().find(e => e.name === name); if(target) network.selectEdges([target.id]); }} else {{ var id = parseInt(this.cells[0].innerText); network.selectNodes([id]); network.focus(id, {{ scale: 1.2, animation: true }}); }} }} }}; }} }} attachTableEvents('nodeTable', false); attachTableEvents('edgeTable', true); // --- UI Utils --- function toggleLabels(checkbox) {{ var show = checkbox.checked; nodes.update(nodes.get().map(n => ({{ id: n.id, label: show ? n._realLabel : " " }}) )); edges.update(edges.get().map(e => ({{ id: e.id, label: show ? e._realLabel : " " }}) )); }} function togglePhysics(checkbox) {{ network.setOptions({{ physics: {{ enabled: checkbox.checked }} }}); if(!checkbox.checked) network.storePositions(); }} function applyColorByName() {{ var nameInput = document.getElementById('style-name').value.trim(); var color = document.getElementById('style-color').value; var targetNode = nodes.get().find(n => n.name === nameInput); if(targetNode) {{ nodes.update({{ id: targetNode.id, color: {{ background: color, border: color, highlight:{{ background: HIGHLIGHT_COLOR, border: HIGHLIGHT_BORDER }} }} }}); return; }} alert("Node not found: " + nameInput); }} function makeResizable(resizer, side) {{ const l = document.getElementById('left-panel'); const r = document.getElementById('right-panel'); const c = document.getElementById('app-container'); resizer.addEventListener('mousedown', function(e) {{ e.preventDefault(); document.addEventListener('mousemove', resize); document.addEventListener('mouseup', stopResize); function resize(ev) {{ if(side==='left') {{ l.style.width = Math.max(150, Math.min(500, ev.clientX))+'px'; }} else {{ let w = c.offsetWidth - ev.clientX; r.style.width = Math.max(150, Math.min(800, w))+'px'; }} }} function stopResize() {{ document.removeEventListener('mousemove', resize); document.removeEventListener('mouseup', stopResize); }} }}); }} makeResizable(document.getElementById('resizer-left'), 'left'); makeResizable(document.getElementById('resizer-right'), 'right'); // Modal Logic function openModal(id) {{ document.getElementById(id).style.display = 'block'; }} function closeModal(id) {{ document.getElementById(id).style.display = 'none'; }} window.onclick = function(event) {{ if (event.target.classList.contains('modal')) event.target.style.display = "none"; }} function addNode() {{ let id = parseInt(document.getElementById('new-node-id').value); let name = document.getElementById('new-node-name').value; let apps = document.getElementById('new-node-apps').value; if(!id || !name) return alert('ID and Name required'); nodes.add({{ id:id, label: " ", _realLabel: name, name: name, title: "ID:"+id+" NAME:"+name, apps: apps.split(',') }}); closeModal('nodeModal'); }} function addEdge() {{ let u = parseInt(document.getElementById('new-edge-src').value); let v = parseInt(document.getElementById('new-edge-dest').value); let bw = parseFloat(document.getElementById('new-edge-bandwidth').value); let fid = parseFloat(document.getElementById('new-edge-fidelity').value); if(!u || !v) return alert('Source and Dest IDs required'); edges.add({{ from:u, to:v, bandwidth: bw, fidelity: fid, name: "Link_"+u+"_"+v, title: "Manually Added" }}); closeModal('edgeModal'); }} </script> """ full_html = ( "<!DOCTYPE html><html><head>" "<meta charset='utf-8'><title>Quantum Network Visualizer</title>" f"{css_styles}</head><body>{html_structure}{js_logic}</body></html>" ) with open(filename, "w", encoding="utf-8") as f: f.write(full_html) webbrowser.open('file://' + os.path.realpath(filename))