0300379fda32fdb3e161cf2d8ce48bfae5099138
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 |