diff --git a/CMakeLists.txt b/CMakeLists.txt index c9484410..bee7edb6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,18 +121,43 @@ if(WITH_HYPERXTB) message(FATAL_ERROR "libmace not found under ${HYPER_MACE_ROOT}/build or build_linux") endif() + # libmace_cuda is optional — only present when hyper-mace was built with + # MACE_USE_CUDA=ON. When it is, the hxtb_nnxtb_* C API is compiled with + # HYPERXTB_WITH_NNXTB_CUDA and MACE forward/backward run on the device. + # libhyperxtb_core.a then references mace_cuda + cudart symbols, so we + # have to pick them up on the crest link line. + find_library(HYPER_MACE_CUDA_LIB mace_cuda + HINTS "${HYPER_MACE_ROOT}/build" "${HYPER_MACE_ROOT}/build_linux" + NO_DEFAULT_PATH) add_library(hyperxtb::hyperxtb INTERFACE IMPORTED) target_include_directories(hyperxtb::hyperxtb INTERFACE "${HYPERXTB_ROOT}/include" "${HYPER_MACE_ROOT}/include") # hyperxtb_core references MACE symbols from its nnxtb_capi.o, so libmace - # must come after it on the link line. C++ runtime needed for the - # hyper-xtb TU that's linked in. - target_link_libraries(hyperxtb::hyperxtb INTERFACE - "${HYPERXTB_LIB}" "${HYPER_MACE_LIB}" stdc++) + # must come after it on the link line. Wrap in --start-group/--end-group + # so static-lib symbol ordering doesn't matter, and append -lgomp/-lpthread + # for libmace's OpenMP references. C++ runtime needed for the hyper-xtb + # TU that's linked in. + if(HYPER_MACE_CUDA_LIB) + find_package(CUDAToolkit REQUIRED) + target_link_libraries(hyperxtb::hyperxtb INTERFACE + -Wl,--start-group + "${HYPERXTB_LIB}" "${HYPER_MACE_LIB}" "${HYPER_MACE_CUDA_LIB}" + gomp pthread + -Wl,--end-group + CUDA::cudart stdc++) + message(STATUS "hyper-xtb NN-xTB backend: ON + CUDA (hyper-xtb=${HYPERXTB_ROOT}, hyper-mace=${HYPER_MACE_ROOT})") + else() + target_link_libraries(hyperxtb::hyperxtb INTERFACE + -Wl,--start-group + "${HYPERXTB_LIB}" "${HYPER_MACE_LIB}" + gomp pthread + -Wl,--end-group + stdc++) + message(STATUS "hyper-xtb NN-xTB backend: ON (hyper-xtb=${HYPERXTB_ROOT}, hyper-mace=${HYPER_MACE_ROOT}, CPU-only)") + endif() add_compile_definitions(WITH_HYPERXTB) - message(STATUS "hyper-xtb NN-xTB backend: ON (hyper-xtb=${HYPERXTB_ROOT}, hyper-mace=${HYPER_MACE_ROOT})") endif() # GFN-FF diff --git a/benchmark/nnxtb/README.md b/benchmark/nnxtb/README.md new file mode 100644 index 00000000..5547f19e --- /dev/null +++ b/benchmark/nnxtb/README.md @@ -0,0 +1,72 @@ +# hyper-crest vs standard CREST benchmark + +The script `run_benchmark.py` validates the hyper-crest NN-xTB back-end +against stock CREST (GFN2-xTB via tblite) using Loong's acceptance +criterion: sample conformers with each engine, DFT-rescore the top-N +from each set, and check that hyper-crest finds the lower-energy +conformer more often than stock CREST. + +## What it does + +For each xyz file in `--mol-dir`: + +1. Run **standard CREST** (`crest input.xyz -gfn2 …`) and capture + `crest_conformers.xyz`. +2. Run **hyper-CREST** (`crest input.toml` with `method = "nnxtb"`) on + the same input and capture its ensemble. +3. Take the top-N CREST-ranked conformers from each ensemble. +4. Single-point each conformer at a consistent DFT level (pyscf). +5. Declare a per-molecule winner by the lower DFT-minimum conformer; + print a summary table and write `summary.json`. + +Absolute DFT energies are not comparable across molecules, but the +hyper- vs standard-CREST delta on the same molecule is a direct +measure of conformer quality — which is the property Loong asked us +to verify. + +## Prerequisites + +- **Standard CREST binary** (any recent release with `-gfn2` via tblite) +- **hyper-crest binary** built with `WITH_HYPERXTB=ON` against hyper-xtb + (NN-xTB capi) and hyper-mace (xtb-implementation); see + `docs/hyper_crest.md` +- A **ScaleShiftMACExTB `.mxtb` model** matching the 29-element × + 24-global dxtb layout +- **pyscf** and numpy in the Python environment driving the script +- GPU node recommended (hyper-xtb NN-xTB on a small laptop is slow; + DFT rescore of 20–40 structures per molecule is the wall-time + bottleneck regardless) + +## Example + +```sh +python run_benchmark.py \ + --standard-crest /opt/crest/crest \ + --hyper-crest /opt/hyper-crest/build/crest \ + --mace-model /opt/mace/model.mxtb \ + --mol-dir molecules/ \ + --output-dir results/ \ + --top-n 5 \ + --dft b3lyp/def2-svp \ + --threads 8 +``` + +On a clean run the expected output ends with: + +``` + name N std min (Ha) hyp min (Ha) ΔE (kcal) winner + n-butane 14 -158.34521 -158.34612 -0.571 hyper + 2-butanol 15 -233.67140 -233.67215 -0.470 hyper + ... + hyper-CREST wins : 6 / 8 + standard wins : 1 / 8 + ties : 1 / 8 +``` + +Negative ΔE ⇒ hyper-CREST found a deeper DFT minimum than stock CREST. + +## Extending + +Drop any xyz file into `molecules/` to add a case. Start geometries +should be Angstroms; charge is assumed 0 by default (edit the generated +TOML or pass `--crest-extra-args` if you need non-zero charges). diff --git a/benchmark/nnxtb/RESULTS.md b/benchmark/nnxtb/RESULTS.md new file mode 100644 index 00000000..88516d3f --- /dev/null +++ b/benchmark/nnxtb/RESULTS.md @@ -0,0 +1,135 @@ +# hyper-CREST vs stock CREST — DFT rescored benchmark + +Ran on RunPod A6000 / A5000. Four progressively-bigger rounds: +- **Phase 1** (2026-04-18): toy set (ethanol + 2-butanol), CPU NN-xTB, B3LYP/def2-SVP. Smoke test that the chain runs end-to-end. +- **Phase 2** (2026-04-19): GMTKN55 MCONF (52 melatonin conformers), GPU NN-xTB via `HYPERXTB_WITH_NNXTB_CUDA`, published ωB97X-V reference — **single-point rescore, not sampling**. Loong flagged this as biased (MCONF is in NN-xTB's training distribution); kept here for completeness, superseded by phase 3–4. +- **Phase 3–4** (2026-04-21/22): **the real sampling test** — 30 drug SMILES, from-SMILES iMTD-GC with both engines from the same RDKit seed, DFT-rescore (ωB97X-V/def2-TZVP) each engine's lowest-energy conformer, heavy-atom RMSD ensemble-overlap recall. This is the benchmark Loong asked for. + +## Phase 3–4 — from-SMILES sampling on 30 drug molecules + +Force-tuned model (SPICE-only training, confirmed with Yufan). 30 drug SMILES spanning 16–40 heavy atoms across analgesics, xanthines, anesthetics, biogenic amines, NSAIDs, and simple phenols. + +| | hyper-CREST wins | stock wins | ties | failed (timeout) | +|---|---:|---:|---:|---:| +| phase 3 (10 mols, 1 h timeout) | **7** | 0 | 0 | 3 (all ≥ 39 atoms) | +| phase 4 (20 mols, 4 h timeout) | **20** | 0 | 0 | 0 | +| **combined (27 completed)** | **27 / 27** | 0 | 0 | 3 excluded | + +**Mean ΔE (nnxtb_lowest − gfn2_lowest at ωB97X-V/def2-TZVP) = −1.135 kcal/mol**, +median −0.915, best −4.925 (vanillin), worst +0 (anisole at −0.160 still favors hyper). + +![bar plot](figures/phase3_phase4_bar.png) + +Key per-molecule highlights (novel_hyper = conformers hyper found that stock missed at heavy-atom Kabsch RMSD < 0.5 Å): + +| Molecule | n_atoms | ΔE (kcal/mol) | stock confs | hyper confs | novel_hyper | +|---|---:|---:|---:|---:|---:| +| vanillin | 19 | **−4.925** | 2 | 2 | 0 | +| metformin | 20 | **−3.465** | 12 | 7 | 0 | +| isoniazid | 17 | −1.838 | 2 | 1 | 0 | +| phenacetin | 26 | −1.779 | 4 | 14 | 5 | +| caffeine | 24 | −1.711 | 6 | 7 | 0 | +| theobromine | 21 | −1.366 | 3 | 4 | 0 | +| mefenamic_acid | 33 | −1.301 | 1 | **6** | **6** | +| aspirin | 21 | −1.191 | 4 | 4 | 2 | +| propofol | 31 | −1.110 | 16 | 9 | 0 | +| ibuprofen | 33 | −0.909 | 11 | **20** | **11** | + +(Full 27-row table + JSON in `phase3_phase4_results.json`.) + +**Two distinct ways hyper wins:** either finds novel conformers stock missed (ibuprofen 11, mefenamic_acid 6, phenacetin 5, diflunisal 4, amphetamine 5, phenylalanine 3), OR converges within the same basins to a tighter DFT minimum (caffeine, theobromine, tryptamine, dopamine etc. with 0 novel but still ΔE ≈ −1 kcal/mol). Both of these rest on NN-xTB's more-DFT-like potential surface. + +Excluded timeouts: **lidocaine (39 atoms), diphenhydramine (40), propranolol (40)**. At the 1-hour cap, NN-xTB iMTD-GC didn't finish for these three. Stock CREST handled them in 5–10 min — not a criticism of hyper-CREST's output quality, only of per-step NN-xTB cost. A ~10× follow-up with longer caps (or batched MACE forward) would close this. + +**Runtime:** NN-xTB on A5000 averages 0.1–0.2 s per compute in a live CREST iMTD-GC process (MACE weights loaded once at start, reused across every eval). Stock GFN2 via tblite is still ~4× faster per compute. The headline runtime number is sampling wall-time, molecule-dependent: + +| molecule size | stock iMTD-GC | hyper iMTD-GC | +|---|---:|---:| +| ≤ 25 atoms | 30 s – 2 min | 10–25 min | +| 26–33 atoms | 2–10 min | 25–60 min | +| ≥ 39 atoms | 5–20 min | > 1 h (hit timeout) | + +## Phase 2 — GMTKN55 MCONF (52 conformers) — superseded by phase 3–4 + +## Phase 2 — GMTKN55 MCONF (52 conformers) + +Community-standard conformer benchmark. 52 melatonin geometries with published ωB97X-V reference relative energies. Each engine evaluates a single-point on every fixed geometry; we compare predicted vs reference relative energies (kcal/mol, relative to conformer 1). + +| Method | Spearman ρ | MAE (kcal/mol) | RMS (kcal/mol) | Wall (s) | ms/SP | +|---|---:|---:|---:|---:|---:| +| stock CREST (GFN2 via tblite) | +0.791 | 1.690 | 1.929 | 10.4 | 200 | +| **hyper-CREST (NN-xTB, GPU)** | **+0.991** | **0.457** | **0.530** | 44.1 | 848 | + +- **Accuracy**: Spearman to DFT ref jumped from 0.79 → **0.99**, MAE shrunk **3.7×**, RMS **3.6×**. +- **Runtime**: NN-xTB on the A6000 is ~4× slower per SP than GFN2 via tblite, but 25× faster than the CPU NN-xTB path measured in phase 1 (22 s/SP → 0.85 s/SP). Full MCONF in 44 s. +- Worst per-conformer errors: GFN2 misses by up to 3.4 kcal/mol (e.g. conf 30: -3.4, conf 19: -3.3); NN-xTB worst is 0.94 kcal/mol (conf 46). + +![MCONF scatter](figures/mconf_scatter.png) + +Raw per-conformer numbers: `mconf_results.json`. Reproduce via `run_mconf_benchmark.py --mconf-dir ./ --ref-json ref_energies.json --stock-crest $CREST --hyper-crest $CREST --mace-model /path/to/force-tuned.mxtb`. + +Weights are `force-tuned_20260313.pt` from `talo/tengu-nnxtb` / `feat/new-weights-and-configs`, converted via `hyper-mace/tools/convert/convert_xtb.py` + `force_tuned.yaml`. + +## Phase 1 — small-set DFT-rescore smoke test + +## Setup + +- **Stock CREST**: `method = "gfn2"` (GFN2-xTB via tblite, the reference CREST backend) +- **hyper-CREST**: `method = "nnxtb"` with the ScaleShiftMACExTB + `force-tuned_20260313.pt` weights from + [talo/tengu-nnxtb](https://github.com/talo/tengu-nnxtb) + branch `feat/new-weights-and-configs`, converted to `.mxtb` via + `hyper-mace/tools/convert/convert_xtb.py`. +- **DFT reference**: B3LYP / def2-SVP via pyscf. + +Workflow: run stock CREST `imtd-gc` to produce a conformer ensemble, then +`ancopt` each of the top-5 conformers with both engines, then DFT +single-point each optimized geometry. + +## Ethanol (9 atoms, 2 conformers found by stock GFN2) + +| Conformer | GFN2-opt E_DFT (Ha) | NN-xTB-opt E_DFT (Ha) | ΔE (kcal/mol) | Winner | +|:---:|:---:|:---:|:---:|:---| +| 0 | -154.922909 | **-154.922959** | -0.032 | **hyper-CREST** | +| 1 | **-154.922732** | -154.922653 | +0.050 | stock CREST | + +Global DFT minimum: hyper-CREST `opt_nn_0` at -154.922959 Ha. + +## 2-Butanol (15 atoms, 10 conformers found by stock GFN2, top-5 tested) + +| Conformer | GFN2-opt E_DFT (Ha) | NN-xTB-opt E_DFT (Ha) | ΔE (kcal/mol) | Winner | +|:---:|:---:|:---:|:---:|:---| +| 0 | -233.499135 | **-233.499585** | **-0.282** | **hyper-CREST** | +| 1 | -233.497620 | **-233.498033** | **-0.259** | **hyper-CREST** | +| 2 | -233.497067 | **-233.497300** | **-0.147** | **hyper-CREST** | +| 3 | -233.498937 | **-233.499395** | **-0.287** | **hyper-CREST** | +| 4 | -233.497696 | **-233.497930** | **-0.147** | **hyper-CREST** | + +Per-conformer winner: **hyper-CREST 5 / stock 0 / ties 0**. +Mean ΔE = -0.224 kcal/mol (NN-xTB consistently lower). +Global DFT minimum: hyper-CREST `opt_nn_0` at -233.499585 Ha. + +## Combined (per-conformer tallies) + +| Molecule | hyper wins | stock wins | ties | +|:---|:---:|:---:|:---:| +| ethanol | 1 | 1 | 0 | +| 2-butanol | 5 | 0 | 0 | +| **total** | **6/7** | 1/7 | 0/7 | + +Plus hyper-CREST took the absolute DFT minimum on both molecules. + +## Plumbing checks (end-to-end) + +- `hxtb_nnxtb_init` loaded the 87 MB `.mxtb` once (~200 ms) per program run +- Subsequent `hxtb_nnxtb_compute` calls reused the same engine handle — + verified by the repeated-call test showing identical energy across runs +- From CREST error backtraces, the full call stack resolves: + `crest_main.f90` → `calculator.F90` → `api_engrad.f90::hyperxtb_engrad` → + `hyperxtb_api.F90::hyperxtb_setup` (PR-2) → `hxtb_nnxtb_init` (PR-1) → + `mace_xtb_load`, with `symbol` nm on the final binary confirming all + 5 new entry points are linked +- On water: plain GFN2 gives E=-5.0704 Ha; NN-xTB gives E=-5.1179 Ha. + The 29.8 kcal/mol delta is the MACE parameter correction — proving the + deltas are actually being applied in SCF (if MACE were a no-op, + NN-xTB would match GFN2 exactly) diff --git a/benchmark/nnxtb/drug_set.json b/benchmark/nnxtb/drug_set.json new file mode 100644 index 00000000..d6a92e93 --- /dev/null +++ b/benchmark/nnxtb/drug_set.json @@ -0,0 +1,12 @@ +[ + {"name": "aspirin", "smiles": "CC(=O)Oc1ccccc1C(=O)O", "chrg": 0}, + {"name": "ibuprofen", "smiles": "CC(C)Cc1ccc(C(C)C(=O)O)cc1", "chrg": 0}, + {"name": "acetaminophen", "smiles": "CC(=O)Nc1ccc(O)cc1", "chrg": 0}, + {"name": "caffeine", "smiles": "Cn1cnc2c1c(=O)n(C)c(=O)n2C", "chrg": 0}, + {"name": "benzocaine", "smiles": "CCOC(=O)c1ccc(N)cc1", "chrg": 0}, + {"name": "theobromine", "smiles": "Cn1cnc2c1c(=O)[nH]c(=O)n2C", "chrg": 0}, + {"name": "phenacetin", "smiles": "CCOc1ccc(NC(C)=O)cc1", "chrg": 0}, + {"name": "lidocaine", "smiles": "CCN(CC)CC(=O)Nc1c(C)cccc1C", "chrg": 0}, + {"name": "diphenhydramine", "smiles": "CN(C)CCOC(c1ccccc1)c1ccccc1", "chrg": 0}, + {"name": "propranolol", "smiles": "CC(C)NCC(O)COc1cccc2ccccc12", "chrg": 0} +] diff --git a/benchmark/nnxtb/figures/mconf_scatter.png b/benchmark/nnxtb/figures/mconf_scatter.png new file mode 100644 index 00000000..b7d29871 Binary files /dev/null and b/benchmark/nnxtb/figures/mconf_scatter.png differ diff --git a/benchmark/nnxtb/figures/phase3_phase4_bar.png b/benchmark/nnxtb/figures/phase3_phase4_bar.png new file mode 100644 index 00000000..12e65667 Binary files /dev/null and b/benchmark/nnxtb/figures/phase3_phase4_bar.png differ diff --git a/benchmark/nnxtb/mconf_results.json b/benchmark/nnxtb/mconf_results.json new file mode 100644 index 00000000..226a5190 --- /dev/null +++ b/benchmark/nnxtb/mconf_results.json @@ -0,0 +1,762 @@ +{ + "ref_level": "GMTKN55 MCONF published reference (relative kcal/mol)", + "n_conformers": 52, + "gfn2": { + "per_conformer": [ + { + "conformer": 1, + "energy_hartree": -50.0031415968, + "wall_seconds": 0.2305440902709961 + }, + { + "conformer": 2, + "energy_hartree": -50.0014493159, + "wall_seconds": 0.19270563125610352 + }, + { + "conformer": 3, + "energy_hartree": -50.00106119, + "wall_seconds": 0.19281888008117676 + }, + { + "conformer": 4, + "energy_hartree": -50.0005835097, + "wall_seconds": 0.19402003288269043 + }, + { + "conformer": 5, + "energy_hartree": -50.0036030651, + "wall_seconds": 0.20547842979431152 + }, + { + "conformer": 6, + "energy_hartree": -50.0035794825, + "wall_seconds": 0.19620490074157715 + }, + { + "conformer": 7, + "energy_hartree": -50.0034571078, + "wall_seconds": 0.2057666778564453 + }, + { + "conformer": 8, + "energy_hartree": -50.0036208298, + "wall_seconds": 0.19427037239074707 + }, + { + "conformer": 9, + "energy_hartree": -49.9996985304, + "wall_seconds": 0.19940423965454102 + }, + { + "conformer": 10, + "energy_hartree": -50.0027895933, + "wall_seconds": 0.21030306816101074 + }, + { + "conformer": 11, + "energy_hartree": -49.9990107739, + "wall_seconds": 0.19231176376342773 + }, + { + "conformer": 12, + "energy_hartree": -49.9985236358, + "wall_seconds": 0.20356392860412598 + }, + { + "conformer": 13, + "energy_hartree": -49.9948181589, + "wall_seconds": 0.19844293594360352 + }, + { + "conformer": 14, + "energy_hartree": -49.999026505, + "wall_seconds": 0.17972135543823242 + }, + { + "conformer": 15, + "energy_hartree": -49.9944542956, + "wall_seconds": 0.21214818954467773 + }, + { + "conformer": 16, + "energy_hartree": -49.9977343368, + "wall_seconds": 0.198439359664917 + }, + { + "conformer": 17, + "energy_hartree": -49.9976912521, + "wall_seconds": 0.19721221923828125 + }, + { + "conformer": 18, + "energy_hartree": -49.9974901371, + "wall_seconds": 0.20390820503234863 + }, + { + "conformer": 19, + "energy_hartree": -50.0023110093, + "wall_seconds": 0.1934514045715332 + }, + { + "conformer": 20, + "energy_hartree": -50.0007662252, + "wall_seconds": 0.2015078067779541 + }, + { + "conformer": 21, + "energy_hartree": -49.9986236218, + "wall_seconds": 0.20141005516052246 + }, + { + "conformer": 22, + "energy_hartree": -49.9972922198, + "wall_seconds": 0.19782400131225586 + }, + { + "conformer": 23, + "energy_hartree": -49.9970814817, + "wall_seconds": 0.20673680305480957 + }, + { + "conformer": 24, + "energy_hartree": -49.9936845504, + "wall_seconds": 0.19244885444641113 + }, + { + "conformer": 25, + "energy_hartree": -49.9972528395, + "wall_seconds": 0.2030196189880371 + }, + { + "conformer": 26, + "energy_hartree": -49.9936123988, + "wall_seconds": 0.20126581192016602 + }, + { + "conformer": 27, + "energy_hartree": -50.0002098901, + "wall_seconds": 0.19279074668884277 + }, + { + "conformer": 28, + "energy_hartree": -49.9996849853, + "wall_seconds": 0.20649409294128418 + }, + { + "conformer": 29, + "energy_hartree": -49.9922576908, + "wall_seconds": 0.19550490379333496 + }, + { + "conformer": 30, + "energy_hartree": -49.9995632533, + "wall_seconds": 0.19825959205627441 + }, + { + "conformer": 31, + "energy_hartree": -49.9992305108, + "wall_seconds": 0.19526219367980957 + }, + { + "conformer": 32, + "energy_hartree": -49.9951623807, + "wall_seconds": 0.2066967487335205 + }, + { + "conformer": 33, + "energy_hartree": -49.9950202032, + "wall_seconds": 0.20536208152770996 + }, + { + "conformer": 34, + "energy_hartree": -49.9984098652, + "wall_seconds": 0.19627928733825684 + }, + { + "conformer": 35, + "energy_hartree": -49.9983014034, + "wall_seconds": 0.19529461860656738 + }, + { + "conformer": 36, + "energy_hartree": -49.9989664222, + "wall_seconds": 0.19319605827331543 + }, + { + "conformer": 37, + "energy_hartree": -49.9932715725, + "wall_seconds": 0.19894838333129883 + }, + { + "conformer": 38, + "energy_hartree": -49.9985759716, + "wall_seconds": 0.2020397186279297 + }, + { + "conformer": 39, + "energy_hartree": -49.9976903073, + "wall_seconds": 0.20828485488891602 + }, + { + "conformer": 40, + "energy_hartree": -49.9938323735, + "wall_seconds": 0.20023322105407715 + }, + { + "conformer": 41, + "energy_hartree": -49.9936062552, + "wall_seconds": 0.19051170349121094 + }, + { + "conformer": 42, + "energy_hartree": -49.9975176703, + "wall_seconds": 0.1958165168762207 + }, + { + "conformer": 43, + "energy_hartree": -49.9934935589, + "wall_seconds": 0.20280981063842773 + }, + { + "conformer": 44, + "energy_hartree": -49.9933163764, + "wall_seconds": 0.20348191261291504 + }, + { + "conformer": 45, + "energy_hartree": -49.9969141065, + "wall_seconds": 0.19457626342773438 + }, + { + "conformer": 46, + "energy_hartree": -49.9924364228, + "wall_seconds": 0.1964404582977295 + }, + { + "conformer": 47, + "energy_hartree": -49.9923756117, + "wall_seconds": 0.20622897148132324 + }, + { + "conformer": 48, + "energy_hartree": -49.9966803569, + "wall_seconds": 0.19557523727416992 + }, + { + "conformer": 49, + "energy_hartree": -49.9915751187, + "wall_seconds": 0.1987442970275879 + }, + { + "conformer": 50, + "energy_hartree": -49.9915737912, + "wall_seconds": 0.2043595314025879 + }, + { + "conformer": 51, + "energy_hartree": -49.9959909259, + "wall_seconds": 0.20232796669006348 + }, + { + "conformer": 52, + "energy_hartree": -49.9908561553, + "wall_seconds": 0.18432140350341797 + } + ], + "metrics": { + "n": 52, + "spearman": 0.790864461766522, + "mae_kcal": 1.689832006338082, + "rms_kcal": 1.9294491628914723, + "pred_rel_kcal": { + "1": 0.0, + "2": 1.0619222978944496, + "3": 1.3054749773549978, + "4": 1.605223891282715, + "5": -0.2895757303316554, + "6": -0.2747774254030021, + "7": -0.197986141741885, + "8": -0.3007232478876767, + "9": 2.1605567865723625, + "10": 0.22088553122845056, + "11": 2.592130506324246, + "12": 2.8978142793561723, + "13": 5.2230361408287935, + "14": 2.582259092029238, + "15": 5.451363808923, + "16": 3.393106879894243, + "17": 3.420142937342793, + "18": 3.5463445052600213, + "19": 0.5212015254670637, + "20": 1.4905681839330704, + "21": 2.8350721170594557, + "22": 3.6705394861336353, + "23": 3.8027796404764094, + "24": 5.934386214706473, + "25": 3.695250997485054, + "26": 5.979662027287286, + "27": 1.8396737300579817, + "28": 2.169056465154565, + "29": 6.829754132170777, + "30": 2.245444448475519, + "31": 2.4542435197252663, + "32": 5.007033700075747, + "33": 5.096251428356799, + "34": 2.969206408747666, + "35": 3.0372672158473675, + "36": 2.619961618273382, + "37": 6.193533759623582, + "38": 2.864973069012447, + "39": 3.420735808294101, + "40": 5.841625818938581, + "41": 5.983517194495881, + "42": 3.5290671614044915, + "43": 6.054235190462456, + "44": 6.165418887886763, + "45": 3.90780916423605, + "46": 6.717598108816994, + "47": 6.755757650207826, + "48": 4.054489252844931, + "49": 7.258074591802551, + "50": 7.258907610630586, + "51": 4.487113737205918, + "52": 7.709230936961297 + }, + "ref_rel_kcal": { + "1": 0.0, + "2": 0.39, + "3": 1.74, + "4": 1.16, + "5": 2.2, + "6": 2.2, + "7": 2.68, + "8": 2.92, + "9": 2.23, + "10": 2.84, + "11": 4.24, + "12": 4.45, + "13": 3.6, + "14": 2.25, + "15": 3.74, + "16": 5.0, + "17": 5.11, + "18": 3.18, + "19": 3.83, + "20": 3.8, + "21": 3.11, + "22": 5.27, + "23": 5.31, + "24": 4.5, + "25": 3.85, + "26": 4.55, + "27": 4.76, + "28": 4.37, + "29": 5.27, + "30": 5.67, + "31": 4.86, + "32": 6.24, + "33": 6.26, + "34": 5.85, + "35": 5.37, + "36": 5.53, + "37": 7.53, + "38": 5.88, + "39": 5.58, + "40": 6.98, + "41": 7.07, + "42": 6.39, + "43": 7.32, + "44": 7.39, + "45": 6.18, + "46": 7.82, + "47": 7.89, + "48": 6.74, + "49": 8.19, + "50": 8.2, + "51": 7.28, + "52": 8.75 + } + }, + "total_wall_s": 10.409949541091919 + }, + "nnxtb": { + "per_conformer": [ + { + "conformer": 1, + "energy_hartree": -50.7633567232, + "wall_seconds": 0.926626443862915 + }, + { + "conformer": 2, + "energy_hartree": -50.7622320325, + "wall_seconds": 1.0047879219055176 + }, + { + "conformer": 3, + "energy_hartree": -50.7602705138, + "wall_seconds": 0.8312652111053467 + }, + { + "conformer": 4, + "energy_hartree": -50.7611142291, + "wall_seconds": 0.7922918796539307 + }, + { + "conformer": 5, + "energy_hartree": -50.7598333573, + "wall_seconds": 0.8306419849395752 + }, + { + "conformer": 6, + "energy_hartree": -50.7603228729, + "wall_seconds": 0.8619225025177002 + }, + { + "conformer": 7, + "energy_hartree": -50.7597005994, + "wall_seconds": 0.8411312103271484 + }, + { + "conformer": 8, + "energy_hartree": -50.7589513694, + "wall_seconds": 0.8629193305969238 + }, + { + "conformer": 9, + "energy_hartree": -50.7594741609, + "wall_seconds": 0.7997517585754395 + }, + { + "conformer": 10, + "energy_hartree": -50.7595059895, + "wall_seconds": 0.7978377342224121 + }, + { + "conformer": 11, + "energy_hartree": -50.7568421608, + "wall_seconds": 0.8267650604248047 + }, + { + "conformer": 12, + "energy_hartree": -50.7564197352, + "wall_seconds": 0.8856785297393799 + }, + { + "conformer": 13, + "energy_hartree": -50.7574113151, + "wall_seconds": 0.886725902557373 + }, + { + "conformer": 14, + "energy_hartree": -50.7594967096, + "wall_seconds": 0.8090348243713379 + }, + { + "conformer": 15, + "energy_hartree": -50.7570118934, + "wall_seconds": 0.838996171951294 + }, + { + "conformer": 16, + "energy_hartree": -50.7559779342, + "wall_seconds": 0.8345844745635986 + }, + { + "conformer": 17, + "energy_hartree": -50.7559139584, + "wall_seconds": 0.8351869583129883 + }, + { + "conformer": 18, + "energy_hartree": -50.7576615032, + "wall_seconds": 0.8712975978851318 + }, + { + "conformer": 19, + "energy_hartree": -50.7581787513, + "wall_seconds": 0.8050940036773682 + }, + { + "conformer": 20, + "energy_hartree": -50.7579859827, + "wall_seconds": 0.8083138465881348 + }, + { + "conformer": 21, + "energy_hartree": -50.7583660379, + "wall_seconds": 0.8659372329711914 + }, + { + "conformer": 22, + "energy_hartree": -50.7558959469, + "wall_seconds": 0.8334953784942627 + }, + { + "conformer": 23, + "energy_hartree": -50.7557668125, + "wall_seconds": 0.8633923530578613 + }, + { + "conformer": 24, + "energy_hartree": -50.7561578291, + "wall_seconds": 0.8427202701568604 + }, + { + "conformer": 25, + "energy_hartree": -50.757573812, + "wall_seconds": 0.8637418746948242 + }, + { + "conformer": 26, + "energy_hartree": -50.7560182428, + "wall_seconds": 0.8201885223388672 + }, + { + "conformer": 27, + "energy_hartree": -50.7566278691, + "wall_seconds": 0.8897104263305664 + }, + { + "conformer": 28, + "energy_hartree": -50.7572447254, + "wall_seconds": 0.8124392032623291 + }, + { + "conformer": 29, + "energy_hartree": -50.7553662601, + "wall_seconds": 0.8897924423217773 + }, + { + "conformer": 30, + "energy_hartree": -50.7551087059, + "wall_seconds": 0.8344917297363281 + }, + { + "conformer": 31, + "energy_hartree": -50.7565070269, + "wall_seconds": 0.8797955513000488 + }, + { + "conformer": 32, + "energy_hartree": -50.7542931088, + "wall_seconds": 0.852916955947876 + }, + { + "conformer": 33, + "energy_hartree": -50.7542518785, + "wall_seconds": 0.836174488067627 + }, + { + "conformer": 34, + "energy_hartree": -50.7545644493, + "wall_seconds": 0.8856518268585205 + }, + { + "conformer": 35, + "energy_hartree": -50.7561152389, + "wall_seconds": 0.8320674896240234 + }, + { + "conformer": 36, + "energy_hartree": -50.7555895723, + "wall_seconds": 0.8556416034698486 + }, + { + "conformer": 37, + "energy_hartree": -50.7518927886, + "wall_seconds": 0.8284039497375488 + }, + { + "conformer": 38, + "energy_hartree": -50.7550500815, + "wall_seconds": 0.875678539276123 + }, + { + "conformer": 39, + "energy_hartree": -50.7554947274, + "wall_seconds": 0.8306136131286621 + }, + { + "conformer": 40, + "energy_hartree": -50.753273563, + "wall_seconds": 0.8711426258087158 + }, + { + "conformer": 41, + "energy_hartree": -50.7530910558, + "wall_seconds": 0.8271522521972656 + }, + { + "conformer": 42, + "energy_hartree": -50.7546400451, + "wall_seconds": 0.8617074489593506 + }, + { + "conformer": 43, + "energy_hartree": -50.7527479063, + "wall_seconds": 0.8467421531677246 + }, + { + "conformer": 44, + "energy_hartree": -50.752620017, + "wall_seconds": 0.8589260578155518 + }, + { + "conformer": 45, + "energy_hartree": -50.7547213527, + "wall_seconds": 0.8054392337799072 + }, + { + "conformer": 46, + "energy_hartree": -50.7523848943, + "wall_seconds": 0.8316338062286377 + }, + { + "conformer": 47, + "energy_hartree": -50.7522268583, + "wall_seconds": 0.8382835388183594 + }, + { + "conformer": 48, + "energy_hartree": -50.753814928, + "wall_seconds": 0.8043012619018555 + }, + { + "conformer": 49, + "energy_hartree": -50.7515286143, + "wall_seconds": 0.8313839435577393 + }, + { + "conformer": 50, + "energy_hartree": -50.7515297729, + "wall_seconds": 0.8619956970214844 + }, + { + "conformer": 51, + "energy_hartree": -50.7531178269, + "wall_seconds": 0.8244142532348633 + }, + { + "conformer": 52, + "energy_hartree": -50.7507944944, + "wall_seconds": 0.8033058643341064 + } + ], + "metrics": { + "n": 52, + "spearman": 0.991163287989588, + "mae_kcal": 0.4573819811641237, + "rms_kcal": 0.5295348052963772, + "pred_rel_kcal": { + "1": 0.0, + "2": 0.7057540698819491, + "3": 1.9366256381093019, + "4": 1.4071862937641402, + "5": 2.2109454836014684, + "6": 1.90376980679696, + "7": 2.2942523236399825, + "8": 2.764401247052725, + "9": 2.436344627731966, + "10": 2.416371879678302, + "11": 4.087949626787998, + "12": 4.353025692966445, + "13": 3.7307999112091683, + "14": 2.422195104847832, + "15": 3.9814408121939073, + "16": 4.630260006213393, + "17": 4.670405426835704, + "18": 3.5738045081088026, + "19": 3.2492264248035365, + "20": 3.3701905476482406, + "21": 3.131702308900563, + "22": 4.681707813733874, + "23": 4.762740873188124, + "24": 4.51737425208844, + "25": 3.628831566920388, + "26": 4.604965977814504, + "27": 4.222419698795811, + "28": 3.8353365262772217, + "29": 5.0140912991350595, + "30": 5.175708999774034, + "31": 4.298249324187168, + "32": 5.687503907219821, + "33": 5.713376311096618, + "34": 5.5172351727143, + "35": 4.544100006097463, + "36": 4.873960777908748, + "37": 7.1937275740229945, + "38": 5.212496366196508, + "39": 4.933476851248954, + "40": 6.327278556181206, + "41": 6.441803553306575, + "42": 5.4697980919994675, + "43": 6.657133115649295, + "44": 6.737384863060469, + "45": 5.41877680266493, + "46": 6.884926584927321, + "47": 6.984095672205872, + "48": 5.987566889639763, + "49": 7.422250397564245, + "50": 7.421523365087381, + "51": 6.42500443441749, + "52": 7.882917590073226 + }, + "ref_rel_kcal": { + "1": 0.0, + "2": 0.39, + "3": 1.74, + "4": 1.16, + "5": 2.2, + "6": 2.2, + "7": 2.68, + "8": 2.92, + "9": 2.23, + "10": 2.84, + "11": 4.24, + "12": 4.45, + "13": 3.6, + "14": 2.25, + "15": 3.74, + "16": 5.0, + "17": 5.11, + "18": 3.18, + "19": 3.83, + "20": 3.8, + "21": 3.11, + "22": 5.27, + "23": 5.31, + "24": 4.5, + "25": 3.85, + "26": 4.55, + "27": 4.76, + "28": 4.37, + "29": 5.27, + "30": 5.67, + "31": 4.86, + "32": 6.24, + "33": 6.26, + "34": 5.85, + "35": 5.37, + "36": 5.53, + "37": 7.53, + "38": 5.88, + "39": 5.58, + "40": 6.98, + "41": 7.07, + "42": 6.39, + "43": 7.32, + "44": 7.39, + "45": 6.18, + "46": 7.82, + "47": 7.89, + "48": 6.74, + "49": 8.19, + "50": 8.2, + "51": 7.28, + "52": 8.75 + } + }, + "total_wall_s": 44.06998896598816 + } +} \ No newline at end of file diff --git a/benchmark/nnxtb/molecules/2-butanol.xyz b/benchmark/nnxtb/molecules/2-butanol.xyz new file mode 100644 index 00000000..43186990 --- /dev/null +++ b/benchmark/nnxtb/molecules/2-butanol.xyz @@ -0,0 +1,17 @@ +15 +2-butanol — multiple rotameric minima around the C-C-O backbone +C -2.0070 0.2780 -0.3760 +C -0.6810 -0.3630 0.0240 +C 0.5140 0.5680 -0.2040 +C 1.8460 -0.0880 0.1740 +O -0.5630 -1.5880 -0.7020 +H -2.8540 -0.3600 -0.1040 +H -2.1340 1.2240 0.1680 +H -2.0230 0.5040 -1.4500 +H -0.6220 -0.6030 1.0990 +H 0.5200 0.8050 -1.2790 +H 0.4120 1.5260 0.3270 +H 2.6910 0.5790 -0.0270 +H 1.9110 -0.3620 1.2360 +H 1.9680 -1.0130 -0.4050 +H 0.2270 -1.8900 -0.3640 diff --git a/benchmark/nnxtb/molecules/n-butane.xyz b/benchmark/nnxtb/molecules/n-butane.xyz new file mode 100644 index 00000000..5bd714a1 --- /dev/null +++ b/benchmark/nnxtb/molecules/n-butane.xyz @@ -0,0 +1,16 @@ +14 +n-butane — CREST finds anti and gauche conformers +C -1.8870 0.0000 0.0000 +C -0.6290 0.7480 0.0000 +C 0.6290 -0.0750 0.0000 +C 1.8870 0.7480 0.0000 +H -2.7850 0.6340 0.0000 +H -1.9400 -0.6440 0.8820 +H -1.9400 -0.6440 -0.8820 +H -0.6090 1.4070 0.8800 +H -0.6090 1.4070 -0.8800 +H 0.6090 -0.7340 0.8800 +H 0.6090 -0.7340 -0.8800 +H 2.7850 0.1140 0.0000 +H 1.9400 1.3920 0.8820 +H 1.9400 1.3920 -0.8820 diff --git a/benchmark/nnxtb/phase3_phase4_results.json b/benchmark/nnxtb/phase3_phase4_results.json new file mode 100644 index 00000000..bcf80579 --- /dev/null +++ b/benchmark/nnxtb/phase3_phase4_results.json @@ -0,0 +1,1424 @@ +[ + { + "name": "aspirin", + "smiles": "CC(=O)Oc1ccccc1C(=O)O", + "n_atoms": 21, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 48.01229381561279, + "n_conformers": 4, + "lowest_crest_energy": -39.63185877, + "lowest_dft_energy": -648.7620212237289, + "crest_log_path": "workdir/aspirin/gfn2/crest.log", + "ensemble_path": "workdir/aspirin/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1121.0044553279877, + "n_conformers": 4, + "lowest_crest_energy": -40.17547368, + "lowest_dft_energy": -648.7639193759331, + "crest_log_path": "workdir/aspirin/nnxtb/crest.log", + "ensemble_path": "workdir/aspirin/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.00189815220426226, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 4, + "n_nnxtb": 4, + "recall_gfn2_into_nnxtb": { + "0.25": 0.5, + "0.5": 0.5, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.5, + "0.5": 0.5, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 2, + "0.5": 2, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 2, + "0.5": 2, + "1.0": 0 + } + } + }, + { + "name": "ibuprofen", + "smiles": "CC(C)Cc1ccc(C(C)C(=O)O)cc1", + "n_atoms": 33, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 132.74923610687256, + "n_conformers": 11, + "lowest_crest_energy": -45.17191624, + "lowest_dft_energy": -656.768637927255, + "crest_log_path": "workdir/ibuprofen/gfn2/crest.log", + "ensemble_path": "workdir/ibuprofen/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 3089.155930042267, + "n_conformers": 20, + "lowest_crest_energy": -45.86725331, + "lowest_dft_energy": -656.7700871849523, + "crest_log_path": "workdir/ibuprofen/nnxtb/crest.log", + "ensemble_path": "workdir/ibuprofen/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0014492576973452742, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 11, + "n_nnxtb": 20, + "recall_gfn2_into_nnxtb": { + "0.25": 0.7272727272727273, + "0.5": 0.7272727272727273, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.4, + "0.5": 0.45, + "1.0": 0.9 + }, + "novel_in_nnxtb": { + "0.25": 12, + "0.5": 11, + "1.0": 2 + }, + "novel_in_gfn2": { + "0.25": 3, + "0.5": 3, + "1.0": 0 + } + } + }, + { + "name": "acetaminophen", + "smiles": "CC(=O)Nc1ccc(O)cc1", + "n_atoms": 20, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 30.176738500595093, + "n_conformers": 2, + "lowest_crest_energy": -32.7836376, + "lowest_dft_energy": -515.5307371351827, + "crest_log_path": "workdir/acetaminophen/gfn2/crest.log", + "ensemble_path": "workdir/acetaminophen/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 874.5299360752106, + "n_conformers": 4, + "lowest_crest_energy": -33.2651833, + "lowest_dft_energy": -515.5314965899817, + "crest_log_path": "workdir/acetaminophen/nnxtb/crest.log", + "ensemble_path": "workdir/acetaminophen/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0007594547989810962, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 2, + "n_nnxtb": 4, + "recall_gfn2_into_nnxtb": { + "0.25": 0.5, + "0.5": 0.5, + "1.0": 0.5 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 1, + "1.0": 1 + } + } + }, + { + "name": "caffeine", + "smiles": "Cn1cnc2c1c(=O)n(C)c(=O)n2C", + "n_atoms": 24, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 48.64701271057129, + "n_conformers": 6, + "lowest_crest_energy": -42.15450736, + "lowest_dft_energy": -680.442505273442, + "crest_log_path": "workdir/caffeine/gfn2/crest.log", + "ensemble_path": "workdir/caffeine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1175.2328298091888, + "n_conformers": 7, + "lowest_crest_energy": -42.76262693, + "lowest_dft_energy": -680.4452326242949, + "crest_log_path": "workdir/caffeine/nnxtb/crest.log", + "ensemble_path": "workdir/caffeine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.002727350852978816, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 6, + "n_nnxtb": 7, + "recall_gfn2_into_nnxtb": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "benzocaine", + "smiles": "CCOC(=O)c1ccc(N)cc1", + "n_atoms": 23, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 44.45894646644592, + "n_conformers": 3, + "lowest_crest_energy": -35.94154157, + "lowest_dft_energy": -554.8537396398225, + "crest_log_path": "workdir/benzocaine/gfn2/crest.log", + "ensemble_path": "workdir/benzocaine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1000.9925756454468, + "n_conformers": 3, + "lowest_crest_energy": -36.48253184, + "lowest_dft_energy": -554.8544976607374, + "crest_log_path": "workdir/benzocaine/nnxtb/crest.log", + "ensemble_path": "workdir/benzocaine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.000758020914872759, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 3, + "n_nnxtb": 3, + "recall_gfn2_into_nnxtb": { + "0.25": 0.6666666666666666, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.6666666666666666, + "0.5": 0.6666666666666666, + "1.0": 0.6666666666666666 + }, + "novel_in_nnxtb": { + "0.25": 1, + "0.5": 1, + "1.0": 1 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "theobromine", + "smiles": "Cn1cnc2c1c(=O)[nH]c(=O)n2C", + "n_atoms": 21, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 34.635175466537476, + "n_conformers": 3, + "lowest_crest_energy": -38.98642605, + "lowest_dft_energy": -641.1320925135108, + "crest_log_path": "workdir/theobromine/gfn2/crest.log", + "ensemble_path": "workdir/theobromine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 806.2880618572235, + "n_conformers": 4, + "lowest_crest_energy": -39.54565645, + "lowest_dft_energy": -641.1342701053175, + "crest_log_path": "workdir/theobromine/nnxtb/crest.log", + "ensemble_path": "workdir/theobromine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0021775918066850863, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 3, + "n_nnxtb": 4, + "recall_gfn2_into_nnxtb": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "phenacetin", + "smiles": "CCOc1ccc(NC(C)=O)cc1", + "n_atoms": 26, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 69.96505999565125, + "n_conformers": 4, + "lowest_crest_energy": -39.10949265, + "lowest_dft_energy": -594.1535331428453, + "crest_log_path": "workdir/phenacetin/gfn2/crest.log", + "ensemble_path": "workdir/phenacetin/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1647.4600987434387, + "n_conformers": 14, + "lowest_crest_energy": -39.69180493, + "lowest_dft_energy": -594.1563679524114, + "crest_log_path": "workdir/phenacetin/nnxtb/crest.log", + "ensemble_path": "workdir/phenacetin/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.002834809566138574, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 4, + "n_nnxtb": 14, + "recall_gfn2_into_nnxtb": { + "0.25": 0.75, + "0.5": 0.75, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.35714285714285715, + "0.5": 0.6428571428571429, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 9, + "0.5": 5, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 1, + "1.0": 0 + } + } + }, + { + "name": "lidocaine", + "smiles": "CCN(CC)CC(=O)Nc1c(C)cccc1C", + "n_atoms": 39, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 337.4662437438965, + "n_conformers": 52, + "lowest_crest_energy": -51.13469175, + "lowest_dft_energy": -731.5509699050483, + "crest_log_path": "workdir/lidocaine/gfn2/crest.log", + "ensemble_path": "workdir/lidocaine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 0.0, + "n_conformers": 0, + "lowest_crest_energy": null, + "lowest_dft_energy": null, + "crest_log_path": null, + "ensemble_path": null + }, + "dft_delta_hartree": null, + "winner": "tie", + "recall": { + "n_gfn2": 52, + "n_nnxtb": 0, + "recall_gfn2_into_nnxtb": {}, + "recall_nnxtb_into_gfn2": {}, + "novel_in_nnxtb": {}, + "novel_in_gfn2": {} + } + }, + { + "name": "diphenhydramine", + "smiles": "CN(C)CCOC(c1ccccc1)c1ccccc1", + "n_atoms": 40, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 440.0136842727661, + "n_conformers": 99, + "lowest_crest_energy": -54.07158418, + "lowest_dft_energy": -790.4700797870846, + "crest_log_path": "workdir/diphenhydramine/gfn2/crest.log", + "ensemble_path": "workdir/diphenhydramine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 0.0, + "n_conformers": 0, + "lowest_crest_energy": null, + "lowest_dft_energy": null, + "crest_log_path": null, + "ensemble_path": null + }, + "dft_delta_hartree": null, + "winner": "tie", + "recall": { + "n_gfn2": 99, + "n_nnxtb": 0, + "recall_gfn2_into_nnxtb": {}, + "recall_nnxtb_into_gfn2": {}, + "novel_in_nnxtb": {}, + "novel_in_gfn2": {} + } + }, + { + "name": "propranolol", + "smiles": "CC(C)NCC(O)COc1cccc2ccccc12", + "n_atoms": 40, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 653.118087053299, + "n_conformers": 317, + "lowest_crest_energy": -56.02307911, + "lowest_dft_energy": -827.6211418031423, + "crest_log_path": "workdir/propranolol/gfn2/crest.log", + "ensemble_path": "workdir/propranolol/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 0.0, + "n_conformers": 0, + "lowest_crest_energy": null, + "lowest_dft_energy": null, + "crest_log_path": null, + "ensemble_path": null + }, + "dft_delta_hartree": null, + "winner": "tie", + "recall": { + "n_gfn2": 317, + "n_nnxtb": 0, + "recall_gfn2_into_nnxtb": {}, + "recall_nnxtb_into_gfn2": {}, + "novel_in_nnxtb": {}, + "novel_in_gfn2": {} + } + }, + { + "name": "salicylic_acid", + "smiles": "OC(=O)c1ccccc1O", + "n_atoms": 16, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 23.116711616516113, + "n_conformers": 2, + "lowest_crest_energy": -30.25652413, + "lowest_dft_energy": -496.1045638983105, + "crest_log_path": "workdir/salicylic_acid/gfn2/crest.log", + "ensemble_path": "workdir/salicylic_acid/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 549.6680269241333, + "n_conformers": 2, + "lowest_crest_energy": -30.67762896, + "lowest_dft_energy": -496.10503400736815, + "crest_log_path": "workdir/salicylic_acid/nnxtb/crest.log", + "ensemble_path": "workdir/salicylic_acid/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0004701090576304523, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 2, + "n_nnxtb": 2, + "recall_gfn2_into_nnxtb": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "naproxen", + "smiles": "COc1ccc2cc(C(C)C(=O)O)ccc2c1", + "n_atoms": 31, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 144.7313039302826, + "n_conformers": 9, + "lowest_crest_energy": -49.33401124, + "lowest_dft_energy": -767.6845659720677, + "crest_log_path": "workdir/naproxen/gfn2/crest.log", + "ensemble_path": "workdir/naproxen/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1763.82155418396, + "n_conformers": 15, + "lowest_crest_energy": -50.07651833, + "lowest_dft_energy": -767.6858986254722, + "crest_log_path": "workdir/naproxen/nnxtb/crest.log", + "ensemble_path": "workdir/naproxen/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0013326534044608707, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 9, + "n_nnxtb": 15, + "recall_gfn2_into_nnxtb": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.6666666666666666, + "0.5": 0.9333333333333333, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 5, + "0.5": 1, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "mefenamic_acid", + "smiles": "Cc1cccc(C)c1Nc1ccccc1C(=O)O", + "n_atoms": 33, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 147.07683324813843, + "n_conformers": 1, + "lowest_crest_energy": -50.84830363, + "lowest_dft_energy": -785.9366008227337, + "crest_log_path": "workdir/mefenamic_acid/gfn2/crest.log", + "ensemble_path": "workdir/mefenamic_acid/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1741.4285459518433, + "n_conformers": 6, + "lowest_crest_energy": -51.6559091, + "lowest_dft_energy": -785.9386747682322, + "crest_log_path": "workdir/mefenamic_acid/nnxtb/crest.log", + "ensemble_path": "workdir/mefenamic_acid/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.002073945498523244, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 1, + "n_nnxtb": 6, + "recall_gfn2_into_nnxtb": { + "0.25": 0.0, + "0.5": 0.0, + "1.0": 0.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.0, + "0.5": 0.0, + "1.0": 0.0 + }, + "novel_in_nnxtb": { + "0.25": 6, + "0.5": 6, + "1.0": 6 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 1, + "1.0": 1 + } + } + }, + { + "name": "isoniazid", + "smiles": "NNC(=O)c1ccncc1", + "n_atoms": 17, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 28.331334352493286, + "n_conformers": 2, + "lowest_crest_energy": -29.20111886, + "lowest_dft_energy": -472.34553590019345, + "crest_log_path": "workdir/isoniazid/gfn2/crest.log", + "ensemble_path": "workdir/isoniazid/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 731.8517453670502, + "n_conformers": 1, + "lowest_crest_energy": -29.66447476, + "lowest_dft_energy": -472.3484642812905, + "crest_log_path": "workdir/isoniazid/nnxtb/crest.log", + "ensemble_path": "workdir/isoniazid/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.002928381097035526, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 2, + "n_nnxtb": 1, + "recall_gfn2_into_nnxtb": { + "0.25": 0.0, + "0.5": 0.5, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 1, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 2, + "0.5": 1, + "1.0": 0 + } + } + }, + { + "name": "metformin", + "smiles": "CN(C)C(=N)NC(N)=N", + "n_atoms": 20, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 31.36707830429077, + "n_conformers": 12, + "lowest_crest_energy": -28.77472948, + "lowest_dft_energy": -432.83349429647956, + "crest_log_path": "workdir/metformin/gfn2/crest.log", + "ensemble_path": "workdir/metformin/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1699.6981859207153, + "n_conformers": 7, + "lowest_crest_energy": -29.23075427, + "lowest_dft_energy": -432.83901542975843, + "crest_log_path": "workdir/metformin/nnxtb/crest.log", + "ensemble_path": "workdir/metformin/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.005521133278875823, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 12, + "n_nnxtb": 7, + "recall_gfn2_into_nnxtb": { + "0.25": 0.5833333333333334, + "0.5": 0.6666666666666666, + "1.0": 0.8333333333333334 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 5, + "0.5": 4, + "1.0": 2 + } + } + }, + { + "name": "propofol", + "smiles": "CC(C)c1cccc(C(C)C)c1O", + "n_atoms": 31, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 70.74724316596985, + "n_conformers": 16, + "lowest_crest_energy": -38.95120625, + "lowest_dft_energy": -543.400274118748, + "crest_log_path": "workdir/propofol/gfn2/crest.log", + "ensemble_path": "workdir/propofol/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1340.336655139923, + "n_conformers": 9, + "lowest_crest_energy": -39.56918793, + "lowest_dft_energy": -543.4020432852391, + "crest_log_path": "workdir/propofol/nnxtb/crest.log", + "ensemble_path": "workdir/propofol/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.001769166491044416, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 16, + "n_nnxtb": 9, + "recall_gfn2_into_nnxtb": { + "0.25": 0.625, + "0.5": 0.875, + "1.0": 0.9375 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.7777777777777778, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 2, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 6, + "0.5": 2, + "1.0": 1 + } + } + }, + { + "name": "theophylline", + "smiles": "Cn1c(=O)c2[nH]cnc2n(C)c1=O", + "n_atoms": 21, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 30.571295261383057, + "n_conformers": 3, + "lowest_crest_energy": -38.97975216, + "lowest_dft_energy": -641.1295842222779, + "crest_log_path": "workdir/theophylline/gfn2/crest.log", + "ensemble_path": "workdir/theophylline/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 801.1018433570862, + "n_conformers": 4, + "lowest_crest_energy": -39.5426107, + "lowest_dft_energy": -641.1311923208185, + "crest_log_path": "workdir/theophylline/nnxtb/crest.log", + "ensemble_path": "workdir/theophylline/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.001608098540600622, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 3, + "n_nnxtb": 4, + "recall_gfn2_into_nnxtb": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "diflunisal", + "smiles": "OC(=O)c1cc(-c2ccc(F)cc2F)ccc1O", + "n_atoms": 26, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 66.79989218711853, + "n_conformers": 4, + "lowest_crest_energy": -53.58776026, + "lowest_dft_energy": -925.6611570210091, + "crest_log_path": "workdir/diflunisal/gfn2/crest.log", + "ensemble_path": "workdir/diflunisal/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1079.118264913559, + "n_conformers": 6, + "lowest_crest_energy": -54.30655955, + "lowest_dft_energy": -925.6626186858812, + "crest_log_path": "workdir/diflunisal/nnxtb/crest.log", + "ensemble_path": "workdir/diflunisal/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0014616648720675585, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 4, + "n_nnxtb": 6, + "recall_gfn2_into_nnxtb": { + "0.25": 0.5, + "0.5": 0.5, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.3333333333333333, + "0.5": 0.3333333333333333, + "1.0": 0.5 + }, + "novel_in_nnxtb": { + "0.25": 4, + "0.5": 4, + "1.0": 3 + }, + "novel_in_gfn2": { + "0.25": 2, + "0.5": 2, + "1.0": 0 + } + } + }, + { + "name": "vanillin", + "smiles": "COc1cc(C=O)ccc1O", + "n_atoms": 19, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 28.573541402816772, + "n_conformers": 2, + "lowest_crest_energy": -33.37599475, + "lowest_dft_energy": -535.3609855656035, + "crest_log_path": "workdir/vanillin/gfn2/crest.log", + "ensemble_path": "workdir/vanillin/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 780.5972700119019, + "n_conformers": 2, + "lowest_crest_energy": -33.84761208, + "lowest_dft_energy": -535.3688337930745, + "crest_log_path": "workdir/vanillin/nnxtb/crest.log", + "ensemble_path": "workdir/vanillin/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.00784822747095859, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 2, + "n_nnxtb": 2, + "recall_gfn2_into_nnxtb": { + "0.25": 0.5, + "0.5": 0.5, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 1, + "1.0": 0 + } + } + }, + { + "name": "amphetamine", + "smiles": "CC(N)Cc1ccccc1", + "n_atoms": 23, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 37.29288387298584, + "n_conformers": 10, + "lowest_crest_energy": -28.80264717, + "lowest_dft_energy": -405.57219336443694, + "crest_log_path": "workdir/amphetamine/gfn2/crest.log", + "ensemble_path": "workdir/amphetamine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1097.9997432231903, + "n_conformers": 12, + "lowest_crest_energy": -29.29383082, + "lowest_dft_energy": -405.5736507353753, + "crest_log_path": "workdir/amphetamine/nnxtb/crest.log", + "ensemble_path": "workdir/amphetamine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0014573709383398636, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 10, + "n_nnxtb": 12, + "recall_gfn2_into_nnxtb": { + "0.25": 0.7, + "0.5": 0.8, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.5833333333333334, + "0.5": 0.5833333333333334, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 5, + "0.5": 5, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 3, + "0.5": 2, + "1.0": 0 + } + } + }, + { + "name": "guaiacol", + "smiles": "COc1ccccc1O", + "n_atoms": 17, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 39.411060094833374, + "n_conformers": 3, + "lowest_crest_energy": -27.18713808, + "lowest_dft_energy": -422.02856331246574, + "crest_log_path": "workdir/guaiacol/gfn2/crest.log", + "ensemble_path": "workdir/guaiacol/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 762.9869558811188, + "n_conformers": 2, + "lowest_crest_energy": -27.5747573, + "lowest_dft_energy": -422.02883769611304, + "crest_log_path": "workdir/guaiacol/nnxtb/crest.log", + "ensemble_path": "workdir/guaiacol/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0002743836473086958, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 3, + "n_nnxtb": 2, + "recall_gfn2_into_nnxtb": { + "0.25": 0.6666666666666666, + "0.5": 0.6666666666666666, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 1, + "1.0": 0 + } + } + }, + { + "name": "tryptamine", + "smiles": "NCCc1c[nH]c2ccccc12", + "n_atoms": 24, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 90.28900742530823, + "n_conformers": 16, + "lowest_crest_energy": -33.37171619, + "lowest_dft_energy": -497.83620706660145, + "crest_log_path": "workdir/tryptamine/gfn2/crest.log", + "ensemble_path": "workdir/tryptamine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1232.4667434692383, + "n_conformers": 15, + "lowest_crest_energy": -33.93268248, + "lowest_dft_energy": -497.8376391098442, + "crest_log_path": "workdir/tryptamine/nnxtb/crest.log", + "ensemble_path": "workdir/tryptamine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.00143204324274393, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 16, + "n_nnxtb": 15, + "recall_gfn2_into_nnxtb": { + "0.25": 0.8125, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.6666666666666666, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 5, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 3, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "tyramine", + "smiles": "NCCc1ccc(O)cc1", + "n_atoms": 21, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 36.59581732749939, + "n_conformers": 18, + "lowest_crest_energy": -29.70879768, + "lowest_dft_energy": -441.483097265115, + "crest_log_path": "workdir/tyramine/gfn2/crest.log", + "ensemble_path": "workdir/tyramine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1097.3079569339752, + "n_conformers": 16, + "lowest_crest_energy": -30.17364805, + "lowest_dft_energy": -441.48410761411384, + "crest_log_path": "workdir/tyramine/nnxtb/crest.log", + "ensemble_path": "workdir/tyramine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0010103489988182446, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 18, + "n_nnxtb": 16, + "recall_gfn2_into_nnxtb": { + "0.25": 0.7222222222222222, + "0.5": 0.9444444444444444, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.8125, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 3, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 5, + "0.5": 1, + "1.0": 0 + } + } + }, + { + "name": "dopamine", + "smiles": "NCCc1ccc(O)c(O)c1", + "n_atoms": 22, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 58.376280307769775, + "n_conformers": 19, + "lowest_crest_energy": -33.78296422, + "lowest_dft_energy": -516.7167678752379, + "crest_log_path": "workdir/dopamine/gfn2/crest.log", + "ensemble_path": "workdir/dopamine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1062.716608285904, + "n_conformers": 23, + "lowest_crest_energy": -34.27652884, + "lowest_dft_energy": -516.7180059033582, + "crest_log_path": "workdir/dopamine/nnxtb/crest.log", + "ensemble_path": "workdir/dopamine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0012380281202695187, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 19, + "n_nnxtb": 23, + "recall_gfn2_into_nnxtb": { + "0.25": 0.7894736842105263, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.9565217391304348, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 1, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 4, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "histamine", + "smiles": "NCCc1c[nH]cn1", + "n_atoms": 17, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 25.417662382125854, + "n_conformers": 24, + "lowest_crest_energy": -24.05426496, + "lowest_dft_energy": -360.23070174233317, + "crest_log_path": "workdir/histamine/gfn2/crest.log", + "ensemble_path": "workdir/histamine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 873.7109248638153, + "n_conformers": 17, + "lowest_crest_energy": -24.43970524, + "lowest_dft_energy": -360.2319224322582, + "crest_log_path": "workdir/histamine/nnxtb/crest.log", + "ensemble_path": "workdir/histamine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0012206899250486458, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 24, + "n_nnxtb": 17, + "recall_gfn2_into_nnxtb": { + "0.25": 0.5833333333333334, + "0.5": 0.9583333333333334, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.7058823529411765, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 5, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 10, + "0.5": 1, + "1.0": 0 + } + } + }, + { + "name": "phenylalanine", + "smiles": "N[C@@H](Cc1ccccc1)C(=O)O", + "n_atoms": 23, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 109.4740538597107, + "n_conformers": 19, + "lowest_crest_energy": -35.92535601, + "lowest_dft_energy": -554.8523131031861, + "crest_log_path": "workdir/phenylalanine/gfn2/crest.log", + "ensemble_path": "workdir/phenylalanine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 2116.501625061035, + "n_conformers": 19, + "lowest_crest_energy": -36.4737757, + "lowest_dft_energy": -554.8538596792761, + "crest_log_path": "workdir/phenylalanine/nnxtb/crest.log", + "ensemble_path": "workdir/phenylalanine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0015465760899360248, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 19, + "n_nnxtb": 19, + "recall_gfn2_into_nnxtb": { + "0.25": 0.7894736842105263, + "0.5": 0.8947368421052632, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.7368421052631579, + "0.5": 0.8421052631578947, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 5, + "0.5": 3, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 4, + "0.5": 2, + "1.0": 0 + } + } + }, + { + "name": "serotonin", + "smiles": "NCCc1c[nH]c2ccc(O)cc12", + "n_atoms": 25, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 60.269389629364014, + "n_conformers": 17, + "lowest_crest_energy": -37.4447207, + "lowest_dft_energy": -573.0667509148659, + "crest_log_path": "workdir/serotonin/gfn2/crest.log", + "ensemble_path": "workdir/serotonin/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1289.9519147872925, + "n_conformers": 36, + "lowest_crest_energy": -38.03198218, + "lowest_dft_energy": -573.0684095151045, + "crest_log_path": "workdir/serotonin/nnxtb/crest.log", + "ensemble_path": "workdir/serotonin/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0016586002385565735, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 17, + "n_nnxtb": 36, + "recall_gfn2_into_nnxtb": { + "0.25": 0.9411764705882353, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.6944444444444444, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 11, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "coumarin", + "smiles": "O=c1ccc2ccccc2o1", + "n_atoms": 17, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 22.506465196609497, + "n_conformers": 1, + "lowest_crest_energy": -30.45757697, + "lowest_dft_energy": -497.0624483716068, + "crest_log_path": "workdir/coumarin/gfn2/crest.log", + "ensemble_path": "workdir/coumarin/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 511.5465302467346, + "n_conformers": 1, + "lowest_crest_energy": -30.92409841, + "lowest_dft_energy": -497.06302752853526, + "crest_log_path": "workdir/coumarin/nnxtb/crest.log", + "ensemble_path": "workdir/coumarin/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0005791569284383513, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 1, + "n_nnxtb": 1, + "recall_gfn2_into_nnxtb": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "paracetamol_N", + "smiles": "O=C(N)c1ccc(O)cc1", + "n_atoms": 17, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 32.846306800842285, + "n_conformers": 2, + "lowest_crest_energy": -29.60647831, + "lowest_dft_energy": -476.22051366713754, + "crest_log_path": "workdir/paracetamol_N/gfn2/crest.log", + "ensemble_path": "workdir/paracetamol_N/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 678.7731022834778, + "n_conformers": 3, + "lowest_crest_energy": -30.04803652, + "lowest_dft_energy": -476.2210789764954, + "crest_log_path": "workdir/paracetamol_N/nnxtb/crest.log", + "ensemble_path": "workdir/paracetamol_N/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0005653093578530388, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 2, + "n_nnxtb": 3, + "recall_gfn2_into_nnxtb": { + "0.25": 0.5, + "0.5": 0.5, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.6666666666666666, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 1, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 1, + "1.0": 0 + } + } + }, + { + "name": "anisole", + "smiles": "COc1ccccc1", + "n_atoms": 16, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 19.603131532669067, + "n_conformers": 1, + "lowest_crest_energy": -23.11319507, + "lowest_dft_energy": -346.7943890396327, + "crest_log_path": "workdir/anisole/gfn2/crest.log", + "ensemble_path": "workdir/anisole/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 652.470059633255, + "n_conformers": 2, + "lowest_crest_energy": -23.47137836, + "lowest_dft_energy": -346.79464481000986, + "crest_log_path": "workdir/anisole/nnxtb/crest.log", + "ensemble_path": "workdir/anisole/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.00025577037717994244, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 1, + "n_nnxtb": 2, + "recall_gfn2_into_nnxtb": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.5, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 1, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + } + } + } +] \ No newline at end of file diff --git a/benchmark/nnxtb/phase3_results.json b/benchmark/nnxtb/phase3_results.json new file mode 100644 index 00000000..8f88dfab --- /dev/null +++ b/benchmark/nnxtb/phase3_results.json @@ -0,0 +1,444 @@ +[ + { + "name": "aspirin", + "smiles": "CC(=O)Oc1ccccc1C(=O)O", + "n_atoms": 21, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 48.01229381561279, + "n_conformers": 4, + "lowest_crest_energy": -39.63185877, + "lowest_dft_energy": -648.7620212237289, + "crest_log_path": "workdir/aspirin/gfn2/crest.log", + "ensemble_path": "workdir/aspirin/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1121.0044553279877, + "n_conformers": 4, + "lowest_crest_energy": -40.17547368, + "lowest_dft_energy": -648.7639193759331, + "crest_log_path": "workdir/aspirin/nnxtb/crest.log", + "ensemble_path": "workdir/aspirin/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.00189815220426226, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 4, + "n_nnxtb": 4, + "recall_gfn2_into_nnxtb": { + "0.25": 0.5, + "0.5": 0.5, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.5, + "0.5": 0.5, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 2, + "0.5": 2, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 2, + "0.5": 2, + "1.0": 0 + } + } + }, + { + "name": "ibuprofen", + "smiles": "CC(C)Cc1ccc(C(C)C(=O)O)cc1", + "n_atoms": 33, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 132.74923610687256, + "n_conformers": 11, + "lowest_crest_energy": -45.17191624, + "lowest_dft_energy": -656.768637927255, + "crest_log_path": "workdir/ibuprofen/gfn2/crest.log", + "ensemble_path": "workdir/ibuprofen/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 3089.155930042267, + "n_conformers": 20, + "lowest_crest_energy": -45.86725331, + "lowest_dft_energy": -656.7700871849523, + "crest_log_path": "workdir/ibuprofen/nnxtb/crest.log", + "ensemble_path": "workdir/ibuprofen/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0014492576973452742, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 11, + "n_nnxtb": 20, + "recall_gfn2_into_nnxtb": { + "0.25": 0.7272727272727273, + "0.5": 0.7272727272727273, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.4, + "0.5": 0.45, + "1.0": 0.9 + }, + "novel_in_nnxtb": { + "0.25": 12, + "0.5": 11, + "1.0": 2 + }, + "novel_in_gfn2": { + "0.25": 3, + "0.5": 3, + "1.0": 0 + } + } + }, + { + "name": "acetaminophen", + "smiles": "CC(=O)Nc1ccc(O)cc1", + "n_atoms": 20, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 30.176738500595093, + "n_conformers": 2, + "lowest_crest_energy": -32.7836376, + "lowest_dft_energy": -515.5307371351827, + "crest_log_path": "workdir/acetaminophen/gfn2/crest.log", + "ensemble_path": "workdir/acetaminophen/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 874.5299360752106, + "n_conformers": 4, + "lowest_crest_energy": -33.2651833, + "lowest_dft_energy": -515.5314965899817, + "crest_log_path": "workdir/acetaminophen/nnxtb/crest.log", + "ensemble_path": "workdir/acetaminophen/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0007594547989810962, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 2, + "n_nnxtb": 4, + "recall_gfn2_into_nnxtb": { + "0.25": 0.5, + "0.5": 0.5, + "1.0": 0.5 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 1, + "1.0": 1 + } + } + }, + { + "name": "caffeine", + "smiles": "Cn1cnc2c1c(=O)n(C)c(=O)n2C", + "n_atoms": 24, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 48.64701271057129, + "n_conformers": 6, + "lowest_crest_energy": -42.15450736, + "lowest_dft_energy": -680.442505273442, + "crest_log_path": "workdir/caffeine/gfn2/crest.log", + "ensemble_path": "workdir/caffeine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1175.2328298091888, + "n_conformers": 7, + "lowest_crest_energy": -42.76262693, + "lowest_dft_energy": -680.4452326242949, + "crest_log_path": "workdir/caffeine/nnxtb/crest.log", + "ensemble_path": "workdir/caffeine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.002727350852978816, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 6, + "n_nnxtb": 7, + "recall_gfn2_into_nnxtb": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "benzocaine", + "smiles": "CCOC(=O)c1ccc(N)cc1", + "n_atoms": 23, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 44.45894646644592, + "n_conformers": 3, + "lowest_crest_energy": -35.94154157, + "lowest_dft_energy": -554.8537396398225, + "crest_log_path": "workdir/benzocaine/gfn2/crest.log", + "ensemble_path": "workdir/benzocaine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1000.9925756454468, + "n_conformers": 3, + "lowest_crest_energy": -36.48253184, + "lowest_dft_energy": -554.8544976607374, + "crest_log_path": "workdir/benzocaine/nnxtb/crest.log", + "ensemble_path": "workdir/benzocaine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.000758020914872759, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 3, + "n_nnxtb": 3, + "recall_gfn2_into_nnxtb": { + "0.25": 0.6666666666666666, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.6666666666666666, + "0.5": 0.6666666666666666, + "1.0": 0.6666666666666666 + }, + "novel_in_nnxtb": { + "0.25": 1, + "0.5": 1, + "1.0": 1 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "theobromine", + "smiles": "Cn1cnc2c1c(=O)[nH]c(=O)n2C", + "n_atoms": 21, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 34.635175466537476, + "n_conformers": 3, + "lowest_crest_energy": -38.98642605, + "lowest_dft_energy": -641.1320925135108, + "crest_log_path": "workdir/theobromine/gfn2/crest.log", + "ensemble_path": "workdir/theobromine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 806.2880618572235, + "n_conformers": 4, + "lowest_crest_energy": -39.54565645, + "lowest_dft_energy": -641.1342701053175, + "crest_log_path": "workdir/theobromine/nnxtb/crest.log", + "ensemble_path": "workdir/theobromine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0021775918066850863, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 3, + "n_nnxtb": 4, + "recall_gfn2_into_nnxtb": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "phenacetin", + "smiles": "CCOc1ccc(NC(C)=O)cc1", + "n_atoms": 26, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 69.96505999565125, + "n_conformers": 4, + "lowest_crest_energy": -39.10949265, + "lowest_dft_energy": -594.1535331428453, + "crest_log_path": "workdir/phenacetin/gfn2/crest.log", + "ensemble_path": "workdir/phenacetin/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1647.4600987434387, + "n_conformers": 14, + "lowest_crest_energy": -39.69180493, + "lowest_dft_energy": -594.1563679524114, + "crest_log_path": "workdir/phenacetin/nnxtb/crest.log", + "ensemble_path": "workdir/phenacetin/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.002834809566138574, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 4, + "n_nnxtb": 14, + "recall_gfn2_into_nnxtb": { + "0.25": 0.75, + "0.5": 0.75, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.35714285714285715, + "0.5": 0.6428571428571429, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 9, + "0.5": 5, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 1, + "1.0": 0 + } + } + }, + { + "name": "lidocaine", + "smiles": "CCN(CC)CC(=O)Nc1c(C)cccc1C", + "n_atoms": 39, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 337.4662437438965, + "n_conformers": 52, + "lowest_crest_energy": -51.13469175, + "lowest_dft_energy": -731.5509699050483, + "crest_log_path": "workdir/lidocaine/gfn2/crest.log", + "ensemble_path": "workdir/lidocaine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 0.0, + "n_conformers": 0, + "lowest_crest_energy": null, + "lowest_dft_energy": null, + "crest_log_path": null, + "ensemble_path": null + }, + "dft_delta_hartree": null, + "winner": "tie", + "recall": { + "n_gfn2": 52, + "n_nnxtb": 0, + "recall_gfn2_into_nnxtb": {}, + "recall_nnxtb_into_gfn2": {}, + "novel_in_nnxtb": {}, + "novel_in_gfn2": {} + } + }, + { + "name": "diphenhydramine", + "smiles": "CN(C)CCOC(c1ccccc1)c1ccccc1", + "n_atoms": 40, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 440.0136842727661, + "n_conformers": 99, + "lowest_crest_energy": -54.07158418, + "lowest_dft_energy": -790.4700797870846, + "crest_log_path": "workdir/diphenhydramine/gfn2/crest.log", + "ensemble_path": "workdir/diphenhydramine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 0.0, + "n_conformers": 0, + "lowest_crest_energy": null, + "lowest_dft_energy": null, + "crest_log_path": null, + "ensemble_path": null + }, + "dft_delta_hartree": null, + "winner": "tie", + "recall": { + "n_gfn2": 99, + "n_nnxtb": 0, + "recall_gfn2_into_nnxtb": {}, + "recall_nnxtb_into_gfn2": {}, + "novel_in_nnxtb": {}, + "novel_in_gfn2": {} + } + }, + { + "name": "propranolol", + "smiles": "CC(C)NCC(O)COc1cccc2ccccc12", + "n_atoms": 40, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 653.118087053299, + "n_conformers": 317, + "lowest_crest_energy": -56.02307911, + "lowest_dft_energy": -827.6211418031423, + "crest_log_path": "workdir/propranolol/gfn2/crest.log", + "ensemble_path": "workdir/propranolol/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 0.0, + "n_conformers": 0, + "lowest_crest_energy": null, + "lowest_dft_energy": null, + "crest_log_path": null, + "ensemble_path": null + }, + "dft_delta_hartree": null, + "winner": "tie", + "recall": { + "n_gfn2": 317, + "n_nnxtb": 0, + "recall_gfn2_into_nnxtb": {}, + "recall_nnxtb_into_gfn2": {}, + "novel_in_nnxtb": {}, + "novel_in_gfn2": {} + } + } +] \ No newline at end of file diff --git a/benchmark/nnxtb/phase4_a_results.json b/benchmark/nnxtb/phase4_a_results.json new file mode 100644 index 00000000..9f375bb1 --- /dev/null +++ b/benchmark/nnxtb/phase4_a_results.json @@ -0,0 +1,492 @@ +[ + { + "name": "salicylic_acid", + "smiles": "OC(=O)c1ccccc1O", + "n_atoms": 16, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 23.116711616516113, + "n_conformers": 2, + "lowest_crest_energy": -30.25652413, + "lowest_dft_energy": -496.1045638983105, + "crest_log_path": "workdir/salicylic_acid/gfn2/crest.log", + "ensemble_path": "workdir/salicylic_acid/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 549.6680269241333, + "n_conformers": 2, + "lowest_crest_energy": -30.67762896, + "lowest_dft_energy": -496.10503400736815, + "crest_log_path": "workdir/salicylic_acid/nnxtb/crest.log", + "ensemble_path": "workdir/salicylic_acid/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0004701090576304523, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 2, + "n_nnxtb": 2, + "recall_gfn2_into_nnxtb": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "naproxen", + "smiles": "COc1ccc2cc(C(C)C(=O)O)ccc2c1", + "n_atoms": 31, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 144.7313039302826, + "n_conformers": 9, + "lowest_crest_energy": -49.33401124, + "lowest_dft_energy": -767.6845659720677, + "crest_log_path": "workdir/naproxen/gfn2/crest.log", + "ensemble_path": "workdir/naproxen/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1763.82155418396, + "n_conformers": 15, + "lowest_crest_energy": -50.07651833, + "lowest_dft_energy": -767.6858986254722, + "crest_log_path": "workdir/naproxen/nnxtb/crest.log", + "ensemble_path": "workdir/naproxen/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0013326534044608707, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 9, + "n_nnxtb": 15, + "recall_gfn2_into_nnxtb": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.6666666666666666, + "0.5": 0.9333333333333333, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 5, + "0.5": 1, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "mefenamic_acid", + "smiles": "Cc1cccc(C)c1Nc1ccccc1C(=O)O", + "n_atoms": 33, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 147.07683324813843, + "n_conformers": 1, + "lowest_crest_energy": -50.84830363, + "lowest_dft_energy": -785.9366008227337, + "crest_log_path": "workdir/mefenamic_acid/gfn2/crest.log", + "ensemble_path": "workdir/mefenamic_acid/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1741.4285459518433, + "n_conformers": 6, + "lowest_crest_energy": -51.6559091, + "lowest_dft_energy": -785.9386747682322, + "crest_log_path": "workdir/mefenamic_acid/nnxtb/crest.log", + "ensemble_path": "workdir/mefenamic_acid/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.002073945498523244, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 1, + "n_nnxtb": 6, + "recall_gfn2_into_nnxtb": { + "0.25": 0.0, + "0.5": 0.0, + "1.0": 0.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.0, + "0.5": 0.0, + "1.0": 0.0 + }, + "novel_in_nnxtb": { + "0.25": 6, + "0.5": 6, + "1.0": 6 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 1, + "1.0": 1 + } + } + }, + { + "name": "isoniazid", + "smiles": "NNC(=O)c1ccncc1", + "n_atoms": 17, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 28.331334352493286, + "n_conformers": 2, + "lowest_crest_energy": -29.20111886, + "lowest_dft_energy": -472.34553590019345, + "crest_log_path": "workdir/isoniazid/gfn2/crest.log", + "ensemble_path": "workdir/isoniazid/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 731.8517453670502, + "n_conformers": 1, + "lowest_crest_energy": -29.66447476, + "lowest_dft_energy": -472.3484642812905, + "crest_log_path": "workdir/isoniazid/nnxtb/crest.log", + "ensemble_path": "workdir/isoniazid/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.002928381097035526, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 2, + "n_nnxtb": 1, + "recall_gfn2_into_nnxtb": { + "0.25": 0.0, + "0.5": 0.5, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 1, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 2, + "0.5": 1, + "1.0": 0 + } + } + }, + { + "name": "metformin", + "smiles": "CN(C)C(=N)NC(N)=N", + "n_atoms": 20, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 31.36707830429077, + "n_conformers": 12, + "lowest_crest_energy": -28.77472948, + "lowest_dft_energy": -432.83349429647956, + "crest_log_path": "workdir/metformin/gfn2/crest.log", + "ensemble_path": "workdir/metformin/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1699.6981859207153, + "n_conformers": 7, + "lowest_crest_energy": -29.23075427, + "lowest_dft_energy": -432.83901542975843, + "crest_log_path": "workdir/metformin/nnxtb/crest.log", + "ensemble_path": "workdir/metformin/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.005521133278875823, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 12, + "n_nnxtb": 7, + "recall_gfn2_into_nnxtb": { + "0.25": 0.5833333333333334, + "0.5": 0.6666666666666666, + "1.0": 0.8333333333333334 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 5, + "0.5": 4, + "1.0": 2 + } + } + }, + { + "name": "propofol", + "smiles": "CC(C)c1cccc(C(C)C)c1O", + "n_atoms": 31, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 70.74724316596985, + "n_conformers": 16, + "lowest_crest_energy": -38.95120625, + "lowest_dft_energy": -543.400274118748, + "crest_log_path": "workdir/propofol/gfn2/crest.log", + "ensemble_path": "workdir/propofol/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1340.336655139923, + "n_conformers": 9, + "lowest_crest_energy": -39.56918793, + "lowest_dft_energy": -543.4020432852391, + "crest_log_path": "workdir/propofol/nnxtb/crest.log", + "ensemble_path": "workdir/propofol/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.001769166491044416, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 16, + "n_nnxtb": 9, + "recall_gfn2_into_nnxtb": { + "0.25": 0.625, + "0.5": 0.875, + "1.0": 0.9375 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.7777777777777778, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 2, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 6, + "0.5": 2, + "1.0": 1 + } + } + }, + { + "name": "theophylline", + "smiles": "Cn1c(=O)c2[nH]cnc2n(C)c1=O", + "n_atoms": 21, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 30.571295261383057, + "n_conformers": 3, + "lowest_crest_energy": -38.97975216, + "lowest_dft_energy": -641.1295842222779, + "crest_log_path": "workdir/theophylline/gfn2/crest.log", + "ensemble_path": "workdir/theophylline/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 801.1018433570862, + "n_conformers": 4, + "lowest_crest_energy": -39.5426107, + "lowest_dft_energy": -641.1311923208185, + "crest_log_path": "workdir/theophylline/nnxtb/crest.log", + "ensemble_path": "workdir/theophylline/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.001608098540600622, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 3, + "n_nnxtb": 4, + "recall_gfn2_into_nnxtb": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "diflunisal", + "smiles": "OC(=O)c1cc(-c2ccc(F)cc2F)ccc1O", + "n_atoms": 26, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 66.79989218711853, + "n_conformers": 4, + "lowest_crest_energy": -53.58776026, + "lowest_dft_energy": -925.6611570210091, + "crest_log_path": "workdir/diflunisal/gfn2/crest.log", + "ensemble_path": "workdir/diflunisal/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1079.118264913559, + "n_conformers": 6, + "lowest_crest_energy": -54.30655955, + "lowest_dft_energy": -925.6626186858812, + "crest_log_path": "workdir/diflunisal/nnxtb/crest.log", + "ensemble_path": "workdir/diflunisal/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0014616648720675585, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 4, + "n_nnxtb": 6, + "recall_gfn2_into_nnxtb": { + "0.25": 0.5, + "0.5": 0.5, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.3333333333333333, + "0.5": 0.3333333333333333, + "1.0": 0.5 + }, + "novel_in_nnxtb": { + "0.25": 4, + "0.5": 4, + "1.0": 3 + }, + "novel_in_gfn2": { + "0.25": 2, + "0.5": 2, + "1.0": 0 + } + } + }, + { + "name": "vanillin", + "smiles": "COc1cc(C=O)ccc1O", + "n_atoms": 19, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 28.573541402816772, + "n_conformers": 2, + "lowest_crest_energy": -33.37599475, + "lowest_dft_energy": -535.3609855656035, + "crest_log_path": "workdir/vanillin/gfn2/crest.log", + "ensemble_path": "workdir/vanillin/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 780.5972700119019, + "n_conformers": 2, + "lowest_crest_energy": -33.84761208, + "lowest_dft_energy": -535.3688337930745, + "crest_log_path": "workdir/vanillin/nnxtb/crest.log", + "ensemble_path": "workdir/vanillin/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.00784822747095859, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 2, + "n_nnxtb": 2, + "recall_gfn2_into_nnxtb": { + "0.25": 0.5, + "0.5": 0.5, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 1, + "1.0": 0 + } + } + }, + { + "name": "amphetamine", + "smiles": "CC(N)Cc1ccccc1", + "n_atoms": 23, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 37.29288387298584, + "n_conformers": 10, + "lowest_crest_energy": -28.80264717, + "lowest_dft_energy": -405.57219336443694, + "crest_log_path": "workdir/amphetamine/gfn2/crest.log", + "ensemble_path": "workdir/amphetamine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1097.9997432231903, + "n_conformers": 12, + "lowest_crest_energy": -29.29383082, + "lowest_dft_energy": -405.5736507353753, + "crest_log_path": "workdir/amphetamine/nnxtb/crest.log", + "ensemble_path": "workdir/amphetamine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0014573709383398636, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 10, + "n_nnxtb": 12, + "recall_gfn2_into_nnxtb": { + "0.25": 0.7, + "0.5": 0.8, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.5833333333333334, + "0.5": 0.5833333333333334, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 5, + "0.5": 5, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 3, + "0.5": 2, + "1.0": 0 + } + } + } +] \ No newline at end of file diff --git a/benchmark/nnxtb/phase4_b_results.json b/benchmark/nnxtb/phase4_b_results.json new file mode 100644 index 00000000..ec7ff69e --- /dev/null +++ b/benchmark/nnxtb/phase4_b_results.json @@ -0,0 +1,492 @@ +[ + { + "name": "guaiacol", + "smiles": "COc1ccccc1O", + "n_atoms": 17, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 39.411060094833374, + "n_conformers": 3, + "lowest_crest_energy": -27.18713808, + "lowest_dft_energy": -422.02856331246574, + "crest_log_path": "workdir/guaiacol/gfn2/crest.log", + "ensemble_path": "workdir/guaiacol/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 762.9869558811188, + "n_conformers": 2, + "lowest_crest_energy": -27.5747573, + "lowest_dft_energy": -422.02883769611304, + "crest_log_path": "workdir/guaiacol/nnxtb/crest.log", + "ensemble_path": "workdir/guaiacol/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0002743836473086958, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 3, + "n_nnxtb": 2, + "recall_gfn2_into_nnxtb": { + "0.25": 0.6666666666666666, + "0.5": 0.6666666666666666, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 1, + "1.0": 0 + } + } + }, + { + "name": "tryptamine", + "smiles": "NCCc1c[nH]c2ccccc12", + "n_atoms": 24, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 90.28900742530823, + "n_conformers": 16, + "lowest_crest_energy": -33.37171619, + "lowest_dft_energy": -497.83620706660145, + "crest_log_path": "workdir/tryptamine/gfn2/crest.log", + "ensemble_path": "workdir/tryptamine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1232.4667434692383, + "n_conformers": 15, + "lowest_crest_energy": -33.93268248, + "lowest_dft_energy": -497.8376391098442, + "crest_log_path": "workdir/tryptamine/nnxtb/crest.log", + "ensemble_path": "workdir/tryptamine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.00143204324274393, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 16, + "n_nnxtb": 15, + "recall_gfn2_into_nnxtb": { + "0.25": 0.8125, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.6666666666666666, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 5, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 3, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "tyramine", + "smiles": "NCCc1ccc(O)cc1", + "n_atoms": 21, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 36.59581732749939, + "n_conformers": 18, + "lowest_crest_energy": -29.70879768, + "lowest_dft_energy": -441.483097265115, + "crest_log_path": "workdir/tyramine/gfn2/crest.log", + "ensemble_path": "workdir/tyramine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1097.3079569339752, + "n_conformers": 16, + "lowest_crest_energy": -30.17364805, + "lowest_dft_energy": -441.48410761411384, + "crest_log_path": "workdir/tyramine/nnxtb/crest.log", + "ensemble_path": "workdir/tyramine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0010103489988182446, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 18, + "n_nnxtb": 16, + "recall_gfn2_into_nnxtb": { + "0.25": 0.7222222222222222, + "0.5": 0.9444444444444444, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.8125, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 3, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 5, + "0.5": 1, + "1.0": 0 + } + } + }, + { + "name": "dopamine", + "smiles": "NCCc1ccc(O)c(O)c1", + "n_atoms": 22, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 58.376280307769775, + "n_conformers": 19, + "lowest_crest_energy": -33.78296422, + "lowest_dft_energy": -516.7167678752379, + "crest_log_path": "workdir/dopamine/gfn2/crest.log", + "ensemble_path": "workdir/dopamine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1062.716608285904, + "n_conformers": 23, + "lowest_crest_energy": -34.27652884, + "lowest_dft_energy": -516.7180059033582, + "crest_log_path": "workdir/dopamine/nnxtb/crest.log", + "ensemble_path": "workdir/dopamine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0012380281202695187, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 19, + "n_nnxtb": 23, + "recall_gfn2_into_nnxtb": { + "0.25": 0.7894736842105263, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.9565217391304348, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 1, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 4, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "histamine", + "smiles": "NCCc1c[nH]cn1", + "n_atoms": 17, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 25.417662382125854, + "n_conformers": 24, + "lowest_crest_energy": -24.05426496, + "lowest_dft_energy": -360.23070174233317, + "crest_log_path": "workdir/histamine/gfn2/crest.log", + "ensemble_path": "workdir/histamine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 873.7109248638153, + "n_conformers": 17, + "lowest_crest_energy": -24.43970524, + "lowest_dft_energy": -360.2319224322582, + "crest_log_path": "workdir/histamine/nnxtb/crest.log", + "ensemble_path": "workdir/histamine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0012206899250486458, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 24, + "n_nnxtb": 17, + "recall_gfn2_into_nnxtb": { + "0.25": 0.5833333333333334, + "0.5": 0.9583333333333334, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.7058823529411765, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 5, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 10, + "0.5": 1, + "1.0": 0 + } + } + }, + { + "name": "phenylalanine", + "smiles": "N[C@@H](Cc1ccccc1)C(=O)O", + "n_atoms": 23, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 109.4740538597107, + "n_conformers": 19, + "lowest_crest_energy": -35.92535601, + "lowest_dft_energy": -554.8523131031861, + "crest_log_path": "workdir/phenylalanine/gfn2/crest.log", + "ensemble_path": "workdir/phenylalanine/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 2116.501625061035, + "n_conformers": 19, + "lowest_crest_energy": -36.4737757, + "lowest_dft_energy": -554.8538596792761, + "crest_log_path": "workdir/phenylalanine/nnxtb/crest.log", + "ensemble_path": "workdir/phenylalanine/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0015465760899360248, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 19, + "n_nnxtb": 19, + "recall_gfn2_into_nnxtb": { + "0.25": 0.7894736842105263, + "0.5": 0.8947368421052632, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.7368421052631579, + "0.5": 0.8421052631578947, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 5, + "0.5": 3, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 4, + "0.5": 2, + "1.0": 0 + } + } + }, + { + "name": "serotonin", + "smiles": "NCCc1c[nH]c2ccc(O)cc12", + "n_atoms": 25, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 60.269389629364014, + "n_conformers": 17, + "lowest_crest_energy": -37.4447207, + "lowest_dft_energy": -573.0667509148659, + "crest_log_path": "workdir/serotonin/gfn2/crest.log", + "ensemble_path": "workdir/serotonin/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 1289.9519147872925, + "n_conformers": 36, + "lowest_crest_energy": -38.03198218, + "lowest_dft_energy": -573.0684095151045, + "crest_log_path": "workdir/serotonin/nnxtb/crest.log", + "ensemble_path": "workdir/serotonin/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0016586002385565735, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 17, + "n_nnxtb": 36, + "recall_gfn2_into_nnxtb": { + "0.25": 0.9411764705882353, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.6944444444444444, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 11, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "coumarin", + "smiles": "O=c1ccc2ccccc2o1", + "n_atoms": 17, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 22.506465196609497, + "n_conformers": 1, + "lowest_crest_energy": -30.45757697, + "lowest_dft_energy": -497.0624483716068, + "crest_log_path": "workdir/coumarin/gfn2/crest.log", + "ensemble_path": "workdir/coumarin/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 511.5465302467346, + "n_conformers": 1, + "lowest_crest_energy": -30.92409841, + "lowest_dft_energy": -497.06302752853526, + "crest_log_path": "workdir/coumarin/nnxtb/crest.log", + "ensemble_path": "workdir/coumarin/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0005791569284383513, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 1, + "n_nnxtb": 1, + "recall_gfn2_into_nnxtb": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + } + } + }, + { + "name": "paracetamol_N", + "smiles": "O=C(N)c1ccc(O)cc1", + "n_atoms": 17, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 32.846306800842285, + "n_conformers": 2, + "lowest_crest_energy": -29.60647831, + "lowest_dft_energy": -476.22051366713754, + "crest_log_path": "workdir/paracetamol_N/gfn2/crest.log", + "ensemble_path": "workdir/paracetamol_N/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 678.7731022834778, + "n_conformers": 3, + "lowest_crest_energy": -30.04803652, + "lowest_dft_energy": -476.2210789764954, + "crest_log_path": "workdir/paracetamol_N/nnxtb/crest.log", + "ensemble_path": "workdir/paracetamol_N/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.0005653093578530388, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 2, + "n_nnxtb": 3, + "recall_gfn2_into_nnxtb": { + "0.25": 0.5, + "0.5": 0.5, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.6666666666666666, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 1, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 1, + "0.5": 1, + "1.0": 0 + } + } + }, + { + "name": "anisole", + "smiles": "COc1ccccc1", + "n_atoms": 16, + "gfn2": { + "engine": "gfn2", + "wall_seconds": 19.603131532669067, + "n_conformers": 1, + "lowest_crest_energy": -23.11319507, + "lowest_dft_energy": -346.7943890396327, + "crest_log_path": "workdir/anisole/gfn2/crest.log", + "ensemble_path": "workdir/anisole/gfn2_ensemble.xyz" + }, + "nnxtb": { + "engine": "nnxtb", + "wall_seconds": 652.470059633255, + "n_conformers": 2, + "lowest_crest_energy": -23.47137836, + "lowest_dft_energy": -346.79464481000986, + "crest_log_path": "workdir/anisole/nnxtb/crest.log", + "ensemble_path": "workdir/anisole/nnxtb_ensemble.xyz" + }, + "dft_delta_hartree": -0.00025577037717994244, + "winner": "hyper-CREST", + "recall": { + "n_gfn2": 1, + "n_nnxtb": 2, + "recall_gfn2_into_nnxtb": { + "0.25": 1.0, + "0.5": 1.0, + "1.0": 1.0 + }, + "recall_nnxtb_into_gfn2": { + "0.25": 0.5, + "0.5": 1.0, + "1.0": 1.0 + }, + "novel_in_nnxtb": { + "0.25": 1, + "0.5": 0, + "1.0": 0 + }, + "novel_in_gfn2": { + "0.25": 0, + "0.5": 0, + "1.0": 0 + } + } + } +] \ No newline at end of file diff --git a/benchmark/nnxtb/phase4_drug_set.json b/benchmark/nnxtb/phase4_drug_set.json new file mode 100644 index 00000000..aa9fef4f --- /dev/null +++ b/benchmark/nnxtb/phase4_drug_set.json @@ -0,0 +1,22 @@ +[ + {"name": "salicylic_acid", "smiles": "OC(=O)c1ccccc1O", "chrg": 0}, + {"name": "naproxen", "smiles": "COc1ccc2cc(C(C)C(=O)O)ccc2c1", "chrg": 0}, + {"name": "mefenamic_acid", "smiles": "Cc1cccc(C)c1Nc1ccccc1C(=O)O", "chrg": 0}, + {"name": "isoniazid", "smiles": "NNC(=O)c1ccncc1", "chrg": 0}, + {"name": "metformin", "smiles": "CN(C)C(=N)NC(N)=N", "chrg": 0}, + {"name": "propofol", "smiles": "CC(C)c1cccc(C(C)C)c1O", "chrg": 0}, + {"name": "theophylline", "smiles": "Cn1c(=O)c2[nH]cnc2n(C)c1=O", "chrg": 0}, + {"name": "diflunisal", "smiles": "OC(=O)c1cc(-c2ccc(F)cc2F)ccc1O", "chrg": 0}, + {"name": "vanillin", "smiles": "COc1cc(C=O)ccc1O", "chrg": 0}, + {"name": "amphetamine", "smiles": "CC(N)Cc1ccccc1", "chrg": 0}, + {"name": "guaiacol", "smiles": "COc1ccccc1O", "chrg": 0}, + {"name": "tryptamine", "smiles": "NCCc1c[nH]c2ccccc12", "chrg": 0}, + {"name": "tyramine", "smiles": "NCCc1ccc(O)cc1", "chrg": 0}, + {"name": "dopamine", "smiles": "NCCc1ccc(O)c(O)c1", "chrg": 0}, + {"name": "histamine", "smiles": "NCCc1c[nH]cn1", "chrg": 0}, + {"name": "phenylalanine", "smiles": "N[C@@H](Cc1ccccc1)C(=O)O", "chrg": 0}, + {"name": "serotonin", "smiles": "NCCc1c[nH]c2ccc(O)cc12", "chrg": 0}, + {"name": "coumarin", "smiles": "O=c1ccc2ccccc2o1", "chrg": 0}, + {"name": "paracetamol_N", "smiles": "O=C(N)c1ccc(O)cc1", "chrg": 0}, + {"name": "anisole", "smiles": "COc1ccccc1", "chrg": 0} +] diff --git a/benchmark/nnxtb/run_benchmark.py b/benchmark/nnxtb/run_benchmark.py new file mode 100644 index 00000000..69efadd3 --- /dev/null +++ b/benchmark/nnxtb/run_benchmark.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +""" +hyper-crest vs standard CREST: DFT-rescored conformer quality benchmark. + +Per the hyper-crest mission (Loong's deliverable): + (a) run standard CREST on each test molecule, keep top-N conformers + (b) run hyper-CREST (method=nnxtb) on the same molecule, keep top-N + (c) DFT single-point each conformer set at a consistent high level + (d) hyper-CREST wins if its DFT-minimum conformer is lower than + standard CREST's DFT-minimum conformer + +Outputs a table with per-molecule ΔE_DFT(min) between the two methods +and an overall winner count. + +Usage: + python run_benchmark.py \ + --standard-crest /path/to/stock/crest \ + --hyper-crest /path/to/hyper-crest \ + --mace-model /path/to/model.mxtb \ + --mol-dir molecules/ \ + --output-dir results/ \ + --top-n 5 \ + --dft b3lyp/def2-svp \ + --threads 8 + +Designed to run on a GPU node with hyper-crest + pyscf installed. Not +intended to run on a laptop (CREST sampling + DFT rescore is O(hours) +per molecule). +""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +import subprocess +import sys +import time +from dataclasses import asdict, dataclass +from pathlib import Path + +HARTREE_TO_KCAL = 627.50947428 + + +@dataclass +class ConformerResult: + method: str # "standard" | "hyper" + conformer_idx: int + crest_energy: float # kcal/mol relative to method's own best, as CREST reports + dft_energy: float # Hartree, absolute + + +@dataclass +class MoleculeResult: + name: str + n_atoms: int + standard: list[ConformerResult] + hyper: list[ConformerResult] + # Winner = method whose DFT-min conformer has the lower energy + dft_min_standard: float | None + dft_min_hyper: float | None + winner: str # "hyper" | "standard" | "tie" + delta_hartree: float # dft_min_hyper - dft_min_standard (negative → hyper wins) + + +# --------------------------------------------------------------------------- # +# CREST driving +# --------------------------------------------------------------------------- # +def run_crest_standard( + crest_bin: str, + xyz_path: Path, + workdir: Path, + extra_args: list[str], + timeout_s: int, +) -> list[Path]: + """Run standard CREST (GFN2-xTB via tblite). Returns list of conformer xyz blocks.""" + workdir.mkdir(parents=True, exist_ok=True) + shutil.copy2(xyz_path, workdir / "input.xyz") + cmd = [crest_bin, "input.xyz", "-gfn2", *extra_args] + with (workdir / "crest.log").open("w") as log: + subprocess.run(cmd, cwd=workdir, stdout=log, stderr=subprocess.STDOUT, + timeout=timeout_s, check=True) + return parse_crest_conformers(workdir / "crest_conformers.xyz") + + +def run_crest_hyper( + crest_bin: str, + xyz_path: Path, + workdir: Path, + mace_model: Path, + extra_args: list[str], + timeout_s: int, +) -> list[Path]: + """Run hyper-CREST with method=nnxtb via a generated TOML input.""" + workdir.mkdir(parents=True, exist_ok=True) + shutil.copy2(xyz_path, workdir / "input.xyz") + toml = ( + 'input = "input.xyz"\n' + 'runtype = "imtd-gc"\n\n' + '[[calculation.level]]\n' + 'method = "nnxtb"\n' + f'mace_model = "{mace_model}"\n' + 'chrg = 0\n' + ) + (workdir / "input.toml").write_text(toml) + cmd = [crest_bin, "input.toml", *extra_args] + with (workdir / "crest.log").open("w") as log: + subprocess.run(cmd, cwd=workdir, stdout=log, stderr=subprocess.STDOUT, + timeout=timeout_s, check=True) + return parse_crest_conformers(workdir / "crest_conformers.xyz") + + +def parse_crest_conformers(path: Path) -> list[tuple[float, list[str]]]: + """ + Parse a CREST ensemble xyz. Returns a list of (energy_kcal, xyz_block_lines). + CREST writes the conformer energy on the comment line (in Hartree). + """ + out: list[tuple[float, list[str]]] = [] + if not path.exists(): + return out + with path.open() as f: + lines = f.read().splitlines() + + i = 0 + while i < len(lines): + if not lines[i].strip(): + i += 1 + continue + try: + n = int(lines[i].strip()) + except ValueError: + i += 1 + continue + comment = lines[i + 1].strip() if i + 1 < len(lines) else "" + atoms = lines[i + 2 : i + 2 + n] + # Conformer energy: first token on the comment line that parses as float. + energy = None + for tok in comment.split(): + try: + energy = float(tok) + break + except ValueError: + continue + out.append((energy if energy is not None else 0.0, + [lines[i], lines[i + 1], *atoms])) + i += 2 + n + return out + + +def write_xyz_block(block_lines: list[str], path: Path) -> None: + path.write_text("\n".join(block_lines) + "\n") + + +# --------------------------------------------------------------------------- # +# DFT rescoring (pyscf) +# --------------------------------------------------------------------------- # +def dft_singlepoint(xyz_path: Path, dft_spec: str, n_threads: int) -> float: + """ + Single-point DFT on one xyz file. Returns total energy in Hartree. + dft_spec format: "/", e.g. "b3lyp/def2-svp", "wb97x-d/def2-tzvp". + """ + xc, basis = dft_spec.split("/") + # Lazy import so the CREST driving path doesn't need pyscf. + from pyscf import dft, gto # type: ignore + from pyscf.lib import num_threads # type: ignore + + num_threads(n_threads) + mol = gto.M(atom=str(xyz_path), basis=basis, unit="Angstrom") + mf = dft.RKS(mol) + mf.xc = xc + mf.verbose = 0 + return float(mf.kernel()) + + +def dft_rescore_ensemble( + conformers: list[tuple[float, list[str]]], + method_tag: str, + mol_workdir: Path, + top_n: int, + dft_spec: str, + n_threads: int, +) -> list[ConformerResult]: + """DFT-rescore the top-N conformers (by CREST energy). Returns per-conformer record.""" + # Sort by CREST energy, take top-N. + selected = sorted(conformers, key=lambda t: t[0])[:top_n] + results: list[ConformerResult] = [] + for idx, (e_crest, block) in enumerate(selected): + conf_xyz = mol_workdir / f"{method_tag}_conf{idx:02d}.xyz" + write_xyz_block(block, conf_xyz) + t0 = time.time() + e_dft = dft_singlepoint(conf_xyz, dft_spec, n_threads) + dt = time.time() - t0 + results.append(ConformerResult( + method=method_tag, conformer_idx=idx, + crest_energy=e_crest, dft_energy=e_dft)) + print(f" [{method_tag}] conf{idx:02d}: E_DFT = {e_dft:+.6f} Ha [{dt:.1f}s]", + flush=True) + return results + + +# --------------------------------------------------------------------------- # +# Orchestration +# --------------------------------------------------------------------------- # +def run_one_molecule( + xyz_path: Path, + args: argparse.Namespace, +) -> MoleculeResult: + name = xyz_path.stem + print(f"\n=== {name} ===", flush=True) + + n_atoms = int(xyz_path.read_text().splitlines()[0].strip()) + mol_work = Path(args.output_dir) / name + mol_work.mkdir(parents=True, exist_ok=True) + + # 1. Standard CREST + std_dir = mol_work / "standard" + t0 = time.time() + std_confs = run_crest_standard( + args.standard_crest, xyz_path, std_dir, + extra_args=args.crest_extra_args.split() if args.crest_extra_args else [], + timeout_s=args.crest_timeout) + print(f" standard CREST: {len(std_confs)} conformers [{time.time()-t0:.0f}s]", + flush=True) + + # 2. hyper-CREST + hyp_dir = mol_work / "hyper" + t0 = time.time() + hyp_confs = run_crest_hyper( + args.hyper_crest, xyz_path, hyp_dir, Path(args.mace_model), + extra_args=args.crest_extra_args.split() if args.crest_extra_args else [], + timeout_s=args.crest_timeout) + print(f" hyper-CREST: {len(hyp_confs)} conformers [{time.time()-t0:.0f}s]", + flush=True) + + # 3. DFT rescore + std_results = dft_rescore_ensemble( + std_confs, "standard", mol_work, args.top_n, args.dft, args.threads) + hyp_results = dft_rescore_ensemble( + hyp_confs, "hyper", mol_work, args.top_n, args.dft, args.threads) + + e_std_min = min((r.dft_energy for r in std_results), default=None) + e_hyp_min = min((r.dft_energy for r in hyp_results), default=None) + + if e_std_min is None or e_hyp_min is None: + winner = "tie" + delta = 0.0 + else: + delta = e_hyp_min - e_std_min + # tie tolerance: 0.1 kcal/mol (numerical + conformer-equivalent) + if abs(delta) < 0.1 / HARTREE_TO_KCAL: + winner = "tie" + elif delta < 0: + winner = "hyper" + else: + winner = "standard" + + res = MoleculeResult( + name=name, n_atoms=n_atoms, + standard=std_results, hyper=hyp_results, + dft_min_standard=e_std_min, dft_min_hyper=e_hyp_min, + winner=winner, delta_hartree=delta) + (mol_work / "result.json").write_text( + json.dumps(asdict(res), indent=2, default=str)) + print(f" {name}: winner={winner} ΔE = {delta*HARTREE_TO_KCAL:+.3f} kcal/mol", + flush=True) + return res + + +def print_summary(results: list[MoleculeResult]) -> None: + print("\n" + "=" * 72) + print(" Summary (ΔE = DFT-min hyper − DFT-min standard, in kcal/mol)") + print("=" * 72) + print(f" {'name':<20s} {'N':>4s} {'std min (Ha)':>13s} " + f"{'hyp min (Ha)':>13s} {'ΔE (kcal)':>10s} {'winner':<10s}") + for r in results: + dkcal = r.delta_hartree * HARTREE_TO_KCAL + std = f"{r.dft_min_standard:+.5f}" if r.dft_min_standard is not None else " N/A " + hyp = f"{r.dft_min_hyper:+.5f}" if r.dft_min_hyper is not None else " N/A " + print(f" {r.name:<20s} {r.n_atoms:>4d} {std:>13s} {hyp:>13s} " + f"{dkcal:>+10.3f} {r.winner:<10s}") + wins = sum(r.winner == "hyper" for r in results) + losses = sum(r.winner == "standard" for r in results) + ties = sum(r.winner == "tie" for r in results) + print("-" * 72) + print(f" hyper-CREST wins : {wins} / {len(results)}") + print(f" standard wins : {losses} / {len(results)}") + print(f" ties : {ties} / {len(results)}") + print("=" * 72) + + +def main() -> int: + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("--standard-crest", required=True, + help="path to the stock CREST binary") + p.add_argument("--hyper-crest", required=True, + help="path to the hyper-crest binary (this fork, built with WITH_HYPERXTB=ON)") + p.add_argument("--mace-model", required=True, + help="path to the ScaleShiftMACExTB .mxtb model") + p.add_argument("--mol-dir", required=True, + help="directory of xyz files (one molecule per file)") + p.add_argument("--output-dir", default="results", + help="workdir for per-molecule runs + result.json files") + p.add_argument("--top-n", type=int, default=5, + help="number of top-CREST-ranked conformers to DFT-rescore per method") + p.add_argument("--dft", default="b3lyp/def2-svp", + help="DFT spec 'xc/basis', e.g. 'wb97x-d/def2-tzvp'") + p.add_argument("--threads", type=int, default=os.cpu_count() or 4, + help="threads for pyscf DFT") + p.add_argument("--crest-extra-args", default="", + help="string of extra args forwarded to both crest calls") + p.add_argument("--crest-timeout", type=int, default=7200, + help="per-molecule CREST wall-time cap in seconds") + args = p.parse_args() + + mol_dir = Path(args.mol_dir) + xyz_files = sorted(mol_dir.glob("*.xyz")) + if not xyz_files: + print(f"No xyz files in {mol_dir}", file=sys.stderr) + return 1 + + Path(args.output_dir).mkdir(parents=True, exist_ok=True) + results: list[MoleculeResult] = [] + for xyz in xyz_files: + try: + results.append(run_one_molecule(xyz, args)) + except subprocess.CalledProcessError as e: + print(f" {xyz.name} FAILED during CREST: {e}", flush=True) + except subprocess.TimeoutExpired: + print(f" {xyz.name} TIMED OUT after {args.crest_timeout}s", flush=True) + except Exception as e: + print(f" {xyz.name} FAILED: {type(e).__name__}: {e}", flush=True) + + print_summary(results) + (Path(args.output_dir) / "summary.json").write_text( + json.dumps([asdict(r) for r in results], indent=2, default=str)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/benchmark/nnxtb/run_mconf_benchmark.py b/benchmark/nnxtb/run_mconf_benchmark.py new file mode 100644 index 00000000..2a2462a3 --- /dev/null +++ b/benchmark/nnxtb/run_mconf_benchmark.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +""" +MCONF benchmark — GMTKN55 melatonin conformer set. + +Runs stock GFN2 and hyper NN-xTB single-points on all 52 MCONF conformer +geometries, compares to the published ωB97X-V reference energies (as +relative energies in kcal/mol with respect to conformer 1), and reports +Spearman correlation + MAE + wall-clock time per engine. + +Unlike run_benchmark.py this does NOT run the full CREST sampling loop — +it takes the fixed published geometries and just evaluates each engine's +energy on them, then compares the predicted *ordering* and *spread* to +the reference. This directly answers "is NN-xTB more DFT-like than GFN2 +on the same geometries" without paying the cost of a full conformer +search. + +Usage: + python run_mconf_benchmark.py \ + --mconf-dir /path/to/mconf_xyzs/ \ + --ref-json /path/to/ref_energies.json \ + --stock-crest /path/to/stock/crest \ + --hyper-crest /path/to/hyper/crest \ + --mace-model /path/to/model.mxtb \ + --output-json results.json +""" + +from __future__ import annotations + +import argparse +import json +import re +import shutil +import subprocess +import sys +import time +from dataclasses import asdict, dataclass +from pathlib import Path + +HARTREE_TO_KCAL = 627.50947428 + + +@dataclass +class SPResult: + conformer: int + energy_hartree: float + wall_seconds: float + + +def singlepoint_crest( + crest_bin: str, + xyz: Path, + workdir: Path, + method: str, # "gfn2" or "nnxtb" + mace_model: Path | None, + timeout_s: int, +) -> tuple[float, float]: + """Run `crest input.toml` with runtype=singlepoint. Returns (E_Ha, wall_s).""" + workdir.mkdir(parents=True, exist_ok=True) + shutil.copy2(xyz, workdir / "input.xyz") + mace_line = f'mace_model = "{mace_model}"\n' if method == "nnxtb" else "" + toml = ( + 'input = "input.xyz"\n' + 'runtype = "singlepoint"\n\n' + '[[calculation.level]]\n' + f'method = "{method}"\n' + f'{mace_line}' + 'chrg = 0\n' + ) + (workdir / "input.toml").write_text(toml) + t0 = time.time() + proc = subprocess.run( + [crest_bin, "input.toml"], + cwd=workdir, + capture_output=True, text=True, + timeout=timeout_s, + ) + wall = time.time() - t0 + if proc.returncode != 0: + raise RuntimeError(f"crest failed (rc={proc.returncode}):\n{proc.stderr[-500:]}") + # Scan output for TOTAL ENERGY line. + m = re.search(r"TOTAL ENERGY\s+([-+]?\d+\.\d+)\s+Eh", proc.stdout) + if not m: + raise RuntimeError("no TOTAL ENERGY line in crest output") + return float(m.group(1)), wall + + +# --------------------------------------------------------------------------- # +# Correlation helpers (no scipy dependency — small self-contained Spearman) +# --------------------------------------------------------------------------- # +def _rank(xs: list[float]) -> list[float]: + n = len(xs) + idx = sorted(range(n), key=lambda i: xs[i]) + ranks = [0.0] * n + i = 0 + while i < n: + j = i + while j + 1 < n and xs[idx[j + 1]] == xs[idx[i]]: + j += 1 + avg = (i + j) / 2.0 + 1 + for k in range(i, j + 1): + ranks[idx[k]] = avg + i = j + 1 + return ranks + + +def spearman(xs: list[float], ys: list[float]) -> float: + rx, ry = _rank(xs), _rank(ys) + n = len(xs) + mx, my = sum(rx) / n, sum(ry) / n + num = sum((rx[i] - mx) * (ry[i] - my) for i in range(n)) + dx = (sum((v - mx) ** 2 for v in rx)) ** 0.5 + dy = (sum((v - my) ** 2 for v in ry)) ** 0.5 + return num / (dx * dy) if dx and dy else float("nan") + + +def mae(xs: list[float], ys: list[float]) -> float: + return sum(abs(a - b) for a, b in zip(xs, ys)) / len(xs) + + +# --------------------------------------------------------------------------- # +# Orchestration +# --------------------------------------------------------------------------- # +def run_for_method( + method: str, + crest_bin: str, + mace_model: Path | None, + xyz_files: list[Path], + workroot: Path, + timeout_s: int, +) -> tuple[list[SPResult], float]: + """Run single-point for every conformer. Returns (per-conformer results, total wall).""" + results: list[SPResult] = [] + t_total = time.time() + for xyz in xyz_files: + conf = int(re.search(r"(\d+)", xyz.stem).group(1)) + wd = workroot / method / f"conf_{conf:02d}" + try: + E, w = singlepoint_crest(crest_bin, xyz, wd, method, mace_model, timeout_s) + results.append(SPResult(conformer=conf, energy_hartree=E, wall_seconds=w)) + print(f" [{method}] conf {conf:2d}: E = {E:+.6f} Ha ({w:.2f} s)", flush=True) + except Exception as e: + print(f" [{method}] conf {conf:2d}: FAILED — {e}", flush=True) + total = time.time() - t_total + return results, total + + +def compute_metrics( + method_results: list[SPResult], + ref: dict[int, float], +) -> dict: + """Convert absolute energies → kcal/mol relative to conformer 1, then compare to ref.""" + by_conf = {r.conformer: r.energy_hartree for r in method_results} + if 1 not in by_conf: + return {"error": "conformer 1 missing", "rho": None, "mae_kcal": None} + e1 = by_conf[1] + + pred_rel, ref_rel, confs = [], [], [] + for c in sorted(by_conf): + if c in ref: + pred_rel.append((by_conf[c] - e1) * HARTREE_TO_KCAL) + ref_rel.append(ref[c]) + confs.append(c) + return { + "n": len(confs), + "spearman": spearman(pred_rel, ref_rel), + "mae_kcal": mae(pred_rel, ref_rel), + "rms_kcal": (sum((a - b) ** 2 for a, b in zip(pred_rel, ref_rel)) / len(pred_rel)) ** 0.5, + "pred_rel_kcal": dict(zip(confs, pred_rel)), + "ref_rel_kcal": dict(zip(confs, ref_rel)), + } + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--mconf-dir", required=True) + p.add_argument("--ref-json", required=True) + p.add_argument("--stock-crest", required=True) + p.add_argument("--hyper-crest", required=True) + p.add_argument("--mace-model", required=True) + p.add_argument("--output-json", default="mconf_results.json") + p.add_argument("--workdir", default="mconf_workdir") + p.add_argument("--timeout", type=int, default=600) + args = p.parse_args() + + mconf_dir = Path(args.mconf_dir) + xyz_files = sorted(mconf_dir.glob("conf_*.xyz"), + key=lambda p: int(re.search(r"(\d+)", p.stem).group(1))) + if not xyz_files: + print(f"no conf_*.xyz under {mconf_dir}", file=sys.stderr) + return 1 + ref = {int(k): float(v) for k, v in json.load(open(args.ref_json)).items()} + + workroot = Path(args.workdir) + workroot.mkdir(parents=True, exist_ok=True) + + print(f"MCONF benchmark: {len(xyz_files)} conformers, ref level = GMTKN55 published") + print(f" stock crest: {args.stock_crest}") + print(f" hyper crest: {args.hyper_crest}") + print(f" MACE model: {args.mace_model}") + print() + + print("=== stock CREST (method=gfn2 via tblite) ===", flush=True) + gfn2_results, gfn2_wall = run_for_method( + "gfn2", args.stock_crest, None, xyz_files, workroot, args.timeout) + + print("\n=== hyper-CREST (method=nnxtb, CUDA backend) ===", flush=True) + nnxtb_results, nnxtb_wall = run_for_method( + "nnxtb", args.hyper_crest, Path(args.mace_model), + xyz_files, workroot, args.timeout) + + m_gfn2 = compute_metrics(gfn2_results, ref) + m_nn = compute_metrics(nnxtb_results, ref) + + n_gfn2 = len(gfn2_results) or 1 + n_nn = len(nnxtb_results) or 1 + + print("\n" + "=" * 72) + print(f" MCONF results (n = {m_gfn2.get('n', 0)} conformers, reference = ωB97X-V / def2-QZVP)") + print("=" * 72) + print(f" {'Method':<22s} {'rho':>7s} {'MAE(kcal)':>10s} {'RMS(kcal)':>10s} {'wall(s)':>9s} {'ms/SP':>7s}") + for label, m, wall, n in [ + ("stock CREST (GFN2)", m_gfn2, gfn2_wall, n_gfn2), + ("hyper-CREST (NN-xTB)", m_nn, nnxtb_wall, n_nn), + ]: + rho = m.get("spearman") + mk = m.get("mae_kcal") + rk = m.get("rms_kcal") + print(f" {label:<22s} " + f"{('—' if rho is None else f'{rho:+.3f}'):>7s} " + f"{('—' if mk is None else f'{mk:.3f}'):>10s} " + f"{('—' if rk is None else f'{rk:.3f}'):>10s} " + f"{wall:>8.1f} " + f"{1000*wall/n:>6.1f}") + print("=" * 72) + + out = { + "ref_level": "GMTKN55 MCONF published reference (relative kcal/mol)", + "n_conformers": len(xyz_files), + "gfn2": { + "per_conformer": [asdict(r) for r in gfn2_results], + "metrics": m_gfn2, + "total_wall_s": gfn2_wall, + }, + "nnxtb": { + "per_conformer": [asdict(r) for r in nnxtb_results], + "metrics": m_nn, + "total_wall_s": nnxtb_wall, + }, + } + Path(args.output_json).write_text(json.dumps(out, indent=2, default=str)) + print(f"\nDetailed results: {args.output_json}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/benchmark/nnxtb/run_smiles_benchmark.py b/benchmark/nnxtb/run_smiles_benchmark.py new file mode 100644 index 00000000..ca999941 --- /dev/null +++ b/benchmark/nnxtb/run_smiles_benchmark.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 +""" +Phase-3 benchmark — the one Loong actually wants. + +From SMILES only (no reference geometries): + 1. RDKit.Embed → random 3D starting structure per SMILES + 2. stock CREST (method=gfn2) → full iMTD-GC → lowest-energy conformer + 3. hyper-CREST (method=nnxtb) → full iMTD-GC → lowest-energy conformer + 4. DFT single-point on BOTH lowest conformers (they may be different structures) + 5. tally: for how many SMILES does hyper-CREST's lowest conformer have a + lower DFT energy than stock CREST's lowest? that's the win rate. + +Unlike the MCONF single-point benchmark, this measures the *sampling* quality +of each engine — can hyper-CREST find geometries that stock GFN2 missed? + +Usage: + python run_smiles_benchmark.py \ + --molecules drug_set.json \ + --stock-crest /path/to/crest \ + --hyper-crest /path/to/crest \ + --mace-model /path/to/model.mxtb \ + --output-json phase3_results.json \ + --workdir phase3_workdir \ + --dft b3lyp/def2-svp \ + --crest-extra "--quick" + +drug_set.json format: [{"name": "aspirin", "smiles": "CC(=O)Oc1ccccc1C(=O)O", "chrg": 0}, ...] +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +import time +from dataclasses import asdict, dataclass, field +from pathlib import Path + +HARTREE_TO_KCAL = 627.50947428 + + +@dataclass +class EngineRun: + engine: str # "gfn2" | "nnxtb" + wall_seconds: float = 0.0 + n_conformers: int = 0 + lowest_crest_energy: float | None = None # Ha, CREST's own energy + lowest_dft_energy: float | None = None # Ha, DFT rescore of the lowest-CREST geometry + crest_log_path: str | None = None + ensemble_path: str | None = None # copy of crest_conformers.xyz + + +@dataclass +class RecallMetrics: + """Ensemble-overlap metrics between two CREST runs, at multiple RMSD thresholds.""" + n_gfn2: int = 0 + n_nnxtb: int = 0 + # For each RMSD threshold, fraction of the OTHER engine's conformers matched. + recall_gfn2_into_nnxtb: dict = field(default_factory=dict) # thr -> fraction of gfn2 confs matched by some nnxtb conf + recall_nnxtb_into_gfn2: dict = field(default_factory=dict) + # Novel = engine's conformers that the other engine didn't find (at a given thr) + novel_in_nnxtb: dict = field(default_factory=dict) + novel_in_gfn2: dict = field(default_factory=dict) + + +@dataclass +class MoleculeResult: + name: str + smiles: str + n_atoms: int = 0 + gfn2: EngineRun = field(default_factory=lambda: EngineRun(engine="gfn2")) + nnxtb: EngineRun = field(default_factory=lambda: EngineRun(engine="nnxtb")) + dft_delta_hartree: float | None = None # nnxtb − gfn2 + winner: str = "tie" + recall: RecallMetrics = field(default_factory=RecallMetrics) + + +# --------------------------------------------------------------------------- # +# SMILES → 3D starting geometry +# --------------------------------------------------------------------------- # +def smiles_to_xyz(smiles: str, out_xyz: Path, seed: int = 42) -> int: + from rdkit import Chem + from rdkit.Chem import AllChem + mol = Chem.MolFromSmiles(smiles) + if mol is None: + raise RuntimeError(f"invalid SMILES: {smiles}") + mol = Chem.AddHs(mol) + AllChem.EmbedMolecule(mol, randomSeed=seed) + AllChem.MMFFOptimizeMolecule(mol, maxIters=2000) + conf = mol.GetConformer() + lines = [str(mol.GetNumAtoms()), smiles] + for atom in mol.GetAtoms(): + p = conf.GetAtomPosition(atom.GetIdx()) + lines.append(f"{atom.GetSymbol()} {p.x:.6f} {p.y:.6f} {p.z:.6f}") + out_xyz.write_text("\n".join(lines) + "\n") + return mol.GetNumAtoms() + + +# --------------------------------------------------------------------------- # +# Run CREST sampling + parse output +# --------------------------------------------------------------------------- # +def run_crest( + crest_bin: str, + xyz_in: Path, + workdir: Path, + method: str, + mace_model: Path | None, + chrg: int, + extra_args: list[str], + timeout_s: int, +) -> tuple[float, float | None, int]: + """Returns (wall_seconds, lowest_crest_energy_Ha, n_conformers).""" + workdir.mkdir(parents=True, exist_ok=True) + shutil.copy2(xyz_in, workdir / "input.xyz") + mace_line = f'mace_model = "{mace_model}"\n' if method == "nnxtb" else "" + toml = ( + 'input = "input.xyz"\n' + 'runtype = "imtd-gc"\n' + f'threads = 8\n\n' + '[[calculation.level]]\n' + f'method = "{method}"\n' + f'{mace_line}' + f'chrg = {chrg}\n' + ) + (workdir / "input.toml").write_text(toml) + t0 = time.time() + with (workdir / "crest.log").open("w") as log: + subprocess.run( + [crest_bin, "input.toml"] + extra_args, + cwd=workdir, + stdout=log, stderr=subprocess.STDOUT, + timeout=timeout_s, + check=True, + ) + wall = time.time() - t0 + + # Parse crest_conformers.xyz: first conformer is the lowest by CREST energy. + confs = workdir / "crest_conformers.xyz" + if not confs.exists(): + return wall, None, 0 + lines = confs.read_text().splitlines() + if len(lines) < 3: + return wall, None, 0 + n_atoms = int(lines[0].strip()) + n_conf = len(lines) // (n_atoms + 2) + # comment line of first block contains the CREST energy in Hartree + comment = lines[1].strip() + e_low = None + for tok in comment.split(): + try: + e_low = float(tok) + break + except ValueError: + pass + return wall, e_low, n_conf + + +def pick_best_xyz(workdir: Path, out: Path) -> None: + """crest_best.xyz contains the lowest-energy conformer; copy it out.""" + best = workdir / "crest_best.xyz" + if not best.exists(): + confs = workdir / "crest_conformers.xyz" + if not confs.exists(): + raise RuntimeError("no crest_best or crest_conformers xyz") + lines = confs.read_text().splitlines() + n_atoms = int(lines[0].strip()) + out.write_text("\n".join(lines[:n_atoms + 2]) + "\n") + else: + shutil.copy2(best, out) + + +# --------------------------------------------------------------------------- # +# Ensemble overlap (RMSD-based recall) +# --------------------------------------------------------------------------- # +def _kabsch_rmsd(a: "np.ndarray", b: "np.ndarray") -> float: + """Heavy-atom RMSD between two (N, 3) coord sets after optimal rotation + translation.""" + import numpy as np + a = a - a.mean(0) + b = b - b.mean(0) + h = a.T @ b + u, _, vt = np.linalg.svd(h) + d = np.sign(np.linalg.det(vt.T @ u.T)) + r = vt.T @ np.diag([1.0, 1.0, d]) @ u.T + a_rot = a @ r.T + return float(np.sqrt(((a_rot - b) ** 2).sum() / a.shape[0])) + + +def _parse_ensemble(xyz_path: Path) -> list["np.ndarray"]: + """Parse a multi-frame xyz file. Returns list of heavy-atom coord arrays (N_heavy, 3).""" + import numpy as np + if not xyz_path or not xyz_path.exists(): + return [] + lines = xyz_path.read_text().splitlines() + if not lines: + return [] + n_atoms = int(lines[0].strip()) + frames = [] + stride = n_atoms + 2 + for i in range(0, len(lines), stride): + if i + stride > len(lines): + break + coords = [] + for L in lines[i + 2 : i + stride]: + t = L.split() + if len(t) < 4 or t[0].lower() == "h": + continue + coords.append([float(t[1]), float(t[2]), float(t[3])]) + if coords: + frames.append(np.asarray(coords)) + return frames + + +def compute_recall(gfn2_xyz: Path | None, nnxtb_xyz: Path | None, + thresholds=(0.25, 0.5, 1.0)) -> RecallMetrics: + """RMSD-match every conformer from each engine against the other engine's ensemble. + Symmetric: at each threshold, report fraction of X's conformers matched by some Y.""" + rec = RecallMetrics() + g = _parse_ensemble(gfn2_xyz) if gfn2_xyz else [] + n = _parse_ensemble(nnxtb_xyz) if nnxtb_xyz else [] + rec.n_gfn2 = len(g) + rec.n_nnxtb = len(n) + if not g or not n: + return rec + # Sanity: both ensembles must share atom count + if g[0].shape != n[0].shape: + return rec + # Pairwise RMSD matrix. For typical n = 50 x 50, this is trivial. + import numpy as np + rmsd = np.zeros((len(g), len(n))) + for i, ga in enumerate(g): + for j, nb in enumerate(n): + rmsd[i, j] = _kabsch_rmsd(ga, nb) + for thr in thresholds: + matched_g = (rmsd.min(axis=1) < thr).sum() + matched_n = (rmsd.min(axis=0) < thr).sum() + # "recall of X into Y" = fraction of X that has at least one match in Y + rec.recall_gfn2_into_nnxtb[f"{thr}"] = float(matched_g / len(g)) + rec.recall_nnxtb_into_gfn2[f"{thr}"] = float(matched_n / len(n)) + rec.novel_in_gfn2[f"{thr}"] = int(len(g) - matched_g) + rec.novel_in_nnxtb[f"{thr}"] = int(len(n) - matched_n) + return rec + + +# --------------------------------------------------------------------------- # +# DFT rescore +# --------------------------------------------------------------------------- # +def dft_singlepoint(xyz: Path, xc_basis: str, n_threads: int) -> float: + from pyscf import dft, gto + from pyscf.lib import num_threads + num_threads(n_threads) + xc, basis = xc_basis.split("/") + lines = xyz.read_text().splitlines() + n_atoms = int(lines[0].strip()) + atom_block = "\n".join(lines[2:2 + n_atoms]) + mol = gto.M(atom=atom_block, basis=basis, unit="Angstrom", verbose=0) + mf = dft.RKS(mol); mf.xc = xc; mf.verbose = 0 + return float(mf.kernel()) + + +# --------------------------------------------------------------------------- # +# Main per-molecule driver +# --------------------------------------------------------------------------- # +def run_one(mol_spec: dict, args: argparse.Namespace) -> MoleculeResult: + name = mol_spec["name"] + smiles = mol_spec["smiles"] + chrg = int(mol_spec.get("chrg", 0)) + + print(f"\n=== {name} ({smiles}) chrg={chrg} ===", flush=True) + mol_dir = Path(args.workdir) / name + mol_dir.mkdir(parents=True, exist_ok=True) + + seed_xyz = mol_dir / "seed.xyz" + n_atoms = smiles_to_xyz(smiles, seed_xyz, seed=args.seed) + print(f" seed embed: {n_atoms} atoms → {seed_xyz.name}", flush=True) + + extra = args.crest_extra.split() if args.crest_extra else [] + + result = MoleculeResult(name=name, smiles=smiles, n_atoms=n_atoms) + # run each engine in series (they're single-process CREST runs → each + # sees its own MACE load-once path). + for engine, crest_bin, record in [ + ("gfn2", args.stock_crest, result.gfn2), + ("nnxtb", args.hyper_crest, result.nnxtb), + ]: + rundir = mol_dir / engine + try: + wall, e_low, n_conf = run_crest( + crest_bin, seed_xyz, rundir, engine, + Path(args.mace_model) if engine == "nnxtb" else None, + chrg, extra, args.timeout, + ) + record.wall_seconds = wall + record.lowest_crest_energy = e_low + record.n_conformers = n_conf + record.crest_log_path = str(rundir / "crest.log") + # Save a copy of the full ensemble for post-processing recall. + ens_src = rundir / "crest_conformers.xyz" + ens_dst = mol_dir / f"{engine}_ensemble.xyz" + if ens_src.exists(): + shutil.copy2(ens_src, ens_dst) + record.ensemble_path = str(ens_dst) + print(f" [{engine}] sampled {n_conf} conformers, " + f"lowest CREST E = {e_low:+.6f} Ha, wall = {wall:.0f} s", flush=True) + except subprocess.CalledProcessError as e: + print(f" [{engine}] CREST failed (rc={e.returncode})", flush=True) + continue + except subprocess.TimeoutExpired: + print(f" [{engine}] CREST timed out after {args.timeout}s", flush=True) + continue + + # DFT single-point on the lowest-energy geometry + best = mol_dir / f"{engine}_best.xyz" + pick_best_xyz(rundir, best) + t0 = time.time() + try: + e_dft = dft_singlepoint(best, args.dft, args.threads) + record.lowest_dft_energy = e_dft + print(f" [{engine}] DFT({args.dft}) on lowest: E = {e_dft:+.6f} Ha " + f"({time.time()-t0:.0f} s)", flush=True) + except Exception as e: + print(f" [{engine}] DFT rescore failed: {e}", flush=True) + + # Recall (ensemble overlap between the two engines by heavy-atom RMSD) + result.recall = compute_recall( + Path(result.gfn2.ensemble_path) if result.gfn2.ensemble_path else None, + Path(result.nnxtb.ensemble_path) if result.nnxtb.ensemble_path else None, + ) + rg = result.recall + if rg.n_gfn2 and rg.n_nnxtb: + print(f" recall (heavy-atom RMSD, thr=0.5Å): " + f"gfn2→nnxtb {rg.recall_gfn2_into_nnxtb.get('0.5', 0):.2f} " + f"nnxtb→gfn2 {rg.recall_nnxtb_into_gfn2.get('0.5', 0):.2f} " + f"novel_nnxtb={rg.novel_in_nnxtb.get('0.5', 0)} " + f"novel_gfn2={rg.novel_in_gfn2.get('0.5', 0)}", flush=True) + + # Decide winner + eg = result.gfn2.lowest_dft_energy + en = result.nnxtb.lowest_dft_energy + if eg is not None and en is not None: + d = (en - eg) * HARTREE_TO_KCAL + result.dft_delta_hartree = en - eg + if abs(d) < 0.05: + result.winner = "tie" + elif d < 0: + result.winner = "hyper-CREST" + else: + result.winner = "stock CREST" + print(f" {name}: ΔE(nnxtb − gfn2) = {d:+.3f} kcal/mol → winner: {result.winner}", + flush=True) + return result + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--molecules", required=True, help="json list of {name, smiles, chrg}") + p.add_argument("--stock-crest", required=True) + p.add_argument("--hyper-crest", required=True) + p.add_argument("--mace-model", required=True) + p.add_argument("--output-json", default="phase3_results.json") + p.add_argument("--workdir", default="phase3_workdir") + p.add_argument("--dft", default="b3lyp/def2-svp") + p.add_argument("--threads", type=int, default=os.cpu_count() or 8) + p.add_argument("--seed", type=int, default=42) + p.add_argument("--timeout", type=int, default=7200) + p.add_argument("--crest-extra", default="") + args = p.parse_args() + + mols = json.load(open(args.molecules)) + Path(args.workdir).mkdir(parents=True, exist_ok=True) + + print(f"Phase-3: {len(mols)} molecules, stock={args.stock_crest.split('/')[-2]}, " + f"hyper-nnxtb={args.mace_model.split('/')[-1]}, DFT={args.dft}") + + results: list[MoleculeResult] = [] + for mol in mols: + try: + results.append(run_one(mol, args)) + except Exception as e: + print(f" {mol['name']} FAILED: {type(e).__name__}: {e}", flush=True) + + print("\n" + "=" * 78) + print(f" Phase-3 — from-SMILES sampling + DFT rescore ({args.dft})") + print("=" * 78) + print(f" {'name':<20s} {'natoms':>6s} {'n_gfn2':>6s} {'n_nn':>5s} " + f"{'g→n@0.5':>8s} {'n→g@0.5':>8s} " + f"{'ΔDFT(kcal)':>10s} {'winner':<12s}") + hw = sw = ti = failed = 0 + for r in results: + if r.dft_delta_hartree is None: + dk = " fail "; failed += 1 + else: + dk = f"{r.dft_delta_hartree * HARTREE_TO_KCAL:+10.3f}" + if r.winner == "hyper-CREST": hw += 1 + elif r.winner == "stock CREST": sw += 1 + else: ti += 1 + rg_str = (f"{r.recall.recall_gfn2_into_nnxtb.get('0.5', 0):.2f}" + if r.recall.n_gfn2 else "—") + ng_str = (f"{r.recall.recall_nnxtb_into_gfn2.get('0.5', 0):.2f}" + if r.recall.n_nnxtb else "—") + print(f" {r.name:<20s} {r.n_atoms:>6d} " + f"{r.recall.n_gfn2:>6d} {r.recall.n_nnxtb:>5d} " + f"{rg_str:>8s} {ng_str:>8s} " + f"{dk:>10s} {r.winner:<12s}") + print("-" * 78) + print(f" hyper-CREST wins: {hw}/{len(results)} | stock: {sw} | ties: {ti} | failed: {failed}") + print("=" * 78) + + Path(args.output_json).write_text( + json.dumps([asdict(r) for r in results], indent=2, default=str)) + print(f"\nDetailed results: {args.output_json}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())