17#include <torch/csrc/jit/runtime/graph_executor.h>
25using namespace std::string_literals;
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;
39void move_atomistic_model(metatensor_torch::Module &model,
40 torch::Device device) {
43 }
catch (
const c10::Error &e) {
44 if (!is_mixed_mts_to_error(e)) {
53 if (s.empty() || s ==
"off")
54 return torch::nullopt;
61 model_(torch::jit::Module()),
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,
79 at::globalContext().setBenchmarkCuDNN(false);
81 "[MetatomicPotential] Deterministic algorithms enabled "
86 "[MetatomicPotential] Deterministic algorithms disabled "
87 "(faster, may diverge across runs / NEB images)");
93 QUILL_LOG_INFO(
m_log,
"[MetatomicPotential] Initializing...");
96 torch::optional<std::string> extensions_directory = torch::nullopt;
98 extensions_directory = m_metatomic_opts.extensions_directory;
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: {}",
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()) {
120 .toCustomClass<metatomic_torch::NeighborListOptionsHolder>();
121 this->nl_requests_.push_back(request);
125 torch::optional<std::string> desired = torch::nullopt;
127 desired = m_metatomic_opts.device;
130 this->capabilities_->supported_devices, desired);
133 QUILL_LOG_INFO(
m_log,
"[MetatomicPotential] Using device: {}",
device_.str());
139 this->dtype_ = torch::kFloat64;
141 this->dtype_ = torch::kFloat32;
143 throw std::runtime_error(
"Unsupported dtype: " +
144 this->capabilities_->dtype());
146 QUILL_LOG_INFO(
m_log,
"[MetatomicPotential] Using dtype: {}",
151 auto outputs = this->capabilities_->outputs();
166 this->energy_key_ = m_metatomic_opts.energy_output;
169 metatomic_torch::pick_output(
"energy", outputs, v_energy);
178 if (!outputs.contains(this->nc_forces_key_)) {
179 throw std::runtime_error(
180 "Missing explicit force_output in metatomic model: " +
185 "non_conservative_force", outputs, v_force);
187 QUILL_LOG_INFO(
m_log,
188 "[MetatomicPotential] Non-conservative forces from '{}'",
192 QUILL_LOG_INFO(
m_log,
193 "[MetatomicPotential] Symmetry averaging over {} rotations",
197 m_log,
"[MetatomicPotential] Per-call random SO(3) rotation enabled");
200 if (!outputs.contains(this->energy_key_)) {
203 "[MetatomicPotential] The model does not provide an '{}' output.",
205 throw std::runtime_error(
"Missing energy output in metatomic model");
209 this->evaluations_options_ =
210 torch::make_intrusive<metatomic_torch::ModelEvaluationOptionsHolder>();
213 auto model_output = outputs.at(this->energy_key_);
214 auto requested_output =
215 torch::make_intrusive<metatomic_torch::ModelOutputHolder>();
218 requested_output->set_sample_kind(model_output->sample_kind());
219 requested_output->explicit_gradients = {};
220 requested_output->set_unit(
"eV");
225 auto nc_info = outputs.at(this->nc_forces_key_);
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");
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) {
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_);
254 this->energy_uncertainty_key_ = metatomic_torch::pick_output(
255 "energy_uncertainty", outputs, v_energy_uq);
256 } catch (
const std::exception &e) {
258 m_log,
"[MetatomicPotential] No uncertainty output available: {}",
260 this->uncertainty_threshold_ = -1.0;
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_);
280 QUILL_LOG_DEBUG(m_log,
281 "[MetatomicPotential] Model provides '{}' "
282 "but sample_kind is not \"atom\"; skipping uncertainty "
284 this->energy_uncertainty_key_);
285 this->uncertainty_threshold_ = -1.0;
290 this->check_consistency_ = m_metatomic_opts.check_consistency;
291 QUILL_LOG_INFO(m_log,
"[MetatomicPotential] Initialization complete.");
302torch::Tensor random_so3(torch::Device device, torch::ScalarType dtype) {
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);
321 const int *atomicNrs,
double *forces,
322 double *energy,
double *variance,
332 throw std::runtime_error(
333 "[MetatomicPotential] `atomicNrs` must be provided.");
339 const bool use_rotation =
342 const long n_passes =
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));
351 double energy_acc = 0.0;
352 auto forces_acc = torch::zeros({nAtoms, 3}, f64_options);
353 bool variance_set =
false;
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_));
362 auto R_cpu = R.to(torch::kCPU).to(torch::kFloat64);
363 auto R_T = R.transpose(0, 1);
365 auto pos_cpu = torch::from_blob(
const_cast<double *
>(positions),
366 {nAtoms, 3}, f64_options)
369 torch::from_blob(
const_cast<double *
>(box), {3, 3}, f64_options)
372 pos_cpu = pos_cpu.matmul(R_cpu.transpose(0, 1));
374 cell_cpu = cell_cpu.matmul(R_cpu.transpose(0, 1));
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>(),
384 auto torch_positions =
385 torch::from_blob(pos_buf.data(), {nAtoms, 3}, f64_options)
390 auto torch_cell = torch::from_blob(cell_buf.data(), {3, 3}, f64_options)
394 auto cell_norms = torch::norm(torch_cell, 2, 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>()};
399 auto atomic_types = atomic_types_cpu.to(this->
device_);
401 auto system = torch::make_intrusive<metatomic_torch::SystemHolder>(
402 atomic_types, torch_positions, torch_cell, torch_pbc);
406 cell_buf.data(), periodic);
407 metatomic_torch::register_autograd_neighbors(system, neighbors,
409 system->add_neighbor_list(request, neighbors);
412 torch::Tensor forces_tensor;
414 auto ivalue_output = this->
model_.forward({
415 std::vector<metatomic_torch::System>{system},
419 auto dict_output = ivalue_output.toGenericDict();
420 auto output_map = dict_output.at(this->
energy_key_)
421 .toCustomClass<metatensor_torch::TensorMapHolder>();
425 if (dict_output.contains(this->energy_uncertainty_key_)) {
426 auto uncertainty_map =
428 .toCustomClass<metatensor_torch::TensorMapHolder>();
429 auto uncertainty_block =
430 metatensor_torch::TensorMapHolder::block_by_id(uncertainty_map,
432 auto flat_uncertainty =
433 uncertainty_block->values().reshape({-1}).to(torch::kCPU);
434 if (variance !=
nullptr && flat_uncertainty.numel() > 0) {
437 flat_uncertainty.to(torch::kFloat64).mean().item<
double>();
440 QUILL_LOG_DEBUG(
m_log,
441 "[MetatomicPotential] Failed to compute mean "
442 "uncertainty for variance.");
445 auto atoms_above_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) {
458 ss << atom_indices_above[i].item<int32_t>();
461 if (atom_indices_above.size(0) > n_report) {
462 ss <<
" and " << (atom_indices_above.size(0) - n_report)
467 "[MetatomicPotential] The uncertainty on atomic energies for "
468 "{} are larger than the threshold of {}. (Key: {}) Be "
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_);
476 }
catch (
const std::exception &e) {
477 QUILL_LOG_WARNING(
m_log,
478 "[MetatomicPotential] Failed to check {}: {}",
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>();
490 .toCustomClass<metatensor_torch::TensorMapHolder>();
492 metatensor_torch::TensorMapHolder::block_by_id(nc_map, 0);
493 forces_tensor = nc_block->values()
494 .reshape({nAtoms, 3})
496 .to(torch::kFloat64);
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);
502 }
catch (
const std::exception &e) {
503 QUILL_LOG_ERROR(
m_log,
"[MetatomicPotential] Model evaluation failed: {}",
511 forces_tensor = forces_tensor.matmul(R_cpu);
513 forces_acc += forces_tensor;
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;
522 std::memcpy(forces, forces_acc.contiguous().data_ptr<
double>(),
523 nAtoms * 3 *
sizeof(
double));
531 metatomic_torch::NeighborListOptions request,
long nAtoms,
532 const double *positions,
const double *box,
const bool periodic[3]) {
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;
544 options.return_vectors =
true;
546 VesinNeighborList *vesin_neighbor_list =
new VesinNeighborList();
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);
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);
561 err_str +=
" (no message; vesin header/lib ABI mismatch? need vesin>=0.6 "
562 "with matching engine)";
564 delete vesin_neighbor_list;
565 throw std::runtime_error(err_str);
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);
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];
586 auto deleter = [=](
void *) {
587 vesin_free(vesin_neighbor_list);
588 delete vesin_neighbor_list;
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));
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_));
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));
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);
622 const double *
const *positions,
623 const int *
const *atomicNrs,
624 double *
const *forces,
double *energies,
626 const double *
const *boxes) {
630 for (
long s = 0; s < nSystems; s++) {
632 force(nAtoms, positions[s], atomicNrs[s], forces[s], &energies[s], &var,
645void MetatomicPotential::forceBatchNative(
long nSystems,
long nAtoms,
646 const double *
const *positions,
647 const int *
const *atomicNrs,
648 double *
const *forces,
double *energies,
650 const double *
const *boxes) {
657 torch::TensorOptions().dtype(torch::kFloat64).device(torch::kCPU);
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));
664 for (
long s = 0; s < nSystems; s++) {
665 auto torch_positions =
666 torch::from_blob(
const_cast<double *
>(positions[s]), {nAtoms, 3},
670 .set_requires_grad(
true);
671 pos_tensors.push_back(torch_positions);
674 torch::from_blob(
const_cast<double *
>(boxes[s]), {3, 3}, f64_options)
678 auto cell_norms = torch::norm(torch_cell, 2, 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>()};
684 throw std::runtime_error(
685 "[MetatomicPotential] `atomicNrs` must be provided.");
687 std::vector<int32_t> types_vec(atomicNrs[s], atomicNrs[s] + nAtoms);
689 torch::tensor(types_vec, torch::TensorOptions().dtype(torch::kInt32))
692 auto system = torch::make_intrusive<metatomic_torch::SystemHolder>(
693 atomic_types, torch_positions, torch_cell, torch_pbc);
699 metatomic_torch::register_autograd_neighbors(system, neighbors,
701 system->add_neighbor_list(request, neighbors);
704 systems.push_back(system);
708 metatensor_torch::TensorMap output_map;
710 auto ivalue_output = this->
model_.forward({
715 auto dict_output = ivalue_output.toGenericDict();
717 .toCustomClass<metatensor_torch::TensorMapHolder>();
718 }
catch (
const std::exception &e) {
719 QUILL_LOG_ERROR(
m_log,
720 "[MetatomicPotential] Batched model evaluation failed: {}",
730 metatensor_torch::TensorMapHolder::block_by_id(output_map, 0);
731 auto energy_values = energy_block->values();
732 auto samples = energy_block->samples();
735 bool per_atom = samples->size() > 0 && samples->names().size() > 1;
739 auto system_col = samples->column(
"system").to(torch::kCPU);
740 auto flat_energies = energy_values.reshape({-1}).to(torch::kCPU);
743 for (
long s = 0; s < nSystems; s++) {
744 auto mask = (system_col == s);
746 flat_energies.index({mask}).sum().to(torch::kFloat64).item<
double>();
752 energy_values.sum().backward();
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>();
759 energy_values.backward(torch::ones_like(energy_values));
763 for (
long s = 0; s < nSystems; s++) {
764 auto positions_grad = pos_tensors[s].grad();
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));
773 for (
long s = 0; s < nSystems; s++) {
static PotRegistry & get() noexcept
Process-lifetime singleton.
void on_force_call(PotType t) noexcept
std::atomic< size_t > forceCallCounter
Potential(PotType a_ptype)