Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions rmgpy/data/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1066,9 +1066,10 @@ def descend_tree(self, structure, atoms, root=None, strict=False):
else:
return root
else:
# logging.warning('For {0}, a node {1} with overlapping children {2} was encountered '
# 'in tree with top level nodes {3}. Assuming the first match is the '
# 'better one.'.format(structure, root, next, self.top))
# Multiple children match - pick the first in tree order.
# The tree is constructed deterministically, so this is deterministic.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this comment is correct, that tree construction is deterministic.

# (Sorting by label can pick the wrong node when siblings overlap,
# e.g. Nitrites sorts before Nitro but Nitro is the intended match.)
Comment on lines -1069 to +1072

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logging line was commented out 12 year ago e8c7f1d, so is probably fine to remove.

What does the "sorting by label" part of the new comment refer to?

return self.descend_tree(structure, atoms, next_node[0], strict)

def are_siblings(self, node, node_other):
Expand Down
2 changes: 1 addition & 1 deletion rmgpy/data/thermo.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ def combine_cycles(cycle1, cycle2):
"""
set1 = set(cycle1)
set2 = set(cycle2)
return list(set1.union(set2))
return sorted(set1.union(set2))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where in the code is combine_cycles used? I only see it in the tests and there its output immediately gets converted to a set.



def is_aromatic_ring(submol):
Expand Down
21 changes: 19 additions & 2 deletions rmgpy/molecule/graph.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,10 @@ cdef class Graph(object):
cpdef sort_vertices(self, bint save_order=False):
"""
Sort the vertices in the graph. This can make certain operations, e.g.
the isomorphism functions, much more efficient.
the isomorphism functions, much more efficient. Vertices that have a
``sorting_key`` attribute (e.g. :class:`Atom`) will use it as a chemical
tiebreaker for vertices with identical connectivity values, ensuring
deterministic ordering.
"""
cdef Vertex vertex
cdef int index
Expand All @@ -485,7 +488,21 @@ cdef class Graph(object):
# If we need to sort then let's also update the connecitivities so
# we're sure they are right, since the sorting labels depend on them
self.update_connectivity_values()
self.vertices.sort(key=get_vertex_connectivity_value)

# Build sort keys with optional chemical tiebreaker (sorting_key attribute)
# to ensure deterministic ordering when connectivity values are identical.
# Index is included as a final tiebreaker to avoid comparing vertex

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need a final tiebreaker? I thought python's list.sort() was stable, meaning if there is a tie, the order remains the same, which is also the effect of your sorting by index.

# objects directly (GroupAtom has no __lt__).
# Sort (key, index) pairs, then use the resulting index order to reorder.
paired = []
for index, vertex in enumerate(self.vertices):
conn = get_vertex_connectivity_value(vertex)
sort_key = getattr(vertex, 'sorting_key', None)
paired.append(((conn, sort_key if sort_key is not None else ()), index))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not paired.append(((conn, sort_key if sort_key is not None else (conn,)), index))? In other words, the current code uses get_vertex_connectivity_value as the primary sorting key. Your change makes it so that if you are sorting something without sorting_key, then the sorting is only done by the index. Is that intentional?


paired.sort()
ordered = [self.vertices[idx] for _, idx in paired]
self.vertices = ordered
for index, vertex in enumerate(self.vertices):
vertex.sorting_label = index

Expand Down
10 changes: 5 additions & 5 deletions rmgpy/molecule/molecule.py
Original file line number Diff line number Diff line change
Expand Up @@ -2838,8 +2838,8 @@ def get_polycycles(self):
if vertex in cycle:
polycyclic_cycle.update(cycle)

# convert each set to a list
continuous_cycles = [list(cycle) for cycle in continuous_cycles]
# convert each set to a list (sorted for deterministic atom ordering)
continuous_cycles = [sorted(cycle) for cycle in continuous_cycles]
return continuous_cycles

def get_monocycles(self):
Expand Down Expand Up @@ -2888,9 +2888,9 @@ def get_disparate_cycles(self):
# Merge connected cycles
monocyclic_cycles, polycyclic_cycles = self._merge_cycles(cycle_sets)

# Convert cycles back to lists
monocyclic_cycles = [list(cycle_set) for cycle_set in monocyclic_cycles]
polycyclic_cycles = [list(cycle_set) for cycle_set in polycyclic_cycles]
# Convert cycles back to lists (sorted for deterministic atom ordering)
monocyclic_cycles = [sorted(cycle_set) for cycle_set in monocyclic_cycles]
polycyclic_cycles = [sorted(cycle_set) for cycle_set in polycyclic_cycles]

return monocyclic_cycles, polycyclic_cycles

Expand Down
7 changes: 6 additions & 1 deletion rmgpy/reaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,12 @@ def is_isomorphic(self, other, either_direction=True, check_identical=False, che
save_order=save_order)

# Compare specific_collider to specific_collider
collider_match = (self.specific_collider == other.specific_collider)
if self.specific_collider is None:
collider_match = other.specific_collider is None
elif other.specific_collider is None:
collider_match = False
else:
collider_match = self.specific_collider.is_isomorphic(other.specific_collider)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commit comment says this is to fix: When two models are loaded from separate Chemkin files, the specific_collider (N2) species are distinct object instances despite being chemically identical, causing == to always return False. Maybe this should use is_identical instead of is_isomorphic as that is closer to the original equality check while still allowing the objects to not be the same in memory.


# Return now, if we can
if forward_reactants_match and forward_products_match and collider_match:
Expand Down
10 changes: 8 additions & 2 deletions rmgpy/rmg/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1110,7 +1110,13 @@ def execute(self, initialize=True, **kwargs):
# These should be Species or Network objects
logging.info("")

objects_to_enlarge = list(set(objects_to_enlarge))
# objects_to_enlarge can contain Species, Network, or tuples (PDepNetwork, Species)
# Sort deterministically: by label for objects with .label, by first element for tuples
def _sort_key(obj):
if isinstance(obj, tuple):
return (1, getattr(obj[0], 'label', str(obj[0])))
return (0, getattr(obj, 'label', str(obj)))
objects_to_enlarge = sorted(set(objects_to_enlarge), key=_sort_key)

# Add objects to enlarge to the core first
for objectToEnlarge in objects_to_enlarge:
Expand Down Expand Up @@ -1927,7 +1933,7 @@ def process_to_species_networks(self, obj):
rspcs = self.process_reactions_to_species([k for k in obj if isinstance(k, Reaction)])
spcs = {k for k in obj if isinstance(k, Species)} | rspcs
nworks, pspcs = self.process_pdep_networks([k for k in obj if isinstance(k, PDepNetwork)])
spcs = list(spcs - pspcs) # avoid duplicate species
spcs = sorted(spcs - pspcs, key=lambda s: s.label) # avoid duplicate species, sort deterministically
return spcs + nworks
else:
raise TypeError("improper call, obj input was incorrect")
Expand Down
4 changes: 2 additions & 2 deletions test/regression/RMS_CSTR_liquid_oxidation/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@
toleranceMoveToCore=0.01,
toleranceKeepInEdge=0.001,
toleranceInterruptSimulation=1e8,
maximumEdgeSpecies=10000,
maximumEdgeSpecies=300,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the changes to this file still required after your other changes, or could this be reverted? Changing from 10000 to 300 seems a little drastic to me. Can you comment on this change? (and the similar change to maxNumSpecies?

minCoreSizeForPrune=10,
minSpeciesExistIterationsForPrune=2,
maxNumObjsPerIter=3,
filterReactions=True,
maxNumSpecies=35,
maxNumSpecies=15,
)

options(
Expand Down
Loading