From 85c278a395b15c243f5d43136be04198af45a47b Mon Sep 17 00:00:00 2001 From: chrisjonesBSU Date: Sat, 30 May 2026 16:22:41 +0100 Subject: [PATCH 1/8] Build dict of connections rather than iterate for each label --- gmso/parameterization/molecule_utils.py | 45 +++++++++++++++++++ .../topology_parameterizer.py | 38 ++++++++++------ 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/gmso/parameterization/molecule_utils.py b/gmso/parameterization/molecule_utils.py index 79152552f..22fd94c45 100644 --- a/gmso/parameterization/molecule_utils.py +++ b/gmso/parameterization/molecule_utils.py @@ -56,3 +56,48 @@ def assert_no_boundary_bonds(top): assert site1.molecule == site2.molecule, assertion_msg.format( site1.name, site1.molecule, site2.name, site2.molecule ) + + +def build_molecule_connection_index(top, is_group=False): + """Build a dict mapping molecule/group label -> connection lists. + + Scans each connection type once. Use this before a per-molecule + parameterization loop to avoid O(M * C) repeated filtering. + + Returns: + dict: { + label: { + "bonds": [...], + "angles": [...], + "dihedrals": [...], + "impropers": [...] + } + } + """ + index = {} + + def _label_of(site): + return getattr(site, "group") if is_group else site.molecule.name + + def _bucket(connections, key): + for conn in connections: + members = conn.connection_members + labels = {_label_of(s) for s in members} + if len(labels) == 1: # all members same molecule + label = labels.pop() + entry = index.setdefault( + label, { + "bonds": [], + "angles": [], + "dihedrals": [], + "impropers": [] + } + ) + entry[key].append(conn) + + _bucket(top.bonds, "bonds") + _bucket(top.angles, "angles") + _bucket(top.dihedrals, "dihedrals") + _bucket(top.impropers, "impropers") + + return index diff --git a/gmso/parameterization/topology_parameterizer.py b/gmso/parameterization/topology_parameterizer.py index e78287c9c..52ed37706 100644 --- a/gmso/parameterization/topology_parameterizer.py +++ b/gmso/parameterization/topology_parameterizer.py @@ -24,6 +24,7 @@ molecule_bonds, molecule_dihedrals, molecule_impropers, + build_molecule_connection_index, ) from gmso.parameterization.utils import POTENTIAL_GROUPS from gmso.utils.connectivity import identify_virtual_sites @@ -132,19 +133,25 @@ def _parameterize_connections( ff, label_type=None, label=None, + connection_index=None, ): """Parameterize connections with appropriate potentials from the forcefield.""" if label_type and label: - bonds = molecule_bonds(top, label, True if label_type == "group" else False) - angles = molecule_angles( - top, label, True if label_type == "group" else False - ) - dihedrals = molecule_dihedrals( - top, label, True if label_type == "group" else False - ) - impropers = molecule_impropers( - top, label, True if label_type == "group" else False - ) + if connection_index is not None: + entry = connection_index.get( + label, + {"bonds": [],"angles": [], "dihedrals": [], "impropers": []} + ) + bonds = entry["bonds"] + angles = entry["angles"] + dihedrals = entry["dihedrals"] + impropers = entry["impropers"] + else: + is_group = True if label_type == "group" else False + bonds = molecule_bonds(top, label, is_group) + angles = molecule_angles(top, label, is_group) + dihedrals = molecule_dihedrals(top, label, is_group) + impropers = molecule_impropers(top, label, is_group) else: bonds = top.bonds angles = top.angles @@ -269,7 +276,7 @@ def _apply_virtual_site_parameters(self, virtual_sites, ff, error_on_missing=Tru ) def _parameterize( - self, top, typemap, label_type=None, label=None, speedup_by_moltag=False + self, top, typemap, label_type=None, label=None, speedup_by_moltag=False, connection_index=None ): """Parameterize a topology/subtopology based on an atomtype map.""" if label and label_type: @@ -289,6 +296,7 @@ def _parameterize( forcefield, label_type, label, + connection_index=connection_index ) if forcefield.virtual_types: self._parameterize_virtual_sites(top, sites, bonds, forcefield) @@ -380,6 +388,8 @@ def run_parameterization(self): ) assert_no_boundary_bonds(self.topology) + is_group = (self.config.match_ff_by == "group") + connection_index = build_molecule_connection_index(self.topology, is_group=is_group) for label in labels: if label not in self.forcefields: logger.warning( @@ -395,12 +405,14 @@ def run_parameterization(self): self.config.speedup_by_moltag, self.config.speedup_by_molgraph, ) + self._parameterize( - self.topology, - typemap, + top=self.topology, + typemap=typemap, label_type=self.config.match_ff_by, label=label, speedup_by_moltag=self.config.speedup_by_moltag, # This will be removed from the future iterations + connection_index=connection_index, ) else: typemap = self._get_atomtypes( From 9ffbb5b914210c9916e34a02556ecdef29d04b18 Mon Sep 17 00:00:00 2001 From: chrisjonesBSU Date: Sun, 31 May 2026 00:31:38 +0100 Subject: [PATCH 2/8] Add sig cache for connections to prevent repeated identifier generations --- .../topology_parameterizer.py | 78 +++++++++++-------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/gmso/parameterization/topology_parameterizer.py b/gmso/parameterization/topology_parameterizer.py index 52ed37706..551dec28f 100644 --- a/gmso/parameterization/topology_parameterizer.py +++ b/gmso/parameterization/topology_parameterizer.py @@ -20,11 +20,11 @@ ) from gmso.parameterization.molecule_utils import ( assert_no_boundary_bonds, + build_molecule_connection_index, molecule_angles, molecule_bonds, molecule_dihedrals, molecule_impropers, - build_molecule_connection_index, ) from gmso.parameterization.utils import POTENTIAL_GROUPS from gmso.utils.connectivity import identify_virtual_sites @@ -139,8 +139,7 @@ def _parameterize_connections( if label_type and label: if connection_index is not None: entry = connection_index.get( - label, - {"bonds": [],"angles": [], "dihedrals": [], "impropers": []} + label, {"bonds": [], "angles": [], "dihedrals": [], "impropers": []} ) bonds = entry["bonds"] angles = entry["angles"] @@ -201,27 +200,39 @@ def _parameterize_virtual_sites(self, top, sites, bonds, ff): def _apply_connection_parameters(self, connections, ff, error_on_missing=True): """Find and assign potentials from the forcefield for the provided connections.""" visited = dict() + sig_cache = dict() for connection in connections: - group, connection_identifiers = self.connection_identifier(connection) - match = None - for identifier_key in connection_identifiers: - if tuple(identifier_key) in visited: - match = visited[tuple(identifier_key)] - break - - match = ff.get_potential( - group=group, - key=identifier_key, - exact_match=True, - ) - if match: - visited[tuple(identifier_key)] = match - break + sig = tuple(site.atom_type.name for site in connection.connection_members) + if getattr(connection, "bonds", None): + sig += tuple(b.bond_order for b in connection.bonds) + elif hasattr(connection, "bond_order"): + sig += (connection.bond_order,) + + if sig in sig_cache: + match = sig_cache[sig] + else: + group, connection_identifiers = self.connection_identifier(connection) + match = None + for identifier_key in connection_identifiers: + id_tuple = tuple(identifier_key) + if id_tuple in visited: + match = visited[id_tuple] + break + match = ff.get_potential( + group=group, + key=identifier_key, + exact_match=True, + ) + if match: + visited[id_tuple] = match + break + sig_cache[sig] = match if not match and error_on_missing: + group = POTENTIAL_GROUPS[type(connection)] raise ParameterizationError( - f"No parameters found for connection {connection}, group: {group}, " - f"identifiers: {connection_identifiers} in the Forcefield." + f"No parameters found for connection {connection}, group: {group} " + f"in the Forcefield." ) elif match: setattr(connection, group, match[0].clone(self.config.fast_copy)) @@ -276,7 +287,13 @@ def _apply_virtual_site_parameters(self, virtual_sites, ff, error_on_missing=Tru ) def _parameterize( - self, top, typemap, label_type=None, label=None, speedup_by_moltag=False, connection_index=None + self, + top, + typemap, + label_type=None, + label=None, + speedup_by_moltag=False, + connection_index=None, ): """Parameterize a topology/subtopology based on an atomtype map.""" if label and label_type: @@ -292,11 +309,7 @@ def _parameterize( sites, typemap, forcefield, speedup_by_moltag=speedup_by_moltag ) self._parameterize_connections( - top, - forcefield, - label_type, - label, - connection_index=connection_index + top, forcefield, label_type, label, connection_index=connection_index ) if forcefield.virtual_types: self._parameterize_virtual_sites(top, sites, bonds, forcefield) @@ -388,8 +401,10 @@ def run_parameterization(self): ) assert_no_boundary_bonds(self.topology) - is_group = (self.config.match_ff_by == "group") - connection_index = build_molecule_connection_index(self.topology, is_group=is_group) + is_group = self.config.match_ff_by == "group" + connection_index = build_molecule_connection_index( + self.topology, is_group=is_group + ) for label in labels: if label not in self.forcefields: logger.warning( @@ -451,12 +466,7 @@ def connection_identifier( ): # This can extended to incorporate a pluggable object from the forcefield. """Return the group and list of identifiers for a connection to query the forcefield for its potential.""" group = POTENTIAL_GROUPS[type(connection)] - return ( - group, - [ - *connection.get_connection_identifiers(), # the viable keys made up of the bond orders - ], - ) + return group, connection.get_connection_identifiers() @staticmethod def virtual_site_identifier( From cc18401c1afcb0efc36a32f0e4dfb83dcb9900d4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:45:20 +0000 Subject: [PATCH 3/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- gmso/parameterization/molecule_utils.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/gmso/parameterization/molecule_utils.py b/gmso/parameterization/molecule_utils.py index 22fd94c45..d0653713e 100644 --- a/gmso/parameterization/molecule_utils.py +++ b/gmso/parameterization/molecule_utils.py @@ -83,15 +83,10 @@ def _bucket(connections, key): for conn in connections: members = conn.connection_members labels = {_label_of(s) for s in members} - if len(labels) == 1: # all members same molecule + if len(labels) == 1: # all members same molecule label = labels.pop() entry = index.setdefault( - label, { - "bonds": [], - "angles": [], - "dihedrals": [], - "impropers": [] - } + label, {"bonds": [], "angles": [], "dihedrals": [], "impropers": []} ) entry[key].append(conn) From ad5c0443a0a9d8b77dda3661f064809ae6f26152 Mon Sep 17 00:00:00 2001 From: chrisjonesBSU Date: Tue, 2 Jun 2026 10:56:28 +0100 Subject: [PATCH 4/8] Update doc strings --- gmso/parameterization/molecule_utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/gmso/parameterization/molecule_utils.py b/gmso/parameterization/molecule_utils.py index d0653713e..73397de6c 100644 --- a/gmso/parameterization/molecule_utils.py +++ b/gmso/parameterization/molecule_utils.py @@ -61,9 +61,6 @@ def assert_no_boundary_bonds(top): def build_molecule_connection_index(top, is_group=False): """Build a dict mapping molecule/group label -> connection lists. - Scans each connection type once. Use this before a per-molecule - parameterization loop to avoid O(M * C) repeated filtering. - Returns: dict: { label: { From 15405e93a0aad8524916ce3d41a47be7b1aa6637 Mon Sep 17 00:00:00 2001 From: chrisjonesBSU Date: Tue, 9 Jun 2026 10:34:02 +0100 Subject: [PATCH 5/8] revert back to original error message --- gmso/parameterization/topology_parameterizer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gmso/parameterization/topology_parameterizer.py b/gmso/parameterization/topology_parameterizer.py index 551dec28f..284eda08d 100644 --- a/gmso/parameterization/topology_parameterizer.py +++ b/gmso/parameterization/topology_parameterizer.py @@ -231,8 +231,8 @@ def _apply_connection_parameters(self, connections, ff, error_on_missing=True): if not match and error_on_missing: group = POTENTIAL_GROUPS[type(connection)] raise ParameterizationError( - f"No parameters found for connection {connection}, group: {group} " - f"in the Forcefield." + f"No parameters found for connection {connection}, group: {group}, " + f"identifiers: {connection_identifiers} in the force field." ) elif match: setattr(connection, group, match[0].clone(self.config.fast_copy)) From cf30c593550d6ea406a93aa29fdf7b88000d1de5 Mon Sep 17 00:00:00 2001 From: chrisjonesBSU Date: Tue, 9 Jun 2026 11:24:36 +0100 Subject: [PATCH 6/8] Check for and use atom classes --- gmso/parameterization/topology_parameterizer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gmso/parameterization/topology_parameterizer.py b/gmso/parameterization/topology_parameterizer.py index 284eda08d..601393584 100644 --- a/gmso/parameterization/topology_parameterizer.py +++ b/gmso/parameterization/topology_parameterizer.py @@ -202,12 +202,15 @@ def _apply_connection_parameters(self, connections, ff, error_on_missing=True): visited = dict() sig_cache = dict() for connection in connections: - sig = tuple(site.atom_type.name for site in connection.connection_members) + use_classes = all([site.atom_type.atomclass for site in connection.connection_members]) + if use_classes: + sig = tuple(site.atom_type.atomclass for site in connection.connection_members) + else: + sig = tuple(site.atom_type.name for site in connection.connection_members) if getattr(connection, "bonds", None): sig += tuple(b.bond_order for b in connection.bonds) elif hasattr(connection, "bond_order"): sig += (connection.bond_order,) - if sig in sig_cache: match = sig_cache[sig] else: From 6679a90e3ec66221dea018b5ab5f175af3079ce8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:24:48 +0000 Subject: [PATCH 7/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- gmso/parameterization/topology_parameterizer.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/gmso/parameterization/topology_parameterizer.py b/gmso/parameterization/topology_parameterizer.py index 601393584..791b6edd1 100644 --- a/gmso/parameterization/topology_parameterizer.py +++ b/gmso/parameterization/topology_parameterizer.py @@ -202,11 +202,17 @@ def _apply_connection_parameters(self, connections, ff, error_on_missing=True): visited = dict() sig_cache = dict() for connection in connections: - use_classes = all([site.atom_type.atomclass for site in connection.connection_members]) + use_classes = all( + [site.atom_type.atomclass for site in connection.connection_members] + ) if use_classes: - sig = tuple(site.atom_type.atomclass for site in connection.connection_members) + sig = tuple( + site.atom_type.atomclass for site in connection.connection_members + ) else: - sig = tuple(site.atom_type.name for site in connection.connection_members) + sig = tuple( + site.atom_type.name for site in connection.connection_members + ) if getattr(connection, "bonds", None): sig += tuple(b.bond_order for b in connection.bonds) elif hasattr(connection, "bond_order"): From 661a21560ec2a9e1387860d4479c03b94eed0469 Mon Sep 17 00:00:00 2001 From: chrisjonesBSU Date: Tue, 9 Jun 2026 11:45:45 +0100 Subject: [PATCH 8/8] Remove label length check --- gmso/parameterization/molecule_utils.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/gmso/parameterization/molecule_utils.py b/gmso/parameterization/molecule_utils.py index 73397de6c..77272c8f8 100644 --- a/gmso/parameterization/molecule_utils.py +++ b/gmso/parameterization/molecule_utils.py @@ -80,12 +80,11 @@ def _bucket(connections, key): for conn in connections: members = conn.connection_members labels = {_label_of(s) for s in members} - if len(labels) == 1: # all members same molecule - label = labels.pop() - entry = index.setdefault( - label, {"bonds": [], "angles": [], "dihedrals": [], "impropers": []} - ) - entry[key].append(conn) + label = labels.pop() + entry = index.setdefault( + label, {"bonds": [], "angles": [], "dihedrals": [], "impropers": []} + ) + entry[key].append(conn) _bucket(top.bonds, "bonds") _bucket(top.angles, "angles")