Skip to content

Commit d6f5511

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

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
@@ -1694,17 +1694,10 @@ def _check_distance_constraint_requirements(self) -> None:
16941694
if not constraints:
16951695
return
16961696

1697-
if not self.generate.fixed_composition:
1698-
raise ValueError(
1699-
"Distance constraints require 'generate.fixed_composition = true' "
1700-
+ "so individual atoms can be uniquely addressed."
1701-
)
1702-
17031697
element_composition = self.generate.element_composition
1704-
if not element_composition:
1705-
raise ValueError(
1706-
"Distance constraints require explicit element composition entries."
1707-
)
1698+
# When composition is not fixed, defer feasibility checks until runtime.
1699+
if not self.generate.fixed_composition or not element_composition:
1700+
return
17081701

17091702
for constraint in constraints:
17101703
counts = constraint.required_counts()
@@ -1726,8 +1719,8 @@ def _check_distance_constraint_requirements(self) -> None:
17261719
)
17271720

17281721
actual = min_count
1729-
if actual != expected:
1722+
if actual < expected:
17301723
raise ValueError(
1731-
f"Distance constraint {constraint} requires exactly "
1724+
f"Distance constraint {constraint} requires at least "
17321725
+ f"{expected} {element_symbol} atom(s) but the composition fixes {actual}."
17331726
)

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)