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.
323 {
324
325
327
328 eonc::FPEHandler fpeh;
330
331 if (!atomicNrs) {
332 throw std::runtime_error(
333 "[MetatomicPotential] `atomicNrs` must be provided.");
334 }
335
339 const bool use_rotation =
341
342 const long n_passes =
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) {
360 }
361
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
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)
389
390 auto torch_cell = torch::from_blob(cell_buf.data(), {3, 3}, f64_options)
393
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>()};
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
406 cell_buf.data(), periodic);
407 metatomic_torch::register_autograd_neighbors(system, neighbors,
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},
418 });
419 auto dict_output = ivalue_output.toGenericDict();
420 auto output_map = dict_output.at(this->
energy_key_)
421 .toCustomClass<metatensor_torch::TensorMapHolder>();
422
424 try {
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,
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 =
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(
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 {}: {}",
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
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
509
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
526}