3ba900982735b0657388b150ccb4b62480ce79a3
java/com.sap.sailing.windestimation.lab/python/MST_GRAPH_VISUALIZATION_README.md
| ... | ... | @@ -0,0 +1,171 @@ |
| 1 | +# MST Maneuver Graph Visualization |
|
| 2 | + |
|
| 3 | +This directory contains tools for visualizing the MST (Minimum Spanning Tree) |
|
| 4 | +maneuver graph used in wind estimation. |
|
| 5 | + |
|
| 6 | +## Overview |
|
| 7 | + |
|
| 8 | +The MST maneuver graph is a tree structure where: |
|
| 9 | +- Each tree node (`MstGraphLevel`) represents a detected maneuver with its timestamp and position |
|
| 10 | +- Each tree node has 4 "compartments" - one for each possible maneuver classification: |
|
| 11 | + - **TACK** (green): Tacking maneuver |
|
| 12 | + - **JIBE** (blue): Jibing/gybing maneuver |
|
| 13 | + - **HEAD_UP** (orange): Heading up toward the wind |
|
| 14 | + - **BEAR_AWAY** (purple): Bearing away from the wind |
|
| 15 | +- Each compartment shows: |
|
| 16 | + - Classification confidence (probability this maneuver is of this type) |
|
| 17 | + - Estimated wind direction range |
|
| 18 | +- Edges connect compartments between adjacent tree levels with transition probabilities |
|
| 19 | +- The "best path" through the inner graph is highlighted - this determines the final maneuver classifications |
|
| 20 | + |
|
| 21 | +## Files |
|
| 22 | + |
|
| 23 | +### Java (Exporter) |
|
| 24 | + |
|
| 25 | +- `MstGraphExporter.java` - Exports the MST graph to JSON format |
|
| 26 | +- `MstGraphExportHelper.java` - Helper class with convenience methods |
|
| 27 | + |
|
| 28 | +### Python (Visualizers) |
|
| 29 | + |
|
| 30 | +- `mst_graph_visualizer.py` - Basic matplotlib visualization |
|
| 31 | +- `mst_graph_visualizer_graphviz.py` - Advanced graphviz visualization (recommended) |
|
| 32 | + |
|
| 33 | +## Usage |
|
| 34 | + |
|
| 35 | +### Step 1: Export the graph from Java |
|
| 36 | + |
|
| 37 | +In your Java code (e.g., in a test or debug session): |
|
| 38 | + |
|
| 39 | +```java |
|
| 40 | +import com.sap.sailing.windestimation.aggregator.msthmm.MstGraphExportHelper; |
|
| 41 | + |
|
| 42 | +// After building your MST graph: |
|
| 43 | +MstManeuverGraphComponents graphComponents = mstManeuverGraphGenerator.parseGraph(); |
|
| 44 | + |
|
| 45 | +// Export to JSON file: |
|
| 46 | +MstGraphExportHelper.exportToFile(graphComponents, transitionProbabilitiesCalculator, |
|
| 47 | + "/tmp/mst_graph.json"); |
|
| 48 | +``` |
|
| 49 | + |
|
| 50 | +### Step 2: Visualize with Python |
|
| 51 | + |
|
| 52 | +Install dependencies: |
|
| 53 | +```bash |
|
| 54 | +pip install matplotlib graphviz |
|
| 55 | +``` |
|
| 56 | + |
|
| 57 | +For graphviz, also install the system package: |
|
| 58 | +```bash |
|
| 59 | +# Ubuntu/Debian |
|
| 60 | +sudo apt-get install graphviz |
|
| 61 | + |
|
| 62 | +# macOS |
|
| 63 | +brew install graphviz |
|
| 64 | +``` |
|
| 65 | + |
|
| 66 | +Run the visualizer: |
|
| 67 | +```bash |
|
| 68 | +# Basic tree visualization |
|
| 69 | +python mst_graph_visualizer_graphviz.py /tmp/mst_graph.json output.pdf |
|
| 70 | + |
|
| 71 | +# Detailed compartment-level view (for small graphs) |
|
| 72 | +python mst_graph_visualizer_graphviz.py /tmp/mst_graph.json output.pdf --detailed |
|
| 73 | + |
|
| 74 | +# Interactive matplotlib visualization |
|
| 75 | +python mst_graph_visualizer.py /tmp/mst_graph.json |
|
| 76 | +``` |
|
| 77 | + |
|
| 78 | +## Understanding the Visualization |
|
| 79 | + |
|
| 80 | +### Node Structure |
|
| 81 | + |
|
| 82 | +Each node box contains 4 compartments (T, J, H, B): |
|
| 83 | +``` |
|
| 84 | ++------+------+------+------+ |
|
| 85 | +| T | J | H | B | |
|
| 86 | +| 0.68 | 0.05 | 0.13 | 0.13 | |
|
| 87 | +| 224° | 44° | 308°±51° | 89°±51° | |
|
| 88 | ++------+------+------+------+ |
|
| 89 | + 15:30:41 |
|
| 90 | +``` |
|
| 91 | + |
|
| 92 | +- First row: Maneuver type abbreviation |
|
| 93 | +- Second row: Classification confidence |
|
| 94 | +- Third row: Wind direction estimate (or range for HEAD_UP/BEAR_AWAY) |
|
| 95 | +- Below: Timestamp |
|
| 96 | + |
|
| 97 | +### Edge Colors |
|
| 98 | + |
|
| 99 | +- **Red**: Best path edges (selected by Dijkstra) |
|
| 100 | +- **Green**: High transition probability |
|
| 101 | +- **Gray**: Lower transition probability |
|
| 102 | + |
|
| 103 | +### Best Path Highlighting |
|
| 104 | + |
|
| 105 | +The best path represents the most likely sequence of maneuver classifications |
|
| 106 | +considering both: |
|
| 107 | +1. Individual classification confidences |
|
| 108 | +2. Transition probabilities (how likely is it for the wind to change this much given the time/distance between maneuvers) |
|
| 109 | + |
|
| 110 | +## JSON Format |
|
| 111 | + |
|
| 112 | +The exported JSON has this structure: |
|
| 113 | + |
|
| 114 | +```json |
|
| 115 | +{ |
|
| 116 | + "nodes": [ |
|
| 117 | + { |
|
| 118 | + "id": 0, |
|
| 119 | + "depth": 0, |
|
| 120 | + "timestamp": "2011-06-23 15:30:40.500", |
|
| 121 | + "position": {"lat": 54.493, "lon": 10.197}, |
|
| 122 | + "distanceToParent": 0.0, |
|
| 123 | + "compartments": [ |
|
| 124 | + { |
|
| 125 | + "type": "TACK", |
|
| 126 | + "confidence": 0.17, |
|
| 127 | + "windRangeFrom": 224.18, |
|
| 128 | + "windRangeWidth": 0.0, |
|
| 129 | + "windEstimate": 224.18, |
|
| 130 | + "tackAfter": "PORT" |
|
| 131 | + }, |
|
| 132 | + // ... JIBE, HEAD_UP, BEAR_AWAY |
|
| 133 | + ] |
|
| 134 | + } |
|
| 135 | + ], |
|
| 136 | + "edges": [ |
|
| 137 | + { |
|
| 138 | + "from": 0, |
|
| 139 | + "fromType": "TACK", |
|
| 140 | + "to": 1, |
|
| 141 | + "toType": "TACK", |
|
| 142 | + "transitionProbability": 0.023, |
|
| 143 | + "distance": 12.5, |
|
| 144 | + "isBestPath": true |
|
| 145 | + } |
|
| 146 | + ], |
|
| 147 | + "bestPaths": { |
|
| 148 | + "0": "JIBE", |
|
| 149 | + "1": "TACK" |
|
| 150 | + } |
|
| 151 | +} |
|
| 152 | +``` |
|
| 153 | + |
|
| 154 | +## Troubleshooting |
|
| 155 | + |
|
| 156 | +### Large graphs are slow to render |
|
| 157 | + |
|
| 158 | +For graphs with >100 nodes, the visualization limits the display to the first N nodes. |
|
| 159 | +Use the `--detailed` flag only for small subtrees. |
|
| 160 | + |
|
| 161 | +### Graphviz not found |
|
| 162 | + |
|
| 163 | +Make sure graphviz is installed at the system level, not just the Python package: |
|
| 164 | +```bash |
|
| 165 | +which dot # Should return path to dot executable |
|
| 166 | +``` |
|
| 167 | + |
|
| 168 | +### Edges not showing |
|
| 169 | + |
|
| 170 | +Edges with very low transition probabilities are filtered out by default. |
|
| 171 | +Use `show_low_prob_edges=True` in Python to see all edges. |
java/com.sap.sailing.windestimation.lab/python/mst_graph_visualizer.py
| ... | ... | @@ -0,0 +1,338 @@ |
| 1 | +#!/usr/bin/env python3 |
|
| 2 | +""" |
|
| 3 | +MST Maneuver Graph Visualization Script |
|
| 4 | + |
|
| 5 | +This script visualizes the Minimum Spanning Tree (MST) maneuver graph structure |
|
| 6 | +exported from the Java MstGraphExporter. It shows: |
|
| 7 | +- Tree structure of MstGraphLevel nodes |
|
| 8 | +- Each node as a box with 4 compartments (TACK, JIBE, HEAD_UP, BEAR_AWAY) |
|
| 9 | +- Classification confidence and wind direction for each compartment |
|
| 10 | +- Edges between compartments with transition probabilities |
|
| 11 | +- Best path highlighting |
|
| 12 | + |
|
| 13 | +Usage: |
|
| 14 | + python mst_graph_visualizer.py <input_json_file> [output_file] |
|
| 15 | + |
|
| 16 | +Dependencies: |
|
| 17 | + pip install matplotlib networkx |
|
| 18 | +""" |
|
| 19 | + |
|
| 20 | +import json |
|
| 21 | +import sys |
|
| 22 | +import math |
|
| 23 | +from collections import defaultdict |
|
| 24 | + |
|
| 25 | +import matplotlib.pyplot as plt |
|
| 26 | +import matplotlib.patches as patches |
|
| 27 | +from matplotlib.patches import FancyBboxPatch, FancyArrowPatch |
|
| 28 | +import matplotlib.colors as mcolors |
|
| 29 | + |
|
| 30 | +# Maneuver type colors |
|
| 31 | +TYPE_COLORS = { |
|
| 32 | + 'TACK': '#4CAF50', # Green |
|
| 33 | + 'JIBE': '#2196F3', # Blue |
|
| 34 | + 'HEAD_UP': '#FF9800', # Orange |
|
| 35 | + 'BEAR_AWAY': '#9C27B0', # Purple |
|
| 36 | +} |
|
| 37 | + |
|
| 38 | +TYPE_ORDER = ['TACK', 'JIBE', 'HEAD_UP', 'BEAR_AWAY'] |
|
| 39 | + |
|
| 40 | + |
|
| 41 | +class MstGraphVisualizer: |
|
| 42 | + def __init__(self, data): |
|
| 43 | + self.data = data |
|
| 44 | + self.nodes = {n['id']: n for n in data['nodes']} |
|
| 45 | + self.edges = data['edges'] |
|
| 46 | + self.best_paths = data.get('bestPaths', {}) |
|
| 47 | + |
|
| 48 | + # Layout parameters |
|
| 49 | + self.node_width = 4.0 |
|
| 50 | + self.node_height = 1.2 |
|
| 51 | + self.compartment_width = self.node_width / 4 |
|
| 52 | + self.horizontal_spacing = 1.5 |
|
| 53 | + self.vertical_spacing = 2.5 |
|
| 54 | + |
|
| 55 | + # Calculate layout |
|
| 56 | + self.node_positions = {} |
|
| 57 | + self.calculate_layout() |
|
| 58 | + |
|
| 59 | + def calculate_layout(self): |
|
| 60 | + """Calculate x,y positions for each node based on tree structure.""" |
|
| 61 | + # Group nodes by depth |
|
| 62 | + depth_groups = defaultdict(list) |
|
| 63 | + for node in self.data['nodes']: |
|
| 64 | + depth_groups[node['depth']].append(node['id']) |
|
| 65 | + |
|
| 66 | + max_depth = max(depth_groups.keys()) if depth_groups else 0 |
|
| 67 | + |
|
| 68 | + # Assign positions - breadth-first layout |
|
| 69 | + for depth in range(max_depth + 1): |
|
| 70 | + nodes_at_depth = depth_groups[depth] |
|
| 71 | + n = len(nodes_at_depth) |
|
| 72 | + for i, node_id in enumerate(nodes_at_depth): |
|
| 73 | + # Center nodes horizontally at each level |
|
| 74 | + x = (i - (n - 1) / 2) * (self.node_width + self.horizontal_spacing) |
|
| 75 | + y = -depth * (self.node_height + self.vertical_spacing) |
|
| 76 | + self.node_positions[node_id] = (x, y) |
|
| 77 | + |
|
| 78 | + def draw_node(self, ax, node_id): |
|
| 79 | + """Draw a single node as a box with 4 compartments.""" |
|
| 80 | + node = self.nodes[node_id] |
|
| 81 | + x, y = self.node_positions[node_id] |
|
| 82 | + |
|
| 83 | + # Draw outer box |
|
| 84 | + outer_rect = FancyBboxPatch( |
|
| 85 | + (x - self.node_width/2, y - self.node_height/2), |
|
| 86 | + self.node_width, self.node_height, |
|
| 87 | + boxstyle="round,pad=0.02,rounding_size=0.1", |
|
| 88 | + facecolor='white', |
|
| 89 | + edgecolor='black', |
|
| 90 | + linewidth=1.5 |
|
| 91 | + ) |
|
| 92 | + ax.add_patch(outer_rect) |
|
| 93 | + |
|
| 94 | + # Check if this node has a best classification |
|
| 95 | + best_type = self.best_paths.get(str(node_id)) |
|
| 96 | + |
|
| 97 | + # Draw compartments |
|
| 98 | + compartments = {c['type']: c for c in node['compartments']} |
|
| 99 | + for i, type_name in enumerate(TYPE_ORDER): |
|
| 100 | + comp = compartments.get(type_name) |
|
| 101 | + if comp is None: |
|
| 102 | + continue |
|
| 103 | + |
|
| 104 | + cx = x - self.node_width/2 + i * self.compartment_width |
|
| 105 | + cy = y - self.node_height/2 |
|
| 106 | + |
|
| 107 | + # Highlight best type |
|
| 108 | + is_best = (best_type == type_name) |
|
| 109 | + |
|
| 110 | + # Draw compartment background |
|
| 111 | + color = TYPE_COLORS.get(type_name, 'gray') |
|
| 112 | + alpha = 0.8 if is_best else 0.3 |
|
| 113 | + comp_rect = patches.Rectangle( |
|
| 114 | + (cx, cy), |
|
| 115 | + self.compartment_width, self.node_height, |
|
| 116 | + facecolor=color, |
|
| 117 | + alpha=alpha, |
|
| 118 | + edgecolor='black' if is_best else 'gray', |
|
| 119 | + linewidth=2 if is_best else 0.5 |
|
| 120 | + ) |
|
| 121 | + ax.add_patch(comp_rect) |
|
| 122 | + |
|
| 123 | + # Draw text - type abbreviation |
|
| 124 | + type_abbrev = type_name[:1] # T, J, H, B |
|
| 125 | + ax.text(cx + self.compartment_width/2, cy + self.node_height * 0.75, |
|
| 126 | + type_abbrev, ha='center', va='center', fontsize=8, fontweight='bold') |
|
| 127 | + |
|
| 128 | + # Draw confidence |
|
| 129 | + conf_text = f"{comp['confidence']:.2f}" |
|
| 130 | + ax.text(cx + self.compartment_width/2, cy + self.node_height * 0.5, |
|
| 131 | + conf_text, ha='center', va='center', fontsize=6) |
|
| 132 | + |
|
| 133 | + # Draw wind direction |
|
| 134 | + wind_est = comp.get('windEstimate', comp.get('windRangeFrom', 0)) |
|
| 135 | + wind_width = comp.get('windRangeWidth', 0) |
|
| 136 | + if wind_width < 1: |
|
| 137 | + wind_text = f"{wind_est:.0f}°" |
|
| 138 | + else: |
|
| 139 | + wind_text = f"{wind_est:.0f}±{wind_width/2:.0f}°" |
|
| 140 | + ax.text(cx + self.compartment_width/2, cy + self.node_height * 0.25, |
|
| 141 | + wind_text, ha='center', va='center', fontsize=5) |
|
| 142 | + |
|
| 143 | + # Draw timestamp label below node |
|
| 144 | + timestamp = node.get('timestamp', '') |
|
| 145 | + # Extract just time portion |
|
| 146 | + if ' ' in timestamp: |
|
| 147 | + time_part = timestamp.split(' ')[1][:8] # HH:MM:SS |
|
| 148 | + else: |
|
| 149 | + time_part = timestamp |
|
| 150 | + ax.text(x, y - self.node_height/2 - 0.15, time_part, |
|
| 151 | + ha='center', va='top', fontsize=5, color='gray') |
|
| 152 | + |
|
| 153 | + def draw_edge(self, ax, edge): |
|
| 154 | + """Draw an edge between two compartments.""" |
|
| 155 | + from_id = edge['from'] |
|
| 156 | + to_id = edge['to'] |
|
| 157 | + from_type = edge['fromType'] |
|
| 158 | + to_type = edge['toType'] |
|
| 159 | + trans_prob = edge['transitionProbability'] |
|
| 160 | + is_best = edge.get('isBestPath', False) |
|
| 161 | + |
|
| 162 | + # Get compartment positions |
|
| 163 | + from_x, from_y = self.node_positions[from_id] |
|
| 164 | + to_x, to_y = self.node_positions[to_id] |
|
| 165 | + |
|
| 166 | + from_type_idx = TYPE_ORDER.index(from_type) |
|
| 167 | + to_type_idx = TYPE_ORDER.index(to_type) |
|
| 168 | + |
|
| 169 | + # Calculate start and end points at compartment centers |
|
| 170 | + start_x = from_x - self.node_width/2 + (from_type_idx + 0.5) * self.compartment_width |
|
| 171 | + start_y = from_y - self.node_height/2 |
|
| 172 | + |
|
| 173 | + end_x = to_x - self.node_width/2 + (to_type_idx + 0.5) * self.compartment_width |
|
| 174 | + end_y = to_y + self.node_height/2 |
|
| 175 | + |
|
| 176 | + # Color based on transition probability and best path status |
|
| 177 | + if is_best: |
|
| 178 | + color = 'red' |
|
| 179 | + linewidth = 2.0 |
|
| 180 | + alpha = 0.9 |
|
| 181 | + else: |
|
| 182 | + # Color from green (high prob) to gray (low prob) |
|
| 183 | + prob_normalized = min(1.0, trans_prob * 100) # Scale up small probabilities |
|
| 184 | + color = plt.cm.RdYlGn(prob_normalized) |
|
| 185 | + linewidth = 0.5 + prob_normalized * 1.5 |
|
| 186 | + alpha = 0.2 + prob_normalized * 0.4 |
|
| 187 | + |
|
| 188 | + # Draw the edge |
|
| 189 | + ax.annotate('', |
|
| 190 | + xy=(end_x, end_y), xytext=(start_x, start_y), |
|
| 191 | + arrowprops=dict( |
|
| 192 | + arrowstyle='-|>', |
|
| 193 | + color=color, |
|
| 194 | + lw=linewidth, |
|
| 195 | + alpha=alpha, |
|
| 196 | + shrinkA=2, shrinkB=2, |
|
| 197 | + connectionstyle="arc3,rad=0.1" if abs(from_type_idx - to_type_idx) > 0 else "arc3,rad=0" |
|
| 198 | + )) |
|
| 199 | + |
|
| 200 | + # Optionally add probability label for best path edges |
|
| 201 | + if is_best: |
|
| 202 | + mid_x = (start_x + end_x) / 2 |
|
| 203 | + mid_y = (start_y + end_y) / 2 |
|
| 204 | + ax.text(mid_x + 0.3, mid_y, f"{trans_prob:.2e}", |
|
| 205 | + fontsize=4, color='red', alpha=0.8) |
|
| 206 | + |
|
| 207 | + def visualize(self, output_file=None, max_nodes=50, show_edges=True, |
|
| 208 | + edge_filter_threshold=0.0001): |
|
| 209 | + """ |
|
| 210 | + Create the visualization. |
|
| 211 | + |
|
| 212 | + Args: |
|
| 213 | + output_file: Path to save the figure (or None to display) |
|
| 214 | + max_nodes: Maximum number of nodes to display (for large graphs) |
|
| 215 | + show_edges: Whether to draw edges |
|
| 216 | + edge_filter_threshold: Only show edges with probability above this |
|
| 217 | + """ |
|
| 218 | + # Limit nodes if graph is too large |
|
| 219 | + nodes_to_draw = list(self.nodes.keys())[:max_nodes] |
|
| 220 | + |
|
| 221 | + # Calculate figure size based on layout |
|
| 222 | + x_coords = [self.node_positions[n][0] for n in nodes_to_draw] |
|
| 223 | + y_coords = [self.node_positions[n][1] for n in nodes_to_draw] |
|
| 224 | + |
|
| 225 | + width = max(x_coords) - min(x_coords) + self.node_width * 2 |
|
| 226 | + height = max(y_coords) - min(y_coords) + self.node_height * 3 |
|
| 227 | + |
|
| 228 | + # Scale figure |
|
| 229 | + scale = 0.8 |
|
| 230 | + fig_width = max(12, width * scale) |
|
| 231 | + fig_height = max(8, height * scale) |
|
| 232 | + |
|
| 233 | + fig, ax = plt.subplots(figsize=(fig_width, fig_height)) |
|
| 234 | + |
|
| 235 | + # Draw edges first (so they're behind nodes) |
|
| 236 | + if show_edges: |
|
| 237 | + # Filter edges to only those involving displayed nodes |
|
| 238 | + relevant_edges = [e for e in self.edges |
|
| 239 | + if e['from'] in nodes_to_draw |
|
| 240 | + and e['to'] in nodes_to_draw |
|
| 241 | + and (e['transitionProbability'] > edge_filter_threshold |
|
| 242 | + or e.get('isBestPath', False))] |
|
| 243 | + |
|
| 244 | + for edge in relevant_edges: |
|
| 245 | + self.draw_edge(ax, edge) |
|
| 246 | + |
|
| 247 | + # Draw nodes |
|
| 248 | + for node_id in nodes_to_draw: |
|
| 249 | + self.draw_node(ax, node_id) |
|
| 250 | + |
|
| 251 | + # Add legend |
|
| 252 | + legend_elements = [ |
|
| 253 | + patches.Patch(facecolor=TYPE_COLORS['TACK'], alpha=0.6, label='TACK'), |
|
| 254 | + patches.Patch(facecolor=TYPE_COLORS['JIBE'], alpha=0.6, label='JIBE'), |
|
| 255 | + patches.Patch(facecolor=TYPE_COLORS['HEAD_UP'], alpha=0.6, label='HEAD_UP'), |
|
| 256 | + patches.Patch(facecolor=TYPE_COLORS['BEAR_AWAY'], alpha=0.6, label='BEAR_AWAY'), |
|
| 257 | + patches.Patch(facecolor='red', alpha=0.6, label='Best Path'), |
|
| 258 | + ] |
|
| 259 | + ax.legend(handles=legend_elements, loc='upper right', fontsize=8) |
|
| 260 | + |
|
| 261 | + # Set axis limits |
|
| 262 | + margin = 2 |
|
| 263 | + ax.set_xlim(min(x_coords) - margin, max(x_coords) + margin) |
|
| 264 | + ax.set_ylim(min(y_coords) - margin, max(y_coords) + margin) |
|
| 265 | + |
|
| 266 | + ax.set_aspect('equal') |
|
| 267 | + ax.axis('off') |
|
| 268 | + |
|
| 269 | + plt.title(f'MST Maneuver Graph ({len(nodes_to_draw)} nodes)', fontsize=14) |
|
| 270 | + plt.tight_layout() |
|
| 271 | + |
|
| 272 | + if output_file: |
|
| 273 | + plt.savefig(output_file, dpi=150, bbox_inches='tight', |
|
| 274 | + facecolor='white', edgecolor='none') |
|
| 275 | + print(f"Saved visualization to {output_file}") |
|
| 276 | + else: |
|
| 277 | + plt.show() |
|
| 278 | + |
|
| 279 | + plt.close() |
|
| 280 | + |
|
| 281 | + def visualize_subtree(self, root_id, depth_limit=5, output_file=None): |
|
| 282 | + """Visualize a subtree starting from a specific node.""" |
|
| 283 | + # Collect nodes in subtree |
|
| 284 | + subtree_nodes = set() |
|
| 285 | + def collect_subtree(node_id, current_depth): |
|
| 286 | + if current_depth > depth_limit: |
|
| 287 | + return |
|
| 288 | + subtree_nodes.add(node_id) |
|
| 289 | + node = self.nodes[node_id] |
|
| 290 | + # Find children by looking at edges |
|
| 291 | + for edge in self.edges: |
|
| 292 | + if edge['from'] == node_id: |
|
| 293 | + collect_subtree(edge['to'], current_depth + 1) |
|
| 294 | + |
|
| 295 | + collect_subtree(root_id, 0) |
|
| 296 | + |
|
| 297 | + # Create filtered data |
|
| 298 | + filtered_data = { |
|
| 299 | + 'nodes': [n for n in self.data['nodes'] if n['id'] in subtree_nodes], |
|
| 300 | + 'edges': [e for e in self.edges if e['from'] in subtree_nodes and e['to'] in subtree_nodes], |
|
| 301 | + 'bestPaths': {k: v for k, v in self.best_paths.items() if int(k) in subtree_nodes} |
|
| 302 | + } |
|
| 303 | + |
|
| 304 | + # Create new visualizer for subtree |
|
| 305 | + subtree_viz = MstGraphVisualizer(filtered_data) |
|
| 306 | + subtree_viz.visualize(output_file=output_file) |
|
| 307 | + |
|
| 308 | + |
|
| 309 | +def main(): |
|
| 310 | + if len(sys.argv) < 2: |
|
| 311 | + print("Usage: python mst_graph_visualizer.py <input_json_file> [output_file]") |
|
| 312 | + print("\nOptions:") |
|
| 313 | + print(" input_json_file - JSON file exported from MstGraphExporter") |
|
| 314 | + print(" output_file - Optional output image file (PNG, PDF, SVG)") |
|
| 315 | + sys.exit(1) |
|
| 316 | + |
|
| 317 | + input_file = sys.argv[1] |
|
| 318 | + output_file = sys.argv[2] if len(sys.argv) > 2 else None |
|
| 319 | + |
|
| 320 | + print(f"Loading graph from {input_file}...") |
|
| 321 | + with open(input_file, 'r') as f: |
|
| 322 | + data = json.load(f) |
|
| 323 | + |
|
| 324 | + print(f"Loaded {len(data['nodes'])} nodes, {len(data['edges'])} edges") |
|
| 325 | + |
|
| 326 | + visualizer = MstGraphVisualizer(data) |
|
| 327 | + |
|
| 328 | + # For large graphs, show only first N nodes |
|
| 329 | + num_nodes = len(data['nodes']) |
|
| 330 | + if num_nodes > 100: |
|
| 331 | + print(f"Graph has {num_nodes} nodes, showing first 50 for clarity") |
|
| 332 | + visualizer.visualize(output_file=output_file, max_nodes=50) |
|
| 333 | + else: |
|
| 334 | + visualizer.visualize(output_file=output_file, max_nodes=num_nodes) |
|
| 335 | + |
|
| 336 | + |
|
| 337 | +if __name__ == '__main__': |
|
| 338 | + main() |
java/com.sap.sailing.windestimation.lab/python/mst_graph_visualizer_graphviz.py
| ... | ... | @@ -0,0 +1,373 @@ |
| 1 | +#!/usr/bin/env python3 |
|
| 2 | +""" |
|
| 3 | +MST Maneuver Graph Visualization using Graphviz |
|
| 4 | + |
|
| 5 | +This creates a hierarchical visualization of the MST graph that better handles |
|
| 6 | +large trees. It uses DOT language to create the graph and renders it with graphviz. |
|
| 7 | + |
|
| 8 | +Features: |
|
| 9 | +- Proper tree layout (top to bottom) |
|
| 10 | +- Each node shows 4 compartments as an HTML-like table |
|
| 11 | +- Best path highlighted in red |
|
| 12 | +- Edge labels with transition probabilities |
|
| 13 | +- Color-coded confidence levels |
|
| 14 | + |
|
| 15 | +Usage: |
|
| 16 | + python mst_graph_visualizer_graphviz.py <input_json_file> [output_file] |
|
| 17 | + |
|
| 18 | +Dependencies: |
|
| 19 | + pip install graphviz |
|
| 20 | + |
|
| 21 | +Also requires graphviz to be installed on the system: |
|
| 22 | + sudo apt-get install graphviz # Ubuntu/Debian |
|
| 23 | + brew install graphviz # macOS |
|
| 24 | +""" |
|
| 25 | + |
|
| 26 | +import json |
|
| 27 | +import sys |
|
| 28 | +from graphviz import Digraph |
|
| 29 | + |
|
| 30 | +# Maneuver type colors (HTML hex format) |
|
| 31 | +TYPE_COLORS = { |
|
| 32 | + 'TACK': '#4CAF50', # Green |
|
| 33 | + 'JIBE': '#2196F3', # Blue |
|
| 34 | + 'HEAD_UP': '#FF9800', # Orange |
|
| 35 | + 'BEAR_AWAY': '#9C27B0', # Purple |
|
| 36 | +} |
|
| 37 | + |
|
| 38 | +TYPE_ORDER = ['TACK', 'JIBE', 'HEAD_UP', 'BEAR_AWAY'] |
|
| 39 | +TYPE_ABBREV = {'TACK': 'T', 'JIBE': 'J', 'HEAD_UP': 'H', 'BEAR_AWAY': 'B'} |
|
| 40 | + |
|
| 41 | + |
|
| 42 | +def confidence_to_intensity(confidence): |
|
| 43 | + """Convert confidence [0,1] to color intensity for background.""" |
|
| 44 | + # Higher confidence = more saturated color |
|
| 45 | + base_intensity = 40 # Minimum intensity (very light) |
|
| 46 | + max_intensity = 200 # Maximum intensity |
|
| 47 | + intensity = int(base_intensity + confidence * (max_intensity - base_intensity)) |
|
| 48 | + return intensity |
|
| 49 | + |
|
| 50 | + |
|
| 51 | +def format_wind(comp): |
|
| 52 | + """Format wind direction string.""" |
|
| 53 | + wind_est = comp.get('windEstimate', comp.get('windRangeFrom', 0)) |
|
| 54 | + wind_width = comp.get('windRangeWidth', 0) |
|
| 55 | + if wind_width < 1: |
|
| 56 | + return f"{wind_est:.0f}°" |
|
| 57 | + else: |
|
| 58 | + return f"{wind_est:.0f}±{wind_width/2:.0f}°" |
|
| 59 | + |
|
| 60 | + |
|
| 61 | +def create_node_label(node, best_type=None): |
|
| 62 | + """Create HTML-like label for a node with 4 compartments.""" |
|
| 63 | + compartments = {c['type']: c for c in node['compartments']} |
|
| 64 | + |
|
| 65 | + # Extract time from timestamp |
|
| 66 | + timestamp = node.get('timestamp', '') |
|
| 67 | + if ' ' in timestamp: |
|
| 68 | + time_part = timestamp.split(' ')[1][:8] # HH:MM:SS |
|
| 69 | + else: |
|
| 70 | + time_part = timestamp[:8] if len(timestamp) >= 8 else timestamp |
|
| 71 | + |
|
| 72 | + # Build HTML table |
|
| 73 | + html = '<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="2">' |
|
| 74 | + html += '<TR>' |
|
| 75 | + |
|
| 76 | + for type_name in TYPE_ORDER: |
|
| 77 | + comp = compartments.get(type_name) |
|
| 78 | + if comp is None: |
|
| 79 | + continue |
|
| 80 | + |
|
| 81 | + confidence = comp['confidence'] |
|
| 82 | + is_best = (best_type == type_name) |
|
| 83 | + |
|
| 84 | + # Calculate background color |
|
| 85 | + base_color = TYPE_COLORS[type_name] |
|
| 86 | + if is_best: |
|
| 87 | + # Highlight best with full color and border |
|
| 88 | + bg_color = base_color |
|
| 89 | + border = ' BGCOLOR="{}" COLOR="red"'.format(base_color) |
|
| 90 | + else: |
|
| 91 | + # Fade color based on confidence |
|
| 92 | + intensity = confidence_to_intensity(confidence) |
|
| 93 | + # Make it lighter by blending with white |
|
| 94 | + r = int(int(base_color[1:3], 16) * confidence + 255 * (1-confidence)) |
|
| 95 | + g = int(int(base_color[3:5], 16) * confidence + 255 * (1-confidence)) |
|
| 96 | + b = int(int(base_color[5:7], 16) * confidence + 255 * (1-confidence)) |
|
| 97 | + bg_color = f"#{min(255,r):02x}{min(255,g):02x}{min(255,b):02x}" |
|
| 98 | + border = f' BGCOLOR="{bg_color}"' |
|
| 99 | + |
|
| 100 | + wind_str = format_wind(comp) |
|
| 101 | + abbrev = TYPE_ABBREV[type_name] |
|
| 102 | + |
|
| 103 | + # Create cell content |
|
| 104 | + cell_content = f'<B>{abbrev}</B><BR/><FONT POINT-SIZE="8">{confidence:.2f}</FONT><BR/><FONT POINT-SIZE="7">{wind_str}</FONT>' |
|
| 105 | + |
|
| 106 | + html += f'<TD{border}>{cell_content}</TD>' |
|
| 107 | + |
|
| 108 | + html += '</TR>' |
|
| 109 | + html += f'<TR><TD COLSPAN="4"><FONT POINT-SIZE="8">{time_part}</FONT></TD></TR>' |
|
| 110 | + html += '</TABLE>>' |
|
| 111 | + |
|
| 112 | + return html |
|
| 113 | + |
|
| 114 | + |
|
| 115 | +def visualize_mst_graph(data, output_file=None, max_nodes=100, show_low_prob_edges=False): |
|
| 116 | + """ |
|
| 117 | + Create graphviz visualization of MST graph. |
|
| 118 | + |
|
| 119 | + Args: |
|
| 120 | + data: Parsed JSON data from MstGraphExporter |
|
| 121 | + output_file: Output file path (without extension; .pdf/.png will be added) |
|
| 122 | + max_nodes: Maximum nodes to show |
|
| 123 | + show_low_prob_edges: Whether to show edges with very low probability |
|
| 124 | + """ |
|
| 125 | + nodes = {n['id']: n for n in data['nodes']} |
|
| 126 | + edges = data['edges'] |
|
| 127 | + best_paths = data.get('bestPaths', {}) |
|
| 128 | + |
|
| 129 | + # Create directed graph |
|
| 130 | + dot = Digraph(comment='MST Maneuver Graph') |
|
| 131 | + dot.attr(rankdir='TB') # Top to bottom |
|
| 132 | + dot.attr('node', shape='plaintext') # Use HTML labels |
|
| 133 | + |
|
| 134 | + # Limit nodes |
|
| 135 | + nodes_to_draw = list(nodes.keys())[:max_nodes] |
|
| 136 | + nodes_set = set(nodes_to_draw) |
|
| 137 | + |
|
| 138 | + # Add nodes |
|
| 139 | + for node_id in nodes_to_draw: |
|
| 140 | + node = nodes[node_id] |
|
| 141 | + best_type = best_paths.get(str(node_id)) |
|
| 142 | + label = create_node_label(node, best_type) |
|
| 143 | + dot.node(str(node_id), label) |
|
| 144 | + |
|
| 145 | + # Collect best path edges for highlighting |
|
| 146 | + best_edge_keys = set() |
|
| 147 | + for edge in edges: |
|
| 148 | + if edge.get('isBestPath', False): |
|
| 149 | + key = (edge['from'], edge['to'], edge['fromType'], edge['toType']) |
|
| 150 | + best_edge_keys.add(key) |
|
| 151 | + |
|
| 152 | + # Group edges by (from_node, to_node) for simplicity |
|
| 153 | + # Only show best path edges and edges between same types |
|
| 154 | + edge_groups = {} |
|
| 155 | + for edge in edges: |
|
| 156 | + from_id = edge['from'] |
|
| 157 | + to_id = edge['to'] |
|
| 158 | + |
|
| 159 | + if from_id not in nodes_set or to_id not in nodes_set: |
|
| 160 | + continue |
|
| 161 | + |
|
| 162 | + is_best = edge.get('isBestPath', False) |
|
| 163 | + trans_prob = edge['transitionProbability'] |
|
| 164 | + |
|
| 165 | + # Filter: show best path edges, same-type edges, or high probability edges |
|
| 166 | + same_type = edge['fromType'] == edge['toType'] |
|
| 167 | + high_prob = trans_prob > 0.001 |
|
| 168 | + |
|
| 169 | + if is_best or (same_type and high_prob) or show_low_prob_edges: |
|
| 170 | + key = (from_id, to_id) |
|
| 171 | + if key not in edge_groups: |
|
| 172 | + edge_groups[key] = [] |
|
| 173 | + edge_groups[key].append(edge) |
|
| 174 | + |
|
| 175 | + # Add edges (simplified - one edge per node pair, prioritizing best path) |
|
| 176 | + for (from_id, to_id), edge_list in edge_groups.items(): |
|
| 177 | + # Find best edge in this group |
|
| 178 | + best_edge = None |
|
| 179 | + for e in edge_list: |
|
| 180 | + if e.get('isBestPath', False): |
|
| 181 | + best_edge = e |
|
| 182 | + break |
|
| 183 | + |
|
| 184 | + if best_edge is None: |
|
| 185 | + # Use edge with highest probability |
|
| 186 | + best_edge = max(edge_list, key=lambda x: x['transitionProbability']) |
|
| 187 | + |
|
| 188 | + is_best = best_edge.get('isBestPath', False) |
|
| 189 | + trans_prob = best_edge['transitionProbability'] |
|
| 190 | + from_type = best_edge['fromType'] |
|
| 191 | + to_type = best_edge['toType'] |
|
| 192 | + |
|
| 193 | + # Style based on best path |
|
| 194 | + if is_best: |
|
| 195 | + color = 'red' |
|
| 196 | + penwidth = '2.5' |
|
| 197 | + style = 'bold' |
|
| 198 | + else: |
|
| 199 | + # Color based on probability |
|
| 200 | + if trans_prob > 0.01: |
|
| 201 | + color = 'darkgreen' |
|
| 202 | + elif trans_prob > 0.001: |
|
| 203 | + color = 'gray40' |
|
| 204 | + else: |
|
| 205 | + color = 'gray80' |
|
| 206 | + penwidth = '1.0' |
|
| 207 | + style = 'solid' |
|
| 208 | + |
|
| 209 | + # Label shows type transition and probability |
|
| 210 | + label = f'{TYPE_ABBREV[from_type]}→{TYPE_ABBREV[to_type]}\\n{trans_prob:.1e}' |
|
| 211 | + |
|
| 212 | + dot.edge(str(from_id), str(to_id), |
|
| 213 | + label=label, |
|
| 214 | + color=color, |
|
| 215 | + penwidth=penwidth, |
|
| 216 | + style=style, |
|
| 217 | + fontsize='8', |
|
| 218 | + fontcolor=color) |
|
| 219 | + |
|
| 220 | + # Render |
|
| 221 | + if output_file: |
|
| 222 | + # Determine format from extension |
|
| 223 | + if '.' in output_file: |
|
| 224 | + base, fmt = output_file.rsplit('.', 1) |
|
| 225 | + else: |
|
| 226 | + base = output_file |
|
| 227 | + fmt = 'pdf' |
|
| 228 | + |
|
| 229 | + dot.render(base, format=fmt, cleanup=True) |
|
| 230 | + print(f"Saved visualization to {base}.{fmt}") |
|
| 231 | + else: |
|
| 232 | + # Try to display |
|
| 233 | + dot.view() |
|
| 234 | + |
|
| 235 | + return dot |
|
| 236 | + |
|
| 237 | + |
|
| 238 | +def create_detailed_edge_graph(data, output_file=None, max_depth=10): |
|
| 239 | + """ |
|
| 240 | + Create a more detailed graph showing all compartment-to-compartment edges. |
|
| 241 | + Each node compartment becomes its own graphviz node. |
|
| 242 | + |
|
| 243 | + This is useful for small subtrees to see the full inner graph structure. |
|
| 244 | + """ |
|
| 245 | + nodes = {n['id']: n for n in data['nodes']} |
|
| 246 | + edges = data['edges'] |
|
| 247 | + best_paths = data.get('bestPaths', {}) |
|
| 248 | + |
|
| 249 | + # Filter to first N levels |
|
| 250 | + nodes_to_draw = [n for n in data['nodes'] if n['depth'] <= max_depth] |
|
| 251 | + nodes_set = {n['id'] for n in nodes_to_draw} |
|
| 252 | + |
|
| 253 | + dot = Digraph(comment='MST Detailed Inner Graph') |
|
| 254 | + dot.attr(rankdir='TB') |
|
| 255 | + |
|
| 256 | + # Create subgraph for each tree node (to keep compartments together) |
|
| 257 | + for node in nodes_to_draw: |
|
| 258 | + node_id = node['id'] |
|
| 259 | + best_type = best_paths.get(str(node_id)) |
|
| 260 | + |
|
| 261 | + with dot.subgraph(name=f'cluster_{node_id}') as c: |
|
| 262 | + c.attr(label=f"Node {node_id}") |
|
| 263 | + c.attr(style='rounded') |
|
| 264 | + |
|
| 265 | + for comp in node['compartments']: |
|
| 266 | + type_name = comp['type'] |
|
| 267 | + comp_id = f"{node_id}_{type_name}" |
|
| 268 | + |
|
| 269 | + is_best = (best_type == type_name) |
|
| 270 | + color = TYPE_COLORS[type_name] |
|
| 271 | + |
|
| 272 | + if is_best: |
|
| 273 | + style = 'filled,bold' |
|
| 274 | + fillcolor = color |
|
| 275 | + fontcolor = 'white' |
|
| 276 | + else: |
|
| 277 | + style = 'filled' |
|
| 278 | + # Lighter version |
|
| 279 | + fillcolor = f"{color}40" # 40 = 25% opacity in hex |
|
| 280 | + fontcolor = 'black' |
|
| 281 | + |
|
| 282 | + label = f"{TYPE_ABBREV[type_name]}\\n{comp['confidence']:.2f}\\n{format_wind(comp)}" |
|
| 283 | + |
|
| 284 | + c.node(comp_id, label, |
|
| 285 | + shape='box', |
|
| 286 | + style=style, |
|
| 287 | + fillcolor=fillcolor, |
|
| 288 | + fontcolor=fontcolor, |
|
| 289 | + fontsize='10') |
|
| 290 | + |
|
| 291 | + # Add edges between compartments |
|
| 292 | + for edge in edges: |
|
| 293 | + from_id = edge['from'] |
|
| 294 | + to_id = edge['to'] |
|
| 295 | + |
|
| 296 | + if from_id not in nodes_set or to_id not in nodes_set: |
|
| 297 | + continue |
|
| 298 | + |
|
| 299 | + from_comp_id = f"{from_id}_{edge['fromType']}" |
|
| 300 | + to_comp_id = f"{to_id}_{edge['toType']}" |
|
| 301 | + |
|
| 302 | + is_best = edge.get('isBestPath', False) |
|
| 303 | + trans_prob = edge['transitionProbability'] |
|
| 304 | + |
|
| 305 | + # Only show significant edges |
|
| 306 | + if trans_prob < 0.0001 and not is_best: |
|
| 307 | + continue |
|
| 308 | + |
|
| 309 | + if is_best: |
|
| 310 | + color = 'red' |
|
| 311 | + penwidth = '2.0' |
|
| 312 | + else: |
|
| 313 | + # Color by probability |
|
| 314 | + if trans_prob > 0.01: |
|
| 315 | + color = 'darkgreen' |
|
| 316 | + penwidth = '1.5' |
|
| 317 | + elif trans_prob > 0.001: |
|
| 318 | + color = 'gray50' |
|
| 319 | + penwidth = '1.0' |
|
| 320 | + else: |
|
| 321 | + color = 'gray80' |
|
| 322 | + penwidth = '0.5' |
|
| 323 | + |
|
| 324 | + dot.edge(from_comp_id, to_comp_id, |
|
| 325 | + label=f'{trans_prob:.1e}', |
|
| 326 | + color=color, |
|
| 327 | + penwidth=penwidth, |
|
| 328 | + fontsize='7', |
|
| 329 | + fontcolor=color) |
|
| 330 | + |
|
| 331 | + if output_file: |
|
| 332 | + if '.' in output_file: |
|
| 333 | + base, fmt = output_file.rsplit('.', 1) |
|
| 334 | + else: |
|
| 335 | + base = output_file |
|
| 336 | + fmt = 'pdf' |
|
| 337 | + dot.render(base, format=fmt, cleanup=True) |
|
| 338 | + print(f"Saved detailed visualization to {base}.{fmt}") |
|
| 339 | + |
|
| 340 | + return dot |
|
| 341 | + |
|
| 342 | + |
|
| 343 | +def main(): |
|
| 344 | + if len(sys.argv) < 2: |
|
| 345 | + print("Usage: python mst_graph_visualizer_graphviz.py <input_json_file> [output_file] [--detailed]") |
|
| 346 | + print("\nOptions:") |
|
| 347 | + print(" input_json_file - JSON file exported from MstGraphExporter") |
|
| 348 | + print(" output_file - Output file (extension determines format: .pdf, .png, .svg)") |
|
| 349 | + print(" --detailed - Create detailed compartment-level graph (for small graphs)") |
|
| 350 | + sys.exit(1) |
|
| 351 | + |
|
| 352 | + input_file = sys.argv[1] |
|
| 353 | + output_file = sys.argv[2] if len(sys.argv) > 2 and not sys.argv[2].startswith('--') else None |
|
| 354 | + detailed = '--detailed' in sys.argv |
|
| 355 | + |
|
| 356 | + print(f"Loading graph from {input_file}...") |
|
| 357 | + with open(input_file, 'r') as f: |
|
| 358 | + data = json.load(f) |
|
| 359 | + |
|
| 360 | + num_nodes = len(data['nodes']) |
|
| 361 | + num_edges = len(data['edges']) |
|
| 362 | + print(f"Loaded {num_nodes} nodes, {num_edges} edges") |
|
| 363 | + |
|
| 364 | + if detailed: |
|
| 365 | + print("Creating detailed compartment-level visualization...") |
|
| 366 | + create_detailed_edge_graph(data, output_file, max_depth=10) |
|
| 367 | + else: |
|
| 368 | + print("Creating tree visualization...") |
|
| 369 | + visualize_mst_graph(data, output_file, max_nodes=100) |
|
| 370 | + |
|
| 371 | + |
|
| 372 | +if __name__ == '__main__': |
|
| 373 | + main() |
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/aggregator/graph/DijkstraShortestPathFinderImpl.java
| ... | ... | @@ -92,7 +92,6 @@ public class DijkstraShortestPathFinderImpl<T extends ElementWithQuality> implem |
| 92 | 92 | result.append("\n"); |
| 93 | 93 | predecessor = nodeOnShortestPath; |
| 94 | 94 | } |
| 95 | - |
|
| 96 | 95 | return result.toString(); |
| 97 | 96 | } |
| 98 | 97 |
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/aggregator/msthmm/MstGraphExportHelper.java
| ... | ... | @@ -0,0 +1,84 @@ |
| 1 | +package com.sap.sailing.windestimation.aggregator.msthmm; |
|
| 2 | + |
|
| 3 | +import java.io.FileWriter; |
|
| 4 | +import java.io.IOException; |
|
| 5 | +import java.io.StringWriter; |
|
| 6 | +import java.io.Writer; |
|
| 7 | +import java.util.logging.Logger; |
|
| 8 | + |
|
| 9 | +import com.sap.sailing.windestimation.aggregator.msthmm.MstManeuverGraphGenerator.MstManeuverGraphComponents; |
|
| 10 | + |
|
| 11 | +/** |
|
| 12 | + * Example usage of MstGraphExporter for debugging and visualization. |
|
| 13 | + * |
|
| 14 | + * <p>This class can be used to export the MST graph to JSON format, which can then |
|
| 15 | + * be visualized using the Python script mst_graph_visualizer.py.</p> |
|
| 16 | + * |
|
| 17 | + * <p>Example usage in a test or debug context:</p> |
|
| 18 | + * <pre> |
|
| 19 | + * // After building your MST graph: |
|
| 20 | + * MstManeuverGraphComponents graphComponents = mstManeuverGraphGenerator.parseGraph(); |
|
| 21 | + * |
|
| 22 | + * // Export to JSON file: |
|
| 23 | + * MstGraphExportHelper.exportToFile(graphComponents, transitionProbabilitiesCalculator, |
|
| 24 | + * "/tmp/mst_graph.json"); |
|
| 25 | + * |
|
| 26 | + * // Then run from command line: |
|
| 27 | + * // python mst_graph_visualizer.py /tmp/mst_graph.json /tmp/mst_graph.png |
|
| 28 | + * </pre> |
|
| 29 | + * |
|
| 30 | + * @author Generated for visualization purposes using Claude Opus 4.5 |
|
| 31 | + */ |
|
| 32 | +public class MstGraphExportHelper { |
|
| 33 | + private static final Logger logger = Logger.getLogger(MstGraphExportHelper.class.getName()); |
|
| 34 | + |
|
| 35 | + /** |
|
| 36 | + * Exports the MST graph to a JSON file. |
|
| 37 | + * |
|
| 38 | + * @param graphComponents The MST graph components to export |
|
| 39 | + * @param transitionProbabilitiesCalculator Calculator for edge probabilities |
|
| 40 | + * @param filePath Path to write the JSON file |
|
| 41 | + * @throws IOException if writing fails |
|
| 42 | + */ |
|
| 43 | + public static void exportToFile(MstManeuverGraphComponents graphComponents, |
|
| 44 | + MstGraphNodeTransitionProbabilitiesCalculator transitionProbabilitiesCalculator, |
|
| 45 | + String filePath) throws IOException { |
|
| 46 | + final MstGraphExporter exporter = new MstGraphExporter(transitionProbabilitiesCalculator); |
|
| 47 | + try (final FileWriter writer = new FileWriter(filePath)) { |
|
| 48 | + exporter.exportToJson(graphComponents, writer); |
|
| 49 | + } |
|
| 50 | + logger.info("Exported MST graph to: " + filePath); |
|
| 51 | + logger.info("Visualize with: python mst_graph_visualizer.py " + filePath + " output.png"); |
|
| 52 | + } |
|
| 53 | + |
|
| 54 | + /** |
|
| 55 | + * Exports the MST graph to a JSON string (useful for testing/debugging). |
|
| 56 | + * |
|
| 57 | + * @param graphComponents The MST graph components to export |
|
| 58 | + * @param transitionProbabilitiesCalculator Calculator for edge probabilities |
|
| 59 | + * @return JSON string representation of the graph |
|
| 60 | + * @throws IOException if writing fails |
|
| 61 | + */ |
|
| 62 | + public static String exportToString(MstManeuverGraphComponents graphComponents, |
|
| 63 | + MstGraphNodeTransitionProbabilitiesCalculator transitionProbabilitiesCalculator) throws IOException { |
|
| 64 | + final MstGraphExporter exporter = new MstGraphExporter(transitionProbabilitiesCalculator); |
|
| 65 | + final StringWriter writer = new StringWriter(); |
|
| 66 | + exporter.exportToJson(graphComponents, writer); |
|
| 67 | + return writer.toString(); |
|
| 68 | + } |
|
| 69 | + |
|
| 70 | + /** |
|
| 71 | + * Exports the MST graph to a provided writer. |
|
| 72 | + * |
|
| 73 | + * @param graphComponents The MST graph components to export |
|
| 74 | + * @param transitionProbabilitiesCalculator Calculator for edge probabilities |
|
| 75 | + * @param writer Writer to output JSON to |
|
| 76 | + * @throws IOException if writing fails |
|
| 77 | + */ |
|
| 78 | + public static void exportToWriter(MstManeuverGraphComponents graphComponents, |
|
| 79 | + MstGraphNodeTransitionProbabilitiesCalculator transitionProbabilitiesCalculator, |
|
| 80 | + Writer writer) throws IOException { |
|
| 81 | + final MstGraphExporter exporter = new MstGraphExporter(transitionProbabilitiesCalculator); |
|
| 82 | + exporter.exportToJson(graphComponents, writer); |
|
| 83 | + } |
|
| 84 | +} |
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/aggregator/msthmm/MstGraphExporter.java
| ... | ... | @@ -0,0 +1,237 @@ |
| 1 | +package com.sap.sailing.windestimation.aggregator.msthmm; |
|
| 2 | + |
|
| 3 | +import java.io.IOException; |
|
| 4 | +import java.io.Writer; |
|
| 5 | +import java.text.SimpleDateFormat; |
|
| 6 | +import java.util.HashMap; |
|
| 7 | +import java.util.HashSet; |
|
| 8 | +import java.util.Map; |
|
| 9 | +import java.util.Set; |
|
| 10 | + |
|
| 11 | +import com.sap.sailing.windestimation.aggregator.graph.DijkstraShortestPathFinderImpl; |
|
| 12 | +import com.sap.sailing.windestimation.aggregator.graph.DijsktraShortestPathFinder; |
|
| 13 | +import com.sap.sailing.windestimation.aggregator.graph.ElementAdjacencyQualityMetric; |
|
| 14 | +import com.sap.sailing.windestimation.aggregator.graph.InnerGraphSuccessorSupplier; |
|
| 15 | +import com.sap.sailing.windestimation.aggregator.hmm.GraphLevelInference; |
|
| 16 | +import com.sap.sailing.windestimation.aggregator.hmm.GraphNode; |
|
| 17 | +import com.sap.sailing.windestimation.aggregator.hmm.WindCourseRange; |
|
| 18 | +import com.sap.sailing.windestimation.aggregator.msthmm.MstManeuverGraphGenerator.MstManeuverGraphComponents; |
|
| 19 | +import com.sap.sailing.windestimation.data.ManeuverForEstimation; |
|
| 20 | +import com.sap.sailing.windestimation.data.ManeuverTypeForClassification; |
|
| 21 | + |
|
| 22 | +import java.util.function.Supplier; |
|
| 23 | + |
|
| 24 | +/** |
|
| 25 | + * Exports MST graph data to JSON format for visualization. |
|
| 26 | + * The output can be consumed by a Python visualization script. |
|
| 27 | + * |
|
| 28 | + * @author Generated for visualization purposes using Claude Opus 4.5 |
|
| 29 | + */ |
|
| 30 | +public class MstGraphExporter { |
|
| 31 | + |
|
| 32 | + private final MstGraphNodeTransitionProbabilitiesCalculator transitionProbabilitiesCalculator; |
|
| 33 | + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); |
|
| 34 | + |
|
| 35 | + public MstGraphExporter(MstGraphNodeTransitionProbabilitiesCalculator transitionProbabilitiesCalculator) { |
|
| 36 | + this.transitionProbabilitiesCalculator = transitionProbabilitiesCalculator; |
|
| 37 | + } |
|
| 38 | + |
|
| 39 | + /** |
|
| 40 | + * Exports the MST graph components to JSON format. |
|
| 41 | + * |
|
| 42 | + * @param graphComponents The MST graph to export |
|
| 43 | + * @param writer The writer to output JSON to |
|
| 44 | + * @throws IOException if writing fails |
|
| 45 | + */ |
|
| 46 | + public void exportToJson(MstManeuverGraphComponents graphComponents, Writer writer) throws IOException { |
|
| 47 | + // Collect all best paths for highlighting |
|
| 48 | + final Set<String> bestPathEdges = collectBestPathEdges(graphComponents); |
|
| 49 | + final Map<String, String> bestNodePerLevel = collectBestNodePerLevel(graphComponents); |
|
| 50 | + writer.write("{\n"); |
|
| 51 | + writer.write(" \"nodes\": [\n"); |
|
| 52 | + // Export all nodes starting from root |
|
| 53 | + final MstGraphLevel root = graphComponents.getRoot(); |
|
| 54 | + final boolean[] firstNode = {true}; |
|
| 55 | + final Map<MstGraphLevel, Integer> levelToId = new HashMap<>(); |
|
| 56 | + final int[] nodeIdCounter = {0}; |
|
| 57 | + exportNode(root, writer, firstNode, levelToId, nodeIdCounter, 0); |
|
| 58 | + writer.write("\n ],\n"); |
|
| 59 | + writer.write(" \"edges\": [\n"); |
|
| 60 | + // Export edges between nodes |
|
| 61 | + final boolean[] firstEdge = {true}; |
|
| 62 | + exportEdges(root, writer, firstEdge, levelToId, bestPathEdges); |
|
| 63 | + writer.write("\n ],\n"); |
|
| 64 | + // Export best path information |
|
| 65 | + writer.write(" \"bestPaths\": {\n"); |
|
| 66 | + boolean firstBest = true; |
|
| 67 | + for (Map.Entry<String, String> entry : bestNodePerLevel.entrySet()) { |
|
| 68 | + if (!firstBest) { |
|
| 69 | + writer.write(",\n"); |
|
| 70 | + } |
|
| 71 | + firstBest = false; |
|
| 72 | + writer.write(" \"" + entry.getKey() + "\": \"" + entry.getValue() + "\""); |
|
| 73 | + } |
|
| 74 | + writer.write("\n }\n"); |
|
| 75 | + writer.write("}\n"); |
|
| 76 | + } |
|
| 77 | + |
|
| 78 | + private void exportNode(MstGraphLevel level, Writer writer, boolean[] firstNode, |
|
| 79 | + Map<MstGraphLevel, Integer> levelToId, int[] nodeIdCounter, int depth) throws IOException { |
|
| 80 | + final int nodeId = nodeIdCounter[0]++; |
|
| 81 | + levelToId.put(level, nodeId); |
|
| 82 | + if (!firstNode[0]) { |
|
| 83 | + writer.write(",\n"); |
|
| 84 | + } |
|
| 85 | + firstNode[0] = false; |
|
| 86 | + final ManeuverForEstimation maneuver = level.getManeuver(); |
|
| 87 | + writer.write(" {\n"); |
|
| 88 | + writer.write(" \"id\": " + nodeId + ",\n"); |
|
| 89 | + writer.write(" \"depth\": " + depth + ",\n"); |
|
| 90 | + writer.write(" \"timestamp\": \"" + DATE_FORMAT.format(maneuver.getManeuverTimePoint().asDate()) + "\",\n"); |
|
| 91 | + writer.write(" \"position\": {\"lat\": " + maneuver.getManeuverPosition().getLatDeg() + |
|
| 92 | + ", \"lon\": " + maneuver.getManeuverPosition().getLngDeg() + "},\n"); |
|
| 93 | + writer.write(" \"distanceToParent\": " + level.getDistanceToParent() + ",\n"); |
|
| 94 | + writer.write(" \"compartments\": [\n"); |
|
| 95 | + |
|
| 96 | + // Export each maneuver type compartment |
|
| 97 | + boolean firstCompartment = true; |
|
| 98 | + for (GraphNode<MstGraphLevel> node : level.getLevelNodes()) { |
|
| 99 | + if (!firstCompartment) { |
|
| 100 | + writer.write(",\n"); |
|
| 101 | + } |
|
| 102 | + firstCompartment = false; |
|
| 103 | + final ManeuverTypeForClassification type = node.getManeuverType(); |
|
| 104 | + final WindCourseRange windRange = node.getValidWindRange(); |
|
| 105 | + final double confidence = node.getConfidence(); |
|
| 106 | + writer.write(" {\n"); |
|
| 107 | + writer.write(" \"type\": \"" + type.name() + "\",\n"); |
|
| 108 | + writer.write(" \"confidence\": " + confidence + ",\n"); |
|
| 109 | + writer.write(" \"windRangeFrom\": " + windRange.getFromPortside() + ",\n"); |
|
| 110 | + writer.write(" \"windRangeWidth\": " + windRange.getAngleTowardStarboard() + ",\n"); |
|
| 111 | + |
|
| 112 | + // Calculate single wind direction estimate for TACK/JIBE (width ~0) |
|
| 113 | + double windEstimate; |
|
| 114 | + if (windRange.getAngleTowardStarboard() < 1.0) { |
|
| 115 | + windEstimate = windRange.getFromPortside(); |
|
| 116 | + } else { |
|
| 117 | + // For HEAD_UP/BEAR_AWAY, use middle of range |
|
| 118 | + windEstimate = windRange.getFromPortside() + windRange.getAngleTowardStarboard() / 2.0; |
|
| 119 | + if (windEstimate >= 360) { |
|
| 120 | + windEstimate -= 360; |
|
| 121 | + } |
|
| 122 | + } |
|
| 123 | + writer.write(" \"windEstimate\": " + windEstimate + ",\n"); |
|
| 124 | + writer.write(" \"tackAfter\": \"" + node.getTackAfter() + "\"\n"); |
|
| 125 | + writer.write(" }"); |
|
| 126 | + } |
|
| 127 | + writer.write("\n ]\n"); |
|
| 128 | + writer.write(" }"); |
|
| 129 | + // Recursively export children |
|
| 130 | + for (MstGraphLevel child : level.getChildren()) { |
|
| 131 | + exportNode(child, writer, firstNode, levelToId, nodeIdCounter, depth + 1); |
|
| 132 | + } |
|
| 133 | + } |
|
| 134 | + |
|
| 135 | + private void exportEdges(MstGraphLevel level, Writer writer, boolean[] firstEdge, |
|
| 136 | + Map<MstGraphLevel, Integer> levelToId, Set<String> bestPathEdges) throws IOException { |
|
| 137 | + final int parentId = levelToId.get(level); |
|
| 138 | + for (final MstGraphLevel child : level.getChildren()) { |
|
| 139 | + final int childId = levelToId.get(child); |
|
| 140 | + // Export edges between all compartment pairs |
|
| 141 | + for (final GraphNode<MstGraphLevel> parentNode : level.getLevelNodes()) { |
|
| 142 | + for (final GraphNode<MstGraphLevel> childNode : child.getLevelNodes()) { |
|
| 143 | + // Calculate transition probability |
|
| 144 | + final double transitionProb = transitionProbabilitiesCalculator.getTransitionProbability( |
|
| 145 | + childNode, parentNode, child.getDistanceToParent()); |
|
| 146 | + final String edgeKey = parentId + "_" + parentNode.getManeuverType().ordinal() + |
|
| 147 | + "_" + childId + "_" + childNode.getManeuverType().ordinal(); |
|
| 148 | + final boolean isBestPath = bestPathEdges.contains(edgeKey); |
|
| 149 | + if (!firstEdge[0]) { |
|
| 150 | + writer.write(",\n"); |
|
| 151 | + } |
|
| 152 | + firstEdge[0] = false; |
|
| 153 | + writer.write(" {\n"); |
|
| 154 | + writer.write(" \"from\": " + parentId + ",\n"); |
|
| 155 | + writer.write(" \"fromType\": \"" + parentNode.getManeuverType().name() + "\",\n"); |
|
| 156 | + writer.write(" \"to\": " + childId + ",\n"); |
|
| 157 | + writer.write(" \"toType\": \"" + childNode.getManeuverType().name() + "\",\n"); |
|
| 158 | + writer.write(" \"transitionProbability\": " + transitionProb + ",\n"); |
|
| 159 | + writer.write(" \"distance\": " + child.getDistanceToParent() + ",\n"); |
|
| 160 | + writer.write(" \"isBestPath\": " + isBestPath + "\n"); |
|
| 161 | + writer.write(" }"); |
|
| 162 | + } |
|
| 163 | + } |
|
| 164 | + // Recursively export edges for children |
|
| 165 | + exportEdges(child, writer, firstEdge, levelToId, bestPathEdges); |
|
| 166 | + } |
|
| 167 | + } |
|
| 168 | + |
|
| 169 | + private Set<String> collectBestPathEdges(MstManeuverGraphComponents graphComponents) { |
|
| 170 | + final Set<String> bestPathEdges = new HashSet<>(); |
|
| 171 | + final Map<MstGraphLevel, Integer> levelToId = new HashMap<>(); |
|
| 172 | + final int[] nodeIdCounter = {0}; |
|
| 173 | + assignNodeIds(graphComponents.getRoot(), levelToId, nodeIdCounter); |
|
| 174 | + final ElementAdjacencyQualityMetric<GraphNode<MstGraphLevel>> edgeQualityMetric = (previousNode, currentNode) -> { |
|
| 175 | + return transitionProbabilitiesCalculator.getTransitionProbability(currentNode, previousNode, |
|
| 176 | + previousNode.getGraphLevel() == null ? 0.0 : previousNode.getGraphLevel().getDistanceToParent()); |
|
| 177 | + }; |
|
| 178 | + for (final MstGraphLevel leaf : graphComponents.getLeaves()) { |
|
| 179 | + final InnerGraphSuccessorSupplier<GraphNode<MstGraphLevel>, MstGraphLevel> innerGraphSuccessorSupplier = |
|
| 180 | + new InnerGraphSuccessorSupplier<>(graphComponents, |
|
| 181 | + (final Supplier<String> nameSupplier) -> new GraphNode<MstGraphLevel>( |
|
| 182 | + null, null, new WindCourseRange(0, 360), 1.0, 0, null) { |
|
| 183 | + @Override |
|
| 184 | + public String toString() { |
|
| 185 | + return nameSupplier.get(); |
|
| 186 | + } |
|
| 187 | + }); |
|
| 188 | + final DijsktraShortestPathFinder<GraphNode<MstGraphLevel>> dijkstra = |
|
| 189 | + new DijkstraShortestPathFinderImpl<>( |
|
| 190 | + innerGraphSuccessorSupplier.getArtificialLeaf(leaf), |
|
| 191 | + innerGraphSuccessorSupplier.getArtificialRoot(), |
|
| 192 | + innerGraphSuccessorSupplier, edgeQualityMetric); |
|
| 193 | + GraphNode<MstGraphLevel> prev = null; |
|
| 194 | + for (final GraphNode<MstGraphLevel> node : dijkstra.getShortestPath()) { |
|
| 195 | + if (prev != null && prev.getGraphLevel() != null && node.getGraphLevel() != null) { |
|
| 196 | + Integer prevId = levelToId.get(prev.getGraphLevel()); |
|
| 197 | + Integer nodeId = levelToId.get(node.getGraphLevel()); |
|
| 198 | + if (prevId != null && nodeId != null) { |
|
| 199 | + // Dijkstra goes from leaf to root (child to parent) |
|
| 200 | + // We export edges from parent to child, so store the edge in that direction |
|
| 201 | + String edgeKey = nodeId + "_" + node.getManeuverType().ordinal() + |
|
| 202 | + "_" + prevId + "_" + prev.getManeuverType().ordinal(); |
|
| 203 | + bestPathEdges.add(edgeKey); |
|
| 204 | + } |
|
| 205 | + } |
|
| 206 | + prev = node; |
|
| 207 | + } |
|
| 208 | + } |
|
| 209 | + return bestPathEdges; |
|
| 210 | + } |
|
| 211 | + |
|
| 212 | + private Map<String, String> collectBestNodePerLevel(MstManeuverGraphComponents graphComponents) { |
|
| 213 | + final Map<String, String> bestNodePerLevel = new HashMap<>(); |
|
| 214 | + final Map<MstGraphLevel, Integer> levelToId = new HashMap<>(); |
|
| 215 | + final int[] nodeIdCounter = {0}; |
|
| 216 | + assignNodeIds(graphComponents.getRoot(), levelToId, nodeIdCounter); |
|
| 217 | + // Use the MstBestPathsCalculatorImpl to get the best nodes |
|
| 218 | + final MstBestPathsCalculatorImpl calculator = new MstBestPathsCalculatorImpl(transitionProbabilitiesCalculator); |
|
| 219 | + for (final GraphLevelInference<MstGraphLevel> inference : calculator.getBestNodes(graphComponents)) { |
|
| 220 | + final MstGraphLevel level = inference.getGraphNode().getGraphLevel(); |
|
| 221 | + if (level != null) { |
|
| 222 | + Integer nodeId = levelToId.get(level); |
|
| 223 | + if (nodeId != null) { |
|
| 224 | + bestNodePerLevel.put(String.valueOf(nodeId), inference.getGraphNode().getManeuverType().name()); |
|
| 225 | + } |
|
| 226 | + } |
|
| 227 | + } |
|
| 228 | + return bestNodePerLevel; |
|
| 229 | + } |
|
| 230 | + |
|
| 231 | + private void assignNodeIds(MstGraphLevel level, Map<MstGraphLevel, Integer> levelToId, int[] nodeIdCounter) { |
|
| 232 | + levelToId.put(level, nodeIdCounter[0]++); |
|
| 233 | + for (final MstGraphLevel child : level.getChildren()) { |
|
| 234 | + assignNodeIds(child, levelToId, nodeIdCounter); |
|
| 235 | + } |
|
| 236 | + } |
|
| 237 | +} |
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/aggregator/msthmm/MstManeuverGraphGenerator.java
| ... | ... | @@ -28,6 +28,10 @@ public class MstManeuverGraphGenerator extends AbstractMstGraphGenerator<Maneuve |
| 28 | 28 | this.transitionProbabilitiesCalculator = transitionProbabilitiesCalculator; |
| 29 | 29 | } |
| 30 | 30 | |
| 31 | + public MstGraphNodeTransitionProbabilitiesCalculator getTransitionProbabilitiesCalculator() { |
|
| 32 | + return transitionProbabilitiesCalculator; |
|
| 33 | + } |
|
| 34 | + |
|
| 31 | 35 | @Override |
| 32 | 36 | protected double getDistanceBetweenObservations(ManeuverWithProbabilisticTypeClassification o1, |
| 33 | 37 | ManeuverWithProbabilisticTypeClassification o2) { |
| ... | ... | @@ -92,5 +96,24 @@ public class MstManeuverGraphGenerator extends AbstractMstGraphGenerator<Maneuve |
| 92 | 96 | public List<MstGraphLevel> getLeaves() { |
| 93 | 97 | return leaves; |
| 94 | 98 | } |
| 99 | + |
|
| 100 | + @Override |
|
| 101 | + public String toString() { |
|
| 102 | + StringBuilder result = new StringBuilder(); |
|
| 103 | + result.append("MST maneuver graph:\n"); |
|
| 104 | + appendGraphLevelToStringBuilder(root, result, 0); |
|
| 105 | + return result.toString(); |
|
| 106 | + } |
|
| 107 | + |
|
| 108 | + private void appendGraphLevelToStringBuilder(MstGraphLevel graphLevel, StringBuilder result, int indent) { |
|
| 109 | + for (int i = 0; i < indent; i++) { |
|
| 110 | + result.append(" "); |
|
| 111 | + } |
|
| 112 | + result.append(graphLevel); |
|
| 113 | + result.append("\n"); |
|
| 114 | + for (MstGraphLevel child : graphLevel.getChildren()) { |
|
| 115 | + appendGraphLevelToStringBuilder(child, result, indent + 1); |
|
| 116 | + } |
|
| 117 | + } |
|
| 95 | 118 | } |
| 96 | 119 | } |
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/integration/IncrementalMstHmmWindEstimationForTrackedRace.java
| ... | ... | @@ -1,5 +1,7 @@ |
| 1 | 1 | package com.sap.sailing.windestimation.integration; |
| 2 | 2 | |
| 3 | +import java.io.File; |
|
| 4 | +import java.io.IOException; |
|
| 3 | 5 | import java.util.ArrayList; |
| 4 | 6 | import java.util.Collections; |
| 5 | 7 | import java.util.HashMap; |
| ... | ... | @@ -9,6 +11,7 @@ import java.util.Map; |
| 9 | 11 | import java.util.TreeMap; |
| 10 | 12 | import java.util.concurrent.ConcurrentLinkedDeque; |
| 11 | 13 | import java.util.concurrent.Executor; |
| 14 | +import java.util.logging.Level; |
|
| 12 | 15 | import java.util.logging.Logger; |
| 13 | 16 | |
| 14 | 17 | import com.sap.sailing.domain.base.Competitor; |
| ... | ... | @@ -29,6 +32,7 @@ import com.sap.sailing.windestimation.aggregator.hmm.GraphLevelInference; |
| 29 | 32 | import com.sap.sailing.windestimation.aggregator.msthmm.DistanceAndDurationAwareWindTransitionProbabilitiesCalculator; |
| 30 | 33 | import com.sap.sailing.windestimation.aggregator.msthmm.MstBestPathsCalculator; |
| 31 | 34 | import com.sap.sailing.windestimation.aggregator.msthmm.MstBestPathsCalculatorImpl; |
| 35 | +import com.sap.sailing.windestimation.aggregator.msthmm.MstGraphExportHelper; |
|
| 32 | 36 | import com.sap.sailing.windestimation.aggregator.msthmm.MstGraphLevel; |
| 33 | 37 | import com.sap.sailing.windestimation.aggregator.msthmm.MstManeuverGraphGenerator.MstManeuverGraphComponents; |
| 34 | 38 | import com.sap.sailing.windestimation.data.ManeuverWithEstimatedType; |
| ... | ... | @@ -181,6 +185,11 @@ public class IncrementalMstHmmWindEstimationForTrackedRace implements Incrementa |
| 181 | 185 | mstManeuverGraphGenerator.add(competitor, newManeuverSpot, trackTimeInfo); |
| 182 | 186 | } |
| 183 | 187 | graphComponents = mstManeuverGraphGenerator.parseGraph(); |
| 188 | + try { |
|
| 189 | + MstGraphExportHelper.exportToFile(graphComponents, mstManeuverGraphGenerator.getTransitionProbabilitiesCalculator(), File.createTempFile("maneuverExport_", ".json").getCanonicalPath()); |
|
| 190 | + } catch (IOException e) { |
|
| 191 | + logger.log(Level.WARNING, "Exporting the maneuver graph to file failed", e); |
|
| 192 | + } |
|
| 184 | 193 | if (graphComponents != null) { |
| 185 | 194 | Iterable<GraphLevelInference<MstGraphLevel>> bestPath = bestPathsCalculator.getBestNodes(graphComponents); |
| 186 | 195 | for (GraphLevelInference<MstGraphLevel> inference : bestPath) { |