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) {