java/com.sap.sailing.windestimation.lab/python/mst_graph_visualizer_graphviz.py
... ...
@@ -60,10 +60,49 @@ def format_wind(comp):
60 60
return f"{wind_est:.0f}±{wind_width/2:.0f}°"
61 61
62 62
63
+def format_distance(node):
64
+ """
65
+ Format distance/time to parent in human-readable form.
66
+
67
+ The node may contain:
68
+ - spatialDistanceToParentMeters: actual spatial distance in meters
69
+ - timeDiffToParentSeconds: actual time difference in seconds
70
+ - compoundDistanceToParent: sum of predicted std deviations (legacy, for transition probability)
71
+
72
+ We prefer to show actual spatial distance and time if available.
73
+ """
74
+ spatial_dist = node.get('spatialDistanceToParentMeters')
75
+ time_diff = node.get('timeDiffToParentSeconds')
76
+
77
+ if spatial_dist is not None and time_diff is not None:
78
+ # Format spatial distance
79
+ if spatial_dist >= 1000:
80
+ dist_str = f'{spatial_dist/1000:.1f}km'
81
+ elif spatial_dist >= 1:
82
+ dist_str = f'{spatial_dist:.0f}m'
83
+ else:
84
+ dist_str = f'{spatial_dist:.1f}m'
85
+
86
+ # Format time difference
87
+ if time_diff >= 60:
88
+ time_str = f'{time_diff/60:.1f}min'
89
+ else:
90
+ time_str = f'{time_diff:.0f}s'
91
+
92
+ return f'{dist_str}, {time_str}'
93
+
94
+ # Fallback to compound distance (legacy)
95
+ compound_dist = node.get('compoundDistanceToParent') or node.get('distanceToParent')
96
+ if compound_dist is None or compound_dist == 0:
97
+ return None
98
+ return f'σ={compound_dist:.1f}' # Mark as std sum with σ symbol
99
+
100
+
63 101
def create_node_label_with_ports(node, best_type=None):
64 102
"""
65 103
Create HTML-like label for a node with 4 compartments.
66 104
Each compartment has a PORT attribute for edge connections.
105
+ Distance to parent is shown next to timestamp.
67 106
"""
68 107
compartments = {c['type']: c for c in node['compartments']}
69 108
... ...
@@ -74,6 +113,9 @@ def create_node_label_with_ports(node, best_type=None):
74 113
else:
75 114
time_part = timestamp[:8] if len(timestamp) >= 8 else timestamp
76 115
116
+ # Get distance/time to parent (pass whole node for full info)
117
+ dist_str = format_distance(node)
118
+
77 119
# Build HTML table with PORT attributes
78 120
html = '<<TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">'
79 121
html += '<TR>'
... ...
@@ -114,8 +156,12 @@ def create_node_label_with_ports(node, best_type=None):
114 156
html += f'<TD PORT="{port_name}"{border_attr}>{cell_content}</TD>'
115 157
116 158
html += '</TR>'
117
- # Footer row with timestamp
118
- html += f'<TR><TD COLSPAN="4" BGCOLOR="white"><FONT POINT-SIZE="9">{time_part}</FONT></TD></TR>'
159
+ # Footer row with timestamp and distance to parent
160
+ if dist_str:
161
+ footer_content = f'{time_part} <FONT COLOR="blue">↑{dist_str}</FONT>'
162
+ else:
163
+ footer_content = time_part
164
+ html += f'<TR><TD COLSPAN="4" BGCOLOR="white"><FONT POINT-SIZE="9">{footer_content}</FONT></TD></TR>'
119 165
html += '</TABLE>>'
120 166
121 167
return html
... ...
@@ -221,18 +267,23 @@ def visualize_mst_graph(data, output_file=None, max_nodes=100, min_edge_prob=0.0
221 267
222 268
# Format probability label
223 269
if trans_prob >= 0.01:
224
- prob_label = f'{trans_prob:.2f}'
270
+ prob_str = f'{trans_prob:.2f}'
225 271
elif trans_prob >= 0.001:
226
- prob_label = f'{trans_prob:.3f}'
272
+ prob_str = f'{trans_prob:.3f}'
227 273
else:
228
- prob_label = f'{trans_prob:.1e}'
274
+ prob_str = f'{trans_prob:.1e}'
275
+
276
+ # Build edge label with type transition and probability
277
+ from_abbrev = TYPE_ABBREV[from_type]
278
+ to_abbrev = TYPE_ABBREV[to_type]
279
+ edge_label = f'{from_abbrev}→{to_abbrev}\\n{prob_str}'
229 280
230 281
# Use PORT to connect to specific compartments
231 282
from_port = f'{from_id}:{from_type}:s' # :s = south (bottom) of cell
232 283
to_port = f'{to_id}:{to_type}:n' # :n = north (top) of cell
233 284
234 285
dot.edge(from_port, to_port,
235
- label=prob_label,
286
+ label=edge_label,
236 287
color=color,
237 288
penwidth=penwidth,
238 289
style=style,
... ...
@@ -293,8 +344,17 @@ def create_detailed_edge_graph(data, output_file=None, max_depth=10, min_edge_pr
293 344
else:
294 345
time_part = str(node_id)
295 346
347
+ # Get distance/time to parent (pass whole node)
348
+ dist_str = format_distance(node)
349
+
350
+ # Build cluster label with time and distance
351
+ if dist_str:
352
+ cluster_label = f'{time_part} ↑{dist_str}'
353
+ else:
354
+ cluster_label = time_part
355
+
296 356
with dot.subgraph(name=f'cluster_{node_id}') as c:
297
- c.attr(label=f'{time_part}')
357
+ c.attr(label=cluster_label)
298 358
c.attr(style='rounded,filled')
299 359
c.attr(fillcolor='white')
300 360
... ...
@@ -367,14 +427,19 @@ def create_detailed_edge_graph(data, output_file=None, max_depth=10, min_edge_pr
367 427
368 428
# Format probability label
369 429
if trans_prob >= 0.01:
370
- prob_label = f'{trans_prob:.2f}'
430
+ prob_str = f'{trans_prob:.2f}'
371 431
elif trans_prob >= 0.001:
372
- prob_label = f'{trans_prob:.3f}'
432
+ prob_str = f'{trans_prob:.3f}'
373 433
else:
374
- prob_label = f'{trans_prob:.1e}'
434
+ prob_str = f'{trans_prob:.1e}'
435
+
436
+ # Build edge label with type transition and probability
437
+ from_abbrev = TYPE_ABBREV[edge['fromType']]
438
+ to_abbrev = TYPE_ABBREV[edge['toType']]
439
+ edge_label = f'{from_abbrev}→{to_abbrev}\\n{prob_str}'
375 440
376 441
dot.edge(from_comp_id, to_comp_id,
377
- label=prob_label,
442
+ label=edge_label,
378 443
color=color,
379 444
penwidth=penwidth,
380 445
fontsize='7',
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/aggregator/msthmm/MstGraphExporter.java
... ...
@@ -90,9 +90,21 @@ public class MstGraphExporter {
90 90
writer.write(" \"timestamp\": \"" + DATE_FORMAT.format(maneuver.getManeuverTimePoint().asDate()) + "\",\n");
91 91
writer.write(" \"position\": {\"lat\": " + maneuver.getManeuverPosition().getLatDeg() +
92 92
", \"lon\": " + maneuver.getManeuverPosition().getLngDeg() + "},\n");
93
- writer.write(" \"distanceToParent\": " + level.getDistanceToParent() + ",\n");
93
+ // distanceToParent is actually the "compound distance" = sum of two predicted standard deviations
94
+ // It's used for the Gaussian-based transition probability calculation
95
+ writer.write(" \"compoundDistanceToParent\": " + level.getDistanceToParent() + ",\n");
96
+ // Also export actual spatial distance and time difference to parent
97
+ final MstGraphLevel parent = level.getParent();
98
+ if (parent != null) {
99
+ final ManeuverForEstimation parentManeuver = parent.getManeuver();
100
+ final double spatialDistanceMeters = maneuver.getManeuverPosition()
101
+ .getDistance(parentManeuver.getManeuverPosition()).getMeters();
102
+ final long timeDiffMillis = Math.abs(maneuver.getManeuverTimePoint().asMillis()
103
+ - parentManeuver.getManeuverTimePoint().asMillis());
104
+ writer.write(" \"spatialDistanceToParentMeters\": " + spatialDistanceMeters + ",\n");
105
+ writer.write(" \"timeDiffToParentSeconds\": " + (timeDiffMillis / 1000.0) + ",\n");
106
+ }
94 107
writer.write(" \"compartments\": [\n");
95
-
96 108
// Export each maneuver type compartment
97 109
boolean firstCompartment = true;
98 110
for (GraphNode<MstGraphLevel> node : level.getLevelNodes()) {
java/com.sap.sailing.windestimation/src/com/sap/sailing/windestimation/integration/IncrementalMstHmmWindEstimationForTrackedRace.java
... ...
@@ -185,10 +185,13 @@ public class IncrementalMstHmmWindEstimationForTrackedRace implements Incrementa
185 185
mstManeuverGraphGenerator.add(competitor, newManeuverSpot, trackTimeInfo);
186 186
}
187 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);
188
+ if (logger.isLoggable(Level.FINE)) {
189
+ logger.fine("Exporting the maneuver graph to file after updating it with new maneuver spots for competitor "+competitor);
190
+ try {
191
+ MstGraphExportHelper.exportToFile(graphComponents, mstManeuverGraphGenerator.getTransitionProbabilitiesCalculator(), File.createTempFile("maneuverExport_", ".json").getCanonicalPath());
192
+ } catch (IOException e) {
193
+ logger.log(Level.WARNING, "Exporting the maneuver graph to file failed", e);
194
+ }
192 195
}
193 196
if (graphComponents != null) {
194 197
Iterable<GraphLevelInference<MstGraphLevel>> bestPath = bestPathsCalculator.getBestNodes(graphComponents);
java/target/configuration/logging_debug.properties
... ...
@@ -66,4 +66,5 @@ com.sap.sailing.aiagent.impl.RaceListener.level = FINE
66 66
# Show GithubReleasesRepository log output
67 67
com.sap.sse.landscape.impl.GithubReleasesRepository.level = FINE
68 68
69
-com.sap.sailing.windestimation.integration.IncrementalMstHmmWindEstimationForTrackedRaceTest.level = FINE
... ...
\ No newline at end of file
0
+com.sap.sailing.windestimation.integration.IncrementalMstHmmWindEstimationForTrackedRaceTest.level = FINE
1
+com.sap.sailing.windestimation.integration.IncrementalMstHmmWindEstimationForTrackedRace.level = FINE
... ...
\ No newline at end of file