amachine.am_vis
1from pathlib import Path 2import graphviz 3from matplotlib import colormaps 4from matplotlib.colors import to_hex 5import matplotlib.pyplot as plt 6from IPython.display import Image, display 7import matplotlib.image as mpimg 8from io import BytesIO 9import imageio 10from pathlib import Path 11import numpy as np 12from scipy.spatial.distance import pdist, squareform 13 14from matplotlib.colors import Normalize 15from matplotlib import colormaps 16from matplotlib.colors import Colormap 17 18import ternary 19 20def create_digraph(engine="dot"): 21 22 engine_configs = { 23 "dot": { 24 'rankdir': 'LR', 25 # 'rankdir': 'TB', 26 'ranksep': '0.85', 27 'nodesep': '1.0', 28 'splines': 'spline', 29 'constraint' : 'true', 30 'concentrate': 'false', 31 'ratio': 'auto', 32 # 'nslimit' : '0', 33 # 'nslimit1' : '2' 34 }, 35 "neato": { 36 'overlap': 'scale', 37 'overlap_scaling': '-4', 38 'esep': '+2.5', 39 'sep': '+1.75', 40 'model': 'shortpath', 41 'damping': '0.85', 42 'epsilon': '0.00001', 43 'maxiter': '10000', 44 'start': '5', 45 }, 46 "fdp": { 47 'overlap': 'prism', 48 'sep': '+1.5', 49 'K': '1.0', 50 'splines': 'true', 51 'len' : '3.0', 52 'maxiter': '5000' 53 } 54 } 55 56 graph_attr = engine_configs.get(engine, {}) 57 graph_attr['outputorder'] = 'edgesfirst' 58 59 node_attr = { 60 'shape': 'box', 61 'style': 'rounded, filled', 62 'fillcolor': 'lightblue', 63 'fontname': 'Helvetica' 64 } 65 66 edge_attr = { 67 'penwidth': '12.0', 68 'color': 'gray40', 69 'arrowsize' : '1.75' 70 } 71 72 return graphviz.Digraph( 73 engine=engine, 74 graph_attr=graph_attr, 75 node_attr=node_attr, 76 edge_attr=edge_attr 77 ) 78 79 80def draw_graph( 81 aM, 82 output_dir : Path | str | None = None, 83 title : str ="am_graph", 84 view : bool = True, 85 format : str = "png", 86 strongly_connected_components : list[list[int]] | None = None, 87 engine : str ="dot", 88 symbols_only: bool = False, 89 no_labels : bool = False, 90 color_nodes_by : str | None = "strongly_connected_components", 91 color_borders_by : str | None = None, 92 color_edges_by : str | None = None, 93 symbol_categories : str | None = None ): 94 95 if color_edges_by == "category" and symbol_categories is None : 96 raise ValueError( "Coloring edges by category requires passing symbol categories" ) 97 98 ######################################################################################### 99 100 GV = create_digraph(engine=engine) 101 102 ######################################################################################### 103 104 node_cmap = colormaps['tab20_r'] 105 n_node_colors = 8 106 node_colors = None 107 node_classes = None 108 109 if strongly_connected_components and color_nodes_by=="strongly_connected_components" : 110 node_colors = [to_hex(node_cmap(i % n_node_colors)) for i in range(len(strongly_connected_components))] 111 112 elif color_nodes_by : 113 class_code = color_nodes_by.split( "_" )[ 0 ] 114 node_classes = set() 115 for state in aM.states : 116 for cl in state.classes : 117 code = cl.split( "_" )[ 0 ] 118 if code == class_code : 119 node_classes.add( cl ) 120 121 node_classes = list( node_classes ) 122 node_colors = [to_hex(node_cmap(i % n_node_colors)) for i in range(len(node_classes))] 123 124 ######################################################################################### 125 126 edge_colors = None 127 128 if color_edges_by == "category" : 129 edge_cmap = colormaps['Dark2'] 130 n_edge_colors = 8 131 categories = list( set( symbol_categories.values() ) ) 132 edge_colors = [to_hex(edge_cmap(i % n_edge_colors)) for i in range(len(categories))] 133 134 elif color_edges_by == "symbol" : 135 edge_cmap = colormaps['tab20'] 136 n_edge_colors = 20 137 edge_colors = [to_hex(edge_cmap(i % n_edge_colors)) for i in range(len(aM.alphabet))] 138 139 ######################################################################################### 140 141 border_cmap = colormaps['Set1'] 142 n_border_colors = 8 143 border_colors = None 144 border_classes = None 145 146 if strongly_connected_components and color_borders_by=="strongly_connected_components" : 147 border_colors = [to_hex(border_cmap(i % n_border_colors)) for i in range(len(strongly_connected_components))] 148 149 elif color_borders_by : 150 class_code = color_borders_by.split( "_" )[ 0 ] 151 border_classes = set() 152 for state in aM.states : 153 for cl in state.classes : 154 code = cl.split( "_" )[ 0 ] 155 if code == class_code : 156 border_classes.add( cl ) 157 158 border_classes = list( border_classes ) 159 border_colors = [to_hex(border_cmap(i % n_border_colors)) for i in range(len(border_classes))] 160 161 ######################################################################################### 162 163 for node_idx in range( len(aM.states) ) : 164 165 #------------------------------------------------------------------------------------- 166 167 nb_color = [ 'gray', 'black' ] 168 169 nb_colors = ( node_colors, border_colors ) 170 nb_cby = ( color_nodes_by, color_borders_by ) 171 nb_classes = ( node_classes, border_classes ) 172 173 for nb_i, c_by in enumerate( nb_cby ) : 174 175 if nb_classes[ nb_i ] is None : 176 continue 177 178 if strongly_connected_components and c_by=="strongly_connected_components" : 179 180 node_component = -1 181 for i, sg in enumerate( strongly_connected_components ) : 182 if node_idx in sg : 183 node_component = i 184 185 if node_component >= 0 : 186 nb_color[ nb_i ] = nb_colors[ nb_i ][ node_component % len(nb_colors[nb_i]) ] 187 188 elif c_by : 189 state_classes = aM.states[ node_idx ].classes 190 for cl in state_classes : 191 try : 192 color_index = nb_classes[ nb_i ].index( cl ) 193 nb_color[ nb_i ] = nb_colors[ nb_i ][ color_index % len(nb_colors[nb_i]) ] 194 except : 195 continue 196 197 #------------------------------------------------------------------------------------- 198 199 node_tex = aM.states[ node_idx ].name 200 if not node_tex : 201 node_tex = str(node_idx) 202 203 GV.node( 204 str(node_idx), 205 label=node_tex, 206 shape='circle', 207 style='bold,filled', 208 fillcolor=nb_color[ 0 ], 209 color=nb_color[ 1 ], 210 width='2.0', 211 penwidth='26.0' ) 212 213 for tr in aM.transitions : 214 215 u = tr.origin_state_idx 216 v = tr.target_state_idx 217 218 pr_str = str( tr.pq ) if aM.is_q_weighted else str( round( tr.prob, 4 ) ) 219 220 fontsize= '80' if symbols_only else '12' 221 222 if symbols_only : 223 label_text = f"{aM.alphabet[tr.symbol_idx]}" 224 else : 225 label_text = f"{aM.alphabet[tr.symbol_idx]}({pr_str})" 226 227 html_label = ( 228 f'<<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="2">' 229 f'<TR><TD>{label_text}</TD></TR>' 230 f'</TABLE>>' 231 ) 232 233 edge_color = "black" 234 235 if color_edges_by == "category" : 236 symbol = aM.alphabet[ tr.symbol_idx ] 237 if symbol in symbol_categories : 238 cat = symbol_categories[ symbol ] 239 try : 240 cl_idx = categories.index( cat ) 241 edge_color = edge_colors[ cl_idx % n_edge_colors ] 242 except : 243 edge_color = "black" 244 245 elif color_edges_by == "symbol" : 246 edge_color = edge_colors[ tr.symbol_idx % n_edge_colors ] 247 248 if no_labels : 249 html_label = None 250 251 GV.edge( 252 str(u), str(v), 253 label=html_label, 254 headlabel=" ", 255 fontsize=fontsize, 256 fontname='Times-Italic', 257 #labelfloat="true", 258 fontcolor="#1f1f1f", 259 color=edge_color, 260 arrowhead='nonenonenormal' 261 ) 262 263 if view == True : 264 265 GV.graph_attr.update(dpi='150') 266 png_bytes = GV.pipe(format="png") 267 buf = BytesIO( png_bytes ) 268 img = mpimg.imread( buf ) 269 270 plt.figure(figsize=(8, 6)) 271 plt.imshow(img) 272 plt.axis('off') 273 plt.tight_layout() 274 plt.show() 275 276 if output_dir is not None : 277 GV.attr(bgcolor='transparent') 278 GV.render( title, directory=output_dir, view=False, format=format, cleanup=True) 279 280def animated_distance_plot( vectors, max_res=1440, stride=7, apply_threshold=False, threshold=0.01 ) : 281 282 use_cuda = False # cpx is not None 283 284 n_vectors = len( vectors ) 285 max_res = min( n_vectors, max_res ) 286 287 if use_cuda : 288 state_vectors = cp.asarray( vectors[ 0:max_states, : ] ) 289 else : 290 state_vectors = vectors 291 292 imgs = [] 293 for start in range( 0, n_vectors, stride ) : 294 295 if start % (stride*10) == 0 : 296 print( f"offset: {start}" ) 297 298 end = min( start+max_res, n_vectors ) 299 300 if use_cuda : 301 distance_plot = squareform( 302 cpx.scipy.spatial.distance.pdist( state_vectors[ start:end, : ], metric='jensenshannon' ).get() 303 ) 304 else : 305 distance_plot = squareform( 306 pdist( state_vectors[ start:end, : ], metric='jensenshannon' ) 307 ) 308 309 if len( distance_plot ) < max_res : 310 distance_plot = np.pad( 311 distance_plot, 312 ( ( 0, max_res-len( distance_plot ) ), (0, max_res-len( distance_plot )) ), 313 mode='constant' ) 314 315 if apply_threshold : 316 distance_plot = np.where( distance_plot > threshold, 1, 0 ).astype( np.uint8 ) 317 imgs.append( distance_plot ) 318 else : 319 imgs.append( distance_plot.astype( np.float16 ) ) 320 321 print( f"Rendering {len(imgs)} frames." ) 322 to_video( imgs, fps=60 ) 323 324def to_video( 325 images : list[np.ndarray], 326 output_path : str = "output.mp4", 327 fps : int = 3, 328 cmap : str | Colormap = "plasma_r", 329) -> None: 330 if not images: 331 raise ValueError("images list is empty") 332 333 colormap = colormaps[cmap] if isinstance(cmap, str) else cmap 334 335 # Global min/max so colormapping is consistent across frames 336 global_min = min(f.min() for f in images) 337 global_max = max(f.max() for f in images) 338 339 if global_min == global_max: 340 raise ValueError("All frames are constant, cannot normalize") 341 342 norm = Normalize( vmin=global_min, vmax=global_max ) 343 344 with imageio.get_writer(output_path, fps=fps) as writer: 345 for frame in images: 346 rgba = colormap(norm(frame)) 347 rgb = (rgba[..., :3] * 255).astype(np.uint8) 348 writer.append_data(rgb) 349 350def triangular_barycentric_plot( 351 coords : np.ndarray, 352 coord_classes : np.ndarray, 353 axis_labels : tuple[str, str, str], 354 title : str="" ) : 355 356 figure, tax = ternary.figure(scale=40) 357 358 tax.boundary(linewidth=2.0) 359 360 fontsize = 20 361 tax.set_title("Simplex Boundary and Gridlines", fontsize=fontsize) 362 363 tax.left_axis_label( axis_labels[0], fontsize=fontsize) 364 tax.right_axis_label( axis_labels[1], fontsize=fontsize) 365 tax.bottom_axis_label( axis_labels[2], fontsize=fontsize) 366 367 # Remove default Matplotlib Axes 368 tax.clear_matplotlib_ticks() 369 370 tax.scatter( coords, marker='*' ) 371 372 ternary.plt.show()
def
create_digraph(engine='dot'):
22def create_digraph(engine="dot"): 23 24 engine_configs = { 25 "dot": { 26 'rankdir': 'LR', 27 # 'rankdir': 'TB', 28 'ranksep': '0.85', 29 'nodesep': '1.0', 30 'splines': 'spline', 31 'constraint' : 'true', 32 'concentrate': 'false', 33 'ratio': 'auto', 34 # 'nslimit' : '0', 35 # 'nslimit1' : '2' 36 }, 37 "neato": { 38 'overlap': 'scale', 39 'overlap_scaling': '-4', 40 'esep': '+2.5', 41 'sep': '+1.75', 42 'model': 'shortpath', 43 'damping': '0.85', 44 'epsilon': '0.00001', 45 'maxiter': '10000', 46 'start': '5', 47 }, 48 "fdp": { 49 'overlap': 'prism', 50 'sep': '+1.5', 51 'K': '1.0', 52 'splines': 'true', 53 'len' : '3.0', 54 'maxiter': '5000' 55 } 56 } 57 58 graph_attr = engine_configs.get(engine, {}) 59 graph_attr['outputorder'] = 'edgesfirst' 60 61 node_attr = { 62 'shape': 'box', 63 'style': 'rounded, filled', 64 'fillcolor': 'lightblue', 65 'fontname': 'Helvetica' 66 } 67 68 edge_attr = { 69 'penwidth': '12.0', 70 'color': 'gray40', 71 'arrowsize' : '1.75' 72 } 73 74 return graphviz.Digraph( 75 engine=engine, 76 graph_attr=graph_attr, 77 node_attr=node_attr, 78 edge_attr=edge_attr 79 )
def
draw_graph( aM, output_dir: pathlib.Path | str | None = None, title: str = 'am_graph', view: bool = True, format: str = 'png', strongly_connected_components: list[list[int]] | None = None, engine: str = 'dot', symbols_only: bool = False, no_labels: bool = False, color_nodes_by: str | None = 'strongly_connected_components', color_borders_by: str | None = None, color_edges_by: str | None = None, symbol_categories: str | None = None):
82def draw_graph( 83 aM, 84 output_dir : Path | str | None = None, 85 title : str ="am_graph", 86 view : bool = True, 87 format : str = "png", 88 strongly_connected_components : list[list[int]] | None = None, 89 engine : str ="dot", 90 symbols_only: bool = False, 91 no_labels : bool = False, 92 color_nodes_by : str | None = "strongly_connected_components", 93 color_borders_by : str | None = None, 94 color_edges_by : str | None = None, 95 symbol_categories : str | None = None ): 96 97 if color_edges_by == "category" and symbol_categories is None : 98 raise ValueError( "Coloring edges by category requires passing symbol categories" ) 99 100 ######################################################################################### 101 102 GV = create_digraph(engine=engine) 103 104 ######################################################################################### 105 106 node_cmap = colormaps['tab20_r'] 107 n_node_colors = 8 108 node_colors = None 109 node_classes = None 110 111 if strongly_connected_components and color_nodes_by=="strongly_connected_components" : 112 node_colors = [to_hex(node_cmap(i % n_node_colors)) for i in range(len(strongly_connected_components))] 113 114 elif color_nodes_by : 115 class_code = color_nodes_by.split( "_" )[ 0 ] 116 node_classes = set() 117 for state in aM.states : 118 for cl in state.classes : 119 code = cl.split( "_" )[ 0 ] 120 if code == class_code : 121 node_classes.add( cl ) 122 123 node_classes = list( node_classes ) 124 node_colors = [to_hex(node_cmap(i % n_node_colors)) for i in range(len(node_classes))] 125 126 ######################################################################################### 127 128 edge_colors = None 129 130 if color_edges_by == "category" : 131 edge_cmap = colormaps['Dark2'] 132 n_edge_colors = 8 133 categories = list( set( symbol_categories.values() ) ) 134 edge_colors = [to_hex(edge_cmap(i % n_edge_colors)) for i in range(len(categories))] 135 136 elif color_edges_by == "symbol" : 137 edge_cmap = colormaps['tab20'] 138 n_edge_colors = 20 139 edge_colors = [to_hex(edge_cmap(i % n_edge_colors)) for i in range(len(aM.alphabet))] 140 141 ######################################################################################### 142 143 border_cmap = colormaps['Set1'] 144 n_border_colors = 8 145 border_colors = None 146 border_classes = None 147 148 if strongly_connected_components and color_borders_by=="strongly_connected_components" : 149 border_colors = [to_hex(border_cmap(i % n_border_colors)) for i in range(len(strongly_connected_components))] 150 151 elif color_borders_by : 152 class_code = color_borders_by.split( "_" )[ 0 ] 153 border_classes = set() 154 for state in aM.states : 155 for cl in state.classes : 156 code = cl.split( "_" )[ 0 ] 157 if code == class_code : 158 border_classes.add( cl ) 159 160 border_classes = list( border_classes ) 161 border_colors = [to_hex(border_cmap(i % n_border_colors)) for i in range(len(border_classes))] 162 163 ######################################################################################### 164 165 for node_idx in range( len(aM.states) ) : 166 167 #------------------------------------------------------------------------------------- 168 169 nb_color = [ 'gray', 'black' ] 170 171 nb_colors = ( node_colors, border_colors ) 172 nb_cby = ( color_nodes_by, color_borders_by ) 173 nb_classes = ( node_classes, border_classes ) 174 175 for nb_i, c_by in enumerate( nb_cby ) : 176 177 if nb_classes[ nb_i ] is None : 178 continue 179 180 if strongly_connected_components and c_by=="strongly_connected_components" : 181 182 node_component = -1 183 for i, sg in enumerate( strongly_connected_components ) : 184 if node_idx in sg : 185 node_component = i 186 187 if node_component >= 0 : 188 nb_color[ nb_i ] = nb_colors[ nb_i ][ node_component % len(nb_colors[nb_i]) ] 189 190 elif c_by : 191 state_classes = aM.states[ node_idx ].classes 192 for cl in state_classes : 193 try : 194 color_index = nb_classes[ nb_i ].index( cl ) 195 nb_color[ nb_i ] = nb_colors[ nb_i ][ color_index % len(nb_colors[nb_i]) ] 196 except : 197 continue 198 199 #------------------------------------------------------------------------------------- 200 201 node_tex = aM.states[ node_idx ].name 202 if not node_tex : 203 node_tex = str(node_idx) 204 205 GV.node( 206 str(node_idx), 207 label=node_tex, 208 shape='circle', 209 style='bold,filled', 210 fillcolor=nb_color[ 0 ], 211 color=nb_color[ 1 ], 212 width='2.0', 213 penwidth='26.0' ) 214 215 for tr in aM.transitions : 216 217 u = tr.origin_state_idx 218 v = tr.target_state_idx 219 220 pr_str = str( tr.pq ) if aM.is_q_weighted else str( round( tr.prob, 4 ) ) 221 222 fontsize= '80' if symbols_only else '12' 223 224 if symbols_only : 225 label_text = f"{aM.alphabet[tr.symbol_idx]}" 226 else : 227 label_text = f"{aM.alphabet[tr.symbol_idx]}({pr_str})" 228 229 html_label = ( 230 f'<<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="2">' 231 f'<TR><TD>{label_text}</TD></TR>' 232 f'</TABLE>>' 233 ) 234 235 edge_color = "black" 236 237 if color_edges_by == "category" : 238 symbol = aM.alphabet[ tr.symbol_idx ] 239 if symbol in symbol_categories : 240 cat = symbol_categories[ symbol ] 241 try : 242 cl_idx = categories.index( cat ) 243 edge_color = edge_colors[ cl_idx % n_edge_colors ] 244 except : 245 edge_color = "black" 246 247 elif color_edges_by == "symbol" : 248 edge_color = edge_colors[ tr.symbol_idx % n_edge_colors ] 249 250 if no_labels : 251 html_label = None 252 253 GV.edge( 254 str(u), str(v), 255 label=html_label, 256 headlabel=" ", 257 fontsize=fontsize, 258 fontname='Times-Italic', 259 #labelfloat="true", 260 fontcolor="#1f1f1f", 261 color=edge_color, 262 arrowhead='nonenonenormal' 263 ) 264 265 if view == True : 266 267 GV.graph_attr.update(dpi='150') 268 png_bytes = GV.pipe(format="png") 269 buf = BytesIO( png_bytes ) 270 img = mpimg.imread( buf ) 271 272 plt.figure(figsize=(8, 6)) 273 plt.imshow(img) 274 plt.axis('off') 275 plt.tight_layout() 276 plt.show() 277 278 if output_dir is not None : 279 GV.attr(bgcolor='transparent') 280 GV.render( title, directory=output_dir, view=False, format=format, cleanup=True)
def
animated_distance_plot( vectors, max_res=1440, stride=7, apply_threshold=False, threshold=0.01):
282def animated_distance_plot( vectors, max_res=1440, stride=7, apply_threshold=False, threshold=0.01 ) : 283 284 use_cuda = False # cpx is not None 285 286 n_vectors = len( vectors ) 287 max_res = min( n_vectors, max_res ) 288 289 if use_cuda : 290 state_vectors = cp.asarray( vectors[ 0:max_states, : ] ) 291 else : 292 state_vectors = vectors 293 294 imgs = [] 295 for start in range( 0, n_vectors, stride ) : 296 297 if start % (stride*10) == 0 : 298 print( f"offset: {start}" ) 299 300 end = min( start+max_res, n_vectors ) 301 302 if use_cuda : 303 distance_plot = squareform( 304 cpx.scipy.spatial.distance.pdist( state_vectors[ start:end, : ], metric='jensenshannon' ).get() 305 ) 306 else : 307 distance_plot = squareform( 308 pdist( state_vectors[ start:end, : ], metric='jensenshannon' ) 309 ) 310 311 if len( distance_plot ) < max_res : 312 distance_plot = np.pad( 313 distance_plot, 314 ( ( 0, max_res-len( distance_plot ) ), (0, max_res-len( distance_plot )) ), 315 mode='constant' ) 316 317 if apply_threshold : 318 distance_plot = np.where( distance_plot > threshold, 1, 0 ).astype( np.uint8 ) 319 imgs.append( distance_plot ) 320 else : 321 imgs.append( distance_plot.astype( np.float16 ) ) 322 323 print( f"Rendering {len(imgs)} frames." ) 324 to_video( imgs, fps=60 )
def
to_video( images: list[numpy.ndarray], output_path: str = 'output.mp4', fps: int = 3, cmap: str | matplotlib.colors.Colormap = 'plasma_r') -> None:
326def to_video( 327 images : list[np.ndarray], 328 output_path : str = "output.mp4", 329 fps : int = 3, 330 cmap : str | Colormap = "plasma_r", 331) -> None: 332 if not images: 333 raise ValueError("images list is empty") 334 335 colormap = colormaps[cmap] if isinstance(cmap, str) else cmap 336 337 # Global min/max so colormapping is consistent across frames 338 global_min = min(f.min() for f in images) 339 global_max = max(f.max() for f in images) 340 341 if global_min == global_max: 342 raise ValueError("All frames are constant, cannot normalize") 343 344 norm = Normalize( vmin=global_min, vmax=global_max ) 345 346 with imageio.get_writer(output_path, fps=fps) as writer: 347 for frame in images: 348 rgba = colormap(norm(frame)) 349 rgb = (rgba[..., :3] * 255).astype(np.uint8) 350 writer.append_data(rgb)
def
triangular_barycentric_plot( coords: numpy.ndarray, coord_classes: numpy.ndarray, axis_labels: tuple[str, str, str], title: str = ''):
352def triangular_barycentric_plot( 353 coords : np.ndarray, 354 coord_classes : np.ndarray, 355 axis_labels : tuple[str, str, str], 356 title : str="" ) : 357 358 figure, tax = ternary.figure(scale=40) 359 360 tax.boundary(linewidth=2.0) 361 362 fontsize = 20 363 tax.set_title("Simplex Boundary and Gridlines", fontsize=fontsize) 364 365 tax.left_axis_label( axis_labels[0], fontsize=fontsize) 366 tax.right_axis_label( axis_labels[1], fontsize=fontsize) 367 tax.bottom_axis_label( axis_labels[2], fontsize=fontsize) 368 369 # Remove default Matplotlib Axes 370 tax.clear_matplotlib_ticks() 371 372 tax.scatter( coords, marker='*' ) 373 374 ternary.plt.show()