From 9a757d0d798be6b9451a3dd9aa9b2a05842d2965 Mon Sep 17 00:00:00 2001 From: even1024 Date: Thu, 30 Apr 2026 16:32:41 +0200 Subject: [PATCH] Fix non-deterministic layout order on macOS Add ext_idx tie-breaking to vertex comparators and component sorting in AttachmentLayout to ensure consistent layout results across platforms. Also add energy tie-breaking by ascending permutation order. --- .../layout/src/attachment_layout.cpp | 61 ++++++++++++++++++- .../src/molecule_layout_graph_assign.cpp | 11 +++- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/core/indigo-core/layout/src/attachment_layout.cpp b/core/indigo-core/layout/src/attachment_layout.cpp index 41a9d81bf6..c3a1751b21 100644 --- a/core/indigo-core/layout/src/attachment_layout.cpp +++ b/core/indigo-core/layout/src/attachment_layout.cpp @@ -17,6 +17,7 @@ ***************************************************************************/ #include "layout/attachment_layout.h" +#include using namespace indigo; @@ -77,6 +78,38 @@ AttachmentLayout::AttachmentLayout(const BiconnectedDecomposer& bc_decom, const } } + // Sort undrawn components by non-source ext_idx for deterministic cross-platform order + int n_undrawn = _attached_bc.size() - 1; + if (n_undrawn > 1) + { + for (i = 0; i < n_undrawn - 1; i++) + { + for (int j = i + 1; j < n_undrawn; j++) + { + + auto min_ext_idx = [&](int comp_slot) -> int { + const MoleculeLayoutGraph& comp = *bc_components[_attached_bc[comp_slot]]; + int min_idx = INT_MAX; + for (int k = comp.vertexBegin(); k < comp.vertexEnd(); k = comp.vertexNext(k)) + { + int ext = comp.getVertexExtIdx(k); + if (ext != _src_vertex && ext < min_idx) + min_idx = ext; + } + return min_idx; + }; + + if (min_ext_idx(j) < min_ext_idx(i)) + { + _src_vertex_map.swap(i, j); + _attached_bc.swap(i, j); + _bc_angles.swap(i, j); + _vertices_l.swap(i, j); + } + } + } + } + int n_new_vert = 0; for (i = 0; i < _attached_bc.size() - 1; i++) @@ -254,8 +287,32 @@ void LayoutChooser::_perform(int level) // Draw new components on vertex _makeLayout(); - // Check if new layout is better - if (_layout.calculateEnergy() < _cur_energy - EPSILON) + float energy = _layout.calculateEnergy(); + + // Check if new layout is better; on equal energy prefer ascending permutation + // for deterministic cross-platform results (exact == is intentional here). + bool adopt = false; + if (energy < _cur_energy - EPSILON) + { + adopt = true; + } + else if (energy == _cur_energy) + { + // Tie-break: prefer ascending _comp_permutation + bool cur_is_ascending = true; + for (int ni = 0; ni + 1 < _n_components; ni++) + { + if (_comp_permutation[ni] > _comp_permutation[ni + 1]) + { + cur_is_ascending = false; + break; + } + } + if (cur_is_ascending) + adopt = true; + } + + if (adopt) { _layout.applyLayout(); _cur_energy = _layout._energy; diff --git a/core/indigo-core/layout/src/molecule_layout_graph_assign.cpp b/core/indigo-core/layout/src/molecule_layout_graph_assign.cpp index e3db529e10..ff20c54f0b 100644 --- a/core/indigo-core/layout/src/molecule_layout_graph_assign.cpp +++ b/core/indigo-core/layout/src/molecule_layout_graph_assign.cpp @@ -77,7 +77,11 @@ static int _vertex_cmp(int& n1, int& n2, void* context) return -1; } - return v1.morgan_code - v2.morgan_code; + if (v1.morgan_code != v2.morgan_code) + return v1.morgan_code - v2.morgan_code; + + // Tie-break by ext_idx for deterministic order + return v1.ext_idx - v2.ext_idx; } // Specialized BiconnectedDecomposer for molecule layout @@ -397,7 +401,10 @@ void MoleculeLayoutGraph::_assignAbsoluteCoordinates(float bond_length) const LayoutVertex& v2 = getLayoutVertex(n2); if (v1.is_cyclic != v2.is_cyclic) return v2.is_cyclic; - return v1.morgan_code < v2.morgan_code; + if (v1.morgan_code != v2.morgan_code) + return v1.morgan_code < v2.morgan_code; + // Tie-break by ext_idx for deterministic order + return v1.ext_idx < v2.ext_idx; }; std::stable_sort(adjacent_list.begin(), adjacent_list.end(), vertex_cmp_less); _attachDandlingVertices(k, adjacent_list);