Skip to content

Commit 7f2b6b7

Browse files
committed
v0.1.8 upload
- The orbital-ML (`orb_ml`) **backend for xTB** is now `tblite` library rather than `xtb` executable. - **FJK + tblite** now works end-to-end, with a dedicated (but admittedly probably machine-dependent) test (`test_FJK_tblite.py`). - **Local and global SLATM** functions optimized and now JIT-compatible. - **Numba caching** for the more important representation functions. - **Fixed Torch compilation** for some previously broken functions. - Dedicated **representation calculators** for FCHL19, Coulomb Matrix, and Bag-of-Bonds, with more constructor options. - **Revised `test_mode`** to generate diagonal sign matrices via "continuous" rather than "discontinuous" sign-flipping, making the tests more reproducible. - Fixed `l2reg` optimization for SORF models. - Corrected simple-shift calculation for MSORF. - Optimized calculation of regression coefficients for simple fit (no hyperparameter optimization). - Added a fast "short" hyperparameter-optimization mode to the model classes, plus more convenient options for it. - Grid optimization can now be used instead of BOSS. - `stderr` is preserved after an `embarrassingly_parallel` failure. - **Safer defaults**: - `suppress_openmp=False` since it causes problems for bulky representations like aSLATM, - atom-level representation parallelization off by default but controllable via an environmental variable, - process spawning is spawn-based by default but controllable by a submodule-level routine. - `multiprocessing.Pool` instances can now be reused, additional pool-creation options. - Model classes can now reuse precomputed representation vectors stored in `Compound` class. - Conversion coefficients (`au_kcal`, `au_eV`, `Angstrom_Bohr`, `Debye_au`) updated to CODATA 2022 recommended values. - should no longer crash on **macOS**. - now requires **Python 3.11** due to some newer syntax being used. - Revised scripts for [v3 of MSORF manuscript](https://arxiv.org/abs/2505.21247v3). - Updated reference conda environment (see `tests/reference_environments/qml2_test_2025_11_26.yml`). - Tests for all models (except MSORF) use grid rather than BOSS optimization for reproducibility.
1 parent 06bb17b commit 7f2b6b7

89 files changed

Lines changed: 7152 additions & 5548 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.pre-commit-config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ repos:
99
- id: end-of-file-fixer
1010
- id: trailing-whitespace
1111
- id: check-added-large-files
12+
# tests/test_data/ holds committed test fixtures (e.g. the ~1.7MB
13+
# qm7.tar.gz); the default 500KB cap still guards all source.
14+
exclude: ^tests/test_data/
1215
- id: check-ast
1316
- id: check-json
1417
- id: debug-statements

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ test: test-env
3434
$(python) -m pytest -rs ./tests
3535

3636
install:
37+
$(python) uninstall_script.py $(pip)
3738
$(pip) install .$(OPT)
3839

3940
$(doxygen_main):

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,30 @@ Code repo for convenient usage of methods developed in the [`chemspacelab`](http
44

55
## :wrench: Installation
66

7+
**WARNING:** As of 2025.09, due to conflict of several of the code dependencies we recommend only using it on Python 3.12.
8+
79
Install with:
810

911
```bash
1012
pip install .
1113
```
12-
or, if `makefile` is installed,
14+
If a qml2 installation is already present, we recommend uninstalling it first via
15+
16+
```bash
17+
pip uninstall qml2
18+
```
19+
to make sure the cache of Numba-compiled functions is cleared. The two actions can be done together in a single command if `makefile` is installed with
1320

1421
```bash
1522
make install
1623
```
24+
1725
Some parts of the code depend on additional dependencies that can be installed with the corresponding optional dependecies flag. The three defined for this package are
1826

1927
- `orb_ml` - for FJK (machine learning from orbital information).
2028

29+
- `xtb` - use [`tblite`](https://github.com/tblite/tblite) to combine orbital representations with xTB calculations.
30+
2131
- `models` - for hyperparameter optimization procedures in `qml2.models` and `qml2.multilevel_sorf`.
2232

2333
- `morfeus` - for applications dependend on `morfeus-ml` package (everything related to conformer ensemble generation).
@@ -66,14 +76,15 @@ This will create `manual.html` file that can be opened with an Internet browser.
6676

6777
`NUMBA_NUM_THREADS` - since the code is written in Numba OpenMP parallelization is mostly controlled via this variable.
6878

69-
`QML2_NUM_PROCS` - number of processes spawned by parts of the code parallelized via `python.multiprocessing` (training set representations in model-related classes, `pyscf` calculations made by `OML_Compound_list` attribute calls). For limiting number of OpenMP threads spawned in turn by these processes use suppress options (such as `KRRModel` class's `training_reps_suppress_openmp` option). Also see `parallelization.set_default_num_procs`.
79+
`QML2_DEBUG` - set as `1` to compile Numba routines inside QML2 with debug info.
7080

7181
`QML2_NUM_PROCS` - number of processes spawned by parts of the code parallelized via `python.multiprocessing` (training set representations in model-related classes, `pyscf` calculations made by `OML_Compound_list` attribute calls). For limiting number of OpenMP threads spawned in turn by these processes use suppress options (such as `KRRModel` class's `training_reps_suppress_openmp` option). Also see `parallelization.set_default_num_procs`.
7282

7383
`QML2_AVOID_NUMBA_NUMPY_PARALLELIZATION` - some Numba routines in the code call in parallel Numpy routines, which creates problems in some setups (e.g. when both Numba and Numpy try to parallelize over a large number of threads without taking each other into account). Setting this environmental variable to `1` disables Numba parallelization in such routines, leaving them to be parallelized exclusively with Numpy.
7484

7585
`QML2_AVOID_SORF_NUMBA_PARALLELIZATION` - setting this environmental variable to `1` disables Numba parallelization over feature vectors for SORF routines in `qml2.kernels.sorf` and `qml2.kernels.gradient_sorf` (also referred to in the corresponding `qml2.models` classes). Helps if reductors are used in systems where Numba and Numpy try to parallelize simultaneously (see `QML2_AVOID_NUMBA_NUMPY_PARALLELIZATION`).
7686

87+
`QML2_NUMBA_REP_PARALLELIZATION` - setting this environmental variable to `1` enables Numba parallelization of representation functions (e.g. w.r.t. atoms). Default is `0`.
7788

7889
### Experimental
7990

examples/models/ex_Coulomb_Matrix_SORF_model.py

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,52 @@
77
from qml2 import Compound
88
from qml2.models.loss_functions import MAE
99
from qml2.models.sorf import SORFModel
10+
from qml2.representations.calculators import CMCalculator
1011

11-
xyzs = []
12-
energies = []
1312

14-
training_set_size = 2001
15-
test_set_size = 1000
16-
num_mols = training_set_size + test_set_size
13+
def main():
14+
xyzs = []
15+
energies = []
1716

18-
with open("../../tests/test_data/hof_qm7.txt") as csvfile:
19-
reader = csv.reader(csvfile, delimiter=" ")
20-
all_rows = list(reader)
21-
random.shuffle(all_rows)
22-
for row in all_rows[:num_mols]:
23-
xyzs.append(row[0])
24-
energies.append(float(row[1]))
17+
training_set_size = 2001
18+
test_set_size = 1000
19+
num_mols = training_set_size + test_set_size
2520

26-
energies = np.array(energies)
21+
with open("../../tests/test_data/hof_qm7.txt") as csvfile:
22+
reader = csv.reader(csvfile, delimiter=" ")
23+
all_rows = list(reader)
24+
random.shuffle(all_rows)
25+
for row in all_rows[:num_mols]:
26+
xyzs.append(row[0])
27+
energies.append(float(row[1]))
2728

28-
compounds = []
29-
with tarfile.open("../../tests/test_data/qm7.tar.gz") as tar:
30-
for xyz_name in xyzs:
31-
xyz = tar.extractfile(xyz_name)
32-
comp = Compound(xyz=xyz)
33-
compounds.append(comp)
29+
energies = np.array(energies)
3430

35-
train_compounds = compounds[:training_set_size]
36-
test_compounds = compounds[training_set_size:]
31+
compounds = []
32+
with tarfile.open("../../tests/test_data/qm7.tar.gz") as tar:
33+
for xyz_name in xyzs:
34+
xyz = tar.extractfile(xyz_name)
35+
comp = Compound(xyz=xyz)
36+
compounds.append(comp)
3737

38-
train_quantities = energies[:training_set_size]
39-
test_quantities = energies[training_set_size:]
38+
train_compounds = compounds[:training_set_size]
39+
test_compounds = compounds[training_set_size:]
4040

41-
# NOTE: we can use shift_quantities=True, shifting the labels by their mean, but since in this example they are extensive that is not likely to make the results better.
42-
model = SORFModel(shift_quantities=True)
41+
train_quantities = energies[:training_set_size]
42+
test_quantities = energies[training_set_size:]
4343

44-
model.train(training_compounds=train_compounds, training_quantities=train_quantities)
44+
# NOTE: we can use shift_quantities=True, shifting the labels by their mean, but since in this example they are extensive that is not likely to make the results better.
45+
model = SORFModel(shift_quantities=True, representation_function=CMCalculator(size=29))
4546

46-
print("Optimized sigma:", model.sigma)
47-
print("Optimized l2reg divided by average kernel element:", model.l2reg_diag_ratio)
47+
model.train(training_compounds=train_compounds, training_quantities=train_quantities)
4848

49-
predictions = model.predict_from_compounds(test_compounds)
50-
print("Prediction MAE:", MAE()(predictions - test_quantities))
51-
print("Test set quantity STD:", np.std(test_quantities))
49+
print("Optimized sigma:", model.sigma)
50+
print("Optimized l2reg divided by average kernel element:", model.l2reg_diag_ratio)
51+
52+
predictions = model.predict_from_compounds(test_compounds)
53+
print("Prediction MAE:", MAE()(predictions - test_quantities))
54+
print("Test set quantity STD:", np.std(test_quantities))
55+
56+
57+
if __name__ == "__main__":
58+
main()

examples/models/ex_Coulomb_Matrix_model.py

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,53 @@
77
from qml2 import Compound
88
from qml2.models.krr import KRRModel
99
from qml2.models.loss_functions import MAE
10+
from qml2.representations.calculators import CMCalculator
1011

11-
xyzs = []
12-
energies = []
1312

14-
training_set_size = 2001
15-
test_set_size = 1000
16-
num_mols = training_set_size + test_set_size
13+
def main():
14+
xyzs = []
15+
energies = []
1716

18-
with open("../../tests/test_data/hof_qm7.txt") as csvfile:
19-
reader = csv.reader(csvfile, delimiter=" ")
20-
all_rows = list(reader)
21-
random.shuffle(all_rows)
22-
for row in all_rows[:num_mols]:
23-
xyzs.append(row[0])
24-
energies.append(float(row[1]))
17+
training_set_size = 2001
18+
test_set_size = 1000
19+
num_mols = training_set_size + test_set_size
2520

26-
energies = np.array(energies)
21+
with open("../../tests/test_data/hof_qm7.txt") as csvfile:
22+
reader = csv.reader(csvfile, delimiter=" ")
23+
all_rows = list(reader)
24+
random.shuffle(all_rows)
25+
for row in all_rows[:num_mols]:
26+
xyzs.append(row[0])
27+
energies.append(float(row[1]))
2728

28-
compounds = []
29-
with tarfile.open("../../tests/test_data/qm7.tar.gz") as tar:
30-
for xyz_name in xyzs:
31-
xyz = tar.extractfile(xyz_name)
32-
comp = Compound(xyz=xyz)
33-
compounds.append(comp)
29+
energies = np.array(energies)
3430

35-
train_compounds = compounds[:training_set_size]
36-
test_compounds = compounds[training_set_size:]
31+
compounds = []
32+
with tarfile.open("../../tests/test_data/qm7.tar.gz") as tar:
33+
for xyz_name in xyzs:
34+
xyz = tar.extractfile(xyz_name)
35+
comp = Compound(xyz=xyz)
36+
compounds.append(comp)
3737

38-
train_quantities = energies[:training_set_size]
39-
test_quantities = energies[training_set_size:]
38+
train_compounds = compounds[:training_set_size]
39+
test_compounds = compounds[training_set_size:]
4040

41-
# NOTE: we can use shift_quantities=True, shifting the labels by their mean, but since in this example they are extensive that is not likely to make the results better.
42-
model = KRRModel(shift_quantities=False)
41+
train_quantities = energies[:training_set_size]
42+
test_quantities = energies[training_set_size:]
4343

44-
model.train(training_compounds=train_compounds, training_quantities=train_quantities)
44+
# NOTE: we can use shift_quantities=True, shifting the labels by their mean, but since in this example they are extensive that is not likely to make the results better.
45+
CMCalc = CMCalculator(size=29)
46+
model = KRRModel(shift_quantities=False, representation_function=CMCalc)
4547

46-
print("Optimized sigma:", model.sigma)
47-
print("Optimized l2reg divided by average kernel element:", model.l2reg_diag_ratio)
48+
model.train(training_compounds=train_compounds, training_quantities=train_quantities)
4849

49-
predictions = model.predict_from_compounds(test_compounds)
50-
print("Prediction MAE:", MAE()(predictions - test_quantities))
51-
print("Test set quantity STD:", np.std(test_quantities))
50+
print("Optimized sigma:", model.sigma)
51+
print("Optimized l2reg divided by average kernel element:", model.l2reg_diag_ratio)
52+
53+
predictions = model.predict_from_compounds(test_compounds)
54+
print("Prediction MAE:", MAE()(predictions - test_quantities))
55+
print("Test set quantity STD:", np.std(test_quantities))
56+
57+
58+
if __name__ == "__main__":
59+
main()

examples/models/ex_FCHL19_SORF_model.py

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,59 @@
77
from qml2 import Compound
88
from qml2.models.loss_functions import MAE
99
from qml2.models.sorf import SORFLocalModel
10+
from qml2.representations.calculators import FCHL19Calculator
1011

11-
xyzs = []
12-
energies = []
1312

14-
training_set_size = 501
15-
test_set_size = 1000
16-
num_mols = training_set_size + test_set_size
13+
def main():
14+
xyzs = []
15+
energies = []
1716

18-
with open("../../tests/test_data/hof_qm7.txt") as csvfile:
19-
reader = csv.reader(csvfile, delimiter=" ")
20-
all_rows = list(reader)
21-
random.shuffle(all_rows)
22-
for row in all_rows[:num_mols]:
23-
xyzs.append(row[0])
24-
energies.append(float(row[1]))
17+
training_set_size = 501
18+
test_set_size = 1000
19+
num_mols = training_set_size + test_set_size
2520

26-
energies = np.array(energies)
21+
with open("../../tests/test_data/hof_qm7.txt") as csvfile:
22+
reader = csv.reader(csvfile, delimiter=" ")
23+
all_rows = list(reader)
24+
random.shuffle(all_rows)
25+
for row in all_rows[:num_mols]:
26+
xyzs.append(row[0])
27+
energies.append(float(row[1]))
2728

28-
compounds = []
29-
with tarfile.open("../../tests/test_data/qm7.tar.gz") as tar:
30-
for xyz_name in xyzs:
31-
xyz = tar.extractfile(xyz_name)
32-
comp = Compound(xyz=xyz)
33-
compounds.append(comp)
29+
energies = np.array(energies)
3430

35-
train_compounds = compounds[:training_set_size]
36-
test_compounds = compounds[training_set_size:]
31+
compounds = []
32+
with tarfile.open("../../tests/test_data/qm7.tar.gz") as tar:
33+
for xyz_name in xyzs:
34+
xyz = tar.extractfile(xyz_name)
35+
comp = Compound(xyz=xyz)
36+
compounds.append(comp)
3737

38-
train_quantities = energies[:training_set_size]
39-
test_quantities = energies[training_set_size:]
38+
train_compounds = compounds[:training_set_size]
39+
test_compounds = compounds[training_set_size:]
4040

41-
# using "shift_quantites=True" means using dressed atom approach; requires defining `possible_nuclear_charges` though.
42-
model = SORFLocalModel(shift_quantities=True, possible_nuclear_charges=np.array([1, 6, 7, 8, 16]))
41+
train_quantities = energies[:training_set_size]
42+
test_quantities = energies[training_set_size:]
4343

44-
model.train(training_compounds=train_compounds, training_quantities=train_quantities)
44+
possible_nuclear_charges = np.array([1, 6, 7, 8, 16])
45+
FCHL19Calc = FCHL19Calculator(elements=possible_nuclear_charges)
46+
# using "shift_quantites=True" means using dressed atom approach; requires defining `possible_nuclear_charges` though.
47+
# in this example we will use FCHL19; note that it requires defining "elements" keyword argument defining nuclear charges that can be encountered in the chemical space of interest.
48+
model = SORFLocalModel(
49+
shift_quantities=True,
50+
possible_nuclear_charges=possible_nuclear_charges,
51+
representation_function=FCHL19Calc,
52+
)
4553

46-
print("Optimized sigma:", model.sigma)
47-
print("Optimized l2reg divided by average kernel element:", model.l2reg_diag_ratio)
54+
model.train(training_compounds=train_compounds, training_quantities=train_quantities)
4855

49-
predictions = model.predict_from_compounds(test_compounds)
50-
print("Prediction MAE:", MAE()(predictions - test_quantities))
51-
print("Test set quantity STD:", np.std(test_quantities))
56+
print("Optimized sigma:", model.sigma)
57+
print("Optimized l2reg divided by average kernel element:", model.l2reg_diag_ratio)
58+
59+
predictions = model.predict_from_compounds(test_compounds)
60+
print("Prediction MAE:", MAE()(predictions - test_quantities))
61+
print("Test set quantity STD:", np.std(test_quantities))
62+
63+
64+
if __name__ == "__main__":
65+
main()

0 commit comments

Comments
 (0)