Repository: openmm/openmm — pull request #5065 (cpp)
Title: Fix error message formatting for bonds to elements without atoms

Fixes a minor bug in #5020 in which error messages for incorrect bonds to elements without atoms don't get formatted and an unrelated error gets shown instead.  Normally, there shouldn't be bonds to extra sites that would trigger this, but it can happen if one attempts to parameterize a topology with explicitly specified bonds to atoms for which OpenMM couldn't guess the elements.

Resolved issues:
title: Fix error message formatting for bonds to elements without atoms
body: ### Issue Description

**Problem:**  
When attempting to parameterize a molecular topology that includes explicitly specified bonds to atoms with undefined or unrecognized elements, the software fails to generate a meaningful error message. Instead, an unrelated error message is displayed, which does not accurately describe the issue. This can lead to confusion for users trying to debug their input files, as the actual cause of the error is obscured.

**Expected Behavior:**  
When bonds to atoms with undefined or unrecognized elements are encountered during topology parameterization, the software should provide a clear and accurate error message. The message should explicitly indicate the issue, such as the presence of bonds to extra sites or elements without atoms, allowing users to identify and resolve the problem in their input files.

## IMPORTANT INSTRUCTIONS

1. If you edit a python file, you MUST run 'make PythonInstall' inside the build directory
2. If you edit a c++ file, you MUST run 'make -j4 && make install" inside the build directory

Hints:

Fix patch:
diff --git a/wrappers/python/openmm/app/forcefield.py b/wrappers/python/openmm/app/forcefield.py
index 9d7131a89c..9d178e01c4 100644
--- a/wrappers/python/openmm/app/forcefield.py
+++ b/wrappers/python/openmm/app/forcefield.py
@@ -1888,8 +1888,8 @@ def formatAtomDiff(key, diff):
 
     def formatBondDiff(key, diff):
         """Formats a string describing elements associated with a different number of bonds."""
-        name1 = elem.Element.getByAtomicNumber(key[0]).symbol if key else 'extra site'
-        name2 = elem.Element.getByAtomicNumber(key[1]).symbol if key else 'extra site'
+        name1 = elem.Element.getByAtomicNumber(key[0]).symbol if key[0] else 'extra site'
+        name2 = elem.Element.getByAtomicNumber(key[1]).symbol if key[1] else 'extra site'
         return f'{name1}-{name2} bond' + ('' if diff == 1 else 's')
 
     def pickBestMatch(bestMatches):


Test patch:
diff --git a/wrappers/python/tests/TestForceField.py b/wrappers/python/tests/TestForceField.py
index 13c5a4b86a..c0f0d34c2b 100644
--- a/wrappers/python/tests/TestForceField.py
+++ b/wrappers/python/tests/TestForceField.py
@@ -987,6 +987,18 @@ def makeSystem(lines):
         with self.assertRaisesRegex(ValueError, 'No template found for residue.*HOH.*The force field contains no residue templates'):
             makeSystem(pdbLines)
 
+        # Make water with an extra site and an (invalid) bond to it.
+        pdbLines = [
+            'ATOM      0 O    HOH A   1       0       0       0                           O',
+            'ATOM      1 H1   HOH A   1       0       0       0                           H',
+            'ATOM      2 H2   HOH A   1       0       0       0                           H',
+            'ATOM      3 M    HOH A   1       0       0       0                          EP',
+            'CONECT    0    3'
+        ]
+        forcefield = ForceField('opc.xml')
+        with self.assertRaisesRegex(ValueError, 'No template found for residue.*HOH.*The set of atoms matches HOH, but the residue has 1 extra site-O bond too many'):
+            makeSystem(pdbLines)
+
     def test_Wildcard(self):
         """Test that PeriodicTorsionForces using wildcard ('') for atom types / classes in the ffxml are correctly registered"""