Source code for chipfiring.CFVisualizer

from __future__ import annotations
from dash import Dash, html
import dash_cytoscape as cyto
from .CFGraph import CFGraph, Vertex
from .CFDivisor import CFDivisor
from .CFOrientation import CFOrientation

[docs] def _graph_to_cytoscape_elements(graph: CFGraph): """Converts a CFGraph object to a list of elements for Dash Cytoscape.""" nodes = [] for vertex in graph.vertices: nodes.append({ 'data': { 'id': vertex.name, 'label': vertex.name, 'firing_type': 'neutral', 'divisor_sign': 'neutral_divisor_sign' } }) edges = [] # Keep track of added edges to avoid duplicates in undirected graph added_edges = set() for v1 in graph.graph: for v2, valence in graph.graph[v1].items(): # Ensure edge is added only once for undirected graph # Sort by name to create a canonical representation of the edge edge_pair = tuple(sorted((v1.name, v2.name))) if edge_pair not in added_edges: for i in range(valence): edges.append({ 'data': { 'source': v1.name, 'target': v2.name, 'id': f'{edge_pair[0]}-{edge_pair[1]}-{i}', 'oriented': False, 'arrow_shape': 'none' } }) added_edges.add(edge_pair) return nodes + edges
[docs] def _divisor_to_cytoscape_elements(divisor: CFDivisor): """Converts a CFDivisor object to a list of elements for Dash Cytoscape.""" elements = _graph_to_cytoscape_elements(divisor.graph) for element in elements: if 'source' in element.get('data', {}): # It's an edge element['data']['arrow_shape'] = 'none' elif 'id' in element.get('data', {}) and 'label' in element.get('data', {}) and 'firing_type' in element.get('data', {}): # It's a node node_id = element['data']['id'] vertex_obj = Vertex(node_id) if vertex_obj in divisor.degrees: chips = divisor.degrees[vertex_obj] element['data']['label'] = f"{node_id}\n{chips}" if chips < 0: element['data']['divisor_sign'] = 'negative' else: element['data']['divisor_sign'] = 'non-negative' else: element['data']['label'] = f"{node_id}\nN/A" element['data']['divisor_sign'] = 'neutral_divisor_sign' return elements
[docs] def _orientation_to_cytoscape_elements(orientation_obj: CFOrientation): """Converts a CFOrientation object to a list of elements for Dash Cytoscape.""" elements = _graph_to_cytoscape_elements(orientation_obj.graph) for element in elements: if 'source' in element.get('data', {}): # It's an edge edge_id_parts = element['data']['id'].split('-') id_v1_name = edge_id_parts[0] id_v2_name = edge_id_parts[1] oriented_pair = orientation_obj.get_orientation(id_v1_name, id_v2_name) if oriented_pair: actual_source, actual_target = oriented_pair element['data']['source'] = actual_source element['data']['target'] = actual_target element['data']['oriented'] = True element['data']['arrow_shape'] = 'triangle' else: # Edge has NO_ORIENTATION in CFOrientation object element['data']['oriented'] = False element['data']['arrow_shape'] = 'none' # The source and target remain as arbitrarily assigned by graph_to_cytoscape_elements. # The stylesheet will hide the arrow for these. return elements
# Base stylesheet for all visualizations BASE_STYLESHEET = [ { 'selector': 'node', # Default node style (also for neutral firing type) 'style': { 'label': 'data(label)', 'background-color': '#D3D3D3', # Default Light Gray for nodes not otherwise specified 'color': '#000000', # Black text for better contrast on light gray 'text-outline-width': 1, 'text-outline-color': '#D3D3D3', 'text-wrap': 'wrap', 'text-valign': 'center', 'text-halign': 'center', 'width': '50px', 'height': '50px', 'font-size': '10px' } }, { 'selector': 'node[divisor_sign = "non-negative"]', 'style': { 'background-color': '#28a745', # Green for non-negative divisor 'text-outline-color': '#28a745', 'color': '#ffffff' } }, { 'selector': 'node[divisor_sign = "negative"]', 'style': { 'background-color': '#dc3545', # Red for negative divisor 'text-outline-color': '#dc3545', 'color': '#ffffff' } }, { 'selector': 'node[is_q = "true"]', 'style': { 'border-width': '3px', 'border-color': '#007bff' # Blue border for q } }, { 'selector': 'node[is_unburnt = "true"]', 'style': { 'background-color': '#ffc107' # Yellow for unburnt } }, { 'selector': 'node[is_burnt = "true"]', 'style': { 'background-color': '#6c757d' # Dark gray for burnt } }, { 'selector': 'node[is_in_firing_set = "true"]', 'style': { 'border-width': '5px', 'border-color': '#ffc107', 'border-style': 'solid' } }, { 'selector': 'edge', 'style': { 'line-color': '#9DBFB5', 'width': 2, 'curve-style': 'bezier', 'control-point-step-size': '40px', 'target-arrow-shape': 'data(arrow_shape)', 'target-arrow-color': '#555' } } ]
[docs] def visualize(cf_object: any): """ Creates and runs a Dash app to visualize a chip-firing object. Args: cf_object: The chip-firing object (CFGraph, CFDivisor, CFOrientation). debug: Whether to run the Dash app in debug mode. Raises: TypeError: If the object type is not supported for visualization. """ title = "Chip-Firing Visualization" elements = [] if isinstance(cf_object, CFGraph): title = "Graph Visualization" elements = _graph_to_cytoscape_elements(cf_object) for el in elements: if 'source' in el.get('data', {}): # it's an edge el['data']['arrow_shape'] = 'none' elif isinstance(cf_object, CFDivisor): title = "Divisor Visualization" elements = _divisor_to_cytoscape_elements(cf_object) elif isinstance(cf_object, CFOrientation): title = "Orientation Visualization" elements = _orientation_to_cytoscape_elements(cf_object) else: raise TypeError(f"Visualization not supported for object of type {type(cf_object).__name__}") app = Dash(__name__) app.layout = html.Div([ html.H1("Chip-Firing Visualizer"), html.H2(title), cyto.Cytoscape( id='cytoscape-graph', elements=elements, style={'width': '100%', 'height': '600px'}, layout={ 'name': 'cose', 'idealEdgeLength': 150, 'nodeOverlap': 20, 'refresh': 20, 'fit': True, 'padding': 30, 'randomize': False, 'componentSpacing': 100, 'nodeRepulsion': 400000, 'edgeElasticity': 100, 'nestingFactor': 5, 'gravity': 80, 'numIter': 1000, 'initialTemp': 200, 'coolingFactor': 0.95, 'minTemp': 1.0 }, stylesheet=BASE_STYLESHEET ) ]) app.run(debug=False)