Skip to content

Commit c0a62f0

Browse files
Fix key collisions in Interchange.combine (#1315)
* TST: Reproduce Issue #1314 * BUG: Fix `PotentialKey` collision in `Interchange.combine` * FIX: Remove unreachable code * TST: Improve test * DOC: Finalize 0.4.6 release notes
1 parent 7162624 commit c0a62f0

File tree

3 files changed

+50
-0
lines changed

3 files changed

+50
-0
lines changed

docs/releasehistory.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ Dates are given in YYYY-MM-DD format.
1111

1212
Please note that all releases prior to a version 1.0.0 are considered pre-releases and many API changes will come before a stable release.
1313

14+
## 0.4.6 - 2025-09-02
15+
16+
### Bug fixes
17+
18+
* #1312 Fix handling of `DoubleExponentialVirtualSites`
19+
* #1315 Fixes a bug in which `Interchange.combine` did not properly process constraint distances.
20+
21+
### Maintenance
22+
23+
* #1297 Do not duplicate type annotations in docstrings
24+
* #1304 Automatically format `pyproject.toml`
25+
* #1306 Declare minimum Python version in project metadata
26+
1427
## 0.4.5 - 2025-08-20
1528

1629
### New features

openff/interchange/_tests/unit_tests/operations/test_combine.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,32 @@ def test_mix_different_5_6_rounding(self, parsley, sage, ethanol):
190190
).combine(
191191
sage.create_interchange(ethanol.to_topology()),
192192
)
193+
194+
@pytest.mark.parametrize("flip_order", [False, True])
195+
@pytest.mark.parametrize("handler_name", ["Constraints", "Bonds", "Angles", "ProperTorsions"])
196+
def test_constraint_key_collision(self, parsley, sage, ethanol, flip_order, handler_name):
197+
"""Test that key collisions in constraints and valence terms are handled."""
198+
interchanges = [
199+
parsley.create_interchange(
200+
ethanol.to_topology(),
201+
),
202+
sage.create_interchange(
203+
ethanol.to_topology(),
204+
),
205+
]
206+
207+
# want to make sure this behavior is not order-dependent
208+
if flip_order:
209+
interchanges.reverse()
210+
211+
arrays_before = [interchange[handler_name].get_system_parameters() for interchange in interchanges]
212+
213+
numpy.testing.assert_raises(AssertionError, numpy.testing.assert_allclose, *arrays_before)
214+
215+
array_after_combine = interchanges[0].combine(interchanges[1])[handler_name].get_system_parameters()
216+
217+
# check that the contents of the combined Interchange contains each input, without mushing
218+
numpy.testing.assert_allclose(
219+
numpy.vstack(arrays_before),
220+
array_after_combine,
221+
)

openff/interchange/operations/_combine.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""The logic behind `Interchange.combine`."""
22

33
import copy
4+
import logging
45
import warnings
56
from typing import TYPE_CHECKING
67

@@ -18,6 +19,9 @@
1819
if TYPE_CHECKING:
1920
from openff.interchange.components.interchange import Interchange
2021

22+
23+
logger = logging.getLogger(__name__)
24+
2125
DEFAULT_CUTOFF_TOLERANCE = Quantity("1e-6 nanometer")
2226

2327

@@ -147,6 +151,10 @@ def _combine(
147151
_tmp_pot_key.mult = _mult
148152
_mult += 1
149153

154+
if _tmp_pot_key in self_collection.potentials:
155+
logging.info(f"Key collision, fixing. Key is {_tmp_pot_key}")
156+
_tmp_pot_key.id = _tmp_pot_key.id + "_DUPLICATE"
157+
150158
self_collection.key_map.update({new_top_key: _tmp_pot_key})
151159
if collection_name == "Constraints":
152160
self_collection.potentials.update(

0 commit comments

Comments
 (0)