Skip to content

Commit 4e9851a

Browse files
okohlbacherclaude
andcommitted
merge(999.73): #576 total-ordering + #539-hang fix (un-quarantine AssignBondOrder test2)
- AromaticityProcessor::aromatize infinite-loop fix (the 60-min AssignBondOrder CI hang; a regression from the 999.72 #539 fixed-point pass on the SMARTS path) - PartialBondOrderAssignment::operator< made a total strict weak ordering (#576); AssignBondOrderProcessor_test2 un-quarantined (deterministic across STLs) - #627 (Directory) NOT yet included — executor stalled before it; follows separately Verified on macOS-arm64: AssignBondOrderProcessor_test1 3s (was >3600s hang), test2 PASS 2s, AromaticityProcessor_test PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 parents 8342da0 + 5a61576 commit 4e9851a

4 files changed

Lines changed: 75 additions & 31 deletions

File tree

source/QSAR/aromaticityProcessor.C

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,19 @@ namespace BALL
708708
// does the ring share a full edge with the existing aromatic system?
709709
// (a ring bond whose two endpoints are both already on an aromatic bond)
710710
bool shares_aromatic_edge = false;
711-
// is any ring bond still non-aromatic (i.e. is there work to do)?
711+
// is any ring bond still NOT marked aromatic (i.e. is there work to do)?
712+
//
713+
// BUG-576 (#576): the termination signal MUST be measured against the
714+
// SAME state that the aromatization below mutates. The aromatization
715+
// always sets the Bond::IS_AROMATIC *property* (and the bond *order*
716+
// only when overwrite_bond_orders_ is true). The original BUG-539 code
717+
// gated this loop on b->getOrder() != ORDER__AROMATIC, so when
718+
// overwrite_bond_orders_ == false (the SMARTS / evaluatePenalty path,
719+
// e.g. BEWCUB.mol2) the order never changes, has_non_aromatic_ring_bond
720+
// stays true forever and the while(changed) loop never terminates —
721+
// that infinite loop is the CI hang. Gate on the IS_AROMATIC property,
722+
// which the aromatization unconditionally sets, so each fused ring is
723+
// processed exactly once and the fixed point is actually reached.
712724
bool has_non_aromatic_ring_bond = false;
713725
for (HashSet<Atom*>::iterator a = ring.begin(); a != ring.end(); ++a)
714726
{
@@ -719,7 +731,7 @@ namespace BALL
719731
{
720732
continue;
721733
}
722-
if (b->getOrder() != Bond::ORDER__AROMATIC)
734+
if (!b->hasProperty(Bond::IS_AROMATIC))
723735
{
724736
has_non_aromatic_ring_bond = true;
725737
}

source/STRUCTURE/BONDORDERS/partialBondOrderAssignment.C

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
#include <BALL/STRUCTURE/assignBondOrderProcessor.h>
33
#include <BALL/KERNEL/PTE.h>
44

5+
#include <algorithm> // BUG-576: std::lexicographical_compare for the total operator< tie-break
6+
57
namespace BALL
68
{
79
#define INFINITE_PENALTY 1e5
@@ -33,21 +35,56 @@ namespace BALL
3335

3436
// the less operator
3537
// note: we want a reverse sort, hence we actually return a "greater"
38+
//
39+
// BUG-576 (#576): This comparator feeds a std::priority_queue (a max-heap that
40+
// pops the highest-priority element, i.e. the LOWEST penalty, first). For that
41+
// to be deterministic across platforms the ordering MUST be a TOTAL strict weak
42+
// ordering: it must distinguish every pair of entries that differ in content,
43+
// otherwise equivalent entries pop in heap-internal (libc++ vs libstdc++ vs MSVC
44+
// STL) order and apply(n) returns a different n-th solution per platform.
45+
//
46+
// We cascade through content-derived keys, returning as soon as a key differs:
47+
// (1) coarsePenalty() (the optimisation objective)
48+
// (2) finePenalty() ALWAYS (not gated on use_fine_penalty_ — that option
49+
// decides which solutions are optimal, not how the queue
50+
// must order them; applying it always keeps the FINE
51+
// subtest passing and breaks coarse ties deterministically)
52+
// (3) last_bond (Position, numeric)
53+
// (4) bond_orders (lexicographic, vector<short>)
54+
// Returning false in both directions is correct ONLY when penalties AND
55+
// bond_orders are identical, i.e. the entries are genuinely indistinguishable.
56+
//
57+
// Direction: operator< returns true when *this should pop AFTER b (i.e. *this is
58+
// the "greater" entry). At each level we therefore compare *this's key '>' b's key.
3659
bool PartialBondOrderAssignment::operator < (const PartialBondOrderAssignment& b) const
3760
{
3861
bool value = false;
39-
if (coarsePenalty() > b.coarsePenalty())
62+
63+
float coarse_a = coarsePenalty();
64+
float coarse_b = b.coarsePenalty();
65+
if (coarse_a != coarse_b)
4066
{
41-
value = true;
67+
value = (coarse_a > coarse_b);
4268
}
4369
else
4470
{
45-
if (coarsePenalty() == b.coarsePenalty())
71+
float fine_a = finePenalty();
72+
float fine_b = b.finePenalty();
73+
if (fine_a != fine_b)
4674
{
47-
if (abop->use_fine_penalty_ && (finePenalty() > b.finePenalty()))
48-
{
49-
value = true;
50-
}
75+
value = (fine_a > fine_b);
76+
}
77+
else if (last_bond != b.last_bond)
78+
{
79+
value = (last_bond > b.last_bond);
80+
}
81+
else
82+
{
83+
// total tie-break on the bond-order vector itself.
84+
// std::lexicographical_compare(b, a) is true iff b < a lexicographically,
85+
// i.e. iff *this is lexicographically greater -> *this pops after b.
86+
value = std::lexicographical_compare(b.bond_orders.begin(), b.bond_orders.end(),
87+
bond_orders.begin(), bond_orders.end());
5188
}
5289
}
5390

test/AssignBondOrderProcessor_test2.C

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,14 @@ CHECK(Option::USE_FINE_PENALTY)
10061006
storeBondOrders(sys);
10071007
sys.apply(abop);
10081008
TEST_EQUAL(compareBondOrder(sys), false)
1009-
abop.apply(5); //3);
1009+
// BUG-576 (#576): with the deterministic total ordering in
1010+
// PartialBondOrderAssignment::operator< the degenerate optimal solutions for
1011+
// AMPTRB10_kek_sol0 enumerate in a fixed, platform-independent order. The stored
1012+
// (sol0) assignment is now consistently the n-th solution -> index 3 on every
1013+
// platform. The old index 5 was the libc++/heap-internal position on Intel/Linux
1014+
// only and produced a different solution per platform (the non-determinism this
1015+
// fix removes), which is why test2 was previously quarantined on Apple Silicon.
1016+
abop.apply(3);
10101017
TEST_EQUAL(compareBondOrder(sys), true)
10111018
RESULT
10121019

test/CMakeLists.txt

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -81,27 +81,15 @@ SET_TESTS_PROPERTIES (
8181
# as green. Each quarantine has a backlog stub in ROADMAP.md + a row in
8282
# .planning/phases/09-test-suite-triage/09-TRIAGE.md.
8383
#
84-
# AssignBondOrderProcessor_test2 — QUARANTINED (Phase 9, 09-TRIAGE.md row 3)
85-
# Failure: test/AssignBondOrderProcessor_test2.C:1010
86-
# CHECK(Option::USE_FINE_PENALTY): compareBondOrder(sys) returns 0, expected 1
87-
# after abop.apply(5). The fine-penalty ILP/heuristic code path in
88-
# source/STRUCTURE/assignBondOrderProcessor.C produces an incorrect bond-order
89-
# assignment for AMPTRB10_kek_sol0.mol2 when the 5th solution is requested.
90-
# AssignBondOrderProcessor_test1 (default code path) passes — the regression
91-
# is isolated to the fine-penalty branch. Algorithmic fix estimated >2h.
92-
# Backlog: Phase 999.34 closed 2026-05-17 (Windows test gatekeeper only); the
93-
# AssignBondOrderProcessor fine-penalty algorithmic fix is a separate v1.7 /
94-
# v2.0 cycle task (no dedicated phase stub yet — re-file if/when picked up).
95-
#
96-
# NOTE (2026-05-16 Rule-1 fix): WILL_FAIL applies only on Apple Silicon (the
97-
# failure platform). Linux x64/arm64 pass this test cleanly; unconditional
98-
# WILL_FAIL causes an "unexpected pass" error on Linux (ctest exit 8).
99-
IF (APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64")
100-
SET_TESTS_PROPERTIES (
101-
AssignBondOrderProcessor_test2
102-
PROPERTIES WILL_FAIL TRUE
103-
)
104-
ENDIF()
84+
# AssignBondOrderProcessor_test2 — UN-QUARANTINED (BUG-576, #576, Phase 999.73).
85+
# The Apple-Silicon-only WILL_FAIL is gone: the failure was platform-dependent
86+
# solution enumeration, not an algorithmic bug. PartialBondOrderAssignment::
87+
# operator< is now a TOTAL strict weak ordering (coarse -> fine -> last_bond ->
88+
# lexicographic bond_orders), so the degenerate-optimal solutions enumerate in a
89+
# fixed order on libc++/libstdc++/MSVC STL alike; the test asserts the stored sol0
90+
# at its now-deterministic index 3 (was the heap-internal "5"). The 60-minute CI
91+
# hang was a separate defect in AromaticityProcessor::aromatize (BUG-539 fused-ring
92+
# propagation looped forever when overwrite_bond_orders_==false) — also fixed here.
10593

10694
# Regressions from parallel-session PR merge (commit 45dce6971, Phase 999.14,
10795
# feat: merge C++ slice of PRs #546 #554 #550). These tests broke because the

0 commit comments

Comments
 (0)