|
47 | 47 | _CALCULATORS: dict[str, tuple] = {} |
48 | 48 |
|
49 | 49 | #: Levels matverse knows how to build, if their backend happens to be installed. |
50 | | -BUILTIN_LEVELS = ("emt", "lj", "mace-mpa", "mace-omat", "sevennet", "chgnet") |
| 50 | +BUILTIN_LEVELS = ("emt", "lj", "mace-mpa", "mace-omat", "sevennet", "chgnet", |
| 51 | + "m3gnet", "m3gnet-r2scan", "tensornet", "orb", |
| 52 | + "gpaw-pbe", "gpaw-pbe-fast") |
51 | 53 |
|
52 | 54 |
|
53 | 55 | @register_function( |
@@ -122,6 +124,86 @@ def _builtin(name: str): |
122 | 124 | "surrogate": True, "license": "BSD-3-Clause", "uncertainty": None, |
123 | 125 | "note": "Superseded on Matbench Discovery by OMat24-trained models; " |
124 | 126 | "kept because a great deal of published work used it."}) |
| 127 | + if name in ("m3gnet", "m3gnet-r2scan", "tensornet"): |
| 128 | + import matgl |
| 129 | + from matgl.ext.ase import PESCalculator |
| 130 | + checkpoint, functional = { |
| 131 | + "m3gnet": ("M3GNet-PES-MatPES-PBE-2025.2", "PBE"), |
| 132 | + "m3gnet-r2scan": ("M3GNet-PES-MatPES-r2SCAN-2025.2", "r2SCAN"), |
| 133 | + "tensornet": ("TensorNet-PES-MatPES-PBE-2025.2", "PBE"), |
| 134 | + }[name] |
| 135 | + # stress_unit is not optional. matgl returns stress in GPa by |
| 136 | + # default while ASE's contract is eV/A^3, so leaving it alone makes |
| 137 | + # every stress-derived quantity - elastic constants, bulk modulus, |
| 138 | + # pressure - exactly 160.2x too large, with no error anywhere. |
| 139 | + return (lambda: PESCalculator(matgl.load_model(checkpoint), |
| 140 | + stress_unit="eV/A3"), |
| 141 | + {"kind": "mlip", "method": checkpoint, |
| 142 | + "reference": f"{functional} (MatPES)", "surrogate": True, |
| 143 | + "license": "BSD-3-Clause", "uncertainty": None, |
| 144 | + "note": "MatPES-trained, so it reproduces plain " |
| 145 | + f"{functional} rather than the PBE+U of the MP-" |
| 146 | + "trained generation. Which functional a surrogate " |
| 147 | + "was fitted to is not a detail: mixing a PBE+U " |
| 148 | + "surrogate with a PBE one is the same class of error " |
| 149 | + "as mixing PBE with HSE06, and it is why " |
| 150 | + "reference= is recorded on every level."}) |
| 151 | + if name == "orb": |
| 152 | + from orb_models.forcefield import pretrained |
| 153 | + from orb_models.forcefield.calculator import ORBCalculator |
| 154 | + return (lambda: ORBCalculator( |
| 155 | + pretrained.orb_v3_conservative_inf_omat(device="cpu"), |
| 156 | + device="cpu"), |
| 157 | + {"kind": "mlip", "method": "ORB v3 conservative", |
| 158 | + "reference": "PBE+U (OMat24)", "surrogate": True, |
| 159 | + "license": "Apache-2.0", "uncertainty": None, |
| 160 | + "note": "Conservative variant: forces are the energy gradient, " |
| 161 | + "which is what a phonon or a relaxation needs. The " |
| 162 | + "'direct' variants predict forces independently and are " |
| 163 | + "faster and not usable for either."}) |
| 164 | + if name in ("gpaw-pbe", "gpaw-pbe-fast"): |
| 165 | + from gpaw import GPAW, PW |
| 166 | + # Not a surrogate. This is the one level here that solves the |
| 167 | + # Kohn-Sham equations rather than reproducing something that did, and |
| 168 | + # kind/surrogate say so — every downstream record inherits it. |
| 169 | + cutoff, mesh = ((500.0, (8, 8, 8)) if name == "gpaw-pbe" |
| 170 | + else (400.0, (6, 6, 6))) |
| 171 | + return (lambda: GPAW(mode=PW(cutoff), xc="PBE", kpts=mesh, |
| 172 | + txt=None, symmetry={"point_group": False}), |
| 173 | + {"kind": "dft", "method": f"GPAW PBE, PW({cutoff:g} eV)", |
| 174 | + "reference": "PBE", "surrogate": False, |
| 175 | + "license": "GPL-3.0", "uncertainty": None, |
| 176 | + "plane_wave_cutoff_eV": cutoff, |
| 177 | + "kpoint_mesh": list(mesh), |
| 178 | + "note": "Real plane-wave DFT, not a model of it. The cutoff " |
| 179 | + "and the k-point mesh are the two settings that " |
| 180 | + "decide whether a number is converged, so they are " |
| 181 | + "recorded on the level rather than left implicit.\n\n" |
| 182 | + "The mesh is **fixed** rather than set by a k-point " |
| 183 | + "density, and that is not a stylistic choice. A " |
| 184 | + "density-based mesh changes discretely as a cell " |
| 185 | + "changes size, which puts a step in E(V) and " |
| 186 | + "destroys anything fitted to it. Silicon, same " |
| 187 | + "calculator, only the k-points varying:\n\n" |
| 188 | + " density 2.0 -> -879 GPa (125 -> 64 k-points)\n" |
| 189 | + " density 2.5 -> 319 GPa (216 -> 125)\n" |
| 190 | + " density 3.0 -> 125 GPa (343 -> 216)\n" |
| 191 | + " density 4.0 -> 85.7 GPa (729 -> 512)\n" |
| 192 | + " fixed 8x8x8 -> 88.7 GPa (unchanged)\n\n" |
| 193 | + "against a PBE literature 88-89. A negative bulk " |
| 194 | + "modulus is not a soft crystal, it is a " |
| 195 | + "discontinuity. Raising the density only shrinks the " |
| 196 | + "relative size of the jump; it never removes it. The " |
| 197 | + "plane-wave cutoff, by contrast, was already " |
| 198 | + "converged - PW(400) and PW(600) differ by 0.1 GPa - " |
| 199 | + "so the whole error was the mesh.\n\n" |
| 200 | + "A fixed mesh suits small cells. Register your own " |
| 201 | + "level for anything large, where 8x8x8 is wasteful, " |
| 202 | + "and keep it fixed across any volume scan.\n\n" |
| 203 | + "point_group symmetry is off because matverse hands " |
| 204 | + "in displaced and strained cells whose symmetry is " |
| 205 | + "lower than the analyser infers from a rounded " |
| 206 | + "geometry."}) |
125 | 207 | raise KeyError( |
126 | 208 | f"unknown level {name!r}. Runnable here: " |
127 | 209 | f"{sorted(available(check_imports=False))}. Register your own with " |
|
0 commit comments