Skip to content

Commit e232c28

Browse files
authored
Remove need for constraints and specific compositions (#143)
* initial implementation of constraint input Signed-off-by: Marcel Mueller <marcel.mueller@thch.uni-bonn.de> * improve code Signed-off-by: Marcel Mueller <marcel.mueller@thch.uni-bonn.de> * added CHANGELOG and formatted it Signed-off-by: Marcel Mueller <marcel.mueller@thch.uni-bonn.de> * remove need for fixed composition and exact number of atoms Signed-off-by: Marcel Mueller <marcel.mueller@thch.uni-bonn.de> --------- Signed-off-by: Marcel Mueller <marcel.mueller@thch.uni-bonn.de>
1 parent 2ee1739 commit e232c28

4 files changed

Lines changed: 66 additions & 39 deletions

File tree

src/mindlessgen/prog/config.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,17 +1650,10 @@ def _check_distance_constraint_requirements(self) -> None:
16501650
if not constraints:
16511651
return
16521652

1653-
if not self.generate.fixed_composition:
1654-
raise ValueError(
1655-
"Distance constraints require 'generate.fixed_composition = true' "
1656-
+ "so individual atoms can be uniquely addressed."
1657-
)
1658-
16591653
element_composition = self.generate.element_composition
1660-
if not element_composition:
1661-
raise ValueError(
1662-
"Distance constraints require explicit element composition entries."
1663-
)
1654+
# When composition is not fixed, defer feasibility checks until runtime.
1655+
if not self.generate.fixed_composition or not element_composition:
1656+
return
16641657

16651658
for constraint in constraints:
16661659
counts = constraint.required_counts()
@@ -1682,8 +1675,8 @@ def _check_distance_constraint_requirements(self) -> None:
16821675
)
16831676

16841677
actual = min_count
1685-
if actual != expected:
1678+
if actual < expected:
16861679
raise ValueError(
1687-
f"Distance constraint {constraint} requires exactly "
1680+
f"Distance constraint {constraint} requires at least "
16881681
+ f"{expected} {element_symbol} atom(s) but the composition fixes {actual}."
16891682
)

src/mindlessgen/qm/xtb.py

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,12 @@ def optimize(
8282
arguments += ["--uhf", str(molecule.uhf)]
8383
if max_cycles is not None:
8484
arguments += ["--cycles", str(max_cycles)]
85-
print(self.cfg.distance_constraints)
8685
if self.cfg.distance_constraints:
87-
print("Preparing distance constraint file...")
86+
if verbosity > 1:
87+
print("Preparing distance constraint file...")
8888
if self._prepare_distance_constraint_file(molecule, temp_path):
89-
print("Distance constraint file prepared.")
89+
if verbosity > 1:
90+
print("Distance constraint file prepared.")
9091
arguments += ["--input", "xtb.inp"]
9192

9293
if verbosity > 2:
@@ -232,6 +233,7 @@ def _prepare_distance_constraint_file(
232233

233234
constraint_lines: list[str] = []
234235
for constraint in self.cfg.distance_constraints:
236+
self._ensure_constraint_atoms_present(element_map, constraint)
235237
pairs = self._generate_constraint_pairs(element_map, constraint)
236238
if not pairs:
237239
raise RuntimeError(
@@ -260,33 +262,46 @@ def _generate_constraint_pairs(
260262
element_map: dict[int, list[int]], constraint: DistanceConstraint
261263
) -> list[tuple[int, int]]:
262264
"""
263-
Generate all index pairs for the provided constraint.
265+
Generate the index pair for the provided constraint.
264266
"""
265267
atom_a, atom_b = constraint.atomic_numbers
266268
atom_a_idx = atom_a - 1
267269
atom_b_idx = atom_b - 1
268270
indices_a = element_map.get(atom_a_idx, [])
269271
indices_b = element_map.get(atom_b_idx, [])
270272

271-
pairs: set[tuple[int, int]] = set()
272273
if atom_a == atom_b:
273-
for idx, first in enumerate(indices_a):
274-
for second in indices_a[idx + 1 :]:
275-
if first < second:
276-
pairs.add((first, second))
277-
else:
278-
pairs.add((second, first))
279-
else:
280-
for first in indices_a:
281-
for second in indices_b:
282-
if first == second:
283-
continue
284-
if first < second:
285-
pairs.add((first, second))
286-
else:
287-
pairs.add((second, first))
288-
289-
return sorted(pairs)
274+
if len(indices_a) < 2:
275+
return []
276+
first, second = sorted(indices_a[:2])
277+
return [(first, second)]
278+
279+
if not indices_a or not indices_b:
280+
return []
281+
282+
first, second = indices_a[0], indices_b[0]
283+
if first == second:
284+
return []
285+
if first > second:
286+
first, second = second, first
287+
return [(first, second)]
288+
289+
@staticmethod
290+
def _ensure_constraint_atoms_present(
291+
element_map: dict[int, list[int]], constraint: DistanceConstraint
292+
) -> None:
293+
"""
294+
Validate that the molecule contains enough atoms for the constraint.
295+
"""
296+
for atomic_number, required in constraint.required_counts().items():
297+
idx = atomic_number - 1
298+
available = len(element_map.get(idx, []))
299+
if available < required:
300+
symbol = constraint.symbol_for(atomic_number)
301+
raise RuntimeError(
302+
f"Distance constraint {constraint} requires at least "
303+
f"{required} atom(s) of {symbol}, but only {available} present."
304+
)
290305

291306
def _run(self, temp_path: Path, arguments: list[str]) -> tuple[str, str, int]:
292307
"""

test/test_prog/test_distance_constraints.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,25 +36,25 @@ def test_force_constant_validation():
3636
cfg.distance_constraint_force_constant = 0
3737

3838

39-
def test_check_config_requires_fixed_composition():
39+
def test_check_config_allows_non_fixed_composition():
4040
config = ConfigManager()
4141
config.general.parallel = 1
4242
config.refine.ncores = 1
4343
config.xtb.distance_constraints = [DistanceConstraint.from_cli_string("He,He,2.0")]
4444
config.generate.fixed_composition = False
45+
config.generate.element_composition = "He:2-*"
4546
config.refine.engine = "xtb"
4647

47-
with pytest.raises(ValueError):
48-
config.check_config()
48+
config.check_config()
4949

5050

51-
def test_check_config_requires_matching_counts():
51+
def test_check_config_requires_matching_counts_when_fixed():
5252
config = ConfigManager()
5353
config.general.parallel = 1
5454
config.refine.ncores = 1
5555
config.xtb.distance_constraints = [DistanceConstraint.from_cli_string("He,He,2.0")]
5656
config.generate.fixed_composition = True
57-
config.generate.element_composition = "He:1-3"
57+
config.generate.element_composition = "He:1-1"
5858
config.refine.engine = "xtb"
5959

6060
with pytest.raises(ValueError):

test/test_qm/test_xtb.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,25 @@ def test_prepare_distance_constraint_file_missing_atoms(tmp_path):
176176
xtb._prepare_distance_constraint_file(mol, tmp_path)
177177

178178

179+
def test_distance_constraint_applies_only_first_atoms(tmp_path):
180+
cfg = XTBConfig()
181+
cfg.distance_constraints = [
182+
DistanceConstraint.from_mapping({"pair": ["Fe", "Fe"], "distance": 2.5})
183+
]
184+
xtb = XTB("/path/to/xtb", cfg)
185+
mol = Molecule("Fe3")
186+
mol.ati = np.array([25, 25, 25])
187+
188+
assert xtb._prepare_distance_constraint_file(mol, tmp_path) is True
189+
190+
contents = (tmp_path / "xtb.inp").read_text(encoding="utf8").splitlines()
191+
distance_lines = [line for line in contents if "distance:" in line]
192+
193+
assert len(distance_lines) == 1
194+
assert "1, 2" in distance_lines[0]
195+
assert ", 3," not in distance_lines[0]
196+
197+
179198
@pytest.mark.optional
180199
def test_xtb_distance_constraint_enforced(tmp_path):
181200
"""

0 commit comments

Comments
 (0)