From ac8fd4869b3e57526a8d8a50e792cb263baa3196 Mon Sep 17 00:00:00 2001 From: Calvin Pieters Date: Mon, 17 Aug 2026 00:04:04 +0300 Subject: [PATCH 1/2] Order the edges a graph returns by vertex instead of by hash Graph.get_all_edges() de-duplicated the edges through a set of Edge objects and returned list(edge_set), so the order came out of the set's iteration. Atom and Bond hash on their symbols and bond order rather than on identity, and those hashes are derived from string hashes, which Python randomises per process. Every carbon in a molecule therefore lands in one hash bucket and the resulting edge order differs from one process to the next. Molecule perception consumes that order: generate_lewis_structure() walks the bond list in an A* search whose equal-cost states are explored in the order the bonds are listed, so a molecule with several equal-cost Lewis structures could be perceived differently in different processes. Diphenylprolinol methyl ether was perceived with a methoxy C=O double bond and a carbene ring carbon in about 2% of hash seeds, which then failed RDKit's valence check. Return the edges in the graph's vertex order instead, de-duplicating on the edge identities, so the order is the same in every process. --- arc/molecule/graph.pyx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/arc/molecule/graph.pyx b/arc/molecule/graph.pyx index d6382048c9..4440685df2 100644 --- a/arc/molecule/graph.pyx +++ b/arc/molecule/graph.pyx @@ -218,18 +218,25 @@ cdef class Graph(object): cpdef list get_all_edges(self): """ - Returns a list of all edges in the graph. + Returns a list of all edges in the graph, each edge appearing once, + ordered by the graph's vertex order and, within a vertex, by the order + in which its edges were added. The order does not depend on the hash + values of the edges, so it is identical in every process. """ - cdef set edge_set + cdef list edges + cdef set seen cdef Vertex vertex cdef Edge edge - edge_set = set() + edges = [] + seen = set() for vertex in self.vertices: for edge in vertex.edges.values(): - edge_set.add(edge) + if id(edge) not in seen: + seen.add(id(edge)) + edges.append(edge) - return list(edge_set) + return edges cpdef dict get_edges(self, Vertex vertex): """ From 872d8e205a2b83dd822bc498f1ac4d3ad1252af2 Mon Sep 17 00:00:00 2001 From: Calvin Pieters Date: Mon, 17 Aug 2026 00:04:04 +0300 Subject: [PATCH 2/2] Test that a graph's edge order is stable across processes The edge order returned by get_all_edges() used to follow the iteration order of a set of Edge objects, which is governed by the per-process randomized string hash, so this asserts the property directly: two subprocesses started at different PYTHONHASHSEED values must report the same order. The child processes are given PYTHONPATH explicitly. A subprocess inherits the parent's working directory but not pytest's sys.path, so without it the child imports whichever ARC `import arc` resolves to -- which, with an editable install present, is not necessarily the tree under test. The test then either fails spuriously when run from outside the repository root, or passes while having validated a different checkout. --- arc/molecule/graph_test.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/arc/molecule/graph_test.py b/arc/molecule/graph_test.py index 6a8f11b2c1..fae67189cc 100644 --- a/arc/molecule/graph_test.py +++ b/arc/molecule/graph_test.py @@ -1,8 +1,12 @@ #!/usr/bin/env python3 # encoding: utf-8 +import os +import subprocess +import sys import unittest +import arc from arc.molecule.graph import Edge, Graph, Vertex @@ -108,6 +112,37 @@ def test_get_all_edges(self): self.assertIsInstance(edges, list) self.assertEqual(len(edges), 5) + def test_get_all_edges_orders_the_edges_by_vertex(self): + """ + Test that Graph.get_all_edges() returns the edges in vertex order, each edge once. + """ + expected = [] + for vertex in self.graph.vertices: + for edge in vertex.edges.values(): + if not any(edge is seen for seen in expected): + expected.append(edge) + self.assertEqual(self.graph.get_all_edges(), expected) + + def test_get_all_edges_order_does_not_depend_on_the_hash_seed(self): + """ + Test that Graph.get_all_edges() returns the same order in processes with different hash seeds. + """ + script = ('from arc.molecule.molecule import Molecule\n' + 'mol = Molecule(smiles="c1ccccc1Cc1ccccc1")\n' + 'atoms = mol.atoms\n' + 'print([(atoms.index(edge.vertex1), atoms.index(edge.vertex2)) ' + 'for edge in mol.get_all_edges()])\n') + orders = list() + for seed in ('1', '35'): + arc_root = os.path.dirname(os.path.dirname(os.path.abspath(arc.__file__))) + environment = dict(os.environ, PYTHONHASHSEED=seed, PYTHONPATH=arc_root) + result = subprocess.run([sys.executable, '-c', script], capture_output=True, text=True, + env=environment, timeout=300) + self.assertEqual(result.returncode, 0, f'Subprocess failed: {result.stderr}') + orders.append(result.stdout.strip()) + self.assertTrue(orders[0]) + self.assertEqual(orders[0], orders[1]) + def test_has_vertex(self): """ Test the Graph.has_vertex() method.