Peano
Loading...
Searching...
No Matches
TreeVisualizer.py
Go to the documentation of this file.
1# This file is part of the ExaHyPE2 project. For conditions of distribution and
2# use, please see the copyright notice at www.peano-framework.org
3import numpy as np
4import matplotlib.pyplot as plt
5
6# Try to import plotly for HTML export
7try:
8 import plotly.graph_objects as go
9 import plotly.express as px
10 from plotly.subplots import make_subplots
11 PLOTLY_AVAILABLE = True
12except ImportError:
13 PLOTLY_AVAILABLE = False
14
15"""
16Tree Visualisation Module for Peano Load Balancing API
17
18This module provides comprehensive visualisation capabilities for adaptive mesh trees,
19generating both static and interactive representations of tree structures and load
20balancing results.
21
22## Visualisation Formats
23
24### Static Formats (Matplotlib)
25- **PDF**: Vector format for publications, presentations, and high-quality prints
26- **PNG**: Raster format for web display, documentation, and quick previews
27- **Resolution**: 300 DPI for publication-quality output
28- **Layout**: Automatic subplot arrangement for multi-level display
29
30### Interactive Format (Plotly)
31- **HTML**: Self-contained interactive visualisation with JavaScript controls
32- **3D Rotation**: Full 3D manipulation for spatial understanding
33- **Zoom/Pan**: Detailed inspection of specific tree regions
34- **Hover Information**: Real-time display of node coordinates and subtree assignments
35- **Responsive Design**: Adapts to browser window size for optimal viewing
36
37## Technical Implementation
38
39### Colour Management
40- **Subtree Colors**: 20 distinctive colors for subtree differentiation
41- **Colour Cycling**: Automatic colour assignment for large numbers of subtrees
42- **Accessibility**: High contrast ratios for clear distinction
43
44### Marker Sizing
45- **Level-based Scaling**: Larger markers for coarser levels, smaller for finer levels
46- **Mathematical Formula**: size = max(base_size, scale_factor/(level+1)^exponent)
47- **Dimension Adaptation**: Different scaling for 2D vs 3D visualisations
48
49## Usage Patterns
50
51### Basic Visualisation
52```python
53from visualize.TreeVisualizer import TreeVisualizer
54
55# Generate all formats with automatic naming
56TreeVisualizer(tree, dimensions=3, filename="analysis_results")
57# Outputs: analysis_results.pdf, analysis_results.png, analysis_results.html
58```
59
60### Format-specific Generation
61```python
62# Only static formats (when Plotly not available)
63TreeVisualizer(tree, dimensions=2, filename="static_only")
64
65# Interactive-only (custom implementation)
66from visualize.TreeVisualizer import save_interactive_html
67save_interactive_html(tree, dimensions=3, filename="interactive_analysis")
68```
69
70## Integration with Load Balancing Workflow
71
72The visualization system integrates seamlessly with the offline decomposition process:
73
741. **Original Tree Visualisation**: Shows initial mesh structure before optimisation
752. **Split Tree Visualisation**: Displays optimised load distribution after splitting
763. **Comparison Analysis**: Side-by-side comparison of before/after states
77
78## Error Handling and Dependencies
79
80### Dependency Management
81- **Plotly Availability**: Graceful degradation when Plotly not installed
82- **Matplotlib Requirements**: Robust handling of display backend issues
83- **File System**: Proper error handling for permission and disk space issues
84"""
85
86def save_interactive_html(input_tree, dimensions, filename):
87 """
88 Save an interactive HTML visualization of the tree structure using plotly.
89 Creates separate subplots for each level with distinctive colors for subtrees.
90
91 Args:
92 input_tree: Tree object to visualize
93 dimensions: 2 or 3 for 2D/3D visualization
94 filename: Base filename for output files (without extension)
95
96 Returns:
97 None (saves an HTML file)
98 """
99 if not PLOTLY_AVAILABLE:
100 print("Plotly not available - skipping HTML export")
101 return
102
103 # Define distinctive colors for subtrees (up to 20 subtrees)
104 subtree_colors = [
105 '#FF0000', '#0000FF', '#00FF00', '#FFA500', '#800080', # Red, Blue, Green, Orange, Purple
106 '#A52A2A', '#FFC0CB', '#808080', '#808000', '#00FFFF', # Brown, Pink, Gray, Olive, Cyan
107 '#FF1493', '#32CD32', '#FFD700', '#DC143C', '#4169E1', # DeepPink, LimeGreen, Gold, Crimson, RoyalBlue
108 '#FF8C00', '#9932CC', '#2E8B57', '#FF6347', '#4682B4' # DarkOrange, DarkOrchid, SeaGreen, Tomato, SteelBlue
109 ]
110
111 # Track which subtrees we've seen to manage legend display
112 subtrees_shown_in_legend = set()
113
114 if dimensions == 3:
115 # Create 3D subplots for each level
116 fig = make_subplots(
117 rows=1,
118 cols=input_tree._max_level,
119 subplot_titles=[f"Level {i+1}" for i in range(input_tree._max_level)],
120 specs=[[{'type': 'scatter3d'} for _ in range(input_tree._max_level)]]
121 )
122
123 for level in range(input_tree._max_level):
124 plot_array = input_tree._tree_arrays[level]
125
126 # Group nodes by subtree for distinctive coloring
127 subtree_data = {}
128
129 for a in range(3**(level+1)):
130 for b in range(3**(level+1)):
131 for c in range(3**(level+1)):
132 node, subtree_id = plot_array[a][b][c]
133 if node != 0:
134 if subtree_id not in subtree_data:
135 subtree_data[subtree_id] = {'x': [], 'y': [], 'z': []}
136 subtree_data[subtree_id]['x'].append(a)
137 subtree_data[subtree_id]['y'].append(b)
138 subtree_data[subtree_id]['z'].append(c)
139
140 # Add traces for each subtree at this level
141 for subtree_id, coords in subtree_data.items():
142 color = subtree_colors[subtree_id % len(subtree_colors)]
143 marker_size = max(3, 20/(level+1)**1.5)
144
145 # Determine if this subtree should show in legend
146 show_in_legend = subtree_id not in subtrees_shown_in_legend
147 if show_in_legend:
148 subtrees_shown_in_legend.add(subtree_id)
149
150 fig.add_trace(
151 go.Scatter3d(
152 x=coords['x'],
153 y=coords['y'],
154 z=coords['z'],
155 mode='markers',
156 marker=dict(
157 size=marker_size,
158 color=color,
159 opacity=0.8,
160 symbol='square',
161 line=dict(width=1, color='black')
162 ),
163 name=f"Subtree {subtree_id}", # Simplified name for cleaner legend
164 hovertemplate=f"<b>Level {level+1} - Subtree {subtree_id}</b><br>" +
165 "X: %{x}<br>" +
166 "Y: %{y}<br>" +
167 "Z: %{z}<br>" +
168 f"Marker size: {marker_size:.1f}<br>" +
169 "<extra></extra>",
170 legendgroup=f"subtree_{subtree_id}", # Group by subtree across levels
171 showlegend=show_in_legend # Only show each subtree once in legend
172 ),
173 row=1, col=level+1
174 )
175
176 # Update 3D layout with full width
177 fig.update_layout(
178 title=dict(
179 text=f"Interactive 3D Tree Structure - {input_tree._name}<br>" +
180 f"<span style='font-size: 12px'>Use mouse to rotate, zoom, and pan each level</span>",
181 font=dict(size=16)
182 ),
183 width=None, # Let it use full browser width
184 height=800, # Increased height for better visibility
185 margin=dict(t=100, b=50, l=20, r=20), # Reduced left/right margins
186 autosize=True # Enable automatic sizing
187 )
188
189 # Update individual 3D scenes
190 for i in range(input_tree._max_level):
191 scene_name = 'scene' if i == 0 else f'scene{i+1}'
192 fig.update_layout(**{
193 scene_name: dict(
194 xaxis_title="X Coordinate",
195 yaxis_title="Y Coordinate",
196 zaxis_title="Z Coordinate",
197 xaxis=dict(range=[0, 3**(i+1)-1]),
198 yaxis=dict(range=[0, 3**(i+1)-1]),
199 zaxis=dict(range=[0, 3**(i+1)-1]),
200 aspectmode='cube',
201 camera=dict(
202 eye=dict(x=1.5, y=1.5, z=1.5)
203 )
204 )
205 })
206
207 elif dimensions == 2:
208 # Create 2D subplots for each level
209 fig = make_subplots(
210 rows=1,
211 cols=input_tree._max_level,
212 subplot_titles=[f"Level {i+1}" for i in range(input_tree._max_level)]
213 )
214
215 for level in range(input_tree._max_level):
216 plot_array = input_tree._tree_arrays[level]
217
218 # Group nodes by subtree for distinctive coloring
219 subtree_data = {}
220
221 for a in range(3**(level+1)):
222 for b in range(3**(level+1)):
223 node, subtree_id = plot_array[a][b]
224 if node != 0:
225 if subtree_id not in subtree_data:
226 subtree_data[subtree_id] = {'x': [], 'y': []}
227 subtree_data[subtree_id]['x'].append(a)
228 subtree_data[subtree_id]['y'].append(b)
229
230 # Add traces for each subtree at this level
231 for subtree_id, coords in subtree_data.items():
232 color = subtree_colors[subtree_id % len(subtree_colors)]
233 marker_size = max(5, 30/(level+1)**2)
234
235 # Determine if this subtree should show in legend
236 show_in_legend = subtree_id not in subtrees_shown_in_legend
237 if show_in_legend:
238 subtrees_shown_in_legend.add(subtree_id)
239
240 fig.add_trace(
241 go.Scatter(
242 x=coords['x'],
243 y=coords['y'],
244 mode='markers',
245 marker=dict(
246 size=marker_size,
247 color=color,
248 opacity=0.8,
249 symbol='square',
250 line=dict(width=1, color='black')
251 ),
252 name=f"Subtree {subtree_id}", # Simplified name for cleaner legend
253 hovertemplate=f"<b>Level {level+1} - Subtree {subtree_id}</b><br>" +
254 "X: %{x}<br>" +
255 "Y: %{y}<br>" +
256 f"Marker size: {marker_size:.1f}<br>" +
257 "<extra></extra>",
258 legendgroup=f"subtree_{subtree_id}", # Group by subtree across levels
259 showlegend=show_in_legend # Only show each subtree once in legend
260 ),
261 row=1, col=level+1
262 )
263
264 # Update 2D layout with full width
265 fig.update_layout(
266 title=dict(
267 text=f"Interactive 2D Tree Structure - {input_tree._name}<br>" +
268 f"<span style='font-size: 12px'>Use mouse to zoom and pan each level</span>",
269 font=dict(size=16)
270 ),
271 width=None, # Let it use full browser width
272 height=700, # Increased height for better visibility
273 margin=dict(t=100, b=50, l=20, r=20), # Reduced left/right margins
274 autosize=True # Enable automatic sizing
275 )
276
277 # Update individual 2D axes
278 for i in range(input_tree._max_level):
279 fig.update_xaxes(
280 title_text="X Coordinate",
281 range=[-1, 3**(i+1)],
282 row=1, col=i+1
283 )
284 fig.update_yaxes(
285 title_text="Y Coordinate",
286 range=[-1, 3**(i+1)],
287 row=1, col=i+1
288 )
289
290 # Add annotations with tree information
291 fig.add_annotation(
292 xref="paper", yref="paper",
293 x=0.02, y=0.98,
294 text=f"<b>Tree Statistics:</b><br>" +
295 f"Name: {input_tree._name}<br>" +
296 f"Dimensions: {dimensions}D<br>" +
297 f"Max Level: {input_tree._max_level}<br>" +
298 f"Total Leaves: {int(input_tree._leaf_count)}<br>" +
299 f"Subtrees: {input_tree._subtree_count}<br>" +
300 f"Split Pattern: {[int(x) for x in input_tree._split_pattern]}",
301 showarrow=False,
302 font=dict(size=10),
303 bgcolor="rgba(255,255,255,0.9)",
304 bordercolor="black",
305 borderwidth=1,
306 align="left"
307 )
308
309 # Save HTML file with full width configuration
310 config = {
311 'displayModeBar': True,
312 'displaylogo': False,
313 'modeBarButtonsToRemove': ['pan2d', 'lasso2d', 'select2d'],
314 'responsive': True # Make plot responsive to window size
315 }
316
317 fig.write_html(
318 f"{filename}.html",
319 config=config,
320 div_id="plotly-div",
321 include_plotlyjs=True,
322 full_html=True
323 )
324
325 print(f"Interactive HTML visualization saved as {filename}.html")
326 print(f"Features:")
327 print(f" • Mouse rotation and zoom ({'3D rotation' if dimensions == 3 else '2D zoom/pan'})")
328 print(f" • Distinctive colors for {input_tree._subtree_count} subtrees")
329 print(f" • Separate subplots for each of {input_tree._max_level} levels")
330 print(f" • Hover information for each node")
331 print(f" • Full browser width layout with responsive design")
332
333
334def TreeVisualizer(input_tree, dimensions, filename="TreeStructure"):
335 """
336 Visualize tree structure with customizable output filename.
337 Saves in PNG, PDF, and interactive HTML formats.
338
339 Args:
340 input_tree: Tree object to visualize
341 dimensions: 2 or 3 for 2D/3D visualization
342 filename: Base filename for output files (without extension)
343
344 Returns:
345 None (saves files and displays plot)
346 """
347 # Create matplotlib visualization
348 fig = plt.figure(figsize=(9*input_tree._max_level, 8))
349 axes = []
350
351 for level in range(input_tree._max_level):
352 plot_array = input_tree._tree_arrays[level]
353
354 indices = []
355 subtree_ids = []
356 # extract the existing nodes and their indices from the tree arrays and
357 # plot them in a 3D/2D scatter
358 if dimensions == 3:
359 for a in range(3**(level+1)):
360 for b in range(3**(level+1)):
361 for c in range(3**(level+1)):
362 node, subtree_id = plot_array[a][b][c]
363 if node != 0:
364 indices.append((a, b, c))
365 subtree_ids.append(subtree_id)
366
367 if len(indices) > 0:
368 indices = np.array(indices)
369 subtree_ids = np.array(subtree_ids)
370
371 axes.append(fig.add_subplot(1, input_tree._max_level, level+1, projection='3d'))
372 scatter = axes[level].scatter(indices[:, 0], indices[:, 1], indices[:, 2],
373 c=subtree_ids, cmap='tab10', s=1000/(level+1)**3,
374 depthshade=True, marker='s')
375
376 axes[level].set_xlabel('X')
377 axes[level].set_ylabel('Y')
378 axes[level].set_zlabel('Z')
379
380 axes[level].set_xlim([0, 3**(1+level)-1])
381 axes[level].set_ylim([0, 3**(1+level)-1])
382 axes[level].set_zlim([0, 3**(1+level)-1])
383 axes[level].set_title("Level="+str(level+1))
384
385 elif dimensions == 2:
386 for a in range(3**(level+1)):
387 for b in range(3**(level+1)):
388 node, subtree_id = plot_array[a][b]
389 if node != 0:
390 indices.append((a, b))
391 subtree_ids.append(subtree_id)
392
393 if len(indices) > 0:
394 indices = np.array(indices)
395 subtree_ids = np.array(subtree_ids)
396
397 axes.append(fig.add_subplot(1, input_tree._max_level, level+1))
398 scatter = axes[level].scatter(indices[:, 0], indices[:, 1],
399 c=subtree_ids, cmap='tab10', s=5000/(level+1)**4,
400 marker='s')
401
402 axes[level].set_xlabel('X')
403 axes[level].set_ylabel('Y')
404
405 axes[level].set_xlim([-1, 3**(1+level)])
406 axes[level].set_ylim([-1, 3**(1+level)])
407 axes[level].set_title("Level="+str(level+1))
408
409 # Save static formats (matplotlib)
410 plt.tight_layout()
411 plt.savefig(f"{filename}.pdf", dpi=300, bbox_inches='tight')
412 plt.savefig(f"{filename}.png", dpi=300, bbox_inches='tight')
413 print(f"Static visualizations saved as {filename}.pdf and {filename}.png")
414
415 # Save interactive HTML format (plotly)
416 if PLOTLY_AVAILABLE:
417 save_interactive_html(input_tree, dimensions, filename)
418 else:
419 print("Plotly not available. Install with: pip install plotly")
420 print("HTML export skipped - only PDF and PNG saved")
421
422 # Show the matplotlib plot
423 plt.show()
save_interactive_html(input_tree, dimensions, filename)
Save an interactive HTML visualization of the tree structure using plotly.