Loading...
Searching...
No Matches
MetatomicPotential Class Reference

A potential class that uses a metatomic model for energy and force calculations. More...

#include <MetatomicPotential.h>

Inheritance diagram for MetatomicPotential:

Public Member Functions

 MetatomicPotential (const Parameters &params)
 Constructor for the MetatomicPotential.
 ~MetatomicPotential () override=default
 Destructor.
void force (long nAtoms, const double *positions, const int *atomicNrs, double *forces, double *energy, double *variance, const double *box) override
 Calculates the energy and forces for a given atomic configuration.
bool isThreadSafe () const noexcept override
 Single shared instance, serialized via mutex.
bool needsPerImageInstance () const noexcept override
 Whether NEB should create separate Potential instances per image for true parallel force evaluation.
bool supportsBatchEvaluation () const noexcept override
 Batched evaluation via single shared instance.
void forceBatch (long nSystems, long nAtoms, const double *const *positions, const int *const *atomicNrs, double *const *forces, double *energies, double *variances, const double *const *boxes) override
 Evaluate forces for N systems in a single call.
Public Member Functions inherited from eonc::Potential
 Potential (PotType a_ptype)
 Potential (PotType a_ptype, const Parameters &)
 Potential (const Parameters &a_params)
virtual ~Potential ()
std::tuple< double, AtomMatrixget_ef (const AtomMatrix &pos, const VectorXi &atmnrs, const Matrix3d &box)
PotType getType () const
virtual bool isSurrogate () const noexcept
 Whether this is a surrogate (GP) potential.
virtual bool requiresIsolatedMoleculeLayout () const noexcept
 True for molecular QM / non-PBC backends (NWChem socket, ASE ORCA/NWChem, …).
virtual bool isSharedInstanceThreadSafe () const noexcept
 Conservative gate for sharing one Potential instance across threads.

Private Member Functions

metatensor_torch::TensorBlock computeNeighbors (metatomic_torch::NeighborListOptions request, long nAtoms, const double *positions, const double *box, const bool periodic[3])
 Computes neighbor list using the vesin library.

Private Attributes

eonc::log::Scoped m_log
Parameters::metatomic_options_t m_metatomic_opts
metatensor_torch::Module model_
metatomic_torch::ModelCapabilities capabilities_
std::vector< metatomic_torch::NeighborListOptions > nl_requests_
metatomic_torch::ModelEvaluationOptions evaluations_options_
torch::ScalarType dtype_
c10::DeviceType device_type_
torch::Device device_
bool check_consistency_
std::string energy_key_
std::string energy_uncertainty_key_
std::string nc_forces_key_
bool non_conservative_ {false}
bool random_rotation_ {false}
long n_symmetry_rotations_ {0}
double uncertainty_threshold_ {-1.0}
std::mutex inference_mutex_

Additional Inherited Members

Public Attributes inherited from eonc::Potential
std::atomic< size_t > forceCallCounter
Protected Attributes inherited from eonc::Potential
PotType ptype

Detailed Description

A potential class that uses a metatomic model for energy and force calculations.

This class loads a pre-trained atomistic model (via metatomic/PyTorch) and uses it to compute the potential energy and corresponding forces on atoms. It uses the vesin library to compute neighbor lists required by the model.

Definition at line 53 of file MetatomicPotential.h.

Constructor & Destructor Documentation

◆ MetatomicPotential()

MetatomicPotential::MetatomicPotential ( const Parameters & params)

Constructor for the MetatomicPotential.

Parameters
paramsA shared pointer to the simulation parameters object. This object should contain the settings needed for the metatomic model.

Definition at line 58 of file MetatomicPotential.cpp.

61 model_(torch::jit::Module()),
62 device_type_(c10::DeviceType::CPU),
63 device_(torch::Device(device_type_)) {
64
65 // Determinism knobs (see
66 // https://rgoswami.me/snippets/pytorch-deterministic-regression/): JIT
67 // profiling specializes graphs after the first few forwards, so a fresh model
68 // instance can differ at ULP level from a warm one — bad for parallel NEB.
69 // cuBLAS / index_add_ on CUDA are likewise nondeterministic unless forced.
70 // [Metatomic] deterministic=true (default) applies the safe defaults;
71 // deterministic_strict=true requires CUBLAS_WORKSPACE_CONFIG (e.g. :4096:8)
72 // and fails on nondeterministic ops instead of warning.
73 if (m_metatomic_opts.deterministic) {
74 torch::jit::getProfilingMode() = false;
75 const bool strict = m_metatomic_opts.deterministic_strict ||
76 (std::getenv("CUBLAS_WORKSPACE_CONFIG") != nullptr);
77 at::globalContext().setDeterministicAlgorithms(true,
78 /*warn_only=*/!strict);
79 at::globalContext().setBenchmarkCuDNN(false);
80 QUILL_LOG_INFO(m_log,
81 "[MetatomicPotential] Deterministic algorithms enabled "
82 "(strict={})",
83 strict);
84 } else {
85 QUILL_LOG_INFO(m_log,
86 "[MetatomicPotential] Deterministic algorithms disabled "
87 "(faster, may diverge across runs / NEB images)");
88 }
89
90 eonc::FPEHandler fpeh;
91 fpeh.eat_fpe();
92
93 QUILL_LOG_INFO(m_log, "[MetatomicPotential] Initializing...");
94
95 // 1. Load the model from the path specified in parameters
96 torch::optional<std::string> extensions_directory = torch::nullopt;
97 if (!m_metatomic_opts.extensions_directory.empty()) {
98 extensions_directory = m_metatomic_opts.extensions_directory;
99 }
100
101 try {
102 QUILL_LOG_INFO(m_log, "[MetatomicPotential] Loading model from '{}'",
103 m_metatomic_opts.model_path);
104 this->model_ = metatomic_torch::load_atomistic_model(
105 m_metatomic_opts.model_path, extensions_directory);
106 } catch (const std::exception &e) {
107 QUILL_LOG_ERROR(m_log, "[MetatomicPotential] Failed to load model: {}",
108 e.what());
109 throw;
110 }
111
112 // 2. Extract capabilities and neighbor list requests from the model
113 this->capabilities_ =
114 this->model_.run_method("capabilities")
115 .toCustomClass<metatomic_torch::ModelCapabilitiesHolder>();
116 auto requests_ivalue = this->model_.run_method("requested_neighbor_lists");
117 for (const auto &request_ivalue : requests_ivalue.toList()) {
118 auto request =
119 request_ivalue.get()
120 .toCustomClass<metatomic_torch::NeighborListOptionsHolder>();
121 this->nl_requests_.push_back(request);
122 }
123
124 // 3. Determine and set up the device (CPU/CUDA/MPS)
125 torch::optional<std::string> desired = torch::nullopt;
126 if (!m_metatomic_opts.device.empty()) {
127 desired = m_metatomic_opts.device;
128 }
129 device_type_ = metatomic_torch::pick_device(
130 this->capabilities_->supported_devices, desired);
131
132 device_ = torch::Device(device_type_);
133 QUILL_LOG_INFO(m_log, "[MetatomicPotential] Using device: {}", device_.str());
134
135 move_atomistic_model(this->model_, this->device_);
136
137 // 4. Set data type (float32/float64) based on model capabilities
138 if (this->capabilities_->dtype() == "float64") {
139 this->dtype_ = torch::kFloat64;
140 } else if (this->capabilities_->dtype() == "float32") {
141 this->dtype_ = torch::kFloat32;
142 } else {
143 throw std::runtime_error("Unsupported dtype: " +
144 this->capabilities_->dtype());
145 }
146 QUILL_LOG_INFO(m_log, "[MetatomicPotential] Using dtype: {}",
147 this->capabilities_->dtype().c_str());
148
149 // 5. Resolve energy / force output keys: explicit keys (#215) or variants
150 // (#296 for non_conservative_force)
151 auto outputs = this->capabilities_->outputs();
152
153 auto v_base = normalize_variant(m_metatomic_opts.variant.base);
154 auto v_energy = m_metatomic_opts.variant.energy.empty()
155 ? v_base
156 : normalize_variant(m_metatomic_opts.variant.energy);
157 auto v_energy_uq =
158 m_metatomic_opts.variant.energy_uncertainty.empty()
159 ? v_energy
160 : normalize_variant(m_metatomic_opts.variant.energy_uncertainty);
161 auto v_force = m_metatomic_opts.variant.force.empty()
162 ? v_energy
163 : normalize_variant(m_metatomic_opts.variant.force);
164
165 if (!m_metatomic_opts.energy_output.empty()) {
166 this->energy_key_ = m_metatomic_opts.energy_output;
167 } else {
168 this->energy_key_ =
169 metatomic_torch::pick_output("energy", outputs, v_energy);
170 }
171
172 this->non_conservative_ = m_metatomic_opts.non_conservative;
173 this->random_rotation_ = m_metatomic_opts.random_rotation;
174 this->n_symmetry_rotations_ = m_metatomic_opts.n_symmetry_rotations;
175 if (this->non_conservative_) {
176 if (!m_metatomic_opts.force_output.empty()) {
177 this->nc_forces_key_ = m_metatomic_opts.force_output;
178 if (!outputs.contains(this->nc_forces_key_)) {
179 throw std::runtime_error(
180 "Missing explicit force_output in metatomic model: " +
181 this->nc_forces_key_);
182 }
183 } else {
184 this->nc_forces_key_ = metatomic_torch::pick_output(
185 "non_conservative_force", outputs, v_force);
186 }
187 QUILL_LOG_INFO(m_log,
188 "[MetatomicPotential] Non-conservative forces from '{}'",
189 this->nc_forces_key_);
190 }
191 if (this->n_symmetry_rotations_ > 0) {
192 QUILL_LOG_INFO(m_log,
193 "[MetatomicPotential] Symmetry averaging over {} rotations",
195 } else if (this->random_rotation_) {
196 QUILL_LOG_INFO(
197 m_log, "[MetatomicPotential] Per-call random SO(3) rotation enabled");
198 }
199
200 if (!outputs.contains(this->energy_key_)) {
201 QUILL_LOG_ERROR(
202 m_log,
203 "[MetatomicPotential] The model does not provide an '{}' output.",
204 this->energy_key_);
205 throw std::runtime_error("Missing energy output in metatomic model");
206 }
207
208 // 6. Set up evaluation options to request total energy
210 torch::make_intrusive<metatomic_torch::ModelEvaluationOptionsHolder>();
211 evaluations_options_->set_length_unit(m_metatomic_opts.length_unit);
212
213 auto model_output = outputs.at(this->energy_key_);
214 auto requested_output =
215 torch::make_intrusive<metatomic_torch::ModelOutputHolder>();
216
217 // Per-atom granularity is sample_kind == "atom" (get/set_per_atom removed).
218 requested_output->set_sample_kind(model_output->sample_kind());
219 requested_output->explicit_gradients = {};
220 requested_output->set_unit("eV");
221 evaluations_options_->outputs.insert(this->energy_key_, requested_output);
222
223 // Request non-conservative forces when enabled (#296)
224 if (this->non_conservative_ && !this->nc_forces_key_.empty()) {
225 auto nc_info = outputs.at(this->nc_forces_key_);
226 auto requested_nc =
227 torch::make_intrusive<metatomic_torch::ModelOutputHolder>();
228 requested_nc->set_sample_kind(nc_info->sample_kind());
229 requested_nc->explicit_gradients = {};
230 requested_nc->set_unit("eV/Angstrom");
231 evaluations_options_->outputs.insert(this->nc_forces_key_, requested_nc);
232 }
233
234 // 7. Optionally request energy uncertainty if threshold is positive
235 if (m_metatomic_opts.uncertainty_threshold > 0) {
236 this->uncertainty_threshold_ = m_metatomic_opts.uncertainty_threshold;
237 const bool explicit_uq_key =
238 !m_metatomic_opts.energy_uncertainty_output.empty();
239 if (explicit_uq_key) {
240 // User-specified key: hard fail if missing (not soft-disabled).
242 m_metatomic_opts.energy_uncertainty_output;
243 if (!outputs.contains(this->energy_uncertainty_key_)) {
244 QUILL_LOG_ERROR(m_log,
245 "[MetatomicPotential] energy_uncertainty_output '{}' "
246 "is not provided by the model.",
248 throw std::runtime_error(
249 "Missing explicit energy_uncertainty_output in metatomic model: " +
251 }
252 } else {
253 try {
254 this->energy_uncertainty_key_ = metatomic_torch::pick_output(
255 "energy_uncertainty", outputs, v_energy_uq);
256 } catch (const std::exception &e) {
257 QUILL_LOG_DEBUG(
258 m_log, "[MetatomicPotential] No uncertainty output available: {}",
259 e.what());
260 this->uncertainty_threshold_ = -1.0;
261 }
262 }
263
264 if (this->uncertainty_threshold_ > 0) {
265 auto uncertainty_info = outputs.at(this->energy_uncertainty_key_);
266 if (uncertainty_info->sample_kind() == "atom") {
267 auto requested_uncertainty =
268 torch::make_intrusive<metatomic_torch::ModelOutputHolder>();
269 requested_uncertainty->set_sample_kind("atom");
270 requested_uncertainty->explicit_gradients = {};
271 requested_uncertainty->set_unit("eV");
272 evaluations_options_->outputs.insert(this->energy_uncertainty_key_,
273 requested_uncertainty);
274 QUILL_LOG_INFO(m_log,
275 "[MetatomicPotential] Requested per-atom "
276 "'{}' from model (threshold = {})",
279 } else {
280 QUILL_LOG_DEBUG(m_log,
281 "[MetatomicPotential] Model provides '{}' "
282 "but sample_kind is not \"atom\"; skipping uncertainty "
283 "checks.",
285 this->uncertainty_threshold_ = -1.0;
286 }
287 }
288 }
289
290 this->check_consistency_ = m_metatomic_opts.check_consistency;
291 QUILL_LOG_INFO(m_log, "[MetatomicPotential] Initialization complete.");
292
293 fpeh.restore_fpe();
294}
static torch::optional< std::string > normalize_variant(const std::string &s)
std::vector< metatomic_torch::NeighborListOptions > nl_requests_
metatomic_torch::ModelEvaluationOptions evaluations_options_
eonc::log::Scoped m_log
metatomic_torch::ModelCapabilities capabilities_
std::string energy_uncertainty_key_
c10::DeviceType device_type_
Parameters::metatomic_options_t m_metatomic_opts
metatensor_torch::Module model_
torch::ScalarType dtype_
struct eonc::Parameters::metatomic_options_t metatomic_options
Potential(PotType a_ptype)
Definition Potential.h:35

◆ ~MetatomicPotential()

MetatomicPotential::~MetatomicPotential ( )
overridedefault

Destructor.

Member Function Documentation

◆ computeNeighbors()

metatensor_torch::TensorBlock MetatomicPotential::computeNeighbors ( metatomic_torch::NeighborListOptions request,
long nAtoms,
const double * positions,
const double * box,
const bool periodic[3] )
private

Computes neighbor list using the vesin library.

This function calls vesin_neighbors to build a neighbor list based on the model's requirements (cutoff, full/half list) and converts it into a metatensor::TensorBlock suitable for metatomic.

Parameters
requestThe neighbor list options requested by the model.
nAtomsThe number of atoms in the system.
positionsPointer to the atomic positions array.
boxPointer to the simulation box matrix.
Returns
A metatensor_torch::TensorBlock containing the neighbor list.

Definition at line 530 of file MetatomicPotential.cpp.

532 {
533
534 auto cutoff = request->engine_cutoff(m_metatomic_opts.length_unit);
535
536 // Zero-init so vesin 0.6 skin/n_threads stay 0 when compiling against 0.6
537 // headers (must match linked libvesin — pin vesin>=0.6 for metatomic builds).
538 VesinOptions options{};
539 options.cutoff = cutoff;
540 options.full = request->full_list();
541 options.sorted = false;
542 options.return_shifts = true;
543 options.return_distances = false; // we don't need distances
544 options.return_vectors = true; // metatomic uses vectors for autograd
545
546 VesinNeighborList *vesin_neighbor_list = new VesinNeighborList();
547
548 VesinDevice cpu{VesinCPU, 0};
549 const char *error_message = nullptr;
550 int status = vesin_neighbors(reinterpret_cast<const double (*)[3]>(positions),
551 static_cast<size_t>(nAtoms),
552 reinterpret_cast<const double (*)[3]>(box),
553 const_cast<bool *>(periodic), cpu, options,
554 vesin_neighbor_list, &error_message);
555
556 if (status != EXIT_SUCCESS) {
557 std::string err_str = "vesin_neighbors failed";
558 if (error_message != nullptr) {
559 err_str += ": " + std::string(error_message);
560 } else {
561 err_str += " (no message; vesin header/lib ABI mismatch? need vesin>=0.6 "
562 "with matching engine)";
563 }
564 delete vesin_neighbor_list;
565 throw std::runtime_error(err_str);
566 }
567
568 // Convert from vesin to metatomic format
569 auto n_pairs = static_cast<int64_t>(vesin_neighbor_list->length);
570 auto labels_options_cpu =
571 torch::TensorOptions().dtype(torch::kInt32).device(torch::kCPU);
572
573 auto pair_samples_values = torch::empty({n_pairs, 5}, labels_options_cpu);
574 auto pair_samples_values_ptr = pair_samples_values.accessor<int32_t, 2>();
575 for (int64_t i = 0; i < n_pairs; i++) {
576 pair_samples_values_ptr[i][0] =
577 static_cast<int32_t>(vesin_neighbor_list->pairs[i][0]);
578 pair_samples_values_ptr[i][1] =
579 static_cast<int32_t>(vesin_neighbor_list->pairs[i][1]);
580 pair_samples_values_ptr[i][2] = vesin_neighbor_list->shifts[i][0];
581 pair_samples_values_ptr[i][3] = vesin_neighbor_list->shifts[i][1];
582 pair_samples_values_ptr[i][4] = vesin_neighbor_list->shifts[i][2];
583 }
584
585 // Custom deleter to free vesin's memory when the torch tensor is destroyed
586 auto deleter = [=](void *) {
587 vesin_free(vesin_neighbor_list);
588 delete vesin_neighbor_list;
589 };
590
591 auto pair_vectors = torch::from_blob(
592 vesin_neighbor_list->vectors, {n_pairs, 3, 1}, deleter,
593 torch::TensorOptions().dtype(torch::kFloat64).device(torch::kCPU));
594
595 auto neighbor_samples = torch::make_intrusive<metatensor_torch::LabelsHolder>(
596 std::vector<std::string>{"first_atom", "second_atom", "cell_shift_a",
597 "cell_shift_b", "cell_shift_c"},
598 pair_samples_values.to(this->device_));
599
600 auto labels_options_dev =
601 torch::TensorOptions().dtype(torch::kInt32).device(this->device_);
602 auto neighbor_component =
603 torch::make_intrusive<metatensor_torch::LabelsHolder>(
604 "xyz", torch::tensor({0, 1, 2}, labels_options_dev).reshape({3, 1}));
605 auto neighbor_properties =
606 torch::make_intrusive<metatensor_torch::LabelsHolder>(
607 "distance", torch::zeros({1, 1}, labels_options_dev));
608
609 return torch::make_intrusive<metatensor_torch::TensorBlockHolder>(
610 pair_vectors.to(this->dtype_).to(this->device_), neighbor_samples,
611 std::vector<metatensor_torch::Labels>{neighbor_component},
612 neighbor_properties);
613}

◆ force()

void MetatomicPotential::force ( long nAtoms,
const double * positions,
const int * atomicNrs,
double * forces,
double * energy,
double * variance,
const double * box )
overridevirtual

Calculates the energy and forces for a given atomic configuration.

This is the core method of the potential. It takes the current atomic positions, builds the necessary data structures for metatomic, executes the model to get the potential energy, and uses PyTorch's autograd to compute forces.

Parameters
nAtomsNumber of atoms.
positionsFlat array of atomic positions (size nAtoms * 3).
atomicNrsFlat array of atomic numbers (size nAtoms), used for consistency checks.
forcesFlat array to store the calculated forces (size nAtoms * 3).
energyPointer to a double to store the calculated potential energy.
variancePointer to a double to store the variance of the energy (currently unused, set to NULL).
boxThe simulation box vectors (3x3 matrix).

Implements eonc::Potential.

Definition at line 320 of file MetatomicPotential.cpp.

323 {
324 // Serialize concurrent calls -- PyTorch model inference on the same
325 // Module instance is not thread-safe
326 std::lock_guard<std::mutex> lock(inference_mutex_);
327
328 eonc::FPEHandler fpeh;
329 fpeh.eat_fpe();
330
331 if (!atomicNrs) {
332 throw std::runtime_error(
333 "[MetatomicPotential] `atomicNrs` must be provided.");
334 }
335
336 const long n_avg = this->n_symmetry_rotations_ > 0
338 : (this->random_rotation_ ? 1 : 1);
339 const bool use_rotation =
340 this->random_rotation_ || this->n_symmetry_rotations_ > 0;
341 // n_symmetry_rotations averages; random_rotation alone is one rotated eval
342 const long n_passes =
343 this->n_symmetry_rotations_ > 0 ? this->n_symmetry_rotations_ : 1;
344
345 auto f64_options =
346 torch::TensorOptions().dtype(torch::kFloat64).device(torch::kCPU);
347 std::vector<int32_t> types_vec(atomicNrs, atomicNrs + nAtoms);
348 auto atomic_types_cpu =
349 torch::tensor(types_vec, torch::TensorOptions().dtype(torch::kInt32));
350
351 double energy_acc = 0.0;
352 auto forces_acc = torch::zeros({nAtoms, 3}, f64_options);
353 bool variance_set = false;
354
355 for (long i_pass = 0; i_pass < n_passes; ++i_pass) {
356 torch::Tensor R = torch::eye(
357 3, torch::TensorOptions().dtype(this->dtype_).device(this->device_));
358 if (use_rotation) {
359 R = random_so3(this->device_, this->dtype_);
360 }
361 // R is applied to row vectors: pos' = pos @ R^T (equiv. R @ pos for cols)
362 auto R_cpu = R.to(torch::kCPU).to(torch::kFloat64);
363 auto R_T = R.transpose(0, 1);
364
365 auto pos_cpu = torch::from_blob(const_cast<double *>(positions),
366 {nAtoms, 3}, f64_options)
367 .clone();
368 auto cell_cpu =
369 torch::from_blob(const_cast<double *>(box), {3, 3}, f64_options)
370 .clone();
371 if (use_rotation) {
372 pos_cpu = pos_cpu.matmul(R_cpu.transpose(0, 1));
373 // Rotate cell vectors (rows) the same way
374 cell_cpu = cell_cpu.matmul(R_cpu.transpose(0, 1));
375 }
376
377 std::vector<double> pos_buf(static_cast<size_t>(nAtoms) * 3);
378 std::vector<double> cell_buf(9);
379 std::memcpy(pos_buf.data(), pos_cpu.contiguous().data_ptr<double>(),
380 pos_buf.size() * sizeof(double));
381 std::memcpy(cell_buf.data(), cell_cpu.contiguous().data_ptr<double>(),
382 9 * sizeof(double));
383
384 auto torch_positions =
385 torch::from_blob(pos_buf.data(), {nAtoms, 3}, f64_options)
386 .to(this->dtype_)
387 .to(this->device_)
388 .set_requires_grad(!this->non_conservative_);
389
390 auto torch_cell = torch::from_blob(cell_buf.data(), {3, 3}, f64_options)
391 .to(this->dtype_)
392 .to(this->device_);
393
394 auto cell_norms = torch::norm(torch_cell, 2, /*dim=*/1);
395 auto torch_pbc = cell_norms.abs() > 1e-9;
396 bool periodic[3] = {torch_pbc[0].item<bool>(), torch_pbc[1].item<bool>(),
397 torch_pbc[2].item<bool>()};
398
399 auto atomic_types = atomic_types_cpu.to(this->device_);
400
401 auto system = torch::make_intrusive<metatomic_torch::SystemHolder>(
402 atomic_types, torch_positions, torch_cell, torch_pbc);
403
404 for (const auto &request : this->nl_requests_) {
405 auto neighbors = this->computeNeighbors(request, nAtoms, pos_buf.data(),
406 cell_buf.data(), periodic);
407 metatomic_torch::register_autograd_neighbors(system, neighbors,
408 this->check_consistency_);
409 system->add_neighbor_list(request, neighbors);
410 }
411
412 torch::Tensor forces_tensor;
413 try {
414 auto ivalue_output = this->model_.forward({
415 std::vector<metatomic_torch::System>{system},
417 this->check_consistency_,
418 });
419 auto dict_output = ivalue_output.toGenericDict();
420 auto output_map = dict_output.at(this->energy_key_)
421 .toCustomClass<metatensor_torch::TensorMapHolder>();
422
423 if (this->uncertainty_threshold_ > 0 && i_pass == 0) {
424 try {
425 if (dict_output.contains(this->energy_uncertainty_key_)) {
426 auto uncertainty_map =
427 dict_output.at(this->energy_uncertainty_key_)
428 .toCustomClass<metatensor_torch::TensorMapHolder>();
429 auto uncertainty_block =
430 metatensor_torch::TensorMapHolder::block_by_id(uncertainty_map,
431 0);
432 auto flat_uncertainty =
433 uncertainty_block->values().reshape({-1}).to(torch::kCPU);
434 if (variance != nullptr && flat_uncertainty.numel() > 0) {
435 try {
436 *variance =
437 flat_uncertainty.to(torch::kFloat64).mean().item<double>();
438 variance_set = true;
439 } catch (...) {
440 QUILL_LOG_DEBUG(m_log,
441 "[MetatomicPotential] Failed to compute mean "
442 "uncertainty for variance.");
443 }
444 }
445 auto atoms_above_threshold =
446 flat_uncertainty > this->uncertainty_threshold_;
447 if (torch::any(atoms_above_threshold).item<bool>()) {
448 auto samples = uncertainty_block->samples();
449 auto atom_indices_all = samples->column("atom").to(torch::kCPU);
450 auto atom_indices_above =
451 atom_indices_all.index({atoms_above_threshold});
452 std::ostringstream ss;
453 ss << "atoms at index [";
454 auto n_report = std::min<int64_t>(10, atom_indices_above.size(0));
455 for (int64_t i = 0; i < n_report; ++i) {
456 if (i > 0)
457 ss << ", ";
458 ss << atom_indices_above[i].item<int32_t>();
459 }
460 ss << "]";
461 if (atom_indices_above.size(0) > n_report) {
462 ss << " and " << (atom_indices_above.size(0) - n_report)
463 << " more";
464 }
465 QUILL_LOG_WARNING(
466 m_log,
467 "[MetatomicPotential] The uncertainty on atomic energies for "
468 "{} are larger than the threshold of {}. (Key: {}) Be "
469 "careful "
470 "when analyzing the results, and consider retraining the "
471 "model to better describe these configurations.",
472 ss.str(), this->uncertainty_threshold_,
473 this->energy_uncertainty_key_);
474 }
475 }
476 } catch (const std::exception &e) {
477 QUILL_LOG_WARNING(m_log,
478 "[MetatomicPotential] Failed to check {}: {}",
479 this->energy_uncertainty_key_, e.what());
480 }
481 }
482
483 auto energy_block =
484 metatensor_torch::TensorMapHolder::block_by_id(output_map, 0);
485 auto energy_tensor = energy_block->values();
486 energy_acc += energy_tensor.sum().item<double>();
487
488 if (this->non_conservative_ && !this->nc_forces_key_.empty()) {
489 auto nc_map = dict_output.at(this->nc_forces_key_)
490 .toCustomClass<metatensor_torch::TensorMapHolder>();
491 auto nc_block =
492 metatensor_torch::TensorMapHolder::block_by_id(nc_map, 0);
493 forces_tensor = nc_block->values()
494 .reshape({nAtoms, 3})
495 .to(torch::kCPU)
496 .to(torch::kFloat64);
497 } else {
498 energy_tensor.backward(torch::ones_like(energy_tensor));
499 auto positions_grad = system->positions().grad();
500 forces_tensor = (-positions_grad).to(torch::kCPU).to(torch::kFloat64);
501 }
502 } catch (const std::exception &e) {
503 QUILL_LOG_ERROR(m_log, "[MetatomicPotential] Model evaluation failed: {}",
504 e.what());
505 throw;
506 }
507
508 // Rotate forces back to original frame: F = F' @ R (since pos' = pos @
509 // R^T)
510 if (use_rotation) {
511 forces_tensor = forces_tensor.matmul(R_cpu);
512 }
513 forces_acc += forces_tensor;
514 }
515
516 const double inv_n = 1.0 / static_cast<double>(n_passes);
517 *energy = energy_acc * inv_n;
518 forces_acc = forces_acc * inv_n;
519 (void)variance_set;
520 (void)n_avg;
521
522 std::memcpy(forces, forces_acc.contiguous().data_ptr<double>(),
523 nAtoms * 3 * sizeof(double));
524
525 fpeh.restore_fpe();
526}
metatensor_torch::TensorBlock computeNeighbors(metatomic_torch::NeighborListOptions request, long nAtoms, const double *positions, const double *box, const bool periodic[3])
Computes neighbor list using the vesin library.

◆ forceBatch()

void MetatomicPotential::forceBatch ( long nSystems,
long nAtoms,
const double *const * positions,
const int *const * atomicNrs,
double *const * forces,
double * energies,
double * variances,
const double *const * boxes )
overridevirtual

Evaluate forces for N systems in a single call.

Default: loops over force(). Override in potentials that support native batching (e.g. MetatomicPotential uses a single model.forward() for all N systems).

Reimplemented from eonc::Potential.

Definition at line 621 of file MetatomicPotential.cpp.

626 {
627 // Sequential evaluation through force() -- numerically identical to
628 // N individual computePotential() calls. The mutex inside force()
629 // serializes, and all calls share the same model instance + JIT state.
630 for (long s = 0; s < nSystems; s++) {
631 double var = 0;
632 force(nAtoms, positions[s], atomicNrs[s], forces[s], &energies[s], &var,
633 boxes[s]);
634 if (variances)
635 variances[s] = var;
638 }
639}
void force(long nAtoms, const double *positions, const int *atomicNrs, double *forces, double *energy, double *variance, const double *box) override
Calculates the energy and forces for a given atomic configuration.
static PotRegistry & get() noexcept
Process-lifetime singleton.
void on_force_call(PotType t) noexcept
std::atomic< size_t > forceCallCounter
Definition Potential.h:32
PotType ptype
Definition Potential.h:25

◆ isThreadSafe()

bool MetatomicPotential::isThreadSafe ( ) const
inlinenodiscardoverridevirtualnoexcept

Single shared instance, serialized via mutex.

Sequential evaluation through computePotential() ensures correct force counting, removeNetForce, and PotRegistry bookkeeping. JIT profiling is disabled so all calls on the same instance produce deterministic results.

Reimplemented from eonc::Potential.

Definition at line 136 of file MetatomicPotential.h.

136{ return false; }

◆ needsPerImageInstance()

bool MetatomicPotential::needsPerImageInstance ( ) const
inlinenodiscardoverridevirtualnoexcept

Whether NEB should create separate Potential instances per image for true parallel force evaluation.

When true, NEB calls makePotential() once per image instead of sharing one instance. Override in potentials that use internal mutexes (e.g. MetatomicPotential).

Reimplemented from eonc::Potential.

Definition at line 137 of file MetatomicPotential.h.

137 {
138 return false;
139 }

◆ supportsBatchEvaluation()

bool MetatomicPotential::supportsBatchEvaluation ( ) const
inlinenodiscardoverridevirtualnoexcept

Batched evaluation via single shared instance.

Processes each system sequentially through the same model (identical to N force() calls but bypasses Matter::computePotential overhead). Forces, energies, and forceCallCounter are handled correctly.

Reimplemented from eonc::Potential.

Definition at line 145 of file MetatomicPotential.h.

145 {
146 return true;
147 }

Member Data Documentation

◆ capabilities_

metatomic_torch::ModelCapabilities MetatomicPotential::capabilities_
private

Definition at line 59 of file MetatomicPotential.h.

◆ check_consistency_

bool MetatomicPotential::check_consistency_
private

Definition at line 66 of file MetatomicPotential.h.

◆ device_

torch::Device MetatomicPotential::device_
private

Definition at line 65 of file MetatomicPotential.h.

◆ device_type_

c10::DeviceType MetatomicPotential::device_type_
private

Definition at line 64 of file MetatomicPotential.h.

◆ dtype_

torch::ScalarType MetatomicPotential::dtype_
private

Definition at line 63 of file MetatomicPotential.h.

◆ energy_key_

std::string MetatomicPotential::energy_key_
private

Definition at line 68 of file MetatomicPotential.h.

◆ energy_uncertainty_key_

std::string MetatomicPotential::energy_uncertainty_key_
private

Definition at line 69 of file MetatomicPotential.h.

◆ evaluations_options_

metatomic_torch::ModelEvaluationOptions MetatomicPotential::evaluations_options_
private

Definition at line 61 of file MetatomicPotential.h.

◆ inference_mutex_

std::mutex MetatomicPotential::inference_mutex_
mutableprivate

Definition at line 154 of file MetatomicPotential.h.

◆ m_log

eonc::log::Scoped MetatomicPotential::m_log
private

Definition at line 55 of file MetatomicPotential.h.

◆ m_metatomic_opts

Parameters::metatomic_options_t MetatomicPotential::m_metatomic_opts
private

Definition at line 56 of file MetatomicPotential.h.

◆ model_

metatensor_torch::Module MetatomicPotential::model_
private

Definition at line 58 of file MetatomicPotential.h.

◆ n_symmetry_rotations_

long MetatomicPotential::n_symmetry_rotations_ {0}
private

Definition at line 73 of file MetatomicPotential.h.

73{0};

◆ nc_forces_key_

std::string MetatomicPotential::nc_forces_key_
private

Definition at line 70 of file MetatomicPotential.h.

◆ nl_requests_

std::vector<metatomic_torch::NeighborListOptions> MetatomicPotential::nl_requests_
private

Definition at line 60 of file MetatomicPotential.h.

◆ non_conservative_

bool MetatomicPotential::non_conservative_ {false}
private

Definition at line 71 of file MetatomicPotential.h.

71{false};

◆ random_rotation_

bool MetatomicPotential::random_rotation_ {false}
private

Definition at line 72 of file MetatomicPotential.h.

72{false};

◆ uncertainty_threshold_

double MetatomicPotential::uncertainty_threshold_ {-1.0}
private

Definition at line 76 of file MetatomicPotential.h.

76{-1.0};

The documentation for this class was generated from the following files: