Loading...
Searching...
No Matches
MetatomicPotential.cpp
Go to the documentation of this file.
1/*
2** This file is part of eOn.
3**
4** SPDX-License-Identifier: BSD-3-Clause
5**
6** Copyright (c) 2010--present, eOn Development Team
7** All rights reserved.
8**
9** Repo:
10** https://github.com/TheochemUI/eOn
11*/
13#include "eon/Parameters.h"
14#include "eon/fpe_handler.h"
15#include "vesin.h"
16
17#include <torch/csrc/jit/runtime/graph_executor.h>
18
19#include <cstdint>
20#include <random>
21#include <sstream>
22#include <string>
23#include <vector>
24
25using namespace std::string_literals;
26
27namespace {
28
29// metatensor-torch 0.10.3 Module::to() walks every attribute when
30// `_mts_buffer_names` is missing. Exported PET-MAD stores mixed dicts as
31// ordinary attrs; empty containers count as non-metatensor and throw.
32// Weights already moved. Swallow only that mixed-dict error. Do not
33// register `_mts_buffer_names` on scripted modules (JIT slot assert).
34bool is_mixed_mts_to_error(const c10::Error &e) {
35 const std::string w = e.what_without_backtrace();
36 return w.find("metatensor and non-metatensor") != std::string::npos;
37}
38
39void move_atomistic_model(metatensor_torch::Module &model,
40 torch::Device device) {
41 try {
42 model.to(device);
43 } catch (const c10::Error &e) {
44 if (!is_mixed_mts_to_error(e)) {
45 throw;
46 }
47 }
48}
49
50} // namespace
51
52static torch::optional<std::string> normalize_variant(const std::string &s) {
53 if (s.empty() || s == "off")
54 return torch::nullopt;
55 return s;
56}
57
59 : Potential(PotType::METATOMIC),
60 m_metatomic_opts{params.metatomic_options},
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
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
209 this->evaluations_options_ =
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).
241 this->energy_uncertainty_key_ =
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.",
247 this->energy_uncertainty_key_);
248 throw std::runtime_error(
249 "Missing explicit energy_uncertainty_output in metatomic model: " +
250 this->energy_uncertainty_key_);
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 = {})",
277 this->energy_uncertainty_key_,
278 this->uncertainty_threshold_);
279 } else {
280 QUILL_LOG_DEBUG(m_log,
281 "[MetatomicPotential] Model provides '{}' "
282 "but sample_kind is not \"atom\"; skipping uncertainty "
283 "checks.",
284 this->energy_uncertainty_key_);
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}
295
296// --- helpers for random / symmetry rotations (#287, #292) ---
297
298namespace {
299
300// Uniform random rotation in SO(3) via QR of a Gaussian matrix with positive
301// determinant (Arvo / Shoemake style, sufficient for stochastic averaging).
302torch::Tensor random_so3(torch::Device device, torch::ScalarType dtype) {
303 auto A =
304 torch::randn({3, 3}, torch::TensorOptions().dtype(dtype).device(device));
305 auto qr = torch::linalg_qr(A);
306 auto Q = std::get<0>(qr);
307 auto R = std::get<1>(qr);
308 auto d = torch::sign(torch::diagonal(R));
309 Q = Q * d.unsqueeze(0);
310 if (torch::det(Q).item<double>() < 0) {
311 Q.select(1, 0).mul_(-1);
312 }
313 return Q;
314}
315
316} // namespace
317
318// --- MetatomicPotential::force ---
319
320void MetatomicPotential::force(long nAtoms, const double *positions,
321 const int *atomicNrs, double *forces,
322 double *energy, double *variance,
323 const double *box) {
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}
527
528// --- MetatomicPotential::computeNeighbors (helper) ---
529
530metatensor_torch::TensorBlock MetatomicPotential::computeNeighbors(
531 metatomic_torch::NeighborListOptions request, long nAtoms,
532 const double *positions, const double *box, const bool periodic[3]) {
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}
614
615// --- MetatomicPotential::forceBatch ---
616// Processes N systems sequentially through the same model instance.
617// Numerically identical to N individual force() calls. The single-instance
618// design avoids JIT profiling divergence and N model copies. True batched
619// model.forward({sys0..sysN}) is a future optimization (see #if 0 block below).
620
621void MetatomicPotential::forceBatch(long nSystems, long nAtoms,
622 const double *const *positions,
623 const int *const *atomicNrs,
624 double *const *forces, double *energies,
625 double *variances,
626 const double *const *boxes) {
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}
640
641// --- True batched forward (future optimization) ---
642// model.forward({sys0..sysN}) verified identical in Python.
643// C++ energy extraction needs work to match single-system path exactly.
644#if 0
645void MetatomicPotential::forceBatchNative(long nSystems, long nAtoms,
646 const double *const *positions,
647 const int *const *atomicNrs,
648 double *const *forces, double *energies,
649 double *variances,
650 const double *const *boxes) {
651 std::lock_guard<std::mutex> lock(inference_mutex_);
652
653 eonc::FPEHandler fpeh;
654 fpeh.eat_fpe();
655
656 auto f64_options =
657 torch::TensorOptions().dtype(torch::kFloat64).device(torch::kCPU);
658
659 std::vector<metatomic_torch::System> systems;
660 std::vector<torch::Tensor> pos_tensors;
661 systems.reserve(static_cast<size_t>(nSystems));
662 pos_tensors.reserve(static_cast<size_t>(nSystems));
663
664 for (long s = 0; s < nSystems; s++) {
665 auto torch_positions =
666 torch::from_blob(const_cast<double *>(positions[s]), {nAtoms, 3},
667 f64_options)
668 .to(this->dtype_)
669 .to(this->device_)
670 .set_requires_grad(true);
671 pos_tensors.push_back(torch_positions);
672
673 auto torch_cell =
674 torch::from_blob(const_cast<double *>(boxes[s]), {3, 3}, f64_options)
675 .to(this->dtype_)
676 .to(this->device_);
677
678 auto cell_norms = torch::norm(torch_cell, 2, /*dim=*/1);
679 auto torch_pbc = cell_norms.abs() > 1e-9;
680 bool periodic[3] = {torch_pbc[0].item<bool>(), torch_pbc[1].item<bool>(),
681 torch_pbc[2].item<bool>()};
682
683 if (!atomicNrs[s]) {
684 throw std::runtime_error(
685 "[MetatomicPotential] `atomicNrs` must be provided.");
686 }
687 std::vector<int32_t> types_vec(atomicNrs[s], atomicNrs[s] + nAtoms);
688 auto atomic_types =
689 torch::tensor(types_vec, torch::TensorOptions().dtype(torch::kInt32))
690 .to(this->device_);
691
692 auto system = torch::make_intrusive<metatomic_torch::SystemHolder>(
693 atomic_types, torch_positions, torch_cell, torch_pbc);
694
695 // Compute and register neighbor lists for this system
696 for (const auto &request : this->nl_requests_) {
697 auto neighbors = this->computeNeighbors(request, nAtoms, positions[s],
698 boxes[s], periodic);
699 metatomic_torch::register_autograd_neighbors(system, neighbors,
700 this->check_consistency_);
701 system->add_neighbor_list(request, neighbors);
702 }
703
704 systems.push_back(system);
705 }
706
707 // Single batched forward pass
708 metatensor_torch::TensorMap output_map;
709 try {
710 auto ivalue_output = this->model_.forward({
711 systems,
713 this->check_consistency_,
714 });
715 auto dict_output = ivalue_output.toGenericDict();
716 output_map = dict_output.at(this->energy_key_)
717 .toCustomClass<metatensor_torch::TensorMapHolder>();
718 } catch (const std::exception &e) {
719 QUILL_LOG_ERROR(m_log,
720 "[MetatomicPotential] Batched model evaluation failed: {}",
721 e.what());
722 throw;
723 }
724
725 // Extract per-system energies from the output TensorMap.
726 // For per-atom output: samples have ["system", "atom"] dimensions.
727 // For system-level output: samples have ["system"] dimension.
728 // In both cases, sum over all non-system dimensions to get per-system energy.
729 auto energy_block =
730 metatensor_torch::TensorMapHolder::block_by_id(output_map, 0);
731 auto energy_values = energy_block->values();
732 auto samples = energy_block->samples();
733
734 // Check if this is per-atom or per-system output
735 bool per_atom = samples->size() > 0 && samples->names().size() > 1;
736
737 if (per_atom) {
738 // Per-atom output: sum energies by system index
739 auto system_col = samples->column("system").to(torch::kCPU);
740 auto flat_energies = energy_values.reshape({-1}).to(torch::kCPU);
741
742 // Sum per-system energies for output
743 for (long s = 0; s < nSystems; s++) {
744 auto mask = (system_col == s);
745 energies[s] =
746 flat_energies.index({mask}).sum().to(torch::kFloat64).item<double>();
747 }
748
749 // Backward: sum ALL energies, single backward call.
750 // Each system's positions.grad() gets only its own contribution
751 // because energy_i depends only on positions_i.
752 energy_values.sum().backward();
753 } else {
754 // System-level output: values shape is (nSystems, 1) or similar
755 auto cpu_energies = energy_values.to(torch::kCPU).to(torch::kFloat64);
756 for (long s = 0; s < nSystems; s++) {
757 energies[s] = cpu_energies[s].sum().item<double>();
758 }
759 energy_values.backward(torch::ones_like(energy_values));
760 }
761
762 // Extract per-system forces from position gradients
763 for (long s = 0; s < nSystems; s++) {
764 auto positions_grad = pos_tensors[s].grad();
765 auto forces_tensor =
766 -positions_grad.to(torch::kCPU).to(torch::kFloat64);
767 std::memcpy(forces[s], forces_tensor.contiguous().data_ptr<double>(),
768 nAtoms * 3 * sizeof(double));
769 }
770
771 // Variances: not yet supported in batched path
772 if (variances) {
773 for (long s = 0; s < nSystems; s++) {
774 variances[s] = 0.0;
775 }
776 }
777
778 fpeh.restore_fpe();
779}
780#endif
static torch::optional< std::string > normalize_variant(const std::string &s)
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.
std::vector< metatomic_torch::NeighborListOptions > nl_requests_
metatomic_torch::ModelEvaluationOptions evaluations_options_
eonc::log::Scoped m_log
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.
metatomic_torch::ModelCapabilities capabilities_
std::string energy_uncertainty_key_
c10::DeviceType device_type_
MetatomicPotential(const Parameters &params)
Constructor for the MetatomicPotential.
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.
Parameters::metatomic_options_t m_metatomic_opts
metatensor_torch::Module model_
torch::ScalarType dtype_
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
Potential(PotType a_ptype)
Definition Potential.h:35