11import subprocess as sp
22from pathlib import Path
33from types import SimpleNamespace
4+ import numpy as np
45import pytest
6+ from mindlessgen .molecules import Molecule # type: ignore
7+ from mindlessgen .prog import DistanceConstraint , XTBConfig # type: ignore
58from mindlessgen .qm .orca import ORCA
69
710
@@ -21,19 +24,6 @@ def __init__(self, **kwargs):
2124 super ().__init__ (** defaults )
2225
2326
24- class DummyXTBConfig (SimpleNamespace ):
25- def __init__ (self , ** kwargs ):
26- defaults = dict (
27- distance_constraints = None , distance_constraint_force_constant = None
28- )
29- defaults .update (kwargs )
30- super ().__init__ (** defaults )
31-
32-
33- class DummyMolecule :
34- ati = [1 , 1 , 6 , 8 ]
35-
36-
3727@pytest .fixture
3828def make_orca ():
3929 def _factory (cfg = None , xtb_cfg_param = None ):
@@ -43,12 +33,84 @@ def _factory(cfg=None, xtb_cfg_param=None):
4333 return _factory
4434
4535
46- def test_run_xtb_driver_success (monkeypatch , tmp_path , make_orca ):
36+ @pytest .fixture
37+ def xtb_cfg_oh ():
38+ """
39+ xTB config with an O-H distance constraint.
40+ """
41+ cfg = XTBConfig ()
42+ cfg .distance_constraints = [DistanceConstraint .from_cli_string ("O,H,1.0" )]
43+ cfg .distance_constraint_force_constant = 0.5
44+ return cfg
45+
46+
47+ @pytest .fixture
48+ def xtb_cfg_ff ():
49+ """
50+ xTB config with an F-F distance constraint.
51+ """
52+ cfg = XTBConfig ()
53+ cfg .distance_constraints = [DistanceConstraint .from_cli_string ("F,F,1.0" )]
54+ return cfg
55+
56+
57+ @pytest .fixture
58+ def xtb_cfg_fe3 ():
59+ """
60+ xTB config with a Fe-Fe distance constraint.
61+ """
62+ cfg = XTBConfig ()
63+ cfg .distance_constraints = [
64+ DistanceConstraint .from_mapping ({"pair" : ["Fe" , "Fe" ], "distance" : 2.5 })
65+ ]
66+ return cfg
67+
68+
69+ @pytest .fixture
70+ def mol_oh ():
71+ """
72+ Simple O-H molecule for constraint tests.
73+ """
74+ mol = Molecule ("OH" )
75+ mol .ati = np .array ([7 , 0 ])
76+ return mol
77+
78+
79+ @pytest .fixture
80+ def mol_h2 ():
81+ """
82+ Simple H2 molecule for constraint tests.
83+ """
84+ mol = Molecule ("H2" )
85+ mol .ati = np .array ([0 , 0 ])
86+ return mol
87+
88+
89+ @pytest .fixture
90+ def mol_fe3 ():
91+ """
92+ Simple Fe3 molecule for constraint tests.
93+ """
94+ mol = Molecule ("Fe3" )
95+ mol .ati = np .array ([25 , 25 , 25 ])
96+ return mol
97+
98+
99+ @pytest .fixture
100+ def fake_xtb_path (monkeypatch ):
101+ """
102+ Force xTB path discovery to a fake binary.
103+ """
104+ xtb_path = Path ("/fake/xtb" )
105+ monkeypatch .setattr ("mindlessgen.qm.orca.get_xtb_path" , lambda : xtb_path )
106+ return xtb_path
107+
108+
109+ def test_run_xtb_driver_success (monkeypatch , tmp_path , make_orca , fake_xtb_path ):
47110 orca = make_orca (
48111 cfg = DummyORCAConfig (optlevel = "tight" ),
49- xtb_cfg_param = DummyXTBConfig (),
112+ xtb_cfg_param = XTBConfig (),
50113 )
51- monkeypatch .setattr ("mindlessgen.qm.orca.get_xtb_path" , lambda : Path ("/fake/xtb" ))
52114 captured = {}
53115
54116 def fake_run (args , cwd , capture_output , check ):
@@ -57,10 +119,10 @@ def fake_run(args, cwd, capture_output, check):
57119 assert capture_output and check
58120 return SimpleNamespace (stdout = b"ok" , stderr = b"" )
59121
60- monkeypatch .setattr (sp , " run" , fake_run )
122+ monkeypatch .setattr ("mindlessgen.qm.xtb.sp. run" , fake_run )
61123 out , err , code = orca ._run_xtb_driver (tmp_path , "geom.xyz" , "ctrl.inp" , ncores = 4 )
62124 assert captured ["args" ] == [
63- str (Path ( "/fake/xtb" ) ),
125+ str (fake_xtb_path ),
64126 "geom.xyz" ,
65127 "--opt" ,
66128 "tight" ,
@@ -73,35 +135,38 @@ def fake_run(args, cwd, capture_output, check):
73135 assert code == 0
74136
75137
76- def test_run_xtb_driver_failure_returns_error (monkeypatch , tmp_path , make_orca ):
138+ def test_run_xtb_driver_failure_returns_error (
139+ monkeypatch , tmp_path , make_orca , fake_xtb_path
140+ ):
77141 """Ensure the ORCA wrapper surfaces errors from the xTB driver."""
78- orca = make_orca (xtb_cfg_param = DummyXTBConfig ())
79- monkeypatch .setattr ("mindlessgen.qm.orca.get_xtb_path" , lambda : Path ("/fake/xtb" ))
142+ orca = make_orca (xtb_cfg_param = XTBConfig ())
80143
81144 def fake_run (* _ , ** kwargs ):
82145 del kwargs
83146 raise sp .CalledProcessError (1 , "xtb" , output = b"bad" , stderr = b"worse" )
84147
85- monkeypatch .setattr (sp , " run" , fake_run )
148+ monkeypatch .setattr ("mindlessgen.qm.xtb.sp. run" , fake_run )
86149 out , err , code = orca ._run_xtb_driver ( # pylint: disable=protected-access
87150 tmp_path , "geom.xyz" , "ctrl.inp" , ncores = 1
88151 )
89152 assert (out , err , code ) == ("bad" , "worse" , 1 )
90153
91154
92- def test_run_xtb_driver_requires_xtb_cfg (monkeypatch , tmp_path , make_orca ):
155+ def test_run_xtb_driver_requires_xtb_cfg (
156+ monkeypatch , tmp_path , make_orca , fake_xtb_path
157+ ):
93158 orca = make_orca ()
94- monkeypatch .setattr ("mindlessgen.qm.orca.get_xtb_path" , lambda : Path ("/fake/xtb" ))
95159 with pytest .raises (RuntimeError , match = "xTB driver requested" ):
96160 orca ._run_xtb_driver (tmp_path , "geom.xyz" , "ctrl.inp" , ncores = 1 )
97161
98162
99- def test_write_xtb_input_creates_expected_file (monkeypatch , tmp_path , make_orca ):
100- xtb_cfg_instance = DummyXTBConfig (
101- distance_constraints = ["dummy" ], distance_constraint_force_constant = 0.7
102- )
163+ def test_write_xtb_input_creates_expected_file (
164+ monkeypatch , tmp_path , make_orca , fake_xtb_path , mol_oh
165+ ):
166+ xtb_cfg_instance = XTBConfig ()
167+ xtb_cfg_instance .distance_constraints = []
168+ xtb_cfg_instance .distance_constraint_force_constant = 0.7
103169 orca = make_orca (xtb_cfg_param = xtb_cfg_instance )
104- monkeypatch .setattr ("mindlessgen.qm.orca.get_xtb_path" , lambda : Path ("/fake/xtb" ))
105170
106171 def fake_prepare (self , molecule , temp_dir ):
107172 assert temp_dir == tmp_path
@@ -123,7 +188,7 @@ def fake_prepare(self, molecule, temp_dir):
123188 "mindlessgen.qm.orca.XTB._prepare_distance_constraint_file" , fake_prepare
124189 )
125190 target = tmp_path / "xtb.inp"
126- orca ._write_xtb_input (DummyMolecule () , target , "orca.inp" )
191+ orca ._write_xtb_input (mol_oh , target , "orca.inp" )
127192 content = target .read_text ().splitlines ()
128193 assert content [:4 ] == [
129194 "$constrain" ,
@@ -134,3 +199,46 @@ def fake_prepare(self, molecule, temp_dir):
134199 assert "$external" in content
135200 assert " orca input file= orca.inp" in content
136201 assert f" orca bin= { orca .path } " in content
202+
203+
204+ def test_write_xtb_input_generates_constraints (
205+ fake_xtb_path , tmp_path , make_orca , xtb_cfg_oh , mol_oh
206+ ):
207+ orca = make_orca (xtb_cfg_param = xtb_cfg_oh )
208+
209+ xtb_input = tmp_path / "xtb.inp"
210+ orca ._write_xtb_input (mol_oh , xtb_input , "orca.inp" )
211+
212+ contents = xtb_input .read_text (encoding = "utf8" ).splitlines ()
213+ assert contents [0 ] == "$constrain"
214+ assert "force constant= 0.5" in contents [1 ]
215+ distance_lines = [line for line in contents if line .startswith (" distance:" )]
216+ assert distance_lines == [" distance: 1, 2, 1.0" ]
217+ assert "$external" in contents
218+ assert " orca input file= orca.inp" in contents
219+ assert f" orca bin= { orca .path } " in contents
220+
221+
222+ def test_write_xtb_input_missing_atoms (
223+ fake_xtb_path , tmp_path , make_orca , xtb_cfg_ff , mol_h2
224+ ):
225+ orca = make_orca (xtb_cfg_param = xtb_cfg_ff )
226+
227+ with pytest .raises (RuntimeError ):
228+ orca ._write_xtb_input (mol_h2 , tmp_path / "xtb.inp" , "orca.inp" )
229+
230+
231+ def test_distance_constraints_use_first_atoms (
232+ fake_xtb_path , tmp_path , make_orca , xtb_cfg_fe3 , mol_fe3
233+ ):
234+ orca = make_orca (xtb_cfg_param = xtb_cfg_fe3 )
235+
236+ xtb_input = tmp_path / "xtb.inp"
237+ orca ._write_xtb_input (mol_fe3 , xtb_input , "orca.inp" )
238+
239+ contents = xtb_input .read_text (encoding = "utf8" ).splitlines ()
240+ distance_lines = [line for line in contents if "distance:" in line ]
241+
242+ assert len (distance_lines ) == 1
243+ assert "1, 2" in distance_lines [0 ]
244+ assert ", 3," not in distance_lines [0 ]
0 commit comments