diff --git a/cmake/Modules/Packages/COLVARS.cmake b/cmake/Modules/Packages/COLVARS.cmake index 8fa0d84f01..b4dc738626 100644 --- a/cmake/Modules/Packages/COLVARS.cmake +++ b/cmake/Modules/Packages/COLVARS.cmake @@ -26,6 +26,11 @@ if(BUILD_OMP) target_link_libraries(colvars PRIVATE OpenMP::OpenMP_CXX) endif() +if(BUILD_MPI) + target_compile_definitions(colvars PUBLIC -DCOLVARS_MPI) + target_link_libraries(colvars PUBLIC MPI::MPI_CXX) +endif() + if(COLVARS_DEBUG) # Need to export the define publicly to be valid in interface code target_compile_definitions(colvars PUBLIC -DCOLVARS_DEBUG) diff --git a/doc/src/PDF/colvars-refman-lammps.pdf b/doc/src/PDF/colvars-refman-lammps.pdf index b8f049ce01..76d94f8e3a 100644 Binary files a/doc/src/PDF/colvars-refman-lammps.pdf and b/doc/src/PDF/colvars-refman-lammps.pdf differ diff --git a/lib/colvars/Makefile.common b/lib/colvars/Makefile.common index f0282b8caf..9203f9d158 100644 --- a/lib/colvars/Makefile.common +++ b/lib/colvars/Makefile.common @@ -32,6 +32,7 @@ COLVARS_SRCS = \ colvarbias_histogram_reweight_amd.cpp \ colvarbias_meta.cpp \ colvarbias_restraint.cpp \ + colvarbias_opes.cpp \ colvarcomp_alchlambda.cpp \ colvarcomp_angles.cpp \ colvarcomp_apath.cpp \ @@ -40,6 +41,7 @@ COLVARS_SRCS = \ colvarcomp_distances.cpp \ colvarcomp_gpath.cpp \ colvarcomp_neuralnetwork.cpp \ + colvarcomp_torchann.cpp \ colvarcomp_combination.cpp \ colvarcomp_protein.cpp \ colvarcomp_rotations.cpp \ diff --git a/lib/colvars/Makefile.deps b/lib/colvars/Makefile.deps index 61f0b1a335..82d36e53c4 100644 --- a/lib/colvars/Makefile.deps +++ b/lib/colvars/Makefile.deps @@ -52,6 +52,12 @@ $(COLVARS_OBJ_DIR)colvarbias_restraint.o: colvarbias_restraint.cpp \ colvarproxy_tcl.h colvarproxy_volmaps.h colvarvalue.h \ colvarbias_restraint.h colvarbias.h colvar.h colvarparse.h \ colvarparams.h colvardeps.h +$(COLVARS_OBJ_DIR)colvarbias_opes.o: colvarbias_opes.cpp \ + colvarmodule.h colvars_version.h colvarproxy.h colvartypes.h \ + ../../src/math_eigen_impl.h colvarproxy_io.h colvarproxy_system.h \ + colvarproxy_tcl.h colvarproxy_volmaps.h colvarvalue.h \ + colvarbias_opes.h colvarbias.h colvar.h colvarparse.h \ + colvarparams.h colvardeps.h $(COLVARS_OBJ_DIR)colvarcomp_alchlambda.o: colvarcomp_alchlambda.cpp \ colvarmodule.h colvars_version.h colvarvalue.h colvartypes.h \ ../../src/math_eigen_impl.h colvar.h colvarparse.h colvarparams.h \ @@ -101,6 +107,11 @@ $(COLVARS_OBJ_DIR)colvarcomp_neuralnetwork.o: \ colvarproxy.h colvarproxy_io.h colvarproxy_system.h colvarproxy_tcl.h \ colvarproxy_volmaps.h colvar_geometricpath.h \ colvar_neuralnetworkcompute.h +$(COLVARS_OBJ_DIR)colvarcomp_torchann.o: \ + colvarcomp_torchann.cpp colvarmodule.h colvars_version.h \ + colvarvalue.h colvartypes.h colvarparse.h colvarparams.h colvar.h \ + colvardeps.h colvarcomp.h colvarcomp_torchann.h colvaratoms.h colvarproxy.h colvarproxy_io.h \ + colvarproxy_system.h colvarproxy_tcl.h $(COLVARS_OBJ_DIR)colvarcomp_combination.o: colvarcomp_combination.cpp \ colvarcomp.h colvarmodule.h colvars_version.h colvaratoms.h \ colvarproxy.h colvartypes.h ../../src/math_eigen_impl.h colvarproxy_io.h \ @@ -127,7 +138,7 @@ $(COLVARS_OBJ_DIR)colvarcomp_volmaps.o: colvarcomp_volmaps.cpp \ colvar_geometricpath.h $(COLVARS_OBJ_DIR)colvar.o: colvar.cpp colvarmodule.h colvars_version.h \ colvarvalue.h colvartypes.h ../../src/math_eigen_impl.h colvarparse.h \ - colvarparams.h colvarcomp.h colvaratoms.h colvarproxy.h colvarproxy_io.h \ + colvarparams.h colvarcomp.h colvarcomp_torchann.h colvaratoms.h colvarproxy.h colvarproxy_io.h \ colvarproxy_system.h colvarproxy_tcl.h colvarproxy_volmaps.h \ colvardeps.h colvar.h colvar_geometricpath.h colvarbias.h \ colvars_memstream.h @@ -152,7 +163,8 @@ $(COLVARS_OBJ_DIR)colvarmodule.o: colvarmodule.cpp colvarmodule.h \ colvarbias_histogram_reweight_amd.h colvarbias_meta.h colvarscript.h \ colvarscript_commands.h colvarscript_commands_colvar.h \ colvarscript_commands_bias.h colvaratoms.h colvarcomp.h \ - colvar_geometricpath.h colvars_memstream.h colvarmodule_refs.h + colvar_geometricpath.h colvars_memstream.h colvarmodule_refs.h \ + colvarbias_opes.h $(COLVARS_OBJ_DIR)colvarparams.o: colvarparams.cpp colvarmodule.h \ colvars_version.h colvarvalue.h colvartypes.h \ ../../src/math_eigen_impl.h colvarparams.h diff --git a/lib/colvars/colvar.cpp b/lib/colvars/colvar.cpp index 58eb87fd0e..136aa2afd5 100644 --- a/lib/colvars/colvar.cpp +++ b/lib/colvars/colvar.cpp @@ -21,6 +21,7 @@ #include "colvarbias.h" #include "colvars_memstream.h" +#include "colvarcomp_torchann.h" std::map> colvar::global_cvc_map = std::map>(); @@ -95,6 +96,12 @@ int colvar::init(std::string const &conf) if (error_code != COLVARS_OK) { return cvm::get_error(); } +#else + if (key_lookup(conf, "customFunction")) { + return cvm::error( + "Error: customFunction keyword is used, but the Lepton library is not available.\n", + COLVARS_NOT_IMPLEMENTED); + } #endif // Setup colvar as scripted function of components @@ -175,12 +182,6 @@ int colvar::init(std::string const &conf) set_enabled(f_cv_scalar, (value().type() == colvarvalue::type_scalar)); - // If using scripted biases, any colvar may receive bias forces - // and will need its gradient - if (cvm::scripted_forces()) { - enable(f_cv_gradient); - } - // check for linear combinations { bool lin = !(is_enabled(f_cv_scripted) || is_enabled(f_cv_custom_function)); @@ -311,9 +312,27 @@ int colvar::init(std::string const &conf) // Detect if we have a single component that is an alchemical lambda if (is_enabled(f_cv_single_cvc) && cvcs[0]->function_type() == "alchLambda") { enable(f_cv_external); + + static_cast(cvcs[0].get())->init_alchemy(time_step_factor); + } + + // If using scripted biases, any colvar may receive bias forces + if (cvm::scripted_forces()) { + enable(f_cv_apply_force); } error_code |= init_extended_Lagrangian(conf); + + // when total atomic forces are obtained from the previous time step, + // we cannot (currently) have colvar values and projected total forces for the same timestep + // (that would require anticipating the total force request by one timestep) + // i.e. the combination of f_cv_total_force_calc and f_cv_multiple_ts requires f_cv_total_force_current_step + // Because f_cv_total_force_current_step is static, we can hard-code this, once other features are set + // that is f_cv_external and f_cv_extended_Lagrangian + if (!is_enabled(f_cv_total_force_current_step)) { + exclude_feature_self(f_cv_multiple_ts, f_cv_total_force_calc); + } + error_code |= init_output_flags(conf); // Now that the children are defined we can solve dependencies @@ -495,8 +514,6 @@ int colvar::init_grid_parameters(std::string const &conf) { int error_code = COLVARS_OK; - colvarmodule *cv = cvm::main(); - cvm::real default_width = width; if (!key_already_set("width")) { @@ -522,34 +539,68 @@ int colvar::init_grid_parameters(std::string const &conf) if (is_enabled(f_cv_scalar)) { - if (is_enabled(f_cv_single_cvc)) { - // Get the default boundaries from the component + // Record the CVC's intrinsic boundaries, and set them as default values for the user's choice + colvarvalue cvc_lower_boundary, cvc_upper_boundary; + + if (is_enabled(f_cv_single_cvc)) { // Get the intrinsic boundaries of the CVC + if (cvcs[0]->is_enabled(f_cvc_lower_boundary)) { enable(f_cv_lower_boundary); enable(f_cv_hard_lower_boundary); - lower_boundary = + lower_boundary = cvc_lower_boundary = *(reinterpret_cast(cvcs[0]->get_param_ptr("lowerBoundary"))); } + if (cvcs[0]->is_enabled(f_cvc_upper_boundary)) { enable(f_cv_upper_boundary); enable(f_cv_hard_upper_boundary); - upper_boundary = - *(reinterpret_cast(cvcs[0]->get_param_ptr("upperBoundary"))); + upper_boundary = cvc_upper_boundary = + *(reinterpret_cast(cvcs[0]->get_param_ptr("upperBoundary"))); } } if (get_keyval(conf, "lowerBoundary", lower_boundary, lower_boundary)) { enable(f_cv_lower_boundary); - // Because this is the user's choice, we cannot assume it is a true - // physical boundary - disable(f_cv_hard_lower_boundary); + if (is_enabled(f_cv_single_cvc) && is_enabled(f_cv_hard_lower_boundary)) { + if (cvm::sqrt(dist2(lower_boundary, cvc_lower_boundary))/width > colvar_boundaries_tol) { + // The user choice is different from the CVC's default + disable(f_cv_hard_lower_boundary); + } + } } if (get_keyval(conf, "upperBoundary", upper_boundary, upper_boundary)) { enable(f_cv_upper_boundary); - disable(f_cv_hard_upper_boundary); + if (is_enabled(f_cv_single_cvc) && is_enabled(f_cv_hard_upper_boundary)) { + if (cvm::sqrt(dist2(upper_boundary, cvc_upper_boundary))/width > colvar_boundaries_tol) { + disable(f_cv_hard_upper_boundary); + } + } } + get_keyval_feature(this, conf, "hardLowerBoundary", f_cv_hard_lower_boundary, + is_enabled(f_cv_hard_lower_boundary)); + + get_keyval_feature(this, conf, "hardUpperBoundary", f_cv_hard_upper_boundary, + is_enabled(f_cv_hard_upper_boundary)); + + get_keyval(conf, "expandBoundaries", expand_boundaries, expand_boundaries); + + error_code |= parse_legacy_wall_params(conf); + error_code |= check_grid_parameters(); + } + + return error_code; +} + + +int colvar::parse_legacy_wall_params(std::string const &conf) +{ + int error_code = COLVARS_OK; + colvarmodule *cv = cvm::main(); + + if (is_enabled(f_cv_scalar)) { + // Parse legacy wall options and set up a harmonicWalls bias if needed cvm::real lower_wall_k = 0.0, upper_wall_k = 0.0; cvm::real lower_wall = 0.0, upper_wall = 0.0; @@ -603,13 +654,14 @@ harmonicWalls {\n\ } } - get_keyval_feature(this, conf, "hardLowerBoundary", f_cv_hard_lower_boundary, - is_enabled(f_cv_hard_lower_boundary)); + return error_code; +} - get_keyval_feature(this, conf, "hardUpperBoundary", f_cv_hard_upper_boundary, - is_enabled(f_cv_hard_upper_boundary)); - // consistency checks for boundaries and walls +int colvar::check_grid_parameters() +{ + int error_code = COLVARS_OK; + if (is_enabled(f_cv_lower_boundary) && is_enabled(f_cv_upper_boundary)) { if (lower_boundary >= upper_boundary) { error_code |= cvm::error("Error: the upper boundary, "+ @@ -620,7 +672,6 @@ harmonicWalls {\n\ } } - get_keyval(conf, "expandBoundaries", expand_boundaries, expand_boundaries); if (expand_boundaries && periodic_boundaries()) { error_code |= cvm::error("Error: trying to expand boundaries that already " "cover a whole period of a periodic colvar.\n", @@ -654,14 +705,15 @@ int colvar::init_extended_Lagrangian(std::string const &conf) x_ext.type(colvarvalue::type_notset); v_ext.type(value()); fr.type(value()); - const bool temp_provided = get_keyval(conf, "extendedTemp", temp, - proxy->target_temperature()); + const bool temp_provided = get_keyval(conf, "extendedTemp", temp, proxy->target_temperature()); if (is_enabled(f_cv_external)) { - // In the case of an "external" coordinate, there is no coupling potential: + // In the case of a driven external parameter in the back-end, there is no coupling potential: // only the fictitious mass is meaningful get_keyval(conf, "extendedMass", ext_mass); // Ensure that the computed restraint energy term is zero ext_force_k = 0.0; + // Then we need forces from the back-end + enable(f_cv_total_force_calc); } else { // Standard case of coupling to a geometric colvar if (temp <= 0.0) { // Then a finite temperature is required @@ -779,6 +831,7 @@ int colvar::init_components_type(const std::string& conf, const char* def_config &def_conf, &pos) ) { + cvm::increase_depth(); cvm::log("Initializing " "a new \""+std::string(def_config_key)+"\" component"+ (cvm::debug() ? ", with configuration:\n"+def_conf @@ -791,7 +844,6 @@ int colvar::init_components_type(const std::string& conf, const char* def_config } cvcs.push_back(std::shared_ptr(cvcp)); - cvm::increase_depth(); int error_code_this = cvcp->init(def_conf); if (error_code_this == COLVARS_OK) { // Checking for invalid keywords only if the parsing was successful, otherwise any @@ -851,12 +903,8 @@ void colvar::define_component_types() add_component_type("dipole angle", "dipoleAngle"); add_component_type("dihedral", "dihedral"); add_component_type("hydrogen bond", "hBond"); - - if (proxy->check_atom_name_selections_available() == COLVARS_OK) { - add_component_type("alpha helix", "alpha"); - add_component_type("dihedral principal component", "dihedralPC"); - } - + add_component_type("alpha helix", "alpha"); + add_component_type("dihedral principal component", "dihedralPC"); add_component_type("orientation", "orientation"); add_component_type("orientation angle", "orientationAngle"); add_component_type("orientation projection", "orientationProj"); @@ -888,6 +936,8 @@ void colvar::define_component_types() add_component_type("neural network CV for other CVs", "neuralNetwork"); + add_component_type("CV defined by PyTorch artifical neural network models", "torchANN"); + if (proxy->check_volmaps_available() == COLVARS_OK) { add_component_type("total value of atomic map", "mapTotal"); } @@ -1098,6 +1148,9 @@ int colvar::init_dependencies() { init_feature(f_cv_gradient, "gradient", f_type_dynamic); require_feature_children(f_cv_gradient, f_cvc_gradient); + init_feature(f_cv_apply_force, "apply_force", f_type_dynamic); + require_feature_alt(f_cv_apply_force, f_cv_gradient, f_cv_external); + init_feature(f_cv_collect_gradient, "collect_gradient", f_type_dynamic); require_feature_self(f_cv_collect_gradient, f_cv_gradient); require_feature_self(f_cv_collect_gradient, f_cv_scalar); @@ -1116,6 +1169,10 @@ int colvar::init_dependencies() { init_feature(f_cv_total_force, "total_force", f_type_dynamic); require_feature_alt(f_cv_total_force, f_cv_extended_Lagrangian, f_cv_total_force_calc); + // If this is active, the total force reported to biases (ABF / TI) is from the current step + // therefore it does not include Colvars biases -> it is a "system force" + init_feature(f_cv_total_force_current_step, "total_force_current_step", f_type_dynamic); + // Deps for explicit total force calculation init_feature(f_cv_total_force_calc, "total_force_calculation", f_type_dynamic); require_feature_self(f_cv_total_force_calc, f_cv_scalar); @@ -1134,13 +1191,15 @@ int colvar::init_dependencies() { init_feature(f_cv_extended_Lagrangian, "extended_Lagrangian", f_type_user); require_feature_self(f_cv_extended_Lagrangian, f_cv_scalar); - require_feature_self(f_cv_extended_Lagrangian, f_cv_gradient); + require_feature_self(f_cv_extended_Lagrangian, f_cv_apply_force); init_feature(f_cv_Langevin, "Langevin_dynamics", f_type_user); require_feature_self(f_cv_Langevin, f_cv_extended_Lagrangian); - init_feature(f_cv_external, "external", f_type_user); + init_feature(f_cv_external, "external_parameter", f_type_static); require_feature_self(f_cv_external, f_cv_single_cvc); + // External parameters always report the total force for current step + require_feature_self(f_cv_external, f_cv_total_force_current_step); init_feature(f_cv_single_cvc, "single_component", f_type_static); @@ -1201,10 +1260,7 @@ int colvar::init_dependencies() { init_feature(f_cv_linear, "linear", f_type_static); init_feature(f_cv_homogeneous, "homogeneous", f_type_static); - // because total forces are obtained from the previous time step, - // we cannot (currently) have colvar values and total forces for the same timestep init_feature(f_cv_multiple_ts, "multiple_timestep", f_type_static); - exclude_feature_self(f_cv_multiple_ts, f_cv_total_force_calc); // check that everything is initialized for (i = 0; i < colvardeps::f_cv_ntot; i++) { @@ -1225,6 +1281,10 @@ int colvar::init_dependencies() { feature_states[f_cv_fdiff_velocity].available = cvm::main()->proxy->simulation_running(); + // Some back-ends report current total forces for all colvars + if (cvm::main()->proxy->total_forces_same_step()) + enable(f_cv_total_force_current_step); + return COLVARS_OK; } @@ -1351,7 +1411,6 @@ int colvar::calc_cvcs(int first_cvc, size_t num_cvcs) cvm::log("Calculating colvar \""+this->name+"\", components "+ cvm::to_str(first_cvc)+" through "+cvm::to_str(first_cvc+num_cvcs)+".\n"); - colvarproxy *proxy = cvm::main()->proxy; int error_code = COLVARS_OK; error_code |= check_cvc_range(first_cvc, num_cvcs); @@ -1359,7 +1418,7 @@ int colvar::calc_cvcs(int first_cvc, size_t num_cvcs) return error_code; } - if ((cvm::step_relative() > 0) && (!proxy->total_forces_same_step())){ + if ((cvm::step_relative() > 0) && (!is_enabled(f_cv_total_force_current_step))){ // Use Jacobian derivative from previous timestep error_code |= calc_cvc_total_force(first_cvc, num_cvcs); } @@ -1367,7 +1426,7 @@ int colvar::calc_cvcs(int first_cvc, size_t num_cvcs) error_code |= calc_cvc_values(first_cvc, num_cvcs); error_code |= calc_cvc_gradients(first_cvc, num_cvcs); error_code |= calc_cvc_Jacobians(first_cvc, num_cvcs); - if (proxy->total_forces_same_step()){ + if (is_enabled(f_cv_total_force_current_step)){ // Use Jacobian derivative from this timestep error_code |= calc_cvc_total_force(first_cvc, num_cvcs); } @@ -1384,10 +1443,9 @@ int colvar::collect_cvc_data() if (cvm::debug()) cvm::log("Calculating colvar \""+this->name+"\"'s properties.\n"); - colvarproxy *proxy = cvm::main()->proxy; int error_code = COLVARS_OK; - if ((cvm::step_relative() > 0) && (!proxy->total_forces_same_step())){ + if ((cvm::step_relative() > 0) && (!is_enabled(f_cv_total_force_current_step))){ // Total force depends on Jacobian derivative from previous timestep // collect_cvc_total_forces() uses the previous value of jd error_code |= collect_cvc_total_forces(); @@ -1395,7 +1453,7 @@ int colvar::collect_cvc_data() error_code |= collect_cvc_values(); error_code |= collect_cvc_gradients(); error_code |= collect_cvc_Jacobians(); - if (proxy->total_forces_same_step()){ + if (is_enabled(f_cv_total_force_current_step)){ // Use Jacobian derivative from this timestep error_code |= collect_cvc_total_forces(); } @@ -1609,22 +1667,20 @@ int colvar::collect_cvc_total_forces() if (is_enabled(f_cv_total_force_calc)) { ft.reset(); - if (cvm::step_relative() > 0) { - // get from the cvcs the total forces from the PREVIOUS step - for (size_t i = 0; i < cvcs.size(); i++) { - if (!cvcs[i]->is_enabled()) continue; - if (cvm::debug()) - cvm::log("Colvar component no. "+cvm::to_str(i+1)+ - " within colvar \""+this->name+"\" has total force "+ - cvm::to_str((cvcs[i])->total_force(), - cvm::cv_width, cvm::cv_prec)+".\n"); - // linear combination is assumed - ft += (cvcs[i])->total_force() * (cvcs[i])->sup_coeff / active_cvc_square_norm; - } + for (size_t i = 0; i < cvcs.size(); i++) { + if (!cvcs[i]->is_enabled()) continue; + if (cvm::debug()) + cvm::log("Colvar component no. "+cvm::to_str(i+1)+ + " within colvar \""+this->name+"\" has total force "+ + cvm::to_str((cvcs[i])->total_force(), + cvm::cv_width, cvm::cv_prec)+".\n"); + // linear combination is assumed + ft += (cvcs[i])->total_force() * (cvcs[i])->sup_coeff / active_cvc_square_norm; } if (!(is_enabled(f_cv_hide_Jacobian) && is_enabled(f_cv_subtract_applied_force))) { - // add the Jacobian force to the total force, and don't apply any silent + // This is by far the most common case + // Add the Jacobian force to the total force, and don't apply any silent // correction internally: biases such as colvarbias_abf will handle it // If f_cv_hide_Jacobian is enabled, a force of -fj is present in ft due to the // Jacobian-compensating force @@ -1632,6 +1688,10 @@ int colvar::collect_cvc_total_forces() } } + if (is_enabled(f_cv_total_force_current_step)) { + // Report total force value without waiting for calc_colvar_properties() + ft_reported = ft; + } return COLVARS_OK; } @@ -1733,12 +1793,15 @@ int colvar::calc_colvar_properties() // But we report values at the beginning of the timestep (value at t=0 on the first timestep) x_reported = x_ext; v_reported = v_ext; - // the "total force" with the extended Lagrangian is - // calculated in update_forces_energy() below + // the "total force" for the extended Lagrangian is calculated in update_forces_energy() below + // A future improvement could compute a "system force" here, borrowing a part of update_extended_Lagrangian() + // this would change the behavior of eABF with respect to other biases + // by enabling f_cv_total_force_current_step, and reducing the total force to a system force + // giving the behavior of f_cv_subtract_applied_force - this is correct for WTM-eABF etc. } else { - if (is_enabled(f_cv_subtract_applied_force)) { + if (is_enabled(f_cv_subtract_applied_force) && !cvm::proxy->total_forces_same_step()) { // correct the total force only if it has been measured // TODO add a specific test instead of relying on sq norm if (ft.norm2() > 0.0) { @@ -1825,7 +1888,8 @@ void colvar::update_extended_Lagrangian() // Integrate with slow timestep (if time_step_factor != 1) cvm::real dt = cvm::dt() * cvm::real(time_step_factor); - colvarvalue f_ext(fr.type()); // force acting on the extended variable + // Force acting on the extended variable + colvarvalue f_ext(fr.type()); f_ext.reset(); if (is_enabled(f_cv_external)) { @@ -1834,13 +1898,13 @@ void colvar::update_extended_Lagrangian() f += fb_actual; } - // fr: bias force on extended variable (without harmonic spring), for output in trajectory - fr = f; - // External force has been scaled for an inner-timestep impulse (for the back-end integrator) // here we scale it back because this integrator uses only the outer (long) timestep f_ext = f / cvm::real(time_step_factor); + // fr: bias force on extended variable (without harmonic spring), for output in trajectory + fr = f_ext; + colvarvalue f_system(fr.type()); // force exterted by the system on the extended DOF if (is_enabled(f_cv_external)) { @@ -1863,14 +1927,18 @@ void colvar::update_extended_Lagrangian() } f_ext += f_system; - if (is_enabled(f_cv_subtract_applied_force)) { - // Report a "system" force without the biases on this colvar - // that is, just the spring force (or alchemical force) - ft_reported = f_system; - } else { - // The total force acting on the extended variable is f_ext - // This will be used in the next timestep - ft_reported = f_ext; + if ( ! is_enabled(f_cv_total_force_current_step)) { + if (is_enabled(f_cv_subtract_applied_force)) { + // Report a "system" force without the biases on this colvar + // that is, just the spring force (or alchemical force) + ft_reported = f_system; + } else { + // The total force acting on the extended variable is f_ext + // This will be used in the next timestep + ft_reported = f_ext; + } + // Since biases have already been updated, this ft_reported will only be + // communicated to biases at the next timestep } // backup in case we need to revert this integration timestep @@ -2184,12 +2252,10 @@ int colvar::set_cvc_param(std::string const ¶m_name, void const *new_value) bool colvar::periodic_boundaries(colvarvalue const &lb, colvarvalue const &ub) const { if (period > 0.0) { - if ( ((cvm::sqrt(this->dist2(lb, ub))) / this->width) - < 1.0E-10 ) { + if (((cvm::sqrt(this->dist2(lb, ub))) / this->width) < colvar_boundaries_tol) { return true; } } - return false; } @@ -2347,6 +2413,11 @@ int colvar::set_state_params(std::string const &conf) cvm::to_str(x)+"\n"); x_restart = x; after_restart = true; + // Externally driven cv (e.g. alchemical lambda) is imposed by restart value + if (is_enabled(f_cv_external) && is_enabled(f_cv_extended_Lagrangian)) { + // Request immediate sync of driven parameter to back-end code + cvcs[0]->set_value(x, true); + } } if (is_enabled(f_cv_extended_Lagrangian)) { @@ -2489,8 +2560,14 @@ std::string const colvar::get_state_params() const os << " name " << name << "\n" << " x " << std::setprecision(cvm::cv_prec) - << std::setw(cvm::cv_width) - << x << "\n"; + << std::setw(cvm::cv_width); + if (is_enabled(f_cv_external) && is_enabled(f_cv_extended_Lagrangian)) { + // For an external colvar, x is one timestep in the future after integration + // write x at beginning of timestep + os << x_reported << "\n"; + } else { + os << x << "\n"; + } if (is_enabled(f_cv_output_velocity)) { os << " v " diff --git a/lib/colvars/colvar.h b/lib/colvars/colvar.h index 443e1e4bdd..1db313f416 100644 --- a/lib/colvars/colvar.h +++ b/lib/colvars/colvar.h @@ -263,6 +263,12 @@ public: /// Init defaults for grid options int init_grid_parameters(std::string const &conf); + /// Consistency check for the grid paramaters + int check_grid_parameters(); + + /// Read legacy wall keyword (these are biases now) + int parse_legacy_wall_params(std::string const &conf); + /// Init extended Lagrangian parameters int init_extended_Lagrangian(std::string const &conf); @@ -633,6 +639,7 @@ public: class euler_psi; class euler_theta; class neuralNetwork; + class torchANN; class customColvar; // non-scalar components @@ -753,7 +760,7 @@ inline colvarvalue const & colvar::total_force() const inline void colvar::add_bias_force(colvarvalue const &force) { - check_enabled(f_cv_gradient, + check_enabled(f_cv_apply_force, std::string("applying a force to the variable \""+name+"\"")); if (cvm::debug()) { cvm::log("Adding biasing force "+cvm::to_str(force)+" to colvar \""+name+"\".\n"); @@ -778,4 +785,10 @@ inline void colvar::reset_bias_force() { fb_actual.reset(); } + +namespace { + // Tolerance parameter to decide when two boundaries coincide + constexpr cvm::real colvar_boundaries_tol = 1.0e-10; +} + #endif diff --git a/lib/colvars/colvar_rotation_derivative.h b/lib/colvars/colvar_rotation_derivative.h index 50f4f1aa97..ceaf728c36 100644 --- a/lib/colvars/colvar_rotation_derivative.h +++ b/lib/colvars/colvar_rotation_derivative.h @@ -5,11 +5,21 @@ #include #include +#ifndef _noalias +#if defined(__INTEL_COMPILER) || (defined(__PGI) && !defined(__NVCOMPILER)) +#define _noalias restrict +#elif defined(__GNUC__) || defined(__INTEL_LLVM_COMPILER) || defined(__NVCOMPILER) +#define _noalias __restrict +#else +#define _noalias +#endif +#endif + /// \brief Helper function for loading the ia-th atom in the vector pos to x, y and z (C++11 SFINAE is used) template ::value, bool>::type = true> inline void read_atom_coord( size_t ia, const std::vector& pos, - cvm::real* x, cvm::real* y, cvm::real* z) { + cvm::real* _noalias x, cvm::real* _noalias y, cvm::real* _noalias z) { *x = pos[ia].x; *y = pos[ia].y; *z = pos[ia].z; @@ -18,7 +28,7 @@ inline void read_atom_coord( template ::value, bool>::type = true> inline void read_atom_coord( size_t ia, const std::vector& pos, - cvm::real* x, cvm::real* y, cvm::real* z) { + cvm::real* _noalias x, cvm::real* _noalias y, cvm::real* _noalias z) { *x = pos[ia].pos.x; *y = pos[ia].pos.y; *z = pos[ia].pos.z; @@ -26,9 +36,9 @@ inline void read_atom_coord( /// \brief Helper enum class for specifying options in rotation_derivative::prepare_derivative enum class rotation_derivative_dldq { - /// Require the derivative of the leading eigenvalue with respect to the atom coordinats + /// Require the derivative of the leading eigenvalue with respect to the atom coordinates use_dl = 1 << 0, - /// Require the derivative of the leading eigenvector with respect to the atom coordinats + /// Require the derivative of the leading eigenvector with respect to the atom coordinates use_dq = 1 << 1 }; @@ -327,12 +337,13 @@ struct rotation_derivative { * @param[out] dq0_out The output of derivative of Q * @param[out] ds_out The output of derivative of overlap matrix S */ + template void calc_derivative_impl( const cvm::rvector (&ds)[4][4], - cvm::rvector* const dl0_out, - cvm::vector1d* const dq0_out, - cvm::matrix2d* const ds_out) const { - if (ds_out != nullptr) { + cvm::rvector* _noalias const dl0_out, + cvm::vector1d* _noalias const dq0_out, + cvm::matrix2d* _noalias const ds_out) const { + if (use_ds) { // this code path is for debug_gradients, so not necessary to unroll the loop *ds_out = cvm::matrix2d(4, 4); for (int i = 0; i < 4; ++i) { @@ -341,7 +352,7 @@ struct rotation_derivative { } } } - if (dl0_out != nullptr) { + if (use_dl) { /* manually loop unrolling of the following loop: dl0_1.reset(); for (size_t i = 0; i < 4; i++) { @@ -367,7 +378,7 @@ struct rotation_derivative { tmp_Q0Q0[3][2] * ds[3][2] + tmp_Q0Q0[3][3] * ds[3][3]; } - if (dq0_out != nullptr) { + if (use_dq) { // we can skip this check if a fixed-size array is used if (dq0_out->size() != 4) dq0_out->resize(4); /* manually loop unrolling of the following loop: @@ -462,32 +473,21 @@ struct rotation_derivative { * @param[out] ds_1_out The output of derivative of overlap matrix S with * respect to ia-th atom of group 1 */ + template void calc_derivative_wrt_group1( - size_t ia, cvm::rvector* const dl0_1_out = nullptr, - cvm::vector1d* const dq0_1_out = nullptr, - cvm::matrix2d* const ds_1_out = nullptr) const { - if (dl0_1_out == nullptr && dq0_1_out == nullptr) return; + size_t ia, cvm::rvector* _noalias const dl0_1_out = nullptr, + cvm::vector1d* _noalias const dq0_1_out = nullptr, + cvm::matrix2d* _noalias const ds_1_out = nullptr) const { + // if (dl0_1_out == nullptr && dq0_1_out == nullptr) return; cvm::real a2x, a2y, a2z; // we can get rid of the helper function read_atom_coord if C++17 (constexpr) is available read_atom_coord(ia, m_pos2, &a2x, &a2y, &a2z); - cvm::rvector ds_1[4][4]; - ds_1[0][0].set( a2x, a2y, a2z); - ds_1[1][0].set( 0.0, a2z, -a2y); - ds_1[0][1] = ds_1[1][0]; - ds_1[2][0].set(-a2z, 0.0, a2x); - ds_1[0][2] = ds_1[2][0]; - ds_1[3][0].set( a2y, -a2x, 0.0); - ds_1[0][3] = ds_1[3][0]; - ds_1[1][1].set( a2x, -a2y, -a2z); - ds_1[2][1].set( a2y, a2x, 0.0); - ds_1[1][2] = ds_1[2][1]; - ds_1[3][1].set( a2z, 0.0, a2x); - ds_1[1][3] = ds_1[3][1]; - ds_1[2][2].set(-a2x, a2y, -a2z); - ds_1[3][2].set( 0.0, a2z, a2y); - ds_1[2][3] = ds_1[3][2]; - ds_1[3][3].set(-a2x, -a2y, a2z); - calc_derivative_impl(ds_1, dl0_1_out, dq0_1_out, ds_1_out); + const cvm::rvector ds_1[4][4] = { + {{ a2x, a2y, a2z}, { 0.0, a2z, -a2y}, {-a2z, 0.0, a2x}, { a2y, -a2x, 0.0}}, + {{ 0.0, a2z, -a2y}, { a2x, -a2y, -a2z}, { a2y, a2x, 0.0}, { a2z, 0.0, a2x}}, + {{-a2z, 0.0, a2x}, { a2y, a2x, 0.0}, {-a2x, a2y, -a2z}, { 0.0, a2z, a2y}}, + {{ a2y, -a2x, 0.0}, { a2z, 0.0, a2x}, { 0.0, a2z, a2y}, {-a2x, -a2y, a2z}}}; + calc_derivative_impl(ds_1, dl0_1_out, dq0_1_out, ds_1_out); } /*! @brief Calculate the derivatives of S, the leading eigenvalue L and * the leading eigenvector Q with respect to `m_pos2` @@ -499,32 +499,21 @@ struct rotation_derivative { * @param[out] ds_2_out The output of derivative of overlap matrix S with * respect to ia-th atom of group 2 */ + template void calc_derivative_wrt_group2( - size_t ia, cvm::rvector* const dl0_2_out = nullptr, - cvm::vector1d* const dq0_2_out = nullptr, - cvm::matrix2d* const ds_2_out = nullptr) const { - if (dl0_2_out == nullptr && dq0_2_out == nullptr) return; + size_t ia, cvm::rvector* _noalias const dl0_2_out = nullptr, + cvm::vector1d* _noalias const dq0_2_out = nullptr, + cvm::matrix2d* _noalias const ds_2_out = nullptr) const { + // if (dl0_2_out == nullptr && dq0_2_out == nullptr) return; cvm::real a1x, a1y, a1z; // we can get rid of the helper function read_atom_coord if C++17 (constexpr) is available read_atom_coord(ia, m_pos1, &a1x, &a1y, &a1z); - cvm::rvector ds_2[4][4]; - ds_2[0][0].set( a1x, a1y, a1z); - ds_2[1][0].set( 0.0, -a1z, a1y); - ds_2[0][1] = ds_2[1][0]; - ds_2[2][0].set( a1z, 0.0, -a1x); - ds_2[0][2] = ds_2[2][0]; - ds_2[3][0].set(-a1y, a1x, 0.0); - ds_2[0][3] = ds_2[3][0]; - ds_2[1][1].set( a1x, -a1y, -a1z); - ds_2[2][1].set( a1y, a1x, 0.0); - ds_2[1][2] = ds_2[2][1]; - ds_2[3][1].set( a1z, 0.0, a1x); - ds_2[1][3] = ds_2[3][1]; - ds_2[2][2].set(-a1x, a1y, -a1z); - ds_2[3][2].set( 0.0, a1z, a1y); - ds_2[2][3] = ds_2[3][2]; - ds_2[3][3].set(-a1x, -a1y, a1z); - calc_derivative_impl(ds_2, dl0_2_out, dq0_2_out, ds_2_out); + const cvm::rvector ds_2[4][4] = { + {{ a1x, a1y, a1z}, { 0.0, -a1z, a1y}, { a1z, 0.0, -a1x}, {-a1y, a1x, 0.0}}, + {{ 0.0, -a1z, a1y}, { a1x, -a1y, -a1z}, { a1y, a1x, 0.0}, { a1z, 0.0, a1x}}, + {{ a1z, 0.0, -a1x}, { a1y, a1x, 0.0}, {-a1x, a1y, -a1z}, { 0.0, a1z, a1y}}, + {{-a1y, a1x, 0.0}, { a1z, 0.0, a1x}, { 0.0, a1z, a1y}, {-a1x, -a1y, a1z}}}; + calc_derivative_impl(ds_2, dl0_2_out, dq0_2_out, ds_2_out); } }; @@ -585,10 +574,7 @@ void debug_gradients( cvm::real S_new_eigval[4]; cvm::real S_new_eigvec[4][4]; for (size_t ia = 0; ia < pos2.size(); ++ia) { - // cvm::real const &a1x = pos1[ia].x; - // cvm::real const &a1y = pos1[ia].y; - // cvm::real const &a1z = pos1[ia].z; - deriv.calc_derivative_wrt_group2(ia, &dl0_2, &dq0_2, &ds_2); + deriv.template calc_derivative_wrt_group2(ia, &dl0_2, &dq0_2, &ds_2); // make an infitesimal move along each cartesian coordinate of // this atom, and solve again the eigenvector problem for (size_t comp = 0; comp < 3; comp++) { diff --git a/lib/colvars/colvaratoms.cpp b/lib/colvars/colvaratoms.cpp index e15b9301a1..054db505f3 100644 --- a/lib/colvars/colvaratoms.cpp +++ b/lib/colvars/colvaratoms.cpp @@ -673,7 +673,7 @@ int cvm::atom_group::add_atom_numbers(std::string const &numbers_conf) } -int cvm::atom_group::add_index_group(std::string const &index_group_name) +int cvm::atom_group::add_index_group(std::string const &index_group_name, bool silent) { std::vector const &index_group_names = cvm::main()->index_group_names; @@ -687,7 +687,10 @@ int cvm::atom_group::add_index_group(std::string const &index_group_name) } if (i_group >= index_group_names.size()) { - return cvm::error("Error: could not find index group "+ + if (silent) + return COLVARS_INPUT_ERROR; + else + return cvm::error("Error: could not find index group "+ index_group_name+" among those already provided.\n", COLVARS_INPUT_ERROR); } @@ -1055,6 +1058,14 @@ void cvm::atom_group::calc_apply_roto_translation() } } + if (is_enabled(f_ag_fit_gradients) && !b_dummy) { + // Save the unrotated frame for fit gradients + pos_unrotated.resize(size()); + for (size_t i = 0; i < size(); ++i) { + pos_unrotated[i] = atoms[i].pos; + } + } + if (is_enabled(f_ag_rotate)) { // rotate the group (around the center of geometry if f_ag_center is // enabled, around the origin otherwise) @@ -1217,23 +1228,30 @@ void cvm::atom_group::calc_fit_gradients() if (cvm::debug()) cvm::log("Calculating fit gradients.\n"); + cvm::atom_group *group_for_fit = fitting_group ? fitting_group : this; + + auto accessor_main = [this](size_t i){return atoms[i].grad;}; + auto accessor_fitting = [&group_for_fit](size_t j, const cvm::rvector& grad){group_for_fit->fit_gradients[j] = grad;}; if (is_enabled(f_ag_center) && is_enabled(f_ag_rotate)) - calc_fit_gradients_impl(); + calc_fit_forces_impl(accessor_main, accessor_fitting); if (is_enabled(f_ag_center) && !is_enabled(f_ag_rotate)) - calc_fit_gradients_impl(); + calc_fit_forces_impl(accessor_main, accessor_fitting); if (!is_enabled(f_ag_center) && is_enabled(f_ag_rotate)) - calc_fit_gradients_impl(); + calc_fit_forces_impl(accessor_main, accessor_fitting); if (!is_enabled(f_ag_center) && !is_enabled(f_ag_rotate)) - calc_fit_gradients_impl(); + calc_fit_forces_impl(accessor_main, accessor_fitting); if (cvm::debug()) cvm::log("Done calculating fit gradients.\n"); } -template -void cvm::atom_group::calc_fit_gradients_impl() { - cvm::atom_group *group_for_fit = fitting_group ? fitting_group : this; +template +void cvm::atom_group::calc_fit_forces_impl( + main_force_accessor_T accessor_main, + fitting_force_accessor_T accessor_fitting) const { + const cvm::atom_group *group_for_fit = fitting_group ? fitting_group : this; // the center of geometry contribution to the gradients cvm::rvector atom_grad; // the rotation matrix contribution to the gradients @@ -1243,17 +1261,13 @@ void cvm::atom_group::calc_fit_gradients_impl() { cvm::vector1d dq0_1(4); // loop 1: iterate over the current atom group for (size_t i = 0; i < size(); i++) { - cvm::atom_pos pos_orig; if (B_ag_center) { - atom_grad += atoms[i].grad; - if (B_ag_rotate) pos_orig = rot_inv * (atoms[i].pos - ref_pos_cog); - } else { - if (B_ag_rotate) pos_orig = atoms[i].pos; + atom_grad += accessor_main(i); } if (B_ag_rotate) { // calculate \partial(R(q) \vec{x}_i)/\partial q) \cdot \partial\xi/\partial\vec{x}_i cvm::quaternion const dxdq = - rot.q.position_derivative_inner(pos_orig, atoms[i].grad); + rot.q.position_derivative_inner(pos_unrotated[i], accessor_main(i)); sum_dxdq[0] += dxdq[0]; sum_dxdq[1] += dxdq[1]; sum_dxdq[2] += dxdq[2]; @@ -1261,26 +1275,45 @@ void cvm::atom_group::calc_fit_gradients_impl() { } } if (B_ag_center) { - if (B_ag_rotate) atom_grad = rot.inverse().matrix() * atom_grad; + if (B_ag_rotate) atom_grad = rot_inv * atom_grad; atom_grad *= (-1.0)/(cvm::real(group_for_fit->size())); } // loop 2: iterate over the fitting group if (B_ag_rotate) rot_deriv->prepare_derivative(rotation_derivative_dldq::use_dq); for (size_t j = 0; j < group_for_fit->size(); j++) { + cvm::rvector fitting_force_grad{0, 0, 0}; if (B_ag_center) { - group_for_fit->fit_gradients[j] = atom_grad; + fitting_force_grad += atom_grad; } if (B_ag_rotate) { - rot_deriv->calc_derivative_wrt_group1(j, nullptr, &dq0_1); + rot_deriv->calc_derivative_wrt_group1(j, nullptr, &dq0_1); // multiply by {\partial q}/\partial\vec{x}_j and add it to the fit gradients - group_for_fit->fit_gradients[j] += sum_dxdq[0] * dq0_1[0] + - sum_dxdq[1] * dq0_1[1] + - sum_dxdq[2] * dq0_1[2] + - sum_dxdq[3] * dq0_1[3]; + fitting_force_grad += sum_dxdq[0] * dq0_1[0] + + sum_dxdq[1] * dq0_1[1] + + sum_dxdq[2] * dq0_1[2] + + sum_dxdq[3] * dq0_1[3]; } + if (cvm::debug()) { + cvm::log(cvm::to_str(fitting_force_grad)); + } + accessor_fitting(j, fitting_force_grad); } } +template +void cvm::atom_group::calc_fit_forces( + main_force_accessor_T accessor_main, + fitting_force_accessor_T accessor_fitting) const { + if (is_enabled(f_ag_center) && is_enabled(f_ag_rotate)) + calc_fit_forces_impl(accessor_main, accessor_fitting); + if (is_enabled(f_ag_center) && !is_enabled(f_ag_rotate)) + calc_fit_forces_impl(accessor_main, accessor_fitting); + if (!is_enabled(f_ag_center) && is_enabled(f_ag_rotate)) + calc_fit_forces_impl(accessor_main, accessor_fitting); + if (!is_enabled(f_ag_center) && !is_enabled(f_ag_rotate)) + calc_fit_forces_impl(accessor_main, accessor_fitting); +} + std::vector cvm::atom_group::positions() const { @@ -1452,17 +1485,72 @@ void cvm::atom_group::apply_force(cvm::rvector const &force) return; } - if (is_enabled(f_ag_rotate)) { + auto ag_force = get_group_force_object(); + for (size_t i = 0; i < size(); ++i) { + ag_force.add_atom_force(i, atoms[i].mass / total_mass * force); + } +} - const auto rot_inv = rot.inverse().matrix(); - for (cvm::atom_iter ai = this->begin(); ai != this->end(); ai++) { - ai->apply_force(rot_inv * ((ai->mass/total_mass) * force)); +cvm::atom_group::group_force_object cvm::atom_group::get_group_force_object() { + return cvm::atom_group::group_force_object(this); +} + +cvm::atom_group::group_force_object::group_force_object(cvm::atom_group* ag): +m_ag(ag), m_group_for_fit(m_ag->fitting_group ? m_ag->fitting_group : m_ag), +m_has_fitting_force(m_ag->is_enabled(f_ag_center) || m_ag->is_enabled(f_ag_rotate)) { + if (m_has_fitting_force) { + if (m_ag->group_forces.size() != m_ag->size()) { + m_ag->group_forces.assign(m_ag->size(), 0); + } else { + std::fill(m_ag->group_forces.begin(), + m_ag->group_forces.end(), 0); } + } +} +cvm::atom_group::group_force_object::~group_force_object() { + if (m_has_fitting_force) { + apply_force_with_fitting_group(); + } +} + +void cvm::atom_group::group_force_object::add_atom_force(size_t i, const cvm::rvector& force) { + if (m_has_fitting_force) { + m_ag->group_forces[i] += force; } else { + // Apply the force directly if we don't use fitting + (*m_ag)[i].apply_force(force); + } +} - for (cvm::atom_iter ai = this->begin(); ai != this->end(); ai++) { - ai->apply_force((ai->mass/total_mass) * force); +void cvm::atom_group::group_force_object::apply_force_with_fitting_group() { + const cvm::rmatrix rot_inv = m_ag->rot.inverse().matrix(); + if (cvm::debug()) { + cvm::log("Applying force on main group " + m_ag->name + ":\n"); + } + for (size_t ia = 0; ia < m_ag->size(); ++ia) { + const cvm::rvector f_ia = rot_inv * m_ag->group_forces[ia]; + (*m_ag)[ia].apply_force(f_ia); + if (cvm::debug()) { + cvm::log(cvm::to_str(f_ia)); + } + } + // Gradients are only available with scalar components, so for a scalar component, + // if f_ag_fit_gradients is disabled, then the forces on the fitting group is not + // computed. For a vector component, we can only know the forces on the fitting + // group, but checking this flag can mimic results that the users expect (if + // "enableFitGradients no" then there is no force on the fitting group). + if (!m_ag->b_dummy && m_ag->is_enabled(f_ag_fit_gradients)) { + auto accessor_main = [this](size_t i){return m_ag->group_forces[i];}; + auto accessor_fitting = [this](size_t j, const cvm::rvector& fitting_force){ + (*(m_group_for_fit))[j].apply_force(fitting_force); + }; + if (cvm::debug()) { + cvm::log("Applying force on the fitting group of main group" + m_ag->name + ":\n"); + } + m_ag->calc_fit_forces(accessor_main, accessor_fitting); + if (cvm::debug()) { + cvm::log("Done applying force on the fitting group of main group" + m_ag->name + ":\n"); } } } diff --git a/lib/colvars/colvaratoms.h b/lib/colvars/colvaratoms.h index d16ca7bd56..528e849df0 100644 --- a/lib/colvars/colvaratoms.h +++ b/lib/colvars/colvaratoms.h @@ -194,7 +194,7 @@ public: int add_atom_numbers(std::string const &numbers_conf); int add_atoms_of_group(atom_group const * ag); - int add_index_group(std::string const &index_group_name); + int add_index_group(std::string const &index_group_name, bool silent = false); int add_atom_numbers_range(std::string const &range_conf); int add_atom_name_residue_range(std::string const &psf_segid, std::string const &range_conf); @@ -257,8 +257,63 @@ protected: /// \brief Index in the colvarproxy arrays (if the group is scalable) int index; + /// \brief The temporary forces acting on the main group atoms. + /// Currently this is only used for calculating the fitting group forces for + /// non-scalar components. + std::vector group_forces; + public: + /*! @class group_force_object + * @brief A helper class for applying forces on an atom group in a way that + * is aware of the fitting group. NOTE: you are encouraged to use + * get_group_force_object() to get an instance of group_force_object + * instead of constructing directly. + */ + class group_force_object { + public: + /*! @brief Constructor of group_force_object + * @param ag The pointer to the atom group that forces will be applied on. + */ + group_force_object(cvm::atom_group* ag); + /*! @brief Destructor of group_force_object + */ + ~group_force_object(); + /*! @brief Apply force to atom i + * @param i The i-th of atom in the atom group. + * @param force The force being added to atom i. + * + * The function can be used as follows, + * @code + * // In your colvar::cvc::apply_force() loop of a component: + * auto ag_force = atoms->get_group_force_object(); + * for (ia = 0; ia < atoms->size(); ia++) { + * const cvm::rvector f = compute_force_on_atom_ia(); + * ag_force.add_atom_force(ia, f); + * } + * @endcode + * There are actually two scenarios under the hood: + * (i) If the atom group does not have a fitting group, then the force is + * added to atom i directly; + * (ii) If the atom group has a fitting group, the force on atom i will just + * be temporary stashed into ag->group_forces. At the end of the loop + * of apply_force(), the destructor ~group_force_object() will be called, + * which then call apply_force_with_fitting_group(). The forces on the + * main group will be rotated back by multiplying ag->group_forces with + * the inverse rotation. The forces on the fitting group (if + * enableFitGradients is on) will be calculated by calling + * calc_fit_forces. + */ + void add_atom_force(size_t i, const cvm::rvector& force); + private: + cvm::atom_group* m_ag; + cvm::atom_group* m_group_for_fit; + bool m_has_fitting_force; + void apply_force_with_fitting_group(); + }; + + group_force_object get_group_force_object(); + inline cvm::atom & operator [] (size_t const i) { return atoms[i]; @@ -423,6 +478,9 @@ private: /// \brief Center of geometry before any fitting cvm::atom_pos cog_orig; + /// \brief Unrotated atom positions for fit gradients + std::vector pos_unrotated; + public: /// \brief Return the center of geometry of the atomic positions @@ -497,15 +555,60 @@ public: /// \brief Calculate the derivatives of the fitting transformation void calc_fit_gradients(); -/*! @brief Actual implementation of `calc_fit_gradients`. The template is +/*! @brief Actual implementation of `calc_fit_gradients` and + * `calc_fit_forces`. The template is * used to avoid branching inside the loops in case that the CPU * branch prediction is broken (or further migration to GPU code). * @tparam B_ag_center Centered the reference to origin? This should follow * the value of `is_enabled(f_ag_center)`. * @tparam B_ag_rotate Calculate the optimal rotation? This should follow * the value of `is_enabled(f_ag_rotate)`. + * @tparam main_force_accessor_T The type of accessor of the main + * group forces or gradients acting on the rotated frame. + * @tparam fitting_force_accessor_T The type of accessor of the fitting group + * forces or gradients. + * @param accessor_main The accessor of the main group forces or gradients. + * accessor_main(i) should return the i-th force or gradient of the + * rotated main group. + * @param accessor_fitting The accessor of the fitting group forces or gradients. + * accessor_fitting(j, v) should store/apply the j-th atom gradient or + * force in the fitting group. + * + * This function is used to (i) project the gradients of CV with respect to + * rotated main group atoms to fitting group atoms, or (ii) project the forces + * on rotated main group atoms to fitting group atoms, by the following two steps + * (using the goal (ii) for example): + * (1) Loop over the positions of main group atoms and call cvm::quaternion::position_derivative_inner + * to project the forces on rotated main group atoms to the forces on quaternion. + * (2) Loop over the positions of fitting group atoms, compute the gradients of + * \f$\mathbf{q}\f$ with respect to the position of each atom, and then multiply + * that with the force on \f$\mathbf{q}\f$ (chain rule). */ - template void calc_fit_gradients_impl(); + template + void calc_fit_forces_impl( + main_force_accessor_T accessor_main, + fitting_force_accessor_T accessor_fitting) const; + +/*! @brief Calculate or apply the fitting group forces from the main group forces. + * @tparam main_force_accessor_T The type of accessor of the main + * group forces or gradients. + * @tparam fitting_force_accessor_T The type of accessor of the fitting group + * forces or gradients. + * @param accessor_main The accessor of the main group forces or gradients. + * accessor_main(i) should return the i-th force or gradient of the + * main group. + * @param accessor_fitting The accessor of the fitting group forces or gradients. + * accessor_fitting(j, v) should store/apply the j-th atom gradient or + * force in the fitting group. + * + * This function just dispatches the parameters to calc_fit_forces_impl that really + * performs the calculations. + */ + template + void calc_fit_forces( + main_force_accessor_T accessor_main, + fitting_force_accessor_T accessor_fitting) const; /// \brief Derivatives of the fitting transformation std::vector fit_gradients; diff --git a/lib/colvars/colvarbias.cpp b/lib/colvars/colvarbias.cpp index fdffdc1794..43b54d672f 100644 --- a/lib/colvars/colvarbias.cpp +++ b/lib/colvars/colvarbias.cpp @@ -93,6 +93,8 @@ int colvarbias::init(std::string const &conf) cvm::log("Reinitializing bias \""+name+"\".\n"); } + feature_states[f_cvb_step_zero_data].available = true; + colvar_values.resize(num_variables()); for (i = 0; i < num_variables(); i++) { colvar_values[i].type(colvars[i]->value().type()); @@ -157,7 +159,7 @@ int colvarbias::init_dependencies() { init_feature(f_cvb_step_zero_data, "step_zero_data", f_type_user); init_feature(f_cvb_apply_force, "apply_force", f_type_user); - require_feature_children(f_cvb_apply_force, f_cv_gradient); + require_feature_children(f_cvb_apply_force, f_cv_apply_force); init_feature(f_cvb_bypass_ext_lagrangian, "bypass_extended_Lagrangian_coordinates", f_type_user); @@ -199,6 +201,8 @@ int colvarbias::init_dependencies() { init_feature(f_cvb_extended, "Bias on extended-Lagrangian variables", f_type_static); + init_feature(f_cvb_smp, "smp_computation", f_type_user); + // check that everything is initialized for (i = 0; i < colvardeps::f_cvb_ntot; i++) { if (is_not_set(i)) { @@ -221,8 +225,9 @@ int colvarbias::init_dependencies() { // The feature f_cvb_bypass_ext_lagrangian is only implemented by some derived classes // (initially, harmonicWalls) feature_states[f_cvb_bypass_ext_lagrangian].available = false; - // disabled by default; can be changed by derived classes that implement it - feature_states[f_cvb_bypass_ext_lagrangian].enabled = false; + + // Most biases cannot currently be processed in parallel over threads + feature_states[f_cvb_smp].available = false; return COLVARS_OK; } @@ -704,7 +709,7 @@ int colvarbias::read_state_string(char const *buffer) std::ostream &colvarbias::write_state_data_key(std::ostream &os, std::string const &key, - bool header) + bool header) const { os << (header ? "\n" : "") << key << (header ? "\n" : " "); return os; @@ -712,7 +717,7 @@ std::ostream &colvarbias::write_state_data_key(std::ostream &os, std::string con cvm::memory_stream &colvarbias::write_state_data_key(cvm::memory_stream &os, std::string const &key, - bool /* header */) + bool /* header */) const { os << std::string(key); return os; @@ -792,6 +797,8 @@ int colvarbias_ti::init(std::string const &conf) { int error_code = COLVARS_OK; + key_lookup(conf, "grid", &grid_conf); + get_keyval_feature(this, conf, "writeTISamples", f_cvb_write_ti_samples, is_enabled(f_cvb_write_ti_samples)); @@ -800,18 +807,16 @@ int colvarbias_ti::init(std::string const &conf) f_cvb_write_ti_pmf, is_enabled(f_cvb_write_ti_pmf)); + if (is_enabled(f_cvb_write_ti_pmf)) { + enable(f_cvb_write_ti_samples); + } + if ((num_variables() > 1) && is_enabled(f_cvb_write_ti_pmf)) { return cvm::error("Error: only 1-dimensional PMFs can be written " "on the fly.\n" "Consider using writeTISamples instead and " "post-processing the sampled free-energy gradients.\n", COLVARS_NOT_IMPLEMENTED); - } else { - error_code |= init_grids(); - } - - if (is_enabled(f_cvb_write_ti_pmf)) { - enable(f_cvb_write_ti_samples); } if (is_enabled(f_cvb_calc_ti_samples)) { @@ -831,6 +836,8 @@ int colvarbias_ti::init(std::string const &conf) } } + error_code |= colvarbias_ti::init_grids(); + if (is_enabled(f_cvb_write_ti_pmf) || is_enabled(f_cvb_write_ti_samples)) { cvm::main()->cite_feature("Internal-forces free energy estimator"); } @@ -844,16 +851,15 @@ int colvarbias_ti::init_grids() if (is_enabled(f_cvb_calc_ti_samples)) { if (!ti_avg_forces) { ti_bin.resize(num_variables()); + ti_bin.assign(ti_bin.size(), -1); ti_system_forces.resize(num_variables()); for (size_t icv = 0; icv < num_variables(); icv++) { ti_system_forces[icv].type(variables(icv)->value()); ti_system_forces[icv].is_derivative(); ti_system_forces[icv].reset(); } - ti_avg_forces.reset(new colvar_grid_gradient(colvars)); - ti_count.reset(new colvar_grid_count(colvars)); - ti_avg_forces->samples = ti_count; - ti_count->has_parent_data = true; + ti_count.reset(new colvar_grid_count(colvars, grid_conf)); + ti_avg_forces.reset(new colvar_grid_gradient(colvars, ti_count)); } } @@ -884,8 +890,12 @@ int colvarbias_ti::update_system_forces(std::vector const size_t i; - if (proxy->total_forces_same_step()) { - for (i = 0; i < num_variables(); i++) { + if (cvm::debug()) { + cvm::log("TI bin for bias \"" + name + "\" = " + cvm::to_str(ti_bin) + ".\n"); + } + + for (i = 0; i < num_variables(); i++) { + if (variables(i)->is_enabled(f_cv_total_force_current_step)) { ti_bin[i] = ti_avg_forces->current_bin_scalar(i); } } @@ -894,8 +904,10 @@ int colvarbias_ti::update_system_forces(std::vector const if ((cvm::step_relative() > 0) || proxy->total_forces_same_step()) { if (ti_avg_forces->index_ok(ti_bin)) { for (i = 0; i < num_variables(); i++) { - if (variables(i)->is_enabled(f_cv_subtract_applied_force)) { + if (variables(i)->is_enabled(f_cv_subtract_applied_force) || + (cvm::proxy->total_forces_same_step() && !variables(i)->is_enabled(f_cv_external))) { // this colvar is already subtracting all applied forces + // or the "total force" is really a system force at current step ti_system_forces[i] = variables(i)->total_force(); } else { ti_system_forces[i] = variables(i)->total_force() - @@ -904,14 +916,17 @@ int colvarbias_ti::update_system_forces(std::vector const } } if (cvm::step_relative() > 0 || is_enabled(f_cvb_step_zero_data)) { + if (cvm::debug()) { + cvm::log("Accumulating TI forces for bias \"" + name + "\".\n"); + } ti_avg_forces->acc_value(ti_bin, ti_system_forces); } } } - if (!proxy->total_forces_same_step()) { - // Set the index for use in the next iteration, when total forces come in - for (i = 0; i < num_variables(); i++) { + for (i = 0; i < num_variables(); i++) { + if (!variables(i)->is_enabled(f_cv_total_force_current_step)) { + // Set the index for use in the next iteration, when total forces come in ti_bin[i] = ti_avg_forces->current_bin_scalar(i); } } diff --git a/lib/colvars/colvarbias.h b/lib/colvars/colvarbias.h index 03f93f4315..35438752a1 100644 --- a/lib/colvars/colvarbias.h +++ b/lib/colvars/colvarbias.h @@ -174,14 +174,14 @@ public: /// \param[in,out] os Output stream /// \param[in] key Keyword labeling the header block /// \param[in] header Whether this is the header of a multi-line segment vs a single line - std::ostream &write_state_data_key(std::ostream &os, std::string const &key, bool header = true); + std::ostream &write_state_data_key(std::ostream &os, std::string const &key, bool header = true) const; /// Write a keyword header for a data sequence to an unformatted stream /// \param[in,out] os Output stream /// \param[in] key Keyword labeling the header block /// \param[in] header Ignored cvm::memory_stream &write_state_data_key(cvm::memory_stream &os, std::string const &key, - bool header = true); + bool header = true) const; private: @@ -358,6 +358,9 @@ protected: /// \brief Forces exerted from the system to the associated variables std::vector ti_system_forces; + /// Grid configuration parameters (also used by grids in derived classes) + std::string grid_conf; + /// Averaged system forces std::shared_ptr ti_avg_forces; diff --git a/lib/colvars/colvarbias_abf.cpp b/lib/colvars/colvarbias_abf.cpp index 6327650863..b01e9de853 100644 --- a/lib/colvars/colvarbias_abf.cpp +++ b/lib/colvars/colvarbias_abf.cpp @@ -87,24 +87,25 @@ int colvarbias_abf::init(std::string const &conf) get_keyval(conf, "shared", shared_on, false); if (shared_on) { cvm::main()->cite_feature("Multiple-walker ABF implementation"); - if ((proxy->replica_enabled() != COLVARS_OK) || - (proxy->num_replicas() <= 1)) { - return cvm::error("Error: shared ABF requires more than one replica.", - COLVARS_INPUT_ERROR); - } - cvm::log("shared ABF will be applied among "+ - cvm::to_str(proxy->num_replicas()) + " replicas.\n"); + cvm::main()->cite_feature("Updated multiple-walker ABF implementation"); + + + // Cannot check this here because the replica communicator is obtained later + // in Gromacs + + // if ((proxy->check_replicas_enabled() != COLVARS_OK) || + // (proxy->num_replicas() <= 1)) { + // return cvm::error("Error: shared ABF requires more than one replica.", + // COLVARS_INPUT_ERROR); + // } + // cvm::log("shared ABF will be applied among "+ + // cvm::to_str(proxy->num_replicas()) + " replicas.\n"); // If shared_freq is not set, we default to output_freq get_keyval(conf, "sharedFreq", shared_freq, output_freq); if ( shared_freq && output_freq % shared_freq ) { return cvm::error("Error: outputFreq must be a multiple of sharedFreq.\n"); } - - // Allocate these at init time if possible - local_samples.reset(new colvar_grid_count(colvars)); - local_gradients.reset(new colvar_grid_gradient(colvars, local_samples)); - local_pmf.reset(new integrate_potential(colvars, local_gradients)); } // ************* checking the associated colvars ******************* @@ -124,10 +125,17 @@ int colvarbias_abf::init(std::string const &conf) colvars[i]->enable(f_cv_hide_Jacobian); } - // If any colvar is extended-system, we need to collect the extended - // system gradient - if (colvars[i]->is_enabled(f_cv_extended_Lagrangian)) + // If any colvar is extended-system (restrained, not driven external param), we are running eABF + if (colvars[i]->is_enabled(f_cv_extended_Lagrangian) + && !colvars[i]->is_enabled(f_cv_external)) { enable(f_cvb_extended); + } + + if (!colvars[i]->is_enabled(f_cv_total_force_current_step)) { + // If any colvar does not have current-step total force, then + // we can't do step 0 data + provide(f_cvb_step_zero_data, false); + } // Cannot mix and match coarse time steps with ABF because it gives // wrong total force averages - total force needs to be averaged over @@ -181,12 +189,23 @@ int colvarbias_abf::init(std::string const &conf) cvm::log("Allocating count and free energy gradient grids.\n"); } - samples.reset(new colvar_grid_count(colvars)); - gradients.reset(new colvar_grid_gradient(colvars, samples)); + { + /// Optional custom configuration string for grid parameters + std::string grid_conf; + key_lookup(conf, "grid", &grid_conf); + + samples.reset(new colvar_grid_count(colvars, grid_conf)); + } + gradients.reset(new colvar_grid_gradient(colvars, samples)); // Also use samples as template for sizes gradients->full_samples = full_samples; gradients->min_samples = min_samples; + if (shared_on) { + local_samples.reset(new colvar_grid_count(colvars, samples)); + local_gradients.reset(new colvar_grid_gradient(colvars, local_samples)); + } + // Data for eABF z-based estimator if (is_enabled(f_cvb_extended)) { get_keyval(conf, "CZARestimator", b_CZAR_estimator, true); @@ -198,11 +217,11 @@ int colvarbias_abf::init(std::string const &conf) colvarparse::parse_silent); z_bin.assign(num_variables(), 0); - z_samples.reset(new colvar_grid_count(colvars)); + z_samples.reset(new colvar_grid_count(colvars, samples)); z_samples->request_actual_value(); z_gradients.reset(new colvar_grid_gradient(colvars, z_samples)); z_gradients->request_actual_value(); - czar_gradients.reset(new colvar_grid_gradient(colvars)); + czar_gradients.reset(new colvar_grid_gradient(colvars, nullptr, samples)); } get_keyval(conf, "integrate", b_integrate, num_variables() <= 3); // Integrate for output if d<=3 @@ -216,6 +235,9 @@ int colvarbias_abf::init(std::string const &conf) if (b_CZAR_estimator) { czar_pmf.reset(new integrate_potential(colvars, czar_gradients)); } + if (shared_on) { + local_pmf.reset(new integrate_potential(colvars, local_gradients)); + } // Parameters for integrating initial (and final) gradient data get_keyval(conf, "integrateMaxIterations", integrate_iterations, 10000, colvarparse::parse_silent); get_keyval(conf, "integrateTol", integrate_tol, 1e-6, colvarparse::parse_silent); @@ -228,9 +250,9 @@ int colvarbias_abf::init(std::string const &conf) if (b_CZAR_estimator && shared_on && cvm::main()->proxy->replica_index() == 0) { // The pointers below are used for outputting CZAR data // Allocate grids for collected global data, on replica 0 only - global_z_samples.reset(new colvar_grid_count(colvars)); + global_z_samples.reset(new colvar_grid_count(colvars, samples)); global_z_gradients.reset(new colvar_grid_gradient(colvars, global_z_samples)); - global_czar_gradients.reset(new colvar_grid_gradient(colvars)); + global_czar_gradients.reset(new colvar_grid_gradient(colvars, nullptr, samples)); global_czar_pmf.reset(new integrate_potential(colvars, global_czar_gradients)); } else { // otherwise they are just aliases for the local CZAR grids @@ -244,10 +266,10 @@ int colvarbias_abf::init(std::string const &conf) // This used to be only if "shared" was defined, // but now we allow calling share externally (e.g. from Tcl). if (b_CZAR_estimator) { - z_samples_in.reset(new colvar_grid_count(colvars)); + z_samples_in.reset(new colvar_grid_count(colvars, samples)); z_gradients_in.reset(new colvar_grid_gradient(colvars, z_samples_in)); } - last_samples.reset(new colvar_grid_count(colvars)); + last_samples.reset(new colvar_grid_count(colvars, samples)); last_gradients.reset(new colvar_grid_gradient(colvars, last_samples)); // Any data collected after now is new for shared ABF purposes shared_last_step = cvm::step_absolute(); @@ -315,27 +337,36 @@ int colvarbias_abf::update() size_t i; for (i = 0; i < num_variables(); i++) { bin[i] = samples->current_bin_scalar(i); + if (colvars[i]->is_enabled(f_cv_total_force_current_step)) { + force_bin[i] = bin[i]; + } } - // *********************************************************** // ****** ABF Part I: update the FE gradient estimate ****** // *********************************************************** - if (cvm::proxy->total_forces_same_step()) { - // e.g. in LAMMPS, total forces are current - force_bin = bin; + // Share data first, so that 2d/3d PMF is refreshed using new data for mw-pABF. + // shared_on can be true with shared_freq 0 if we are sharing via script + if (shared_on && shared_freq && + shared_last_step >= 0 && // we have already collected some data + cvm::step_absolute() > shared_last_step && // time has passed since the last sharing timestep + // (avoid re-sharing at last and first ts of successive run statements) + cvm::step_absolute() % shared_freq == 0) { + // Share gradients and samples for shared ABF. + replica_share(); } if (can_accumulate_data() && is_enabled(f_cvb_history_dependent)) { if (cvm::step_relative() > 0 || cvm::proxy->total_forces_same_step()) { + // Note: this will skip step 0 data when available in some cases (extended system), + // but not doing so would make the code more complex if (samples->index_ok(force_bin)) { // Only if requested and within bounds of the grid... - // get total forces (lagging by 1 timestep) from colvars - // and subtract previous ABF force if necessary + // get total force and subtract previous ABF force if necessary update_system_force(); gradients->acc_force(force_bin, system_force); @@ -368,21 +399,11 @@ int colvarbias_abf::update() } } - if (!(cvm::proxy->total_forces_same_step())) { - // e.g. in NAMD, total forces will be available for next timestep - // hence we store the current colvar bin - force_bin = bin; - } + // In some cases, total forces are stored for next timestep + // hence we store the current colvar bin - this is overwritten on a per-colvar basis + // at the top of update() + force_bin = bin; - // Share data after force sample is collected for this time step - // shared_on can be true with shared_freq 0 if we are sharing via script - if (shared_on && shared_freq && - cvm::step_absolute() > shared_last_step && // time has passed since the last sharing timestep - // (avoid re-sharing at last and first ts of successive run statements) - cvm::step_absolute() % shared_freq == 0) { - // Share gradients and samples for shared ABF. - replica_share(); - } // ****************************************************************** // ****** ABF Part II: calculate and apply the biasing force ****** @@ -452,10 +473,13 @@ int colvarbias_abf::update_system_force() // System force from atomic forces (or extended Lagrangian if applicable) for (i = 0; i < num_variables(); i++) { - if (colvars[i]->is_enabled(f_cv_subtract_applied_force)) { + if (colvars[i]->is_enabled(f_cv_subtract_applied_force) + || colvars[i]->is_enabled(f_cv_total_force_current_step)) { // this colvar is already subtracting the ABF force + // or the "total force" is from current step and cannot possibly contain Colvars biases system_force[i] = colvars[i]->total_force().real_value; } else { + // Subtract previous step's bias force from previous step's total force system_force[i] = colvars[i]->total_force().real_value - colvar_forces[i].real_value; } @@ -525,7 +549,7 @@ int colvarbias_abf::replica_share() { colvarproxy *proxy = cvm::main()->proxy; - if (proxy->replica_enabled() != COLVARS_OK) { + if (proxy->check_replicas_enabled() != COLVARS_OK) { cvm::error("Error: shared ABF: No replicas.\n"); return COLVARS_ERROR; } @@ -542,7 +566,7 @@ int colvarbias_abf::replica_share() { if (!local_samples) { // We arrive here if sharing has just been enabled by a script // in which case local arrays have not been initialized yet - local_samples.reset(new colvar_grid_count(colvars)); + local_samples.reset(new colvar_grid_count(colvars, samples)); local_gradients.reset(new colvar_grid_gradient(colvars, local_samples)); local_pmf.reset(new integrate_potential(colvars, local_gradients)); } @@ -662,9 +686,9 @@ int colvarbias_abf::replica_share_CZAR() { // We arrive here if sharing has just been enabled by a script // Allocate grids for collective data, on replica 0 only // overriding CZAR grids that are equal to local ones by default - global_z_samples.reset(new colvar_grid_count(colvars)); + global_z_samples.reset(new colvar_grid_count(colvars, samples)); global_z_gradients.reset(new colvar_grid_gradient(colvars, global_z_samples)); - global_czar_gradients.reset(new colvar_grid_gradient(colvars)); + global_czar_gradients.reset(new colvar_grid_gradient(colvars, nullptr, samples)); global_czar_pmf.reset(new integrate_potential(colvars, global_czar_gradients)); } diff --git a/lib/colvars/colvarbias_histogram.cpp b/lib/colvars/colvarbias_histogram.cpp index 98de275304..aab2c8f593 100644 --- a/lib/colvars/colvarbias_histogram.cpp +++ b/lib/colvars/colvarbias_histogram.cpp @@ -98,10 +98,10 @@ int colvarbias_histogram::init(std::string const &conf) } { - std::string grid_conf; - if (key_lookup(conf, "histogramGrid", &grid_conf)) { + if (key_lookup(conf, "histogramGrid", &grid_conf) || + key_lookup(conf, "grid", &grid_conf)) { grid->parse_params(grid_conf); - grid->check_keywords(grid_conf, "histogramGrid"); + grid->check_keywords(grid_conf, "grid"); } } diff --git a/lib/colvars/colvarbias_histogram.h b/lib/colvars/colvarbias_histogram.h index 2c6ee84d1f..ed3ee346a6 100644 --- a/lib/colvars/colvarbias_histogram.h +++ b/lib/colvars/colvarbias_histogram.h @@ -38,6 +38,7 @@ protected: /// n-dim histogram colvar_grid_scalar *grid; + std::string grid_conf; std::vector bin; std::string out_name, out_name_dx; diff --git a/lib/colvars/colvarbias_histogram_reweight_amd.cpp b/lib/colvars/colvarbias_histogram_reweight_amd.cpp index de2f6d9b8a..3c31e175f2 100644 --- a/lib/colvars/colvarbias_histogram_reweight_amd.cpp +++ b/lib/colvars/colvarbias_histogram_reweight_amd.cpp @@ -11,43 +11,9 @@ #include "colvarproxy.h" #include "colvars_memstream.h" -colvarbias_reweightaMD::colvarbias_reweightaMD(char const *key) - : colvarbias_histogram(key), grid_count(NULL), grid_dV(NULL), - grid_dV_square(NULL), pmf_grid_exp_avg(NULL), pmf_grid_cumulant(NULL), - grad_grid_exp_avg(NULL), grad_grid_cumulant(NULL) -{ -} +colvarbias_reweightaMD::colvarbias_reweightaMD(char const *key) : colvarbias_histogram(key) {} -colvarbias_reweightaMD::~colvarbias_reweightaMD() { - if (grid_dV) { - delete grid_dV; - grid_dV = NULL; - } - if (grid_dV_square) { - delete grid_dV_square; - grid_dV_square = NULL; - } - if (grid_count) { - delete grid_count; - grid_count = NULL; - } - if (pmf_grid_exp_avg) { - delete pmf_grid_exp_avg; - pmf_grid_exp_avg = NULL; - } - if (pmf_grid_cumulant) { - delete pmf_grid_cumulant; - pmf_grid_cumulant = NULL; - } - if (grad_grid_exp_avg) { - delete grad_grid_exp_avg; - grad_grid_exp_avg = NULL; - } - if (grad_grid_cumulant) { - delete grad_grid_cumulant; - grad_grid_cumulant = NULL; - } -} +colvarbias_reweightaMD::~colvarbias_reweightaMD() {} int colvarbias_reweightaMD::init(std::string const &conf) { if (cvm::proxy->accelMD_enabled() == false) { @@ -60,21 +26,21 @@ int colvarbias_reweightaMD::init(std::string const &conf) { get_keyval(conf, "WritePMFGradients", b_write_gradients, true); get_keyval(conf, "historyFreq", history_freq, 0); b_history_files = (history_freq > 0); - grid_count = new colvar_grid_scalar(colvars); + grid_count.reset(new colvar_grid_scalar(colvars, nullptr, false, grid_conf)); grid_count->request_actual_value(); grid->request_actual_value(); - pmf_grid_exp_avg = new colvar_grid_scalar(colvars); + pmf_grid_exp_avg.reset(new colvar_grid_scalar(colvars, grid_count)); if (b_write_gradients) { - grad_grid_exp_avg = new colvar_grid_gradient(colvars); + grad_grid_exp_avg.reset(new colvar_grid_gradient(colvars, nullptr, grid_count)); } if (b_use_cumulant_expansion) { - grid_dV = new colvar_grid_scalar(colvars); - grid_dV_square = new colvar_grid_scalar(colvars); - pmf_grid_cumulant = new colvar_grid_scalar(colvars); + grid_dV.reset(new colvar_grid_scalar(colvars, grid_count)); + grid_dV_square.reset(new colvar_grid_scalar(colvars, grid_count)); + pmf_grid_cumulant.reset(new colvar_grid_scalar(colvars, grid_count)); grid_dV->request_actual_value(); grid_dV_square->request_actual_value(); if (b_write_gradients) { - grad_grid_cumulant = new colvar_grid_gradient(colvars); + grad_grid_cumulant.reset(new colvar_grid_gradient(colvars, nullptr, grid_count)); } } previous_bin.assign(num_variables(), -1); @@ -193,7 +159,7 @@ int colvarbias_reweightaMD::write_exponential_reweighted_pmf( pmf_grid_exp_avg->set_value(i, tmp / count); } } - hist_to_pmf(pmf_grid_exp_avg, grid_count); + hist_to_pmf(pmf_grid_exp_avg.get(), grid_count.get()); pmf_grid_exp_avg->write_multicol(pmf_grid_os); if (!keep_open) { cvm::proxy->close_output_stream(output_pmf); @@ -231,9 +197,9 @@ int colvarbias_reweightaMD::write_cumulant_expansion_pmf( if (!pmf_grid_cumulant_os) { return COLVARS_FILE_ERROR; } - compute_cumulant_expansion_factor(grid_dV, grid_dV_square, - grid_count, pmf_grid_cumulant); - hist_to_pmf(pmf_grid_cumulant, grid_count); + compute_cumulant_expansion_factor(grid_dV.get(), grid_dV_square.get(), + grid_count.get(), pmf_grid_cumulant.get()); + hist_to_pmf(pmf_grid_cumulant.get(), grid_count.get()); pmf_grid_cumulant->write_multicol(pmf_grid_cumulant_os); if (!keep_open) { cvm::proxy->close_output_stream(output_pmf); diff --git a/lib/colvars/colvarbias_histogram_reweight_amd.h b/lib/colvars/colvarbias_histogram_reweight_amd.h index 43759b3bde..3e3bdede26 100644 --- a/lib/colvars/colvarbias_histogram_reweight_amd.h +++ b/lib/colvars/colvarbias_histogram_reweight_amd.h @@ -68,9 +68,9 @@ protected: /// Use cumulant expansion to second order? bool b_use_cumulant_expansion; - colvar_grid_scalar* grid_count; - colvar_grid_scalar* grid_dV; - colvar_grid_scalar* grid_dV_square; + std::shared_ptr grid_count; + std::unique_ptr grid_dV; + std::unique_ptr grid_dV_square; /// Number of timesteps between recording data in history files (if non-zero) size_t history_freq; @@ -90,10 +90,10 @@ protected: private: /// temporary grids for evaluating PMFs - colvar_grid_scalar *pmf_grid_exp_avg; - colvar_grid_scalar *pmf_grid_cumulant; - colvar_grid_gradient *grad_grid_exp_avg; - colvar_grid_gradient *grad_grid_cumulant; + std::unique_ptr pmf_grid_exp_avg; + std::unique_ptr pmf_grid_cumulant; + std::unique_ptr grad_grid_exp_avg; + std::unique_ptr grad_grid_cumulant; }; #endif // COLVARBIAS_HISTOGRAM_REWEIGHT_AMD diff --git a/lib/colvars/colvarbias_meta.cpp b/lib/colvars/colvarbias_meta.cpp index 905cd17883..1131c88ec5 100644 --- a/lib/colvars/colvarbias_meta.cpp +++ b/lib/colvars/colvarbias_meta.cpp @@ -11,27 +11,10 @@ #include #include -// Define function to get the absolute path of a replica file -#if defined(_WIN32) && !defined(__CYGWIN__) -#include -#define GETCWD(BUF, SIZE) ::_getcwd(BUF, SIZE) -#define PATHSEP "\\" -#else -#include -#define GETCWD(BUF, SIZE) ::getcwd(BUF, SIZE) -#define PATHSEP "/" -#endif - -#ifdef __cpp_lib_filesystem -// When std::filesystem is available, use it -#include -#undef GETCWD -#define GETCWD(BUF, SIZE) (std::filesystem::current_path().string().c_str()) -#endif - #include "colvarmodule.h" #include "colvarproxy.h" #include "colvar.h" +#include "colvargrid.h" #include "colvarbias_meta.h" #include "colvars_memstream.h" @@ -49,8 +32,6 @@ colvarbias_meta::colvarbias_meta(char const *key) use_grids = true; grids_freq = 0; rebin_grids = false; - hills_energy = NULL; - hills_energy_gradients = NULL; dump_fes = true; keep_hills = false; @@ -161,9 +142,9 @@ int colvarbias_meta::init(std::string const &conf) get_keyval(conf, "keepHills", keep_hills, keep_hills); get_keyval(conf, "keepFreeEnergyFiles", dump_fes_save, dump_fes_save); - if (hills_energy == NULL) { - hills_energy = new colvar_grid_scalar(colvars); - hills_energy_gradients = new colvar_grid_gradient(colvars); + if (!hills_energy) { + hills_energy.reset(new colvar_grid_scalar(colvars, nullptr, false, grid_conf)); + hills_energy_gradients.reset(new colvar_grid_gradient(colvars, nullptr, hills_energy)); } } else { @@ -209,7 +190,7 @@ int colvarbias_meta::init_replicas_params(std::string const &conf) get_keyval(conf, "replicaID", replica_id, replica_id); if (!replica_id.size()) { - if (proxy->replica_enabled() == COLVARS_OK) { + if (proxy->check_replicas_enabled() == COLVARS_OK) { // Obtain replicaID from the communicator replica_id = cvm::to_str(proxy->replica_index()); cvm::log("Setting replicaID from communication layer: replicaID = "+ @@ -272,7 +253,6 @@ int colvarbias_meta::init_ebmeta_params(std::string const &conf) { int error_code = COLVARS_OK; // for ebmeta - target_dist = NULL; get_keyval(conf, "ebMeta", ebmeta, false); if(ebmeta){ cvm::main()->cite_feature("Ensemble-biased metadynamics (ebMetaD)"); @@ -283,7 +263,7 @@ int colvarbias_meta::init_ebmeta_params(std::string const &conf) "targetDistFile accordingly.\n", COLVARS_INPUT_ERROR); } - target_dist = new colvar_grid_scalar(); + target_dist.reset(new colvar_grid_scalar()); error_code |= target_dist->init_from_colvars(colvars); std::string target_dist_file; get_keyval(conf, "targetDistFile", target_dist_file); @@ -336,33 +316,15 @@ colvarbias_meta::~colvarbias_meta() { colvarbias_meta::clear_state_data(); colvarproxy *proxy = cvm::main()->proxy; - proxy->close_output_stream(replica_hills_file); - proxy->close_output_stream(hills_traj_file_name()); - - if (target_dist) { - delete target_dist; - target_dist = NULL; - } } int colvarbias_meta::clear_state_data() { - if (hills_energy) { - delete hills_energy; - hills_energy = NULL; - } - - if (hills_energy_gradients) { - delete hills_energy_gradients; - hills_energy_gradients = NULL; - } - hills.clear(); hills_off_grid.clear(); - return COLVARS_OK; } @@ -451,8 +413,11 @@ int colvarbias_meta::update() error_code |= update_grid_params(); // add new biasing energy/forces error_code |= update_bias(); - // update grid content to reflect new bias - error_code |= update_grid_data(); + + if (use_grids) { + // update grid content to reflect new bias + error_code |= update_grid_data(); + } if (comm != single_replica && (cvm::step_absolute() % replica_update_freq) == 0) { @@ -539,9 +504,9 @@ int colvarbias_meta::update_grid_params() // map everything into new grids colvar_grid_scalar *new_hills_energy = - new colvar_grid_scalar(*hills_energy); + new colvar_grid_scalar(*hills_energy); colvar_grid_gradient *new_hills_energy_gradients = - new colvar_grid_gradient(*hills_energy_gradients); + new colvar_grid_gradient(*hills_energy_gradients); // supply new boundaries to the new grids @@ -556,10 +521,8 @@ int colvarbias_meta::update_grid_params() new_hills_energy->map_grid(*hills_energy); new_hills_energy_gradients->map_grid(*hills_energy_gradients); - delete hills_energy; - delete hills_energy_gradients; - hills_energy = new_hills_energy; - hills_energy_gradients = new_hills_energy_gradients; + hills_energy.reset(new_hills_energy); + hills_energy_gradients.reset(new_hills_energy_gradients); curr_bin = hills_energy->get_colvars_index(); if (cvm::debug()) @@ -641,8 +604,7 @@ int colvarbias_meta::update_grid_data() { if ((cvm::step_absolute() % grids_freq) == 0) { // map the most recent gaussians to the grids - project_hills(new_hills_begin, hills.end(), - hills_energy, hills_energy_gradients); + project_hills(new_hills_begin, hills.end(), hills_energy.get(), hills_energy_gradients.get()); new_hills_begin = hills.end(); // TODO: we may want to condense all into one replicas array, @@ -651,8 +613,8 @@ int colvarbias_meta::update_grid_data() for (size_t ir = 0; ir < replicas.size(); ir++) { replicas[ir]->project_hills(replicas[ir]->new_hills_begin, replicas[ir]->hills.end(), - replicas[ir]->hills_energy, - replicas[ir]->hills_energy_gradients); + replicas[ir]->hills_energy.get(), + replicas[ir]->hills_energy_gradients.get()); replicas[ir]->new_hills_begin = replicas[ir]->hills.end(); } } @@ -670,11 +632,20 @@ int colvarbias_meta::calc_energy(std::vector const *values) replicas[ir]->bias_energy = 0.0; } - std::vector const curr_bin = values ? - hills_energy->get_colvars_index(*values) : - hills_energy->get_colvars_index(); + bool index_ok = false; + std::vector curr_bin; - if (hills_energy->index_ok(curr_bin)) { + if (use_grids) { + + curr_bin = values ? + hills_energy->get_colvars_index(*values) : + hills_energy->get_colvars_index(); + + index_ok = hills_energy->index_ok(curr_bin); + + } + + if ( index_ok ) { // index is within the grid: get the energy from there for (ir = 0; ir < replicas.size(); ir++) { @@ -723,11 +694,20 @@ int colvarbias_meta::calc_forces(std::vector const *values) } } - std::vector const curr_bin = values ? - hills_energy->get_colvars_index(*values) : - hills_energy->get_colvars_index(); + bool index_ok = false; + std::vector curr_bin; - if (hills_energy->index_ok(curr_bin)) { + if (use_grids) { + + curr_bin = values ? + hills_energy->get_colvars_index(*values) : + hills_energy->get_colvars_index(); + + index_ok = hills_energy->index_ok(curr_bin); + + } + + if ( index_ok ) { for (ir = 0; ir < replicas.size(); ir++) { cvm::real const *f = &(replicas[ir]->hills_energy_gradients->value(curr_bin)); for (ic = 0; ic < num_variables(); ic++) { @@ -959,8 +939,7 @@ void colvarbias_meta::project_hills(colvarbias_meta::hill_iter h_first, void colvarbias_meta::recount_hills_off_grid(colvarbias_meta::hill_iter h_first, - colvarbias_meta::hill_iter h_last, - colvar_grid_scalar * /* he */) + colvarbias_meta::hill_iter h_last) { hills_off_grid.clear(); @@ -1078,9 +1057,13 @@ int colvarbias_meta::update_replicas_registry() (replicas.back())->comm = multiple_replicas; if (use_grids) { - (replicas.back())->hills_energy = new colvar_grid_scalar(colvars); - (replicas.back())->hills_energy_gradients = new colvar_grid_gradient(colvars); + (replicas.back()) + ->hills_energy.reset(new colvar_grid_scalar(colvars, hills_energy)); + (replicas.back()) + ->hills_energy_gradients.reset( + new colvar_grid_gradient(colvars, nullptr, hills_energy)); } + if (is_enabled(f_cvb_calc_ti_samples)) { (replicas.back())->enable(f_cvb_calc_ti_samples); (replicas.back())->colvarbias_ti::init_grids(); @@ -1336,34 +1319,40 @@ template IST &colvarbias_meta::read_state_data_template_(IST &is) { if (use_grids) { - colvar_grid_scalar *hills_energy_backup = NULL; - colvar_grid_gradient *hills_energy_gradients_backup = NULL; + std::shared_ptr hills_energy_backup; + std::shared_ptr hills_energy_gradients_backup; - if (has_data) { + bool const need_backup = has_data; + + if (need_backup) { if (cvm::debug()) - cvm::log("Backupping grids for metadynamics bias \""+ - this->name+"\""+ - ((comm != single_replica) ? ", replica \""+replica_id+"\"" : "")+".\n"); - hills_energy_backup = hills_energy; - hills_energy_gradients_backup = hills_energy_gradients; - hills_energy = new colvar_grid_scalar(colvars); - hills_energy_gradients = new colvar_grid_gradient(colvars); + cvm::log("Backing up grids for metadynamics bias \"" + this->name + "\"" + + ((comm != single_replica) ? ", replica \"" + replica_id + "\"" : "") + ".\n"); + + hills_energy_backup = std::move(hills_energy); + hills_energy_gradients_backup = std::move(hills_energy_gradients); + hills_energy.reset(new colvar_grid_scalar(colvars, hills_energy)); + hills_energy_gradients.reset(new colvar_grid_gradient(colvars, nullptr, hills_energy)); } - read_grid_data_template_(is, "hills_energy", hills_energy, - hills_energy_backup); + read_grid_data_template_(is, "hills_energy", hills_energy.get(), + hills_energy_backup.get()); - read_grid_data_template_( - is, "hills_energy_gradients", hills_energy_gradients, hills_energy_gradients_backup); + read_grid_data_template_(is, "hills_energy_gradients", + hills_energy_gradients.get(), + hills_energy_gradients_backup.get()); if (is) { cvm::log(" successfully read the biasing potential and its gradients from grids.\n"); - if (hills_energy_backup != nullptr) { - // Now that we have successfully updated the grids, delete the backup copies - delete hills_energy_backup; - delete hills_energy_gradients_backup; - } } else { + if (need_backup) { + if (cvm::debug()) + cvm::log("Restoring grids from backup for metadynamics bias \"" + this->name + "\"" + + ((comm != single_replica) ? ", replica \"" + replica_id + "\"" : "") + ".\n"); + // Restoring content from original grid + hills_energy->copy_grid(*hills_energy_backup); + hills_energy_gradients->copy_grid(*hills_energy_gradients_backup); + } return is; } } @@ -1451,10 +1440,12 @@ void colvarbias_meta::rebin_grids_after_restart() // read from the configuration file), and project onto them the // grids just read from the restart file - colvar_grid_scalar *new_hills_energy = - new colvar_grid_scalar(colvars); - colvar_grid_gradient *new_hills_energy_gradients = - new colvar_grid_gradient(colvars); + // Create new grids based on the configuration parameters, because reading from the state + // file automatically sets the old parameters + std::shared_ptr new_hills_energy( + new colvar_grid_scalar(colvars, nullptr, false, grid_conf)); + std::shared_ptr new_hills_energy_gradients( + new colvar_grid_gradient(colvars, nullptr, new_hills_energy)); if (cvm::debug()) { std::ostringstream tmp_os; @@ -1468,9 +1459,9 @@ void colvarbias_meta::rebin_grids_after_restart() if (restart_keep_hills && !hills.empty()) { // if there are hills, recompute the new grids from them cvm::log("Rebinning the energy and forces grids from "+ - cvm::to_str(hills.size())+" hills (this may take a while)...\n"); - project_hills(hills.begin(), hills.end(), - new_hills_energy, new_hills_energy_gradients, true); + cvm::to_str(hills.size())+" hills (this may take a bit)...\n"); + project_hills(hills.begin(), hills.end(), new_hills_energy.get(), + new_hills_energy_gradients.get(), true); cvm::log("rebinning done.\n"); } else { @@ -1481,15 +1472,13 @@ void colvarbias_meta::rebin_grids_after_restart() new_hills_energy_gradients->map_grid(*hills_energy_gradients); } - delete hills_energy; - delete hills_energy_gradients; - hills_energy = new_hills_energy; - hills_energy_gradients = new_hills_energy_gradients; + hills_energy = std::move(new_hills_energy); + hills_energy_gradients = std::move(new_hills_energy_gradients); // assuming that some boundaries have expanded, eliminate those // off-grid hills that aren't necessary any more if (!hills.empty()) - recount_hills_off_grid(hills.begin(), hills.end(), hills_energy); + recount_hills_off_grid(hills.begin(), hills.end()); } } @@ -1718,29 +1707,17 @@ int colvarbias_meta::setup_output() if (comm == multiple_replicas) { - // TODO: one may want to specify the path manually for intricated filesystems? - char *pwd = new char[3001]; - if (GETCWD(pwd, 3000) == nullptr) { - if (pwd != nullptr) { // - delete[] pwd; - } - return cvm::error("Error: cannot get the path of the current working directory.\n", - COLVARS_BUG_ERROR); - } - + auto const pwd = cvm::main()->proxy->get_current_work_dir(); replica_list_file = - (std::string(pwd)+std::string(PATHSEP)+ - this->name+"."+replica_id+".files.txt"); + cvm::main()->proxy->join_paths(pwd, this->name + "." + replica_id + ".files.txt"); // replica_hills_file and replica_state_file are those written // by the current replica; within the mirror biases, they are // those by another replica - replica_hills_file = - (std::string(pwd)+std::string(PATHSEP)+ - cvm::output_prefix()+".colvars."+this->name+"."+replica_id+".hills"); - replica_state_file = - (std::string(pwd)+std::string(PATHSEP)+ - cvm::output_prefix()+".colvars."+this->name+"."+replica_id+".state"); - delete[] pwd; + replica_hills_file = cvm::main()->proxy->join_paths( + pwd, cvm::output_prefix() + ".colvars." + this->name + "." + replica_id + ".hills"); + + replica_state_file = cvm::main()->proxy->join_paths( + pwd, cvm::output_prefix() + ".colvars." + this->name + "." + replica_id + ".state"); // now register this replica @@ -1842,7 +1819,7 @@ template OST &colvarbias_meta::write_state_data_template_(OST &os // this is a very good time to project hills, if you haven't done // it already! - project_hills(new_hills_begin, hills.end(), hills_energy, hills_energy_gradients); + project_hills(new_hills_begin, hills.end(), hills_energy.get(), hills_energy_gradients.get()); new_hills_begin = hills.end(); // write down the grids to the restart file diff --git a/lib/colvars/colvarbias_meta.h b/lib/colvars/colvarbias_meta.h index a765a60c71..57aa21ed6b 100644 --- a/lib/colvars/colvarbias_meta.h +++ b/lib/colvars/colvarbias_meta.h @@ -10,12 +10,16 @@ #ifndef COLVARBIAS_META_H #define COLVARBIAS_META_H -#include -#include #include +#include +#include +#include #include "colvarbias.h" -#include "colvargrid.h" + +class colvar_grid_scalar; +class colvar_grid_gradient; + /// Metadynamics bias (implementation of \link colvarbias \endlink) @@ -123,8 +127,7 @@ protected: hill_iter new_hills_off_grid_begin; /// Regenerate the hills_off_grid list - void recount_hills_off_grid(hill_iter h_first, hill_iter h_last, - colvar_grid_scalar *ge); + void recount_hills_off_grid(hill_iter h_first, hill_iter h_last); template OST &write_hill_template_(OST &os, colvarbias_meta::hill const &h); @@ -211,7 +214,7 @@ protected: bool ebmeta; /// Target distribution for EBmeta - colvar_grid_scalar* target_dist; + std::unique_ptr target_dist; /// Number of equilibration steps for EBmeta cvm::step_number ebmeta_equil_steps; @@ -223,15 +226,14 @@ protected: bool safely_read_restart; /// Hill energy, cached on a grid - colvar_grid_scalar *hills_energy; + std::shared_ptr hills_energy; /// Hill forces, cached on a grid - colvar_grid_gradient *hills_energy_gradients; + std::shared_ptr hills_energy_gradients; - /// \brief Project the selected hills onto grids - void project_hills(hill_iter h_first, hill_iter h_last, - colvar_grid_scalar *ge, colvar_grid_gradient *gf, - bool print_progress = false); + /// Project the selected hills onto grids + void project_hills(hill_iter h_first, hill_iter h_last, colvar_grid_scalar *ge, + colvar_grid_gradient *gf, bool print_progress = false); // Multiple Replicas variables and functions diff --git a/lib/colvars/colvarbias_opes.cpp b/lib/colvars/colvarbias_opes.cpp new file mode 100644 index 0000000000..9caeb967e0 --- /dev/null +++ b/lib/colvars/colvarbias_opes.cpp @@ -0,0 +1,1996 @@ +// This code is mainly adapted from the PLUMED opes module, which uses the +// LGPLv3 license as shown below: +/* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + Copyright (c) 2020-2021 of Michele Invernizzi. + + This file is part of the OPES plumed module. + + The OPES plumed module is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + The OPES plumed module is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with plumed. If not, see . ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ + +#include "colvarbias_opes.h" +#include "colvarbias.h" +#include "colvardeps.h" +#include "colvarproxy.h" +#include "colvars_memstream.h" +#include "colvargrid.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +colvarbias_opes::colvarbias_opes(char const *key): + colvarbias(key), m_kbt(0), m_barrier(0), m_biasfactor(0), + m_bias_prefactor(0), m_temperature(0), + m_pace(0), m_adaptive_sigma_stride(0), + m_adaptive_counter(0), m_counter(1), + m_compression_threshold(0), m_compression_threshold2(0), + m_adaptive_sigma(false), m_fixed_sigma(false), + m_no_zed(false), m_nlist(false), m_recursive_merge(true), + m_nlist_param(2, 0), m_epsilon(0), m_sum_weights(0), + m_sum_weights2(0), m_cutoff(0), m_cutoff2(0), + m_zed(1), m_old_kdenorm(0), m_kdenorm(0), + m_val_at_cutoff(0), m_nlist_center(0), m_nlist_index(0), + m_nlist_steps(0), m_nlist_update(false), + m_nlist_pace_reset(false), m_nker(0), m_calc_work(false), + m_work(0), comm(single_replica), m_num_walkers(1), + m_num_threads(1), m_nlker(0), m_traj_output_frequency(0), + m_traj_line(traj_line{0}), m_is_first_step(true), + m_pmf_grid_on(false), m_reweight_grid(nullptr), + m_pmf_grid(nullptr), m_pmf_hist_freq(0), m_pmf_shared(true), + m_explore(false), m_inf_biasfactor(false) +{ +#ifdef OPES_THREADING + provide(f_cvb_smp, cvm::proxy->get_smp_mode() == colvarproxy_smp::smp_mode_t::inner_loop); + if (is_available(f_cv_smp)){ + enable(f_cvb_smp); // Enabled by default + } +#endif +} + +int colvarbias_opes::init(const std::string& conf) { + int error_code = colvarbias::init(conf); + enable(f_cvb_scalar_variables); + get_keyval_feature(this, conf, "applyBias", f_cvb_apply_force, true); + m_temperature = cvm::proxy->target_temperature(); + m_kbt = m_temperature * cvm::proxy->boltzmann(); + get_keyval(conf, "newHillFrequency", m_pace); + get_keyval(conf, "barrier", m_barrier); + get_keyval(conf, "explore", m_explore, false); + if (m_barrier < 0) { + return cvm::error("the barrier should be greater than zero", COLVARS_INPUT_ERROR); + } + std::string biasfactor_str; + get_keyval(conf, "biasfactor", biasfactor_str); + if ((cvm::proxy->target_temperature() == 0.0) && cvm::proxy->simulation_running()) { + cvm::log("WARNING: OPES should not be run without a thermostat or at 0 Kelvin!\n"); + } + m_biasfactor = m_barrier / m_kbt; + m_inf_biasfactor = biasfactor_str == "inf" || biasfactor_str == "INF"; + if (m_inf_biasfactor) { + m_biasfactor = std::numeric_limits::infinity(); + m_bias_prefactor = 1; + if (m_explore) { + return cvm::error("biasfactor cannot be infinity in the explore mode."); + } + } else { + if (biasfactor_str.size() > 0) { + try { + m_biasfactor = std::stod(biasfactor_str); + } catch (const std::exception& e) { + return cvm::error(e.what(), COLVARS_INPUT_ERROR); + } + } + if (m_biasfactor <= 1.0) { + return cvm::error("biasfactor must be greater than one (use \"inf\" for uniform target)"); + } + m_bias_prefactor = 1 - 1.0 / m_biasfactor; + } + if (m_explore) { + m_bias_prefactor = m_biasfactor - 1; + } + get_keyval(conf, "adaptiveSigma", m_adaptive_sigma, false); + m_sigma0.resize(num_variables()); + get_keyval(conf, "gaussianSigma", m_sigma0, std::vector(num_variables())); + m_av_cv.assign(num_variables(), 0); + m_av_M2.assign(num_variables(), 0); + if (m_adaptive_sigma) { + get_keyval(conf, "adaptiveSigmaStride", m_adaptive_sigma_stride, 0); + if (m_inf_biasfactor) { + return cvm::error("cannot use infinite biasfactor with adaptive sigma", + COLVARS_INPUT_ERROR); + } + if (m_adaptive_sigma_stride == 0) { + m_adaptive_sigma_stride = m_pace * 10; + } + if (m_adaptive_sigma_stride < m_pace) { + return cvm::error("It is better to choose an adaptiveSigmaStride >= newHillFrequency.\n", COLVARS_INPUT_ERROR); + } + } else { + if (m_sigma0.size() != num_variables()) { + return cvm::error("number of sigma parameters does not match the number of variables", + COLVARS_INPUT_ERROR); + } + if (m_explore) { + for (size_t i = 0; i < num_variables(); ++i) { + m_sigma0[i] *= std::sqrt(m_biasfactor); + } + } + } + get_keyval(conf, "gaussianSigmaMin", m_sigma_min); + if ((m_sigma_min.size() != 0) && (m_sigma_min.size() != num_variables())) { + return cvm::error("incorrect number of parameters of gaussianSigmaMin"); + } + if (m_sigma_min.size() > 0 && !m_adaptive_sigma) { + for (size_t i = 0; i < num_variables(); ++i) { + if (m_sigma_min[i] > m_sigma0[i]) { + return cvm::error("gaussianSigmaMin of variable " + cvm::to_str(i) + " should be smaller than sigma"); + } + } + } + get_keyval(conf, "epsilon", m_epsilon, std::exp(-m_barrier/m_bias_prefactor/m_kbt)); + if (m_epsilon <= 0) { + return cvm::error("you must choose a value of epsilon greater than zero"); + } + m_sum_weights = std::pow(m_epsilon, m_bias_prefactor); + m_sum_weights2 = m_sum_weights * m_sum_weights; + if (m_explore) { + get_keyval(conf, "kernelCutoff", m_cutoff, std::sqrt(2.0*m_barrier/m_kbt)); + } else { + get_keyval(conf, "kernelCutoff", m_cutoff, std::sqrt(2.0*m_barrier/m_bias_prefactor/m_kbt)); + } + if (m_cutoff <= 0) { + return cvm::error("you must choose a value of kernelCutoff greater than zero"); + } + m_cutoff2 = m_cutoff * m_cutoff; + m_val_at_cutoff = std::exp(-0.5 * m_cutoff2); + get_keyval(conf, "compressionThreshold", m_compression_threshold, 1); + if (m_compression_threshold != 0) { + if (m_compression_threshold < 0 || m_compression_threshold > m_cutoff) { + return cvm::error("compressionThreshold cannot be smaller than 0 or larger than kernelCutoff", COLVARS_INPUT_ERROR); + } + } + m_compression_threshold2 = m_compression_threshold * m_compression_threshold; + get_keyval(conf, "neighborList", m_nlist, false); + if (m_nlist) { + get_keyval(conf, "neighborListNewHillReset", m_nlist_pace_reset, false); + std::vector nlist_param; + get_keyval(conf, "neighborListParameters", nlist_param, std::vector()); + if (nlist_param.empty()) { + m_nlist_param[0] = 3.0; //*cutoff2_ -> max distance of neighbors + m_nlist_param[1] = 0.5; //*nlist_dev2_[i] -> condition for rebuilding + } else { + if (nlist_param.size() != 2) { + return cvm::error("two cutoff parameters are needed for the neighbor list", COLVARS_INPUT_ERROR); + } + if (nlist_param[0] <= 1.0) { + return cvm::error("the first of neighborListParam must be greater than 1.0. The smaller the first, the smaller should be the second as well", COLVARS_INPUT_ERROR); + } + const cvm::real min_PARAM_1 = (1.-1./std::sqrt(nlist_param[0]))+0.16; + if (nlist_param[1] <= 0) { + return cvm::error("the second of neighborListParam must be greater than 0", COLVARS_INPUT_ERROR); + } + if (nlist_param[1] > min_PARAM_1) { + return cvm::error("the second of neighborListParam must be smaller to avoid systematic errors. Largest suggested value is: 1.16-1/sqrt(param_0) = " + cvm::to_str(min_PARAM_1), COLVARS_INPUT_ERROR); + } + m_nlist_param = nlist_param; + } + m_nlist_center.resize(num_variables()); + m_nlist_dev2.resize(num_variables(), 0); + m_nlist_steps = 0; + m_nlist_update = true; + } + get_keyval(conf, "noZed", m_no_zed, false); + if (m_no_zed) { + m_sum_weights = 1; + m_sum_weights2 = 1; + } + get_keyval(conf, "fixedGaussianSigma", m_fixed_sigma, false); + get_keyval(conf, "recursiveMerge", m_recursive_merge, true); + get_keyval(conf, "calcWork", m_calc_work, false); + bool b_replicas = false; + get_keyval(conf, "multipleReplicas", b_replicas, false); + +#ifdef OPES_THREADING + get_keyval_feature(this, conf, "smp", f_cvb_smp, is_enabled(f_cvb_smp)); + if (is_enabled(f_cv_smp)) { + m_num_threads = cvm::proxy->smp_num_threads(); + } else { + m_num_threads = 1; + } +#else + // if (m_num_threads > 1) { + // return cvm::error("Multithreading in OPES is not compiled.\n"); + // } + m_num_threads = 1; +#endif + bool serial = false; + get_keyval(conf, "serial", serial, false); + if (serial) m_num_threads = 1; + comm = b_replicas ? multiple_replicas : single_replica; + if (comm == multiple_replicas) { + colvarproxy *proxy = cvm::main()->proxy; + get_keyval(conf, "replicaID", replica_id, replica_id); + get_keyval(conf, "sharedFreq", shared_freq, output_freq); + if (!replica_id.size()) { + if (proxy->check_replicas_enabled() == COLVARS_OK) { + // Obtain replicaID from the communicator + replica_id = cvm::to_str(proxy->replica_index()); + cvm::log("Setting replicaID from communication layer: replicaID = "+ + replica_id+".\n"); + } else { + return cvm::error("Error: using more than one replica, but replicaID " + "could not be obtained.\n", COLVARS_INPUT_ERROR); + } + } + m_num_walkers = proxy->num_replicas(); + } + get_keyval(conf, "pmf", m_pmf_grid_on, false); + if (m_pmf_grid_on) { + std::vector pmf_cv_name; + get_keyval(conf, "pmfColvars", pmf_cv_name); + for (auto it = pmf_cv_name.begin(); it != pmf_cv_name.end(); ++it) { + bool found = false; + for (size_t i = 0; i < num_variables(); ++i) { + if (variables(i)->name == (*it)) { + if (variables(i)->enable(f_cv_grid) != COLVARS_OK) { + return cvm::error("CV " + (*it) + " does not support grid\n"); + } + m_pmf_cvs.push_back(variables(i)); + found = true; + break; + } + } + if (!found) { + return cvm::error("CV " + (*it) + " not found\n"); + } + } + key_lookup(conf, "grid", &grid_conf); + m_reweight_grid.reset(new colvar_grid_scalar(m_pmf_cvs, nullptr, false, grid_conf)); + m_pmf_grid.reset(new colvar_grid_scalar(m_pmf_cvs, m_reweight_grid)); + get_keyval(conf, "pmfHistoryFrequency", m_pmf_hist_freq, 0); + if (comm == multiple_replicas) { + get_keyval(conf, "pmfShared", m_pmf_shared, true); + if (m_pmf_shared) { + m_global_reweight_grid.reset(new colvar_grid_scalar(m_pmf_cvs, m_reweight_grid)); + m_global_pmf_grid.reset(new colvar_grid_scalar(m_pmf_cvs, m_reweight_grid)); + } + } + } + m_kdenorm = m_explore? m_counter : m_sum_weights; + m_old_kdenorm = m_kdenorm; + m_traj_line.rct = m_kbt * cvm::logn(m_sum_weights / m_counter); + m_traj_line.zed = m_zed; + m_traj_line.neff = (1 + m_sum_weights) * (1 + m_sum_weights) / (1 + m_sum_weights2); + m_traj_line.nker = m_kernels.size(); + get_keyval(conf, "printTrajectoryFrequency", m_traj_output_frequency, cvm::cv_traj_freq); + m_cv.resize(num_variables(), 0); + showInfo(); + return error_code; +} + +void colvarbias_opes::showInfo() const { + // Print information about this bias + auto printInfo = [&](const std::string& info, const std::string& val){ + cvm::log(this->name + ": " + info + val + "\n"); + }; + printInfo("temperature = ", cvm::to_str(m_kbt / cvm::proxy->boltzmann())); + printInfo("beta = ", cvm::to_str(1.0 / m_kbt)); + printInfo("depositing new kernels with newHillFrequency = ", cvm::to_str(m_pace)); + printInfo("expected barrier is ", cvm::to_str(m_barrier)); + printInfo("using target distribution with biasfactor (gamma) = ", m_inf_biasfactor ? "inf" : cvm::to_str(m_biasfactor)); + if (m_inf_biasfactor) { + cvm::log(" (thus a uniform flat target distribution, no well-tempering)\n"); + cvm::log(this->name + ": " + "the equivalent bias temperature = inf\n"); + } else { + cvm::log(this->name + ": " + "the equivalent bias temperature = " + cvm::to_str(cvm::proxy->target_temperature() * (m_biasfactor - 1))); + } + if (m_adaptive_sigma) { + printInfo("adaptive sigma will be used, with adaptiveSigmaStride = ", cvm::to_str(m_adaptive_sigma_stride)); + size_t x = std::ceil(m_adaptive_sigma_stride / m_pace); + printInfo(" thus the first x kernel depositions will be skipped, x = adaptiveSigmaStride/newHillFrequency = ", cvm::to_str(x)); + } else { + std::string sigmas; + for (size_t i = 0; i < num_variables(); ++i) { + sigmas += " " + cvm::to_str(m_sigma0[i]); + } + cvm::log(this->name + ": kernels have initial gaussianSigma = " + sigmas + "\n"); + } + if (m_fixed_sigma) { + cvm::log(this->name + " fixedGaussianSigma: gaussianSigma will not decrease as the simulation proceeds\n"); + } + printInfo("kernels are truncated with kernelCutoff = ", cvm::to_str(m_cutoff)); + if (m_cutoff < 3.5) { + cvm::log(this->name + " +++ WARNING +++ probably kernels are truncated too much\n"); + } + printInfo("the value at cutoff is = ", cvm::to_str(m_val_at_cutoff)); + printInfo("regularization epsilon = ", cvm::to_str(m_epsilon)); + if (m_val_at_cutoff > m_epsilon*(1+1e-6)) { + cvm::log(this->name + " +++ WARNING +++ the kernelCutoff might be too small for the given epsilon\n"); + } + printInfo("kernels will be compressed when closer than compression_threshold = ", cvm::to_str(m_compression_threshold)); + if (m_compression_threshold2 == 0) { + cvm::log(this->name + " +++ WARNING +++ kernels will never merge, expect slowdowns\n"); + } + if (!m_recursive_merge) { + cvm::log(this->name + " -- RECURSIVE_MERGE_OFF: only one merge for each new kernel will be attempted. This is faster only if total number of kernels does not grow too much\n"); + } + if (m_nlist) { + cvm::log(this->name + " neighborList: using neighbor list for kernels, with parameters: " + cvm::to_str(m_nlist_param[0]) + " " + cvm::to_str(m_nlist_param[1]) + "\n"); + if (m_nlist_pace_reset) { + cvm::log(this->name + " neighborListNewHillReset: forcing the neighbor list to update every time when depositing a new hill\n"); + } + } + if (m_no_zed) { + printInfo("noZed: using fixed normalization factor = ", cvm::to_str(m_zed)); + } + if (comm == multiple_replicas && m_num_walkers > 1) { + cvm::log(this->name + " if multiple replicas are present, they will share the same bias\n"); + } + if (m_num_threads > 1) { + printInfo("using multiple threads per simulation: ", cvm::to_str(m_num_threads)); + } + cvm::main()->cite_feature("OPES"); + if (m_adaptive_sigma || m_explore) { + cvm::main()->cite_feature("OPES explore or adaptive kernels"); + } +} + +cvm::real colvarbias_opes::evaluateKernel( + const colvarbias_opes::kernel& G, + const std::vector& x) const { + cvm::real norm2 = 0; + for (size_t i = 0; i < num_variables(); ++i) { + const cvm::real dist2_i = variables(i)->dist2(G.m_center[i], x[i]) / (G.m_sigma[i] * G.m_sigma[i]); + norm2 += dist2_i; + if (norm2 >= m_cutoff2) { + return 0; + } + } + return G.m_height * (std::exp(-0.5 * norm2) - m_val_at_cutoff); +} + +cvm::real colvarbias_opes::evaluateKernel( + const colvarbias_opes::kernel& G, + const std::vector& x, + std::vector& accumulated_derivative, + std::vector& dist) const { + cvm::real norm2 = 0; + for (size_t i = 0; i < num_variables(); ++i) { + dist[i] = 0.5 * variables(i)->dist2_lgrad(x[i], G.m_center[i]) / G.m_sigma[i]; + norm2 += dist[i] * dist[i]; + if (norm2 >= m_cutoff2) { + return 0; + } + } + const cvm::real val = G.m_height * (std::exp(-0.5 * norm2) - m_val_at_cutoff); + // The derivative of norm2 with respect to x + for (size_t i = 0; i < num_variables(); ++i) { + accumulated_derivative[i] -= val * dist[i] / G.m_sigma[i]; + } + return val; +} + +cvm::real colvarbias_opes::getProbAndDerivatives( + const std::vector& cv, std::vector& der_prob) const { + double prob = 0.0; + std::vector dist(num_variables(), 0); + if (!m_nlist) { + if (m_num_threads == 1 || m_kernels.size() < 2 * m_num_threads) { + for (size_t k = 0; k < m_kernels.size(); ++k) { + prob += evaluateKernel(m_kernels[k], cv, der_prob, dist); + } + } else { +#if defined(_OPENMP) + #pragma omp parallel num_threads(m_num_threads) + { + std::vector omp_deriv(der_prob.size(), 0); + std::vector tmp_dist(num_variables()); + #pragma omp for reduction(+:prob) nowait + for (int k = 0; k < static_cast(m_kernels.size()); ++k) { + prob += evaluateKernel(m_kernels[k], cv, omp_deriv, tmp_dist); + } + #pragma omp critical + for (int i = 0; i < static_cast(num_variables()); ++i) { + der_prob[i]+=omp_deriv[i]; + } + #pragma omp single + for (int i = 0; i < static_cast(num_variables()); ++i) { + dist[i] = tmp_dist[i]; + } + } +#elif defined(CMK_SMP) && defined(USE_CKLOOP) + // TODO: Test this once fine-grained parallelization is enabled + std::vector> derivs(m_num_threads, std::vector(num_variables(), 0)); + std::vector> dists(m_num_threads, std::vector(num_variables(), 0)); + auto worker = [&](int start, int end, void* result){ + const int tid = cvm::proxy->smp_thread_id(); + double tmp_prob = 0; + for (int i = start; i <= end; ++i) { + tmp_prob += evaluateKernel(m_kernels[i], cv, derivs[tid], dists[tid]); + } + *(double *)result = tmp_prob; + }; + const size_t numChunks = m_kernels.size(); + const size_t lowerRange = 0; + const size_t upperRange = numChunks - 1; + CkLoop_Parallelize( + numChunks, lowerRange, upperRange, + worker, &prob, CKLOOP_DOUBLE_SUM, NULL); + for (size_t i = 0; i < num_variables(); ++i) { + for (size_t j = 0; j < m_num_threads; ++j) { + if (j == 0) dist[i] = dists[j][i]; + der_prob[i] += derivs[j][i]; + } + } +#else + cvm::error("multiple threads required in OPES, but this binary is not linked with a supported threading library.\n"); +#endif + } + } else { + if (m_num_threads == 1 || m_nlist_index.size() < 2 * m_num_threads) { + for (size_t nk = 0; nk < m_nlist_index.size(); ++nk) { + const size_t k = m_nlist_index[nk]; + prob += evaluateKernel(m_kernels[k], cv, der_prob, dist); + } + } else { +#if defined(_OPENMP) + #pragma omp parallel num_threads(m_num_threads) + { + std::vector omp_deriv(der_prob.size(), 0); + std::vector tmp_dist(num_variables()); + #pragma omp for reduction(+:prob) nowait + for (int nk = 0; nk < static_cast(m_nlist_index.size()); ++nk) { + const size_t k = m_nlist_index[nk]; + prob += evaluateKernel(m_kernels[k], cv, omp_deriv, tmp_dist); + } + #pragma omp critical + for (int i = 0; i < static_cast(num_variables()); ++i) { + der_prob[i]+=omp_deriv[i]; + } + #pragma omp single + for (int i = 0; i < static_cast(num_variables()); ++i) { + dist[i] = tmp_dist[i]; + } + } +#elif defined(CMK_SMP) && defined(USE_CKLOOP) + // TODO: Test this once fine-grained parallelization is enabled + std::vector> derivs(m_num_threads, std::vector(num_variables(), 0)); + std::vector> dists(m_num_threads, std::vector(num_variables(), 0)); + auto worker = [&](int start, int end, void* result){ + const int tid = cvm::proxy->smp_thread_id(); + double tmp_prob = 0; + for (int i = start; i <= end; ++i) { + const size_t k = m_nlist_index[i]; + tmp_prob += evaluateKernel(m_kernels[k], cv, derivs[tid], dists[tid]); + } + *(double *)result = tmp_prob; + }; + const size_t numChunks = m_nlist_index.size(); + const size_t lowerRange = 0; + const size_t upperRange = numChunks - 1; + CkLoop_Parallelize( + numChunks, lowerRange, upperRange, + worker, &prob, CKLOOP_DOUBLE_SUM, NULL); + for (size_t i = 0; i < num_variables(); ++i) { + for (size_t j = 0; j < m_num_threads; ++j) { + if (j == 0) dist[i] = dists[j][i]; + der_prob[i] += derivs[j][i]; + } + } +#else + cvm::error("multiple threads required in OPES, but this binary is not linked with a supported threading library.\n"); +#endif + } + } + prob /= m_kdenorm; + for (size_t i = 0; i < num_variables(); ++i) { + der_prob[i] /= m_kdenorm; + } + return prob; +} + +int colvarbias_opes::calculate_opes() { + if (m_nlist) { + ++m_nlist_steps; + const bool exchange_step = + (comm == multiple_replicas) && + cvm::step_absolute() % shared_freq == 0; + if (exchange_step) { + m_nlist_update = true; + } else { + for (size_t i = 0; i < num_variables(); ++i) { + const cvm::real diff_i2 = variables(i)->dist2(m_cv[i], m_nlist_center[i]); + if (diff_i2 > m_nlist_param[1] * m_nlist_dev2[i]) { + m_nlist_update = true; + break; + } + } + } + if (m_nlist_update) { + updateNlist(m_cv); + } + } + std::vector der_prob(num_variables(), 0); + const cvm::real prob = getProbAndDerivatives(m_cv, der_prob); + const cvm::real bias = m_kbt * m_bias_prefactor * cvm::logn(prob / m_zed + m_epsilon); + bias_energy = bias; + if (is_enabled(f_cvb_apply_force)) { + for (size_t i = 0; i < num_variables(); ++i) { + colvar_forces[i] = -m_kbt * m_bias_prefactor / (prob / m_zed + m_epsilon) * der_prob[i] / m_zed; + } + } + return COLVARS_OK; +} + +int colvarbias_opes::update_opes() { + if (m_adaptive_sigma) { + m_adaptive_counter++; + cvm::step_number tau = m_adaptive_sigma_stride; + if (m_adaptive_counter < m_adaptive_sigma_stride) tau = m_adaptive_counter; + for (size_t i = 0; i < num_variables(); ++i) { + // Welford's online algorithm for standard deviation + const cvm::real diff_i = 0.5 * variables(i)->dist2_lgrad(m_cv[i], m_av_cv[i]); + m_av_cv[i] += diff_i / tau; + m_av_M2[i] += diff_i * 0.5 * variables(i)->dist2_lgrad(m_cv[i], m_av_cv[i]); + } + if (m_adaptive_counter < m_adaptive_sigma_stride && m_counter == 1) { + return COLVARS_OK;; + } + } + if (cvm::step_absolute() % m_pace == 0) { + m_old_kdenorm = m_kdenorm; + m_delta_kernels.clear(); + const size_t old_nker = m_kernels.size(); + // TODO: how could I account for extra biases in Colvars? + const cvm::real log_weight = bias_energy / m_kbt; + cvm::real height = cvm::exp(log_weight); + cvm::real sum_heights = height; + cvm::real sum_heights2 = height * height; + if (m_num_walkers > 1) { + std::vector replica_sum_heights(cvm::proxy->num_replicas() - 1, 0); + // Send all sum_heights to PE 0 + if (cvm::proxy->replica_index() == 0) { + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + if (cvm::proxy->replica_comm_recv((char*)&(replica_sum_heights[p - 1]), sizeof(cvm::real), p) != sizeof(cvm::real)) { + return cvm::error("Error: receiving sum of weights from replica " + cvm::to_str(p)); + } + } + } else { + if (cvm::proxy->replica_comm_send((char*)&sum_heights, sizeof(cvm::real), 0) != sizeof(cvm::real)) { + return cvm::error("Error: sending sum of weights to replica 0."); + } + } + cvm::proxy->replica_comm_barrier(); + // PE 0 sum all sum_heights and broadcast + if (cvm::proxy->replica_index() == 0) { + for (auto it = replica_sum_heights.begin(); it != replica_sum_heights.end(); ++it) { + sum_heights += (*it); + } + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + if (cvm::proxy->replica_comm_send((char*)&sum_heights, sizeof(cvm::real), p) != sizeof(cvm::real)) { + return cvm::error("Error: sending sum of weights to replica " + cvm::to_str(p)); + } + } + } else { + if (cvm::proxy->replica_comm_recv((char*)&sum_heights, sizeof(cvm::real), 0) != sizeof(cvm::real)) { + return cvm::error("Error: receiving sum of weights from replica 0."); + } + } + cvm::proxy->replica_comm_barrier(); + // Send all sum_heights2 to PE 0 + std::vector replica_sum_heights2(cvm::proxy->num_replicas() - 1, 0); + if (cvm::proxy->replica_index() == 0) { + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + if (cvm::proxy->replica_comm_recv((char*)&(replica_sum_heights2[p - 1]), sizeof(cvm::real), p) != sizeof(cvm::real)) { + return cvm::error("Error: getting sum of weights2 from replica " + cvm::to_str(p)); + } + } + } else { + if (cvm::proxy->replica_comm_send((char*)&sum_heights2, sizeof(cvm::real), 0) != sizeof(cvm::real)) { + return cvm::error("Error: sending sum of weights2 from replica."); + } + } + cvm::proxy->replica_comm_barrier(); + // PE 0 sum all sum_heights2 and broadcast + if (cvm::proxy->replica_index() == 0) { + for (auto it = replica_sum_heights2.begin(); it != replica_sum_heights2.end(); ++it) { + sum_heights2 += (*it); + } + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + if (cvm::proxy->replica_comm_send((char*)&sum_heights2, sizeof(cvm::real), p) != sizeof(cvm::real)) { + return cvm::error("Error: sending sum of weights2 to replica " + cvm::to_str(p)); + } + } + } else { + if (cvm::proxy->replica_comm_recv((char*)&sum_heights2, sizeof(cvm::real), 0) != sizeof(cvm::real)) { + return cvm::error("Error: receiving sum of weights2 from replica."); + } + } + cvm::proxy->replica_comm_barrier(); + } + m_counter += m_num_walkers; + m_sum_weights += sum_heights; + m_sum_weights2 += sum_heights2; + m_neff = (1 + m_sum_weights) * (1 + m_sum_weights) / (1 + m_sum_weights2); + m_rct = m_kbt * cvm::logn(m_sum_weights / m_counter); + m_traj_line.neff = m_neff; + m_traj_line.rct = m_rct; + if (m_explore) { + m_kdenorm = m_counter; + height = 1.0; + } else { + m_kdenorm = m_sum_weights; + } + std::vector sigma = m_sigma0; + if (m_adaptive_sigma) { + const cvm::real factor = m_explore ? 1.0 : m_biasfactor; + if (m_counter == 1 + m_num_walkers) { + for (size_t i = 0; i < num_variables(); ++i) { + m_av_M2[i] *= m_biasfactor; + } + for (size_t i = 0; i < num_variables(); ++i) { + m_sigma0[i] = std::sqrt(m_av_M2[i] / m_adaptive_counter / factor); + } + if (m_sigma_min.size() == 0) { + for (size_t i = 0; i < num_variables(); ++i) { + if (m_sigma0[i] < 1e-6) { + cvm::error("Adaptive sigma is suspiciously small for CV " + cvm::to_str(i) + "\nManually provide sigma or set a safe sigma_min to avoid possible issues\n"); + return COLVARS_ERROR; + } + } + } else { + for (size_t i = 0; i < num_variables(); ++i) { + m_sigma0[i] = std::max(m_sigma0[i], m_sigma_min[i]); + } + } + } + for (size_t i = 0; i < num_variables(); ++i) { + sigma[i] = std::sqrt(m_av_M2[i] / m_adaptive_counter / factor); + } + if (m_sigma_min.size() == 0) { + bool sigma_less_than_threshold = false; + for (size_t i = 0; i < num_variables(); ++i) { + if (sigma[i] < 1e-6) { + cvm::log("The adaptive sigma is suspiciously small, you should set a safe sigma_min. 1e-6 will be used here\n"); + sigma[i] = 1e-6; + sigma_less_than_threshold = true; + } + } + if (sigma_less_than_threshold) { + m_sigma_min.assign(num_variables(), 1e-6); + } + } else { + for (size_t i = 0; i < num_variables(); ++i) { + sigma[i] = std::max(sigma[i], m_sigma_min[i]); + } + } + } + if (!m_fixed_sigma) { + const cvm::real size = m_explore ? m_counter : m_neff; + const size_t ncv = num_variables(); + const cvm::real s_rescaling = std::pow(size * (ncv + 2.0) / 4, -1.0 / (4.0 + ncv)); + for (size_t i = 0; i < num_variables(); ++i) { + sigma[i] *= s_rescaling; + } + if (m_sigma_min.size() > 0) { + for (size_t i = 0; i < num_variables(); ++i) { + sigma[i] = std::max(sigma[i], m_sigma_min[i]); + } + } + } + // the height should be divided by sqrt(2*pi)*sigma0_, + // but this overall factor would be canceled when dividing by Zed + // thus we skip it altogether, but keep any other sigma rescaling + for (size_t i = 0; i < num_variables(); ++i) { + height *= (m_sigma0[i] / sigma[i]); + } + if (m_num_walkers == 1) { + addKernel(height, m_cv, sigma, log_weight); + } else { + std::vector all_height(m_num_walkers, 0.0); + std::vector all_center(m_num_walkers * num_variables(), 0.0); + std::vector all_sigma(m_num_walkers * num_variables(), 0.0); + std::vector all_logweight(m_num_walkers, 0.0); + const int my_replica = cvm::proxy->replica_index(); + + // Allgather of heights + if (my_replica == 0) { + all_height[0] = height; + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + if (cvm::proxy->replica_comm_recv((char*)&(all_height[p]), sizeof(decltype(all_height)::value_type), p) != sizeof(decltype(all_height)::value_type)) { + return cvm::error("Error: on receiving height on replica 0 from replica " + cvm::to_str(p)); + } + } + } else { + if (cvm::proxy->replica_comm_send((char*)&height, sizeof(decltype(height)), 0) != sizeof(cvm::real)) { + return cvm::error("Error: on sending height to replica 0 from replica " + cvm::to_str(my_replica)); + } + } + cvm::proxy->replica_comm_barrier(); + // Broadcast heights + if (my_replica == 0) { + const int send_size = sizeof(decltype(all_height)::value_type) * all_height.size(); + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + if (cvm::proxy->replica_comm_send((char*)all_height.data(), send_size, p) != send_size) { + return cvm::error("Error: on sending heights from replica 0 to replica " + cvm::to_str(p)); + } + } + } else { + const int recv_size = sizeof(decltype(all_height)::value_type) * all_height.size(); + if (cvm::proxy->replica_comm_recv((char*)all_height.data(), recv_size, 0) != recv_size) { + return cvm::error("Error: on receiving heights from replica 0 to replica " + cvm::to_str(my_replica)); + } + } + cvm::proxy->replica_comm_barrier(); + + // Allgather of centers + if (my_replica == 0) { + std::copy(m_cv.begin(), m_cv.end(), all_center.begin()); + const int recv_size = sizeof(decltype(m_cv)::value_type) * m_cv.size(); + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + cvm::real* recv_start_ptr = &(all_center[p * m_cv.size()]); + if (cvm::proxy->replica_comm_recv((char*)recv_start_ptr, recv_size, p) != recv_size) { + return cvm::error("Error on receiving centers from replica 0 to replica " + cvm::to_str(p)); + } + } + } else { + const int send_size = sizeof(decltype(m_cv)::value_type) * m_cv.size(); + if (cvm::proxy->replica_comm_send((char*)m_cv.data(), send_size, 0) != send_size) { + return cvm::error("Error on sending centers to replica 0 from replica " + cvm::to_str(my_replica)); + } + } + cvm::proxy->replica_comm_barrier(); + // Broadcast centers + if (my_replica == 0) { + const int send_size = sizeof(decltype(all_center)::value_type) * all_center.size(); + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + if (cvm::proxy->replica_comm_send((char*)all_center.data(), send_size, p) != send_size) { + return cvm::error("Error on sending centers from replica 0 to replica " + cvm::to_str(p)); + } + } + } else { + const int recv_size = sizeof(decltype(all_center)::value_type) * all_center.size(); + if (cvm::proxy->replica_comm_recv((char*)all_center.data(), recv_size, 0) != recv_size) { + return cvm::error("Error on receiving centers from replica 0 to replica " + cvm::to_str(my_replica)); + } + } + cvm::proxy->replica_comm_barrier(); + + // Allgather of sigmas + if (my_replica == 0) { + std::copy(sigma.begin(), sigma.end(), all_sigma.begin()); + const int recv_size = sizeof(decltype(sigma)::value_type) * sigma.size(); + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + cvm::real* recv_start_ptr = &(all_sigma[p * m_cv.size()]); + if (cvm::proxy->replica_comm_recv((char*)recv_start_ptr, recv_size, p) != recv_size) { + return cvm::error("Error on receiving sigmas from replica 0 to replica " + cvm::to_str(p)); + } + } + } else { + const int send_size = sizeof(decltype(sigma)::value_type) * sigma.size(); + if (cvm::proxy->replica_comm_send((char*)sigma.data(), send_size, 0) != send_size) { + return cvm::error("Error on sending sigmas to replica 0 from replica " + cvm::to_str(my_replica)); + } + } + cvm::proxy->replica_comm_barrier(); + // Broadcast sigmas + if (my_replica == 0) { + const int send_size = sizeof(decltype(all_sigma)::value_type) * all_sigma.size(); + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + if (cvm::proxy->replica_comm_send((char*)all_sigma.data(), send_size, p) != send_size) { + return cvm::error("Error on sending sigmas from replica 0 to replica " + cvm::to_str(p)); + } + } + } else { + const int recv_size = sizeof(decltype(all_sigma)::value_type) * all_sigma.size(); + if (cvm::proxy->replica_comm_recv((char*)all_sigma.data(), recv_size, 0) != recv_size) { + return cvm::error("Error on receiving sigmas from replica 0 to replica " + cvm::to_str(my_replica)); + } + } + cvm::proxy->replica_comm_barrier(); + + // Allgather of logweights + if (my_replica == 0) { + all_logweight[0] = log_weight; + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + if (cvm::proxy->replica_comm_recv((char*)&(all_logweight[p]), sizeof(decltype(all_logweight)::value_type), p) != sizeof(decltype(all_logweight)::value_type)) { + return cvm::error("Error on receiving log_weight on replica 0 from replica " + cvm::to_str(p)); + } + } + } else { + if (cvm::proxy->replica_comm_send((char*)&log_weight, sizeof(decltype(log_weight)), 0) != sizeof(cvm::real)) { + return cvm::error("Error on sending log_weight to replica 0 from replica " + cvm::to_str(my_replica)); + } + } + cvm::proxy->replica_comm_barrier(); + // Broadcast log_weight + if (my_replica == 0) { + const int send_size = sizeof(decltype(all_logweight)::value_type) * all_logweight.size(); + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + if (cvm::proxy->replica_comm_send((char*)all_logweight.data(), send_size, p) != send_size) { + return cvm::error("Error on sending log_weight from replica 0 to replica " + cvm::to_str(p)); + } + } + } else { + const int recv_size = sizeof(decltype(all_logweight)::value_type) * all_logweight.size(); + if (cvm::proxy->replica_comm_recv((char*)all_logweight.data(), recv_size, 0) != recv_size) { + return cvm::error("Error on receiving log_weight from replica 0 to replica " + cvm::to_str(my_replica)); + } + } + cvm::proxy->replica_comm_barrier(); + + if (m_nlist) { + std::vector all_nlist_size(m_num_walkers); + const int my_replica = cvm::proxy->replica_index(); + // Get the size of the neighbor list of each replica + if (my_replica == 0) { + all_nlist_size[0] = m_nlist_index.size(); + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + if (cvm::proxy->replica_comm_recv((char*)&(all_nlist_size[p]), sizeof(int), p) != sizeof(int)) { + return cvm::error("Error on receiving neighbor list size from replica " + cvm::to_str(p)); + } + } + } else { + const int nlist_size = m_nlist_index.size(); + if (cvm::proxy->replica_comm_send((char*)&nlist_size, sizeof(int), 0) != sizeof(int)) { + return cvm::error("Error on sending neighbor list size from replica " + cvm::to_str(my_replica)); + } + } + cvm::proxy->replica_comm_barrier(); + // Broadcast the neighbor list sizes to all replicas + if (my_replica == 0) { + const int send_size = sizeof(int) * all_nlist_size.size(); + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + if (cvm::proxy->replica_comm_send((char*)all_nlist_size.data(), send_size, p) != send_size) { + return cvm::error("Error on sending neighbor list sizes from replica 0 to replica " + cvm::to_str(p)); + } + } + } else { + const int recv_size = sizeof(int) * all_nlist_size.size(); + if (cvm::proxy->replica_comm_recv((char*)all_nlist_size.data(), recv_size, 0) != recv_size) { + return cvm::error("Error on receiving neighbor list sizes to replica " + cvm::to_str(my_replica)); + } + } + cvm::proxy->replica_comm_barrier(); + // Gather all neighbor lists from replicas + const int tot_size = std::accumulate(all_nlist_size.begin(), all_nlist_size.end(), 0); + if (tot_size > 0) { + // Allgatherv all neighbor lists from replicas + std::vector all_nlist_index(tot_size); + if (my_replica == 0) { + std::vector recv_start(m_num_walkers); + // Accumulative sum + recv_start[0] = 0; + std::partial_sum(all_nlist_size.begin(), all_nlist_size.end() - 1, recv_start.begin() + 1); + std::copy(m_nlist_index.begin(), m_nlist_index.end(), all_nlist_index.begin()); + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + size_t* recv_start_ptr = &(all_nlist_index[recv_start[p]]); + const int recv_size = all_nlist_size[p] * sizeof(decltype(all_nlist_index)::value_type); + if (cvm::proxy->replica_comm_recv((char*)recv_start_ptr, recv_size, p) != recv_size) { + return cvm::error("Error on receiving neighbor list from replica " + cvm::to_str(p)); + } + } + } else { + const int send_size = sizeof(decltype(m_nlist_index)::value_type) * m_nlist_index.size(); + if (cvm::proxy->replica_comm_send((char*)m_nlist_index.data(), send_size, 0) != send_size) { + return cvm::error("Error on sending neighbor list from replica " + cvm::to_str(my_replica)); + } + } + cvm::proxy->replica_comm_barrier(); + // Broadcast the neighbor list + if (my_replica == 0) { + const int send_size = sizeof(decltype(all_nlist_index)::value_type) * tot_size; + for (int p = 1; p < cvm::proxy->num_replicas(); ++p) { + if (cvm::proxy->replica_comm_send((char*)all_nlist_index.data(), send_size, p) != send_size) { + return cvm::error("Error on sending total neighbor list to replica " + cvm::to_str(p)); + } + } + } else { + const int recv_size = sizeof(decltype(all_nlist_index)::value_type) * tot_size; + if (cvm::proxy->replica_comm_recv((char*)all_nlist_index.data(), recv_size, 0) != recv_size) { + return cvm::error("Error on receiving total neighbor list on replica " + cvm::to_str(my_replica)); + } + } + cvm::proxy->replica_comm_barrier(); + // Deduplicate and sort the merged neighbor list + std::unordered_set all_nlist_index_set; + for (auto it = all_nlist_index.cbegin(); it != all_nlist_index.cend(); ++it) { + all_nlist_index_set.insert(*it); + } + m_nlist_index.assign(all_nlist_index_set.begin(), all_nlist_index_set.end()); + std::sort(m_nlist_index.begin(), m_nlist_index.end()); + } + } + for (size_t w = 0; w < m_num_walkers; ++w) { + std::vector center_w( + all_center.begin() + num_variables() * w, + all_center.begin() + num_variables() * (w + 1)); + std::vector sigma_w( + all_sigma.begin() + num_variables() * w, + all_sigma.begin() + num_variables() * (w + 1)); + addKernel(all_height[w], center_w, sigma_w, all_logweight[w]); + } + } + m_nker = m_kernels.size(); + m_traj_line.nker = m_nker; + if (m_nlist) { + m_nlker = m_nlist_index.size(); + m_traj_line.nlker = m_nlker; + if (m_nlist_pace_reset) { + m_nlist_update = true; + } + } + if (!m_no_zed) { + cvm::real sum_uprob = 0; + const size_t ks = m_kernels.size(); + const size_t ds = m_delta_kernels.size(); + const int num_parallel = 1; // Always 1 + const bool few_kernels = (ks * ks < (3 * ks * ds + 2 * ds * ds * num_parallel + 100)); + if (few_kernels) { + if (m_num_threads == 1) { + for (size_t k = 0; k < m_kernels.size(); ++k) { + for (size_t kk = 0; kk < m_kernels.size(); ++kk) { + sum_uprob += evaluateKernel(m_kernels[kk], m_kernels[k].m_center); + } + } + } else { +#if defined(_OPENMP) + #pragma omp parallel num_threads(m_num_threads) + { + #pragma omp for reduction(+:sum_uprob) nowait + for (int k = 0; k < static_cast(m_kernels.size()); ++k) { + for (int kk = 0; kk < static_cast(m_kernels.size()); ++kk) { + sum_uprob += evaluateKernel(m_kernels[kk], m_kernels[k].m_center); + } + } + } +#elif defined(CMK_SMP) && defined(USE_CKLOOP) + // TODO: Does this work?? + auto worker = [&](int start, int end, void* result) { + double tmp_prob = 0; + for (int i = start; i <= end; ++i) { + for (size_t kk = 0; kk < m_kernels.size(); ++kk) { + tmp_prob += evaluateKernel(m_kernels[kk], m_kernels[i].m_center); + } + } + *(double *)result = tmp_prob; + }; + const size_t numChunks = m_kernels.size(); + const size_t lowerRange = 0; + const size_t upperRange = numChunks - 1; + CkLoop_Parallelize( + numChunks, lowerRange, upperRange, + worker, &sum_uprob, CKLOOP_DOUBLE_SUM, NULL); +#else + cvm::error("OPES cannot run because this binary is not linked with a supported threading library.\n"); +#endif + } + if (num_parallel > 1) { + return cvm::error("Unimplemented feature: OPES in parallel running.\n"); + } + } else { + cvm::real delta_sum_uprob = 0; + if (!m_nlist) { + if (m_num_threads == 1) { + for (size_t i = 0; i < m_kernels.size(); ++i) { + for (size_t d = 0; d < m_delta_kernels.size(); ++d) { + const int sign = m_delta_kernels[d].m_height < 0 ? -1 : 1; + delta_sum_uprob += evaluateKernel(m_delta_kernels[d], m_kernels[i].m_center) + sign * evaluateKernel(m_kernels[i], m_delta_kernels[d].m_center); + } + } + } else { +#if defined(_OPENMP) + #pragma omp parallel num_threads(m_num_threads) + { + #pragma omp for reduction(+:delta_sum_uprob) nowait + for (int i = 0; i < static_cast(m_kernels.size()); ++i) { + for (int d = 0; d < static_cast(m_delta_kernels.size()); ++d) { + const int sign = m_delta_kernels[d].m_height < 0 ? -1 : 1; + delta_sum_uprob += evaluateKernel(m_delta_kernels[d], m_kernels[i].m_center) + sign * evaluateKernel(m_kernels[i], m_delta_kernels[d].m_center); + } + } + } +#elif defined(CMK_SMP) && defined(USE_CKLOOP) + auto worker = [&](int start, int end, void* result) { + double tmp_prob = 0; + for (int i = start; i <= end; ++i) { + for (size_t d = 0; d < m_delta_kernels.size(); ++d) { + const int sign = m_delta_kernels[d].m_height < 0 ? -1 : 1; + tmp_prob += evaluateKernel(m_delta_kernels[d], m_kernels[i].m_center) + sign * evaluateKernel(m_kernels[i], m_delta_kernels[d].m_center); + } + } + *(double *)result = tmp_prob; + }; + const size_t numChunks = m_kernels.size(); + const size_t lowerRange = 0; + const size_t upperRange = numChunks - 1; + CkLoop_Parallelize( + numChunks, lowerRange, upperRange, + worker, &delta_sum_uprob, CKLOOP_DOUBLE_SUM, NULL); +#else + cvm::error("OPES cannot run because this binary is not linked with a supported threading library.\n"); +#endif + } + } else { + if (m_num_threads == 1) { + for (size_t i = 0; i < m_nlist_index.size(); ++i) { + const size_t k = m_nlist_index[i]; + for (size_t d = 0; d < m_delta_kernels.size(); ++d) { + const double sign = m_delta_kernels[d].m_height < 0 ? -1 : 1; + delta_sum_uprob += evaluateKernel(m_delta_kernels[d], m_kernels[k].m_center) + sign * evaluateKernel(m_kernels[k], m_delta_kernels[d].m_center); + } + } + } else { +#if defined(_OPENMP) + #pragma omp parallel num_threads(m_num_threads) + { + #pragma omp for reduction(+:delta_sum_uprob) nowait + for (int i = 0; i < static_cast(m_nlist_index.size()); ++i) { + const size_t k = m_nlist_index[i]; + for (int d = 0; d < static_cast(m_delta_kernels.size()); ++d) { + const double sign = m_delta_kernels[d].m_height < 0 ? -1 : 1; + delta_sum_uprob += evaluateKernel(m_delta_kernels[d], m_kernels[k].m_center) + sign * evaluateKernel(m_kernels[k], m_delta_kernels[d].m_center); + } + } + } +#elif defined(CMK_SMP) && defined(USE_CKLOOP) + auto worker = [&](int start, int end, void* result) { + double tmp_prob = 0; + for (int i = start; i <= end; ++i) { + const size_t k = m_nlist_index[i]; + for (size_t d = 0; d < m_delta_kernels.size(); ++d) { + const double sign = m_delta_kernels[d].m_height < 0 ? -1 : 1; + tmp_prob += evaluateKernel(m_delta_kernels[d], m_kernels[k].m_center) + sign * evaluateKernel(m_kernels[k], m_delta_kernels[d].m_center); + } + } + *(double *)result = tmp_prob; + }; + const size_t numChunks = m_nlist_index.size(); + const size_t lowerRange = 0; + const size_t upperRange = numChunks - 1; + CkLoop_Parallelize( + numChunks, lowerRange, upperRange, + worker, &delta_sum_uprob, CKLOOP_DOUBLE_SUM, NULL); +#else + cvm::error("OPES cannot run because this binary is not linked with a supported threading library.\n"); +#endif + } + } + if (num_parallel > 1) { + return cvm::error("Unimplemented feature: OPES in parallel running.\n"); + } + if (m_num_threads == 1) { + for (size_t d = 0; d < m_delta_kernels.size(); ++d) { + for (size_t dd = 0; dd < m_delta_kernels.size(); ++dd) { + const int sign = m_delta_kernels[d].m_height < 0 ? -1 : 1; + delta_sum_uprob -= sign *evaluateKernel(m_delta_kernels[dd], m_delta_kernels[d].m_center); + } + } + } else { +#if defined(_OPENMP) + #pragma omp parallel num_threads(m_num_threads) + { + #pragma omp for reduction(+:delta_sum_uprob) + for (int d = 0; d < static_cast(m_delta_kernels.size()); ++d) { + for (int dd = 0; dd < static_cast(m_delta_kernels.size()); ++dd) { + const int sign = m_delta_kernels[d].m_height < 0 ? -1 : 1; + delta_sum_uprob -= sign * evaluateKernel(m_delta_kernels[dd], m_delta_kernels[d].m_center); + } + } + } +#elif defined(CMK_SMP) && defined(USE_CKLOOP) + auto worker = [&](int start, int end, void* result) { + double tmp_prob = 0; + for (int d = start; d <= end; ++d) { + for (size_t dd = 0; dd < m_delta_kernels.size(); ++dd) { + const int sign = m_delta_kernels[d].m_height < 0 ? -1 : 1; + tmp_prob += sign * evaluateKernel(m_delta_kernels[dd], m_delta_kernels[d].m_center); + } + } + *(double *)result = tmp_prob; + }; + const size_t numChunks = m_delta_kernels.size(); + const size_t lowerRange = 0; + const size_t upperRange = numChunks - 1; + double tmp = 0; + CkLoop_Parallelize( + numChunks, lowerRange, upperRange, + worker, &tmp, CKLOOP_DOUBLE_SUM, NULL); + delta_sum_uprob -= tmp; +#else + cvm::error("OPES cannot run because this binary is not linked with a supported threading library.\n"); +#endif + } + sum_uprob = m_zed * m_old_kdenorm * old_nker + delta_sum_uprob; + } + m_zed = sum_uprob / m_kdenorm / m_kernels.size(); + m_traj_line.zed = m_zed; + } + if (m_calc_work) { + std::vector dummy(num_variables()); + const cvm::real prob = getProbAndDerivatives(m_cv, dummy); + const cvm::real new_bias = m_kbt * m_bias_prefactor * cvm::logn(prob / m_zed + m_epsilon); + m_work += new_bias - bias_energy; + m_traj_line.work = m_work; + } + } + return COLVARS_OK; +} + +void colvarbias_opes::save_state() { + if (cvm::step_absolute() % cvm::restart_out_freq == 0) { + m_saved_zed = m_zed; + m_saved_sum_weights = m_sum_weights; + m_saved_sum_weights2 = m_sum_weights2; + m_saved_kernels = m_kernels; + } +} + +int colvarbias_opes::update() { + int error_code = COLVARS_OK; + for (size_t i = 0; i < num_variables(); ++i) { + m_cv[i] = variables(i)->value(); + } + error_code |= calculate_opes(); + // NOTE: I don't think that calling dumpStateToFile() after update in + // the PLUMED implementation is correct for step 0, so I save the + // data after calculate() that does not modify the internal state + // of the bias. + save_state(); + if (error_code != COLVARS_OK) return error_code; + if (m_is_first_step) { + // NOTE: Colvars does not allow chainned biases, so I have to implement + // the PRINT here. Even if OPESmetad::update() is skipped we should + // still call Print::update() + writeTrajBuffer(); + if (m_pmf_grid_on) error_code |= collectSampleToPMFGrid(); + m_is_first_step = false; + return COLVARS_OK; + } + error_code |= update_opes(); + if (error_code != COLVARS_OK) return error_code; + writeTrajBuffer(); // Print::update() + if (m_pmf_grid_on) error_code |= collectSampleToPMFGrid(); + return error_code; +} + +int colvarbias_opes::collectSampleToPMFGrid() { + if (m_reweight_grid) { + // Get the bin index + std::vector bin(m_pmf_cvs.size(), 0); + for (size_t i = 0; i < m_pmf_cvs.size(); ++i) { + bin[i] = m_reweight_grid->current_bin_scalar(i); + } + const cvm::real reweighting_factor = cvm::exp(bias_energy / m_kbt); + if (m_reweight_grid->index_ok(bin)) { + m_reweight_grid->acc_value(bin, reweighting_factor); + } + } + return COLVARS_OK; +} + +template OST& colvarbias_opes::write_state_data_template_(OST &os) const { + std::ios_base::fmtflags f; + const bool formatted = !std::is_same::value; + if (formatted) { + f = os.flags(); + os.setf(std::ios::scientific, std::ios::floatfield); + } + write_state_data_key(os, "opes_metad_" + this->name); + auto printFieldReal = [&](const std::string& s, cvm::real x){ + write_state_data_key(os, s, false); + if (formatted) + os << std::setprecision(cvm::en_prec) << std::setw(cvm::en_width); + os << x; + if (formatted) + os << "\n"; + }; + auto printFieldULL = [&](const std::string& s, unsigned long long x){ + write_state_data_key(os, s, false); + if (formatted) + os << std::setprecision(cvm::en_prec) << std::setw(cvm::en_width); + os << x; + if (formatted) + os << "\n"; + }; + auto printFieldString = [&](const std::string& s, const std::string& x){ + write_state_data_key(os, s, false); + if (formatted) + os << std::setprecision(cvm::en_prec) << std::setw(cvm::en_width); + os << x; + if (formatted) + os << "\n"; + }; + std::ostringstream oss; + if (m_inf_biasfactor) { + oss << "inf"; + } else { + oss << m_biasfactor; + } + printFieldString("biasfactor", oss.str()); + printFieldReal("epsilon", m_epsilon); + printFieldReal("kernel_cutoff", cvm::sqrt(m_cutoff2)); + printFieldReal("compression_threshold", m_compression_threshold); + printFieldReal("zed", m_saved_zed); + printFieldReal("sum_weights", m_saved_sum_weights); + printFieldReal("sum_weights2", m_saved_sum_weights2); + printFieldULL("counter", m_counter); + if (m_adaptive_sigma) { + printFieldULL("adaptive_counter", m_adaptive_counter); + for (size_t i = 0; i < num_variables(); ++i) { + printFieldReal("sigma0_" + variables(i)->name, m_sigma0[i]); + printFieldReal("av_cv_" + variables(i)->name, m_av_cv[i]); + printFieldReal("av_M2_" + variables(i)->name, m_av_M2[i]); + } + } + printFieldULL("num_hills", m_saved_kernels.size()); + write_state_data_key(os, "hills", false); + if (formatted) os << "{\n"; + for (size_t k = 0; k < m_saved_kernels.size(); ++k) { + if (formatted) os << "{ "; + os << k; + if (formatted) os << " "; + for (size_t i = 0; i < num_variables(); ++i) { + os << m_saved_kernels[k].m_center[i]; + if (formatted) os << " "; + } + for (size_t i = 0; i < num_variables(); ++i) { + os << m_saved_kernels[k].m_sigma[i]; + if (formatted) os << " "; + } + os << m_saved_kernels[k].m_height; + if (formatted) os << " }\n"; + } + if (formatted) os << "}\n"; + if (formatted) os.setf(f); + if (m_pmf_grid_on) { + write_state_data_key(os, "probability_grid"); + m_reweight_grid->write_raw(os, 8); + } + return os; +} + +std::ostream& colvarbias_opes::write_state_data(std::ostream &os) { + try { + auto& s = write_state_data_template_(os); + return s; + } catch (const std::exception& e) { + cvm::error(e.what()); + } + return os; +} + +cvm::memory_stream& colvarbias_opes::write_state_data(cvm::memory_stream& os) { + try { + auto& s = write_state_data_template_(os); + return s; + } catch (const std::exception& e) { + cvm::error(e.what()); + } + return os; +} + +template IST& colvarbias_opes::read_state_data_template_(IST &is) { + bool const formatted = !std::is_same::value; + std::string tmp_name; + is >> tmp_name; + if (tmp_name.rfind("opes_metad_", 0) != 0) { + throw std::runtime_error("Unknown action name: " + tmp_name + "\n"); + } + auto readFieldString = [&](const std::string& s, std::string& x){ + std::string field; + is >> field; + if (field.compare(s) == 0) { + is >> x; + } else { + throw std::runtime_error("Expect field \"" + s + "\" , but got \"" + field + "\"\n"); + } + }; + auto readFieldReal = [&](const std::string& s, cvm::real& x){ + std::string field; + is >> field; + if (field.compare(s) == 0) { + is >> x; + } else { + throw std::runtime_error("Expect field \"" + s + "\" , but got \"" + field + "\"\n"); + } + }; + auto readFieldULL = [&](const std::string& s, unsigned long long& x){ + std::string field; + is >> field; + if (field.compare(s) == 0) { + is >> x; + } else { + throw std::runtime_error("Expect field \"" + s + "\" , but got \"" + field + "\"\n"); + } + }; + std::string old_biasfactor_str; + cvm::real old_biasfactor; + readFieldString("biasfactor", old_biasfactor_str); + if (old_biasfactor_str == "inf" || old_biasfactor_str == "-inf" || old_biasfactor_str == "+inf" || + old_biasfactor_str == "INF" || old_biasfactor_str == "-INF" || old_biasfactor_str == "+INF") { + old_biasfactor = std::numeric_limits::infinity(); + m_inf_biasfactor = true; + } else { + old_biasfactor = std::stod(old_biasfactor_str); + m_inf_biasfactor = false; + } + if (std::abs(old_biasfactor - m_biasfactor) > 1e-6 * m_biasfactor) { + cvm::log("WARNING: previous bias factor was " + cvm::to_str(old_biasfactor) + + " while now it is " + cvm::to_str(m_biasfactor) + + " (the new one is used).\n"); + } + cvm::real old_epsilon; + readFieldReal("epsilon", old_epsilon); + if (std::abs(old_epsilon - m_epsilon) > 1e-6 * m_epsilon) { + cvm::log("WARNING: previous epsilon was " + cvm::to_str(old_epsilon) + + " while now it is " + cvm::to_str(m_epsilon) + + " (the new one is used).\n"); + } + cvm::real old_cutoff; + readFieldReal("kernel_cutoff", old_cutoff); + if (std::abs(old_cutoff - m_cutoff) > 1e-6 * m_cutoff) { + cvm::log("WARNING: previous cutoff was " + cvm::to_str(old_cutoff) + + " while now it is " + cvm::to_str(m_cutoff) + + " (the new one is used).\n"); + } + m_cutoff2 = m_cutoff * m_cutoff; + cvm::real old_compression_threshold; + readFieldReal("compression_threshold", old_compression_threshold); + if (std::abs(old_compression_threshold - m_compression_threshold) > 1e-6 * m_compression_threshold) { + cvm::log("WARNING: previous cutoff was " + cvm::to_str(old_compression_threshold) + + " while now it is " + cvm::to_str(m_compression_threshold) + + " (the new one is used).\n"); + } + m_compression_threshold2 = m_compression_threshold * m_compression_threshold; + readFieldReal("zed", m_zed); + readFieldReal("sum_weights", m_sum_weights); + readFieldReal("sum_weights2", m_sum_weights2); + unsigned long long tmp_counter = 1; + readFieldULL("counter", tmp_counter); + m_counter = tmp_counter; + if (m_adaptive_sigma) { + readFieldULL("adaptive_counter", tmp_counter); + m_adaptive_counter = tmp_counter; + for (size_t i = 0; i < num_variables(); ++i) { + readFieldReal("sigma0_" + variables(i)->name, m_sigma0[i]); + readFieldReal("av_cv_" + variables(i)->name, m_av_cv[i]); + readFieldReal("av_M2_" + variables(i)->name, m_av_M2[i]); + } + } + unsigned long long kernel_size = 0; + readFieldULL("num_hills", kernel_size); + if (kernel_size > 0) m_kernels.resize(kernel_size); + read_state_data_key(is, "hills"); + auto consume = [&](const std::string& expected_token){ + if (formatted) { + std::string field; + is >> field; + if (field.compare(expected_token) != 0) { + throw std::runtime_error("Expect " + expected_token + " but got " + field + "\n"); + } + } + }; + consume("{"); + for (size_t k = 0; k < m_kernels.size(); ++k) { + consume("{"); + unsigned long long tmp_k = 0; + is >> tmp_k; + if (formatted && k != tmp_k) { + throw std::runtime_error("Corrupt hill data\n"); + } + kernel current_kernel; + current_kernel.m_center.resize(num_variables()); + current_kernel.m_sigma.resize(num_variables()); + for (size_t i = 0; i < num_variables(); ++i) { + is >> current_kernel.m_center[i]; + } + for (size_t i = 0; i < num_variables(); ++i) { + is >> current_kernel.m_sigma[i]; + } + is >> current_kernel.m_height; + m_kernels[k] = current_kernel; + consume("}"); + } + consume("}"); + if (m_pmf_grid_on) { + read_state_data_key(is, "probability_grid"); + m_reweight_grid->read_raw(is); + } + m_kdenorm = m_explore ? m_counter : m_sum_weights; + m_traj_line.rct = m_kbt * cvm::logn(m_sum_weights / m_counter); + m_traj_line.zed = m_zed; + m_traj_line.neff = (1 + m_sum_weights) * (1 + m_sum_weights) / (1 + m_sum_weights2); + m_traj_line.nker = m_kernels.size(); + showInfo(); + return is; +} + +std::istream& colvarbias_opes::read_state_data(std::istream &is) { + try { + auto& ret = read_state_data_template_(is); + return ret; + } catch (const std::exception& e) { + cvm::error(e.what()); + } + return is; +} + +cvm::memory_stream& colvarbias_opes::read_state_data(cvm::memory_stream &is) { + try { + auto& ret = read_state_data_template_(is); + return ret; + } catch (const std::exception& e) { + cvm::error(e.what()); + } + return is; +} + +void colvarbias_opes::addKernel(const double height, const std::vector& center, const std::vector& sigma, const double logweight) { + addKernel(height,center,sigma); + const std::ios_base::fmtflags f = m_kernels_output.flags(); + m_kernels_output << std::right; + // simulation time in ps + m_kernels_output << std::setw(24) << (cvm::step_absolute() * cvm::dt()) * 1e-3; + for (size_t i = 0; i < num_variables(); ++i) { + m_kernels_output << " " << std::setw(24) << std::setprecision(16) << center[i]; + } + for (size_t i = 0; i < num_variables(); ++i) { + m_kernels_output << " " << std::setw(24) << std::setprecision(16) << sigma[i]; + } + m_kernels_output << " " << std::setw(24) << std::setprecision(16) << height; + m_kernels_output << " " << std::setw(24) << std::setprecision(16) << logweight; + m_kernels_output << std::endl; + m_kernels_output.flags(f); +} + +void colvarbias_opes::addKernel(const double height, const std::vector& center, const std::vector& sigma) { + bool no_match = true; + if (m_compression_threshold2 != 0) { + size_t taker_k = getMergeableKernel(center, m_kernels.size()); + if (taker_k < m_kernels.size()) { + no_match = false; + m_delta_kernels.emplace_back(-1 * m_kernels[taker_k].m_height, m_kernels[taker_k].m_center, m_kernels[taker_k].m_sigma); + mergeKernels(m_kernels[taker_k], kernel(height, center, sigma)); + m_delta_kernels.push_back(m_kernels[taker_k]); + if (m_recursive_merge) { + size_t giver_k = taker_k; + taker_k = getMergeableKernel(m_kernels[giver_k].m_center, giver_k); + while (taker_k < m_kernels.size()) { + m_delta_kernels.pop_back(); + m_delta_kernels.emplace_back(-1 * m_kernels[taker_k].m_height, m_kernels[taker_k].m_center, m_kernels[taker_k].m_sigma); + if (taker_k > giver_k) std::swap(taker_k, giver_k); + mergeKernels(m_kernels[taker_k], m_kernels[giver_k]); + m_delta_kernels.push_back(m_kernels[taker_k]); + m_kernels.erase(m_kernels.begin() + giver_k); + if (m_nlist) { + size_t giver_nk = 0; + bool found_giver = false; + for (size_t nk = 0; nk < m_nlist_index.size(); ++nk) { + if (found_giver) m_nlist_index[nk]--; + if (m_nlist_index[nk] == giver_k) { + giver_nk = nk; + found_giver = true; + } + } + if (found_giver == false) { + cvm::error("problem with merging and nlist\n"); + } + m_nlist_index.erase(m_nlist_index.begin() + giver_nk); + } + giver_k = taker_k; + taker_k = getMergeableKernel(m_kernels[giver_k].m_center, giver_k); + } + } + } + } + if (no_match) { + m_kernels.emplace_back(height, center, sigma); + m_delta_kernels.emplace_back(height, center, sigma); + if (m_nlist) m_nlist_index.push_back(m_kernels.size() - 1); + } +} + +void colvarbias_opes::mergeKernels(kernel& k1, const kernel& k2) const { + const double h = k1.m_height + k2.m_height; + for (size_t i = 0; i < k1.m_center.size(); ++i) { + const bool isPeriodic_i = variables(i)->is_enabled(f_cv_periodic); + if (isPeriodic_i) { + k1.m_center[i] = k2.m_center[i] + 0.5 * variables(i)->dist2_lgrad(k1.m_center[i], k2.m_center[i]).real_value; + } + const cvm::real c_i = (k1.m_height * k1.m_center[i] + + k2.m_height * k2.m_center[i]) / h; + const cvm::real ss_k1_part = k1.m_height * (k1.m_sigma[i] * k1.m_sigma[i] + k1.m_center[i] * k1.m_center[i]); + const cvm::real ss_k2_part = k2.m_height * (k2.m_sigma[i] * k2.m_sigma[i] + k2.m_center[i] * k2.m_center[i]); + const cvm::real ss_i = (ss_k1_part + ss_k2_part) / h - c_i * c_i; + if (isPeriodic_i) { + colvarvalue tmp(c_i); + variables(i)->wrap(tmp); + k1.m_center[i] = tmp.real_value; + } else { + k1.m_center[i] = c_i; + } + k1.m_sigma[i] = cvm::sqrt(ss_i); + } + k1.m_height = h; +} + +size_t colvarbias_opes::getMergeableKernel(const std::vector& giver_center, const size_t giver_k) const { + size_t min_k = m_kernels.size(); + cvm::real min_norm2 = m_compression_threshold2; + const int num_parallel = 1; + if (!m_nlist) { + if (m_num_threads == 1) { + for (size_t k = 0; k < m_kernels.size(); ++k) { + if (k == giver_k) continue; + double norm2 = 0; + for (size_t i = 0; i < num_variables(); ++i) { + norm2 += variables(i)->dist2(giver_center[i], m_kernels[k].m_center[i]) / (m_kernels[k].m_sigma[i] * m_kernels[k].m_sigma[i]); + if (norm2 >= min_norm2) break; + } + if (norm2 < min_norm2) { + min_norm2 = norm2; + min_k = k; + } + } + } else { +#if defined(_OPENMP) + #pragma omp parallel num_threads(m_num_threads) + { + int min_k_omp = min_k; + cvm::real min_norm2_omp = m_compression_threshold2; + #pragma omp for nowait + for (int k = 0; k < static_cast(m_kernels.size()); ++k) { + if (k == static_cast(giver_k)) continue; + double norm2 = 0; + for (int i = 0; i < static_cast(num_variables()); ++i) { + norm2 += variables(i)->dist2( giver_center[i], m_kernels[k].m_center[i]) / (m_kernels[k].m_sigma[i] * m_kernels[k].m_sigma[i]); + if (norm2 >= min_norm2_omp) break; + } + if (norm2 < min_norm2_omp) { + min_norm2_omp = norm2; + min_k_omp = k; + } + } + #pragma omp critical + { + if (min_norm2_omp < min_norm2) { + min_norm2 = min_norm2_omp; + min_k = min_k_omp; + } + } + } +#elif defined(CMK_SMP) && defined(USE_CKLOOP) + // NOTE: No existing reduction type for finding the minimum, so I have + // to use such a workaround. + std::vector min_k_smp(m_num_threads, min_k); + std::vector min_norm2_smp(m_num_threads, m_compression_threshold2); + auto worker = [&](int start, int end, void* unused) { + const int tid = cvm::proxy->smp_thread_id(); + for (int k = start; k <= end; ++k) { + if (k == giver_k) continue; + double norm2 = 0; + for (size_t j = 0; j < num_variables(); ++j) { + norm2 += variables(i)->dist2( giver_center[i], m_kernels[k].m_center[i]) / (m_kernels[k].m_sigma[i] * m_kernels[k].m_sigma[i]); + if (norm2 >= min_norm2_smp[tid]) break; + } + if (norm2 < min_norm2_smp[tid]) { + min_norm2_smp[tid] = norm2; + min_k_smp[tid] = k; + } + } + }; + const size_t numChunks = m_kernels.size(); + const size_t lowerRange = 0; + const size_t upperRange = numChunks - 1; + CkLoop_Parallelize( + numChunks, lowerRange, upperRange, + worker, NULL, CKLOOP_NONE, NULL); + const auto it_min = std::min_element(min_norm2_smp.begin(), min_norm2_smp.end()); + min_norm2 = *it_min; + min_k = min_k_smp[std::distance(min_norm2_smp.begin(), it_min)]; +#else + cvm::error("OPES cannot run because this binary is not linked with a supported threading library.\n"); +#endif + } + } else { + if (m_num_threads == 1) { + // size_t min_k_omp = min_k; + // cvm::real min_norm2_omp = m_compression_threshold2; + for (size_t nk = 0; nk < m_nlist_index.size(); ++nk) { + const size_t k = m_nlist_index[nk]; + if (k == giver_k) continue; + double norm2 = 0; + for (size_t i = 0; i < num_variables(); ++i) { + norm2 += variables(i)->dist2(giver_center[i], m_kernels[k].m_center[i]) / (m_kernels[k].m_sigma[i] * m_kernels[k].m_sigma[i]); + if (norm2 >= min_norm2) break; + } + if (norm2 < min_norm2) { + min_norm2 = norm2; + min_k = k; + } + } + } else { +#if defined(_OPENMP) + #pragma omp parallel num_threads(m_num_threads) + { + size_t min_k_omp = min_k; + cvm::real min_norm2_omp = m_compression_threshold2; + #pragma omp for nowait + for (int nk = 0; nk < static_cast(m_nlist_index.size()); ++nk) { + const size_t k = m_nlist_index[nk]; + if (k == giver_k) continue; + double norm2 = 0; + for (int i = 0; i < static_cast(num_variables()); ++i) { + norm2 += variables(i)->dist2(giver_center[i], m_kernels[k].m_center[i]) / (m_kernels[k].m_sigma[i] * m_kernels[k].m_sigma[i]); + if (norm2 >= min_norm2_omp) break; + } + if (norm2 < min_norm2_omp) { + min_norm2_omp = norm2; + min_k_omp = k; + } + } + #pragma omp critical + { + if (min_norm2_omp < min_norm2) { + min_norm2 = min_norm2_omp; + min_k = min_k_omp; + } + } + } +#elif defined(CMK_SMP) && defined(USE_CKLOOP) + // NOTE: No existing reduction type for finding the minimum, so I have + // to use such a workaround. + std::vector min_k_smp(m_num_threads, min_k); + std::vector min_norm2_smp(m_num_threads, m_compression_threshold2); + auto worker = [&](int start, int end, void* unused) { + const int tid = cvm::proxy->smp_thread_id(); + for (int nk = start; nk <= end; ++nk) { + const size_t k = m_nlist_index[nk]; + if (k == giver_k) continue; + double norm2 = 0; + for (size_t j = 0; j < num_variables(); ++j) { + norm2 += variables(i)->dist2( giver_center[i], m_kernels[k].m_center[i]) / (m_kernels[k].m_sigma[i] * m_kernels[k].m_sigma[i]); + if (norm2 >= min_norm2_smp[tid]) break; + } + if (norm2 < min_norm2_smp[tid]) { + min_norm2_smp[tid] = norm2; + min_k_smp[tid] = k; + } + } + }; + const size_t numChunks = m_nlist_index.size(); + const size_t lowerRange = 0; + const size_t upperRange = numChunks - 1; + CkLoop_Parallelize( + numChunks, lowerRange, upperRange, + worker, NULL, CKLOOP_NONE, NULL); + const auto it_min = std::min_element(min_norm2_smp.begin(), min_norm2_smp.end()); + min_norm2 = *it_min; + min_k = min_k_smp[std::distance(min_norm2_smp.begin(), it_min)]; +#else + cvm::error("OPES cannot run because this binary is not linked with a supported threading library.\n"); +#endif + } + } + if (num_parallel > 1) { + cvm::error("The Colvars OPES implementation does not support running OPES in parallel across nodes.\n"); + } + return min_k; +} + +std::string const colvarbias_opes::traj_file_name(const std::string& suffix) const { + return std::string(cvm::output_prefix()+ + ".colvars."+this->name+ + ( (comm != single_replica) ? + ("."+replica_id) : + ("") )+ + suffix); +} + +int colvarbias_opes::write_output_files() { + int error_code = COLVARS_OK; + thread_local static bool firsttime = true; + // Write the kernels + const std::string kernels_filename = traj_file_name(".kernels.dat"); + std::ostream& os_kernels = cvm::proxy->output_stream(kernels_filename, "kernels file"); + const std::ios_base::fmtflags format_kernels = os_kernels.flags(); + if (firsttime) { + os_kernels << "#! FIELDS time "; + for (size_t i = 0; i < num_variables(); ++i) { + os_kernels << variables(i)->name + " "; + } + for (size_t i = 0; i < num_variables(); ++i) { + os_kernels << "sigma_" + variables(i)->name + " "; + } + os_kernels << "height logweight\n"; + // Make sure the action name compatible with the script in https://github.com/invemichele/opes/blob/master/postprocessing/State_from_Kernels.py + if (m_explore) os_kernels << "#! SET action OPES_METAD_EXPLORE_kernels\n"; + else os_kernels << "#! SET action OPES_METAD_kernels\n"; + if (m_inf_biasfactor) { + os_kernels << "#! SET biasfactor " << "inf" << "\n"; + } else { + os_kernels << "#! SET biasfactor " << m_biasfactor << "\n"; + } + os_kernels << "#! SET epsilon " << m_epsilon << "\n"; + os_kernels << "#! SET kernel_cutoff " << m_cutoff << "\n"; + os_kernels << "#! SET compression_threshold " << m_compression_threshold << "\n"; + for (size_t i = 0; i < num_variables(); ++i) { + if (variables(i)->is_enabled(f_cv_periodic)) { + if (variables(i)->is_enabled(f_cv_lower_boundary)) { + os_kernels << "#! SET min_" + variables(i)->name + " " << variables(i)->lower_boundary.real_value << "\n"; + } + if (variables(i)->is_enabled(f_cv_upper_boundary)) { + os_kernels << "#! SET max_" + variables(i)->name + " " << variables(i)->upper_boundary.real_value << "\n"; + } + } + } + } + os_kernels << m_kernels_output.str(); + os_kernels.setf(format_kernels); + error_code |= cvm::proxy->flush_output_stream(kernels_filename); + m_kernels_output.str(""); + m_kernels_output.clear(); + + // Write the trajectory + const std::string traj_filename = traj_file_name(".misc.traj"); + std::ostream& os_traj = cvm::proxy->output_stream(traj_filename, "trajectory of various OPES properties"); + const std::ios_base::fmtflags format_traj = os_traj.flags(); + if (firsttime) { + os_traj << "#! FIELDS time "; + for (size_t i = 0; i < num_variables(); ++i) { + os_traj << variables(i)->name + " "; + } + os_traj << this->name + ".bias "; + os_traj << this->name + ".rct "; + if (!m_no_zed) os_traj << this->name + ".zed "; + os_traj << this->name + ".neff "; + if (m_calc_work) if (!m_no_zed) os_traj << this->name + ".work "; + os_traj << this->name + ".nker "; + if (m_nlist) os_traj << this->name + ".nlker "; + if (m_nlist) os_traj << this->name + ".nlsteps "; + os_traj << "\n"; + for (size_t i = 0; i < num_variables(); ++i) { + if (variables(i)->is_enabled(f_cv_lower_boundary)) { + os_traj << "#! SET min_" + variables(i)->name + " " << variables(i)->lower_boundary.real_value << "\n"; + } + if (variables(i)->is_enabled(f_cv_upper_boundary)) { + os_traj << "#! SET max_" + variables(i)->name + " " << variables(i)->upper_boundary.real_value << "\n"; + } + } + } + os_traj << m_traj_oss.str(); + os_traj.setf(format_traj); + error_code |= cvm::proxy->flush_output_stream(traj_filename); + m_traj_oss.str(""); + m_traj_oss.clear(); + if (firsttime) firsttime = false; + if (m_pmf_grid_on) { + error_code |= computePMF(); + const std::string pmf_filename = traj_file_name(".pmf"); + error_code |= writePMF(m_pmf_grid, pmf_filename, false); + if (comm == multiple_replicas && m_pmf_shared) { + if (cvm::proxy->replica_index() == 0) { + const std::string global_pmf_filename = traj_file_name(".global.pmf"); + error_code |= writePMF(m_global_pmf_grid, global_pmf_filename, false); + } + } + if (m_pmf_hist_freq > 0 && cvm::step_absolute() % m_pmf_hist_freq == 0) { + const std::string pmf_hist_filename = traj_file_name(".hist.pmf"); + error_code |= writePMF(m_pmf_grid, pmf_hist_filename, true); + if (comm == multiple_replicas && m_pmf_shared) { + if (cvm::proxy->replica_index() == 0) { + const std::string global_hist_pmf_filename = traj_file_name(".global.hist.pmf"); + error_code |= writePMF(m_global_pmf_grid, global_hist_pmf_filename, true); + } + } + } + } + // To prevent the case that one replica exits earlier and then destroys all streams + if (comm == multiple_replicas) cvm::proxy->replica_comm_barrier(); + return error_code; +} + +void hist_to_pmf(const cvm::real kbt, const colvar_grid_scalar *hist, std::unique_ptr& pmf) { + // Get the sum of probabilities of all grids + cvm::real norm_factor = 0; + cvm::real max_prob = 0; + auto& prob_data = hist->data; + for (auto it = prob_data.begin(); it != prob_data.end(); ++it) { + norm_factor += (*it); + if ((*it) > max_prob) max_prob = (*it); + } + if (norm_factor > 0) { + const cvm::real min_pmf = (max_prob > 0) ? -1.0 * kbt * cvm::logn(max_prob / norm_factor) : 0; + auto& pmf_data = pmf->data; + for (size_t i = 0; i < pmf_data.size(); ++i) { + if (prob_data[i] > 0) { + pmf_data[i] = -1.0 * kbt * cvm::logn(prob_data[i] / norm_factor) - min_pmf; + } + } + auto max_pmf = *std::max_element(pmf_data.begin(), pmf_data.end()); + for (size_t i = 0; i < pmf_data.size(); ++i) { + if (!(prob_data[i] > 0)) { + pmf_data[i] = max_pmf; + } + } + } +} + +int colvarbias_opes::computePMF() { + // Multiple replica: collect all samples from other replicas + if (comm == multiple_replicas && m_pmf_shared) { + const size_t samples_n = m_reweight_grid->raw_data_num(); + const int msg_size = samples_n * sizeof(cvm::real); + std::vector buffer; + if (cvm::main()->proxy->replica_index() == 0) { + buffer.resize(samples_n * (cvm::proxy->num_replicas() - 1)); + for (int p = 1; p < cvm::proxy->num_replicas(); p++) { + const size_t start_pos = (p - 1) * samples_n; + if (cvm::proxy->replica_comm_recv((char*)&(buffer[start_pos]), msg_size, p) != msg_size) { + return cvm::error("Error getting shared OPES reweighting histogram from replica " + cvm::to_str(p)); + } + } + } else { + if (cvm::proxy->replica_comm_send((char*)(&(m_reweight_grid->data[0])), msg_size, 0) != msg_size) { + return cvm::error("Error sending shared OPES reweighting histogram from replica " + cvm::to_str(cvm::main()->proxy->replica_index())); + } + } + cvm::proxy->replica_comm_barrier(); + // Broadcast m_reweight_grid to all replicas + auto& global_data = m_global_reweight_grid->data; + if (cvm::main()->proxy->replica_index() == 0) { + global_data = m_reweight_grid->data; + // Sum the samples on PE 0 + for (int p = 1; p < cvm::proxy->num_replicas(); p++) { + const size_t start_pos = (p - 1) * samples_n; + for (size_t i = 0 ; i < samples_n; ++i) { + global_data[i] += buffer[start_pos+i]; + } + } + } + } + // Get the sum of probabilities of all grids + hist_to_pmf(m_kbt, m_reweight_grid.get(), m_pmf_grid); + if (comm == multiple_replicas && m_pmf_shared) { + if (cvm::main()->proxy->replica_index() == 0) { + hist_to_pmf(m_kbt, m_global_reweight_grid.get(), m_global_pmf_grid); + } + } + if (comm == multiple_replicas) { + cvm::proxy->replica_comm_barrier(); + } + return COLVARS_OK; +} + +int colvarbias_opes::writePMF(const std::unique_ptr& pmf_grid, const std::string &filename, bool keep_open) { + std::ostream& os = cvm::proxy->output_stream(filename, "output stream of " + filename); + if (!os) { + return COLVARS_FILE_ERROR; + } + pmf_grid->write_multicol(os); + if (!keep_open) { + cvm::proxy->close_output_stream(filename); + } else { + cvm::proxy->flush_output_stream(filename); + } + return COLVARS_OK; +} + +void colvarbias_opes::writeTrajBuffer() { + if (m_traj_output_frequency > 0 && cvm::step_absolute() % m_traj_output_frequency == 0) { + m_traj_oss << std::right; + m_traj_oss << std::scientific << " " << std::setw(cvm::cv_width) << std::setprecision(cvm::cv_prec) << (cvm::step_absolute() * cvm::dt()) * 1e-3; + for (size_t i = 0; i < num_variables(); ++i) { + m_traj_oss << std::scientific << " " << std::setw(cvm::cv_width) << std::setprecision(cvm::cv_prec) << variables(i)->value().real_value; + } + m_traj_oss << std::scientific << " " << std::setw(cvm::cv_width) << std::setprecision(cvm::cv_prec) << bias_energy; + m_traj_oss << std::scientific << " " << std::setw(cvm::cv_width) << std::setprecision(cvm::cv_prec) << m_traj_line.rct; + if (!m_no_zed) m_traj_oss << std::scientific << " " << std::setw(cvm::cv_width) << std::setprecision(cvm::cv_prec) << m_traj_line.zed; + m_traj_oss << std::scientific << " " << std::setw(cvm::cv_width) << std::setprecision(cvm::cv_prec) << m_traj_line.neff; + if (m_calc_work) m_traj_oss << std::scientific << " " << std::setw(cvm::cv_width) << std::setprecision(cvm::cv_prec) << m_traj_line.work; + m_traj_oss << " " << m_traj_line.nker; + if (m_nlist) m_traj_oss << " " << m_traj_line.nlker; + if (m_nlist) m_traj_oss << " " << m_traj_line.nlsteps; + m_traj_oss << "\n"; + } +} + +void colvarbias_opes::updateNlist(const std::vector& center) { + if (m_kernels.empty()) return; + m_nlist_center = center; + m_nlist_index.clear(); + if (m_num_threads == 1 || m_kernels.size() < 2 * m_num_threads) { + for (size_t k = 0; k < m_kernels.size(); ++k) { + cvm::real norm2_k = 0; + for (size_t i = 0; i < num_variables(); ++i) { + norm2_k += variables(i)->dist2(m_nlist_center[i], m_kernels[k].m_center[i]) / (m_kernels[k].m_sigma[i] * m_kernels[k].m_sigma[i]); + } + if (norm2_k <= m_nlist_param[0] * m_cutoff2) { + m_nlist_index.push_back(k); + } + } + } else { +#if defined (_OPENMP) + #pragma omp parallel num_threads(m_num_threads) + { + std::vector private_nlist_index; + #pragma omp for nowait + for (int k = 0; k < static_cast(m_kernels.size()); ++k) { + cvm::real norm2_k = 0; + for (int i = 0; i < static_cast(num_variables()); ++i) { + norm2_k += variables(i)->dist2(m_nlist_center[i], m_kernels[k].m_center[i]) / (m_kernels[k].m_sigma[i] * m_kernels[k].m_sigma[i]); + } + if (norm2_k <= m_nlist_param[0] * m_cutoff2) { + private_nlist_index.push_back(static_cast(k)); + } + } + #pragma omp critical + m_nlist_index.insert(m_nlist_index.end(), private_nlist_index.begin(), private_nlist_index.end()); + } +#elif defined(CMK_SMP) && defined(USE_CKLOOP) + std::vector> private_nlist_index(m_num_threads); + auto worker = [&](int start, int end, void* unused){ + const int tid = cvm::proxy->smp_thread_id(); + for (int k = start; k <= end; ++k) { + cvm::real norm2_k = 0; + for (size_t i = 0; i < num_variables(); ++i) { + norm2_k += variables(i)->dist2(m_nlist_center[i], m_kernels[k].m_center[i]) / (m_kernels[k].m_sigma[i] * m_kernels[k].m_sigma[i]); + } + if (norm2_k <= m_nlist_param[0] * m_cutoff2) { + private_nlist_index[tid].push_back(k); + } + } + }; + const size_t numChunks = m_kernels.size(); + const size_t lowerRange = 0; + const size_t upperRange = numChunks - 1; + CkLoop_Parallelize( + numChunks, lowerRange, upperRange, + worker, NULL, CKLOOP_NONE, NULL); + for (size_t j = 0; j < m_num_threads; ++j) { + m_nlist_index.insert(m_nlist_index.end(), private_nlist_index[i].begin(), private_nlist_index.end()); + } +#else + cvm::error("OPES cannot run because this binary is not linked with a supported threading library.\n"); +#endif + if (m_recursive_merge) { + std::sort(m_nlist_index.begin(), m_nlist_index.end()); + } + } + std::vector dev2(num_variables(), 0); + for (size_t k = 0; k < m_nlist_index.size(); ++k) { + for (size_t i = 0; i < num_variables(); ++i) { + dev2[i] += variables(i)->dist2(m_nlist_center[i], m_kernels[m_nlist_index[k]].m_center[i]); + } + } + for (size_t i = 0; i < num_variables(); ++i) { + if (m_nlist_index.empty()) { + m_nlist_dev2[i] = m_kernels.back().m_sigma[i] * m_kernels.back().m_sigma[i]; + } else { + m_nlist_dev2[i] = dev2[i] / m_nlist_index.size(); + } + } + m_traj_line.nlker = m_nlist_index.size(); + m_traj_line.nlsteps = m_nlist_steps; + m_nlist_steps = 0; + m_nlist_update = false; +} diff --git a/lib/colvars/colvarbias_opes.h b/lib/colvars/colvarbias_opes.h new file mode 100644 index 0000000000..0c52ba2413 --- /dev/null +++ b/lib/colvars/colvarbias_opes.h @@ -0,0 +1,176 @@ +#ifndef COLVARBIAS_OPES_H +#define COLVARBIAS_OPES_H + +// This code is mainly adapted from the PLUMED opes module, which uses the +// LGPLv3 license as shown below: +/* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + Copyright (c) 2020-2021 of Michele Invernizzi. + + This file is part of the OPES plumed module. + + The OPES plumed module is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + The OPES plumed module is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with plumed. If not, see . ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */ + +#include "colvarbias.h" + +#include +#include + +// OPES_METAD implementation: swiped from OPESmetad.cpp of PLUMED +class colvarbias_opes: public colvarbias { +public: + /// The Gaussian kernel data structure + struct kernel { + cvm::real m_height; + std::vector m_center; + std::vector m_sigma; + kernel() {} + kernel(cvm::real h, const std::vector& c, + const std::vector& s): + m_height(h), m_center(c), m_sigma(s) {} + }; + /// Communication between different replicas + enum Communication { + /// One replica (default) + single_replica, + /// Hills added concurrently by several replicas + multiple_replicas + }; + /// Constructor + colvarbias_opes(char const *key); + /// Initializer + int init(std::string const &conf) override; + /// Per-timestep update + int update() override; + /// Save the state to a text file for restarting + std::ostream &write_state_data(std::ostream &os) override; + /// Read the state from a text file for restarting + std::istream &read_state_data(std::istream &is) override; + /// Save the state to a binary file for restarting + cvm::memory_stream &write_state_data(cvm::memory_stream &os) override; + /// Read the state from a binary file for restarting + cvm::memory_stream &read_state_data(cvm::memory_stream &is) override; + /// Write to files at restart steps + int write_output_files() override; +private: + int update_opes(); + int calculate_opes(); + void save_state(); + cvm::real getProbAndDerivatives(const std::vector& cv, std::vector& der_prob) const; + cvm::real evaluateKernel(const kernel& G, const std::vector& x) const; + cvm::real evaluateKernel(const kernel& G, const std::vector& x, std::vector& accumulated_derivative, std::vector& dist) const; + void addKernel(const double height, const std::vector& center, const std::vector& sigma, const double logweight); + void addKernel(const double height, const std::vector& center, const std::vector& sigma); + size_t getMergeableKernel(const std::vector& giver_center, const size_t giver_k) const; + void mergeKernels(kernel& k1, const kernel& k2) const; + void updateNlist(const std::vector& center); + struct traj_line { + double rct; + double zed; + double neff; + double work; + size_t nker; + size_t nlker; + size_t nlsteps; + }; + void writeTrajBuffer(); + void showInfo() const; + template OST &write_state_data_template_(OST &os) const; + template IST &read_state_data_template_(IST &os); + std::string const traj_file_name(const std::string& suffix) const; + int collectSampleToPMFGrid(); + int computePMF(); + int writePMF(const std::unique_ptr& pmf_grid, const std::string &filename, bool keep_open); +private: + cvm::real m_kbt; + cvm::real m_barrier; + cvm::real m_biasfactor; + cvm::real m_bias_prefactor; + cvm::real m_temperature; + cvm::step_number m_pace; + cvm::step_number m_adaptive_sigma_stride; + cvm::step_number m_adaptive_counter; + unsigned long long m_counter; + cvm::real m_compression_threshold; + cvm::real m_compression_threshold2; + bool m_adaptive_sigma; + bool m_fixed_sigma; + bool m_no_zed; + // bool m_restart; + bool m_nlist; + bool m_recursive_merge; + std::vector m_nlist_param; + std::vector m_sigma0; + std::vector m_sigma_min; + cvm::real m_epsilon; + cvm::real m_sum_weights; + cvm::real m_sum_weights2; + cvm::real m_cutoff; + cvm::real m_cutoff2; + cvm::real m_zed; + cvm::real m_old_kdenorm; + cvm::real m_kdenorm; + cvm::real m_val_at_cutoff; + cvm::real m_rct; + cvm::real m_neff; + std::vector m_kernels; + std::vector m_delta_kernels; + std::vector m_av_cv; + std::vector m_av_M2; + std::ostringstream m_kernels_output; + std::vector m_nlist_center; + std::vector m_nlist_index; + std::vector m_nlist_dev2; + size_t m_nlist_steps; + bool m_nlist_update; + bool m_nlist_pace_reset; + size_t m_nker; + bool m_calc_work; + cvm::real m_work; + /// Communication between different replicas + Communication comm; + /// \brief Identifier for this replica + std::string replica_id; + size_t m_num_walkers; + size_t shared_freq; + size_t m_num_threads; + size_t m_nlker; + // size_t m_state_stride; + // std::unordered_map m_kernel_output_components; + std::string m_kernels_output_headers; + cvm::step_number m_traj_output_frequency; + traj_line m_traj_line; + std::ostringstream m_traj_oss; + bool m_is_first_step; + std::vector m_cv; + // For saving states + decltype(m_zed) m_saved_zed; + decltype(m_sum_weights) m_saved_sum_weights; + decltype(m_sum_weights2) m_saved_sum_weights2; + decltype(m_kernels) m_saved_kernels; + // PMF grid from reweighting + bool m_pmf_grid_on; + std::vector m_pmf_cvs; + std::string grid_conf; + std::shared_ptr m_reweight_grid; + std::unique_ptr m_pmf_grid; + cvm::step_number m_pmf_hist_freq; + bool m_pmf_shared; // shared PMF among replicas + std::unique_ptr m_global_reweight_grid; + std::unique_ptr m_global_pmf_grid; + bool m_explore; + bool m_inf_biasfactor; +}; + +#endif // COLVARBIAS_OPES_H diff --git a/lib/colvars/colvarcomp.cpp b/lib/colvars/colvarcomp.cpp index e6729f43a7..9a056f7dd3 100644 --- a/lib/colvars/colvarcomp.cpp +++ b/lib/colvars/colvarcomp.cpp @@ -261,7 +261,6 @@ int colvar::cvc::init_dependencies() { require_feature_children(f_cvc_explicit_gradient, f_ag_explicit_gradient); init_feature(f_cvc_inv_gradient, "inverse_gradient", f_type_dynamic); - require_feature_self(f_cvc_inv_gradient, f_cvc_gradient); init_feature(f_cvc_debug_gradient, "debug_gradient", f_type_user); require_feature_self(f_cvc_debug_gradient, f_cvc_gradient); @@ -525,7 +524,7 @@ void colvar::cvc::calc_force_invgrads() void colvar::cvc::calc_Jacobian_derivative() { - cvm::error("Error: calculation of inverse gradients is not implemented " + cvm::error("Error: calculation of Jacobian derivatives is not implemented " "for colvar components of type \""+function_type()+"\".\n", COLVARS_NOT_IMPLEMENTED); } @@ -533,8 +532,10 @@ void colvar::cvc::calc_Jacobian_derivative() void colvar::cvc::calc_fit_gradients() { - for (size_t ig = 0; ig < atom_groups.size(); ig++) { - atom_groups[ig]->calc_fit_gradients(); + if (is_enabled(f_cvc_explicit_gradient)) { + for (size_t ig = 0; ig < atom_groups.size(); ig++) { + atom_groups[ig]->calc_fit_gradients(); + } } } diff --git a/lib/colvars/colvarcomp.h b/lib/colvars/colvarcomp.h index 334fdc1f6e..53755576c2 100644 --- a/lib/colvars/colvarcomp.h +++ b/lib/colvars/colvarcomp.h @@ -233,8 +233,14 @@ public: /// Forcibly set value of CVC - useful for driving an external coordinate, /// eg. lambda dynamics - inline void set_value(colvarvalue const &new_value) { + inline void set_value(colvarvalue const &new_value, bool now=false) { x = new_value; + // Cache value to be communicated to back-end between time steps + cvm::proxy->set_alch_lambda(x.real_value); + if (now) { + // If requested (e.g. upon restarting), sync to back-end + cvm::proxy->send_alch_lambda(); + } } protected: @@ -1212,9 +1218,11 @@ protected: // No atom groups needed public: alch_lambda(); + int init_alchemy(int time_step_factor); virtual ~alch_lambda() {} virtual void calc_value(); - virtual void calc_gradients(); + virtual void calc_force_invgrads(); + virtual void calc_Jacobian_derivative(); virtual void apply_force(colvarvalue const &force); }; diff --git a/lib/colvars/colvarcomp_alchlambda.cpp b/lib/colvars/colvarcomp_alchlambda.cpp index a175ea43e8..29168d87ce 100644 --- a/lib/colvars/colvarcomp_alchlambda.cpp +++ b/lib/colvars/colvarcomp_alchlambda.cpp @@ -20,22 +20,46 @@ colvar::alch_lambda::alch_lambda() { set_function_type("alchLambda"); - disable(f_cvc_explicit_gradient); - disable(f_cvc_gradient); + provide(f_cvc_explicit_gradient, false); + provide(f_cvc_gradient, false); // Cannot apply forces on this CVC + provide(f_cvc_collect_atom_ids, false); + + provide(f_cvc_inv_gradient); // Projected force is TI derivative + provide(f_cvc_Jacobian); // Zero x.type(colvarvalue::type_scalar); - // Query initial value from back-end + + // Query initial value from back-end; will be overwritten if restarting from a state file cvm::proxy->get_alch_lambda(&x.real_value); } +int colvar::alch_lambda::init_alchemy(int factor) +{ + // We need calculation every time step + // default in Tinker-HP and NAMD2, must be enforced in NAMD3 + // Also checks back-end settings, ie. that alchemy is enabled + // (in NAMD3: alchType TI, computeEnergies at the right frequency) + + // Forbid MTS until fully implemented + if (factor != 1) { + return cvm::error("Error: timeStepFactor > 1 is not yet supported for alchemical variables."); + } + cvm::proxy->request_alch_energy_freq(factor); + + return COLVARS_OK; +} + + void colvar::alch_lambda::calc_value() { - // Special workflow: - // at the beginning of the timestep we get a force instead of calculating the value + // By default, follow external parameter + // This might get overwritten by driving extended dynamics + // (in apply_force() below) + cvm::proxy->get_alch_lambda(&x.real_value); cvm::proxy->get_dE_dlambda(&ft.real_value); - ft.real_value *= -1.0; // Energy derivative to force + ft.real_value *= -1.0; // Convert energy derivative to force // Include any force due to bias on Flambda ft.real_value += cvm::proxy->indirect_lambda_biasing_force; @@ -43,19 +67,24 @@ void colvar::alch_lambda::calc_value() } -void colvar::alch_lambda::calc_gradients() +void colvar::alch_lambda::calc_force_invgrads() { + // All the work is done in calc_value() +} + + +void colvar::alch_lambda::calc_Jacobian_derivative() +{ + jd = 0.0; } void colvar::alch_lambda::apply_force(colvarvalue const & /* force */) { - // new value will be cached and sent at end of timestep - cvm::proxy->set_alch_lambda(x.real_value); + // Forces, if any, are applied in colvar::update_extended_Lagrangian() } - colvar::alch_Flambda::alch_Flambda() { set_function_type("alch_Flambda"); diff --git a/lib/colvars/colvarcomp_angles.cpp b/lib/colvars/colvarcomp_angles.cpp index 56894e9f5c..0e7aed65ed 100644 --- a/lib/colvars/colvarcomp_angles.cpp +++ b/lib/colvars/colvarcomp_angles.cpp @@ -267,74 +267,22 @@ void colvar::dihedral::calc_value() void colvar::dihedral::calc_gradients() { - cvm::rvector A = cvm::rvector::outer(r12, r23); - cvm::real rA = A.norm(); - cvm::rvector B = cvm::rvector::outer(r23, r34); - cvm::real rB = B.norm(); - cvm::rvector C = cvm::rvector::outer(r23, A); - cvm::real rC = C.norm(); + // Eqs. (27i) ~ (27l) from https://doi.org/10.1002/(SICI)1096-987X(19960715)17:9<1132::AID-JCC5>3.0.CO;2-T. - cvm::real const cos_phi = (A*B)/(rA*rB); - cvm::real const sin_phi = (C*B)/(rC*rB); + const cvm::rvector A = cvm::rvector::outer(r12, r23); + const cvm::rvector B = cvm::rvector::outer(r23, r34); + const cvm::real nG = r23.norm(); + const cvm::real A2 = A.norm2(); + const cvm::real B2 = B.norm2(); - cvm::rvector f1, f2, f3; - - rB = 1.0/rB; - B *= rB; - - if (cvm::fabs(sin_phi) > 0.1) { - rA = 1.0/rA; - A *= rA; - cvm::rvector const dcosdA = rA*(cos_phi*A-B); - cvm::rvector const dcosdB = rB*(cos_phi*B-A); - // rA = 1.0; - - cvm::real const K = (1.0/sin_phi) * (180.0/PI); - - f1 = K * cvm::rvector::outer(r23, dcosdA); - f3 = K * cvm::rvector::outer(dcosdB, r23); - f2 = K * (cvm::rvector::outer(dcosdA, r12) - + cvm::rvector::outer(r34, dcosdB)); - } - else { - rC = 1.0/rC; - C *= rC; - cvm::rvector const dsindC = rC*(sin_phi*C-B); - cvm::rvector const dsindB = rB*(sin_phi*B-C); - // rC = 1.0; - - cvm::real const K = (-1.0/cos_phi) * (180.0/PI); - - f1.x = K*((r23.y*r23.y + r23.z*r23.z)*dsindC.x - - r23.x*r23.y*dsindC.y - - r23.x*r23.z*dsindC.z); - f1.y = K*((r23.z*r23.z + r23.x*r23.x)*dsindC.y - - r23.y*r23.z*dsindC.z - - r23.y*r23.x*dsindC.x); - f1.z = K*((r23.x*r23.x + r23.y*r23.y)*dsindC.z - - r23.z*r23.x*dsindC.x - - r23.z*r23.y*dsindC.y); - - f3 = cvm::rvector::outer(dsindB, r23); - f3 *= K; - - f2.x = K*(-(r23.y*r12.y + r23.z*r12.z)*dsindC.x - +(2.0*r23.x*r12.y - r12.x*r23.y)*dsindC.y - +(2.0*r23.x*r12.z - r12.x*r23.z)*dsindC.z - +dsindB.z*r34.y - dsindB.y*r34.z); - f2.y = K*(-(r23.z*r12.z + r23.x*r12.x)*dsindC.y - +(2.0*r23.y*r12.z - r12.y*r23.z)*dsindC.z - +(2.0*r23.y*r12.x - r12.y*r23.x)*dsindC.x - +dsindB.x*r34.z - dsindB.z*r34.x); - f2.z = K*(-(r23.x*r12.x + r23.y*r12.y)*dsindC.z - +(2.0*r23.z*r12.x - r12.z*r23.x)*dsindC.x - +(2.0*r23.z*r12.y - r12.z*r23.y)*dsindC.y - +dsindB.y*r34.x - dsindB.x*r34.y); - } + const cvm::real K = 180.0/PI; + const cvm::rvector f1 = K * nG / A2 * A; + const cvm::rvector f2 = K * ((r12 * r23 / (A2 * nG)) * A + (r34 * r23 / (B2 * nG)) * B); + const cvm::rvector f3 = K * nG / B2 * B; group1->set_weighted_gradient(-f1); - group2->set_weighted_gradient(-f2 + f1); - group3->set_weighted_gradient(-f3 + f2); + group2->set_weighted_gradient( f2 + f1); + group3->set_weighted_gradient(-f3 - f2); group4->set_weighted_gradient(f3); } diff --git a/lib/colvars/colvarcomp_distances.cpp b/lib/colvars/colvarcomp_distances.cpp index 319190c385..6de68264c3 100644 --- a/lib/colvars/colvarcomp_distances.cpp +++ b/lib/colvars/colvarcomp_distances.cpp @@ -384,32 +384,30 @@ void colvar::distance_dir::apply_force(colvarvalue const &force) cvm::real const iprod = force.rvector_value * x.rvector_value; cvm::rvector const force_tang = force.rvector_value - iprod * x.rvector_value; - if (!group1->noforce) - group1->apply_force(-1.0 * force_tang); - - if (!group2->noforce) - group2->apply_force( force_tang); + if (!group1->noforce) { + group1->apply_force(-1.0 / dist_v.norm() * force_tang); + } + if (!group2->noforce) { + group2->apply_force( 1.0 / dist_v.norm() * force_tang); + } } -cvm::real colvar::distance_dir::dist2(colvarvalue const &x1, - colvarvalue const &x2) const +cvm::real colvar::distance_dir::dist2(colvarvalue const &x1, colvarvalue const &x2) const { - return (x1.rvector_value - x2.rvector_value).norm2(); + return x1.dist2(x2); } -colvarvalue colvar::distance_dir::dist2_lgrad(colvarvalue const &x1, - colvarvalue const &x2) const +colvarvalue colvar::distance_dir::dist2_lgrad(colvarvalue const &x1, colvarvalue const &x2) const { - return colvarvalue((x1.rvector_value - x2.rvector_value), colvarvalue::type_unit3vectorderiv); + return x1.dist2_grad(x2); } -colvarvalue colvar::distance_dir::dist2_rgrad(colvarvalue const &x1, - colvarvalue const &x2) const +colvarvalue colvar::distance_dir::dist2_rgrad(colvarvalue const &x1, colvarvalue const &x2) const { - return colvarvalue((x2.rvector_value - x1.rvector_value), colvarvalue::type_unit3vectorderiv); + return x2.dist2_grad(x1); } @@ -1005,7 +1003,7 @@ void colvar::rmsd::calc_Jacobian_derivative() for (size_t ia = 0; ia < atoms->size(); ia++) { // Gradient of optimal quaternion wrt current Cartesian position - atoms->rot_deriv->calc_derivative_wrt_group1(ia, nullptr, &dq); + atoms->rot_deriv->calc_derivative_wrt_group1(ia, nullptr, &dq); g11 = 2.0 * (atoms->rot.q)[1]*dq[1]; g22 = 2.0 * (atoms->rot.q)[2]*dq[2]; @@ -1304,7 +1302,7 @@ void colvar::eigenvector::calc_Jacobian_derivative() // Gradient of optimal quaternion wrt current Cartesian position // trick: d(R^-1)/dx = d(R^t)/dx = (dR/dx)^t // we can just transpose the derivatives of the direct matrix - atoms->rot_deriv->calc_derivative_wrt_group1(ia, nullptr, &dq_1); + atoms->rot_deriv->calc_derivative_wrt_group1(ia, nullptr, &dq_1); g11 = 2.0 * quat0[1]*dq_1[1]; g22 = 2.0 * quat0[2]*dq_1[2]; @@ -1403,11 +1401,12 @@ void colvar::cartesian::apply_force(colvarvalue const &force) size_t ia, j; if (!atoms->noforce) { cvm::rvector f; + auto ag_force = atoms->get_group_force_object(); for (ia = 0; ia < atoms->size(); ia++) { for (j = 0; j < dim; j++) { f[axes[j]] = force.vector1d_value[dim*ia + j]; } - (*atoms)[ia].apply_force(f); + ag_force.add_atom_force(ia, f); } } } diff --git a/lib/colvars/colvarcomp_protein.cpp b/lib/colvars/colvarcomp_protein.cpp index f782095148..832005e2a7 100644 --- a/lib/colvars/colvarcomp_protein.cpp +++ b/lib/colvars/colvarcomp_protein.cpp @@ -28,34 +28,58 @@ colvar::alpha_angles::alpha_angles() int colvar::alpha_angles::init(std::string const &conf) { int error_code = cvc::init(conf); + if (error_code != COLVARS_OK) return error_code; std::string segment_id; - get_keyval(conf, "psfSegID", segment_id, std::string("MAIN")); - std::vector residues; - { - std::string residues_conf = ""; - key_lookup(conf, "residueRange", &residues_conf); + + bool b_use_index_groups = false; + cvm::atom_group group_CA, group_N, group_O; + + std::string residues_conf = ""; + std::string prefix; + + // residueRange is mandatory for the topology-based case + if (key_lookup(conf, "residueRange", &residues_conf)) { if (residues_conf.size()) { std::istringstream is(residues_conf); int initial, final; char dash; if ( (is >> initial) && (initial > 0) && - (is >> dash) && (dash == '-') && - (is >> final) && (final > 0) ) { + (is >> dash) && (dash == '-') && + (is >> final) && (final > 0) ) { for (int rnum = initial; rnum <= final; rnum++) { residues.push_back(rnum); } } } else { - error_code |= - cvm::error("Error: no residues defined in \"residueRange\".\n", COLVARS_INPUT_ERROR); + return cvm::error("Error: no residues defined in \"residueRange\".\n", COLVARS_INPUT_ERROR); } - } - if (residues.size() < 5) { - error_code |= cvm::error("Error: not enough residues defined in \"residueRange\".\n", - COLVARS_INPUT_ERROR); + if (residues.size() < 5) { + return cvm::error("Error: not enough residues defined in \"residueRange\".\n", COLVARS_INPUT_ERROR); + } + get_keyval(conf, "psfSegID", segment_id, std::string("MAIN")); + + } else { + b_use_index_groups = true; + get_keyval(conf, "prefix", prefix, "alpha_"); + + // Not all groups are mandatory, parse silently + group_CA.add_index_group(prefix + "CA", true); + group_N.add_index_group(prefix + "N", true); + group_O.add_index_group(prefix + "O", true); + int na = group_CA.size(); + int nn = group_N.size(); + int no = group_O.size(); + if ((nn != 0 || no != 0) && (nn != no)) { + return cvm::error("Error: If either is provided, atom groups " + prefix + "N and " + prefix + "O must have the same number of atoms.", + COLVARS_INPUT_ERROR); + } + if (nn != 0 && na != 0 && nn != na) { + return cvm::error("Error: If both are provided, atom groups " + prefix + "N and " + prefix + "CA must have the same number of atoms.", + COLVARS_INPUT_ERROR); + } } std::string const &sid = segment_id; @@ -64,8 +88,7 @@ int colvar::alpha_angles::init(std::string const &conf) get_keyval(conf, "hBondCoeff", hb_coeff, hb_coeff); if ((hb_coeff < 0.0) || (hb_coeff > 1.0)) { - error_code |= - cvm::error("Error: hBondCoeff must be defined between 0 and 1.\n", COLVARS_INPUT_ERROR); + return cvm::error("Error: hBondCoeff must be defined between 0 and 1.\n", COLVARS_INPUT_ERROR); } @@ -73,14 +96,29 @@ int colvar::alpha_angles::init(std::string const &conf) get_keyval(conf, "angleTol", theta_tol, theta_tol); if (hb_coeff < 1.0) { - - for (size_t i = 0; i < residues.size()-2; i++) { - theta.push_back(new colvar::angle(cvm::atom(r[i ], "CA", sid), - cvm::atom(r[i+1], "CA", sid), - cvm::atom(r[i+2], "CA", sid))); - register_atom_group(theta.back()->atom_groups[0]); - register_atom_group(theta.back()->atom_groups[1]); - register_atom_group(theta.back()->atom_groups[2]); + if (b_use_index_groups) { + if (group_CA.size() < 5) { + return cvm::error("Not enough atoms (" + cvm::to_str(group_CA.size()) + ") in index group \"" + prefix + "CA\"", + COLVARS_INPUT_ERROR); + } + for (size_t i = 0; i < group_CA.size()-2; i++) { + // Note: the angle constructor constructs copies of the atom objects + theta.push_back(new colvar::angle(group_CA[i], + group_CA[i+1], + group_CA[i+2])); + register_atom_group(theta.back()->atom_groups[0]); + register_atom_group(theta.back()->atom_groups[1]); + register_atom_group(theta.back()->atom_groups[2]); + } + } else { + for (size_t i = 0; i < residues.size()-2; i++) { + theta.push_back(new colvar::angle(cvm::atom(r[i ], "CA", sid), + cvm::atom(r[i+1], "CA", sid), + cvm::atom(r[i+2], "CA", sid))); + register_atom_group(theta.back()->atom_groups[0]); + register_atom_group(theta.back()->atom_groups[1]); + register_atom_group(theta.back()->atom_groups[2]); + } } } else { @@ -93,14 +131,27 @@ int colvar::alpha_angles::init(std::string const &conf) get_keyval(conf, "hBondExpDenom", ed, ed); if (hb_coeff > 0.0) { - - for (size_t i = 0; i < residues.size()-4; i++) { - hb.push_back(new colvar::h_bond(cvm::atom(r[i ], "O", sid), - cvm::atom(r[i+4], "N", sid), - r0, en, ed)); - register_atom_group(hb.back()->atom_groups[0]); + if (b_use_index_groups) { + if (group_N.size() < 5) { + return cvm::error("Not enough atoms (" + cvm::to_str(group_N.size()) + ") in index group \"" + prefix + "N\"", + COLVARS_INPUT_ERROR); + } + for (size_t i = 0; i < group_N.size()-4; i++) { + // Note: we need to call the atom copy constructor here because + // the h_bond constructor does not make copies of the provided atoms + hb.push_back(new colvar::h_bond(cvm::atom(group_O[i]), + cvm::atom(group_N[i+4]), + r0, en, ed)); + register_atom_group(hb.back()->atom_groups[0]); + } + } else { + for (size_t i = 0; i < residues.size()-4; i++) { + hb.push_back(new colvar::h_bond(cvm::atom(r[i ], "O", sid), + cvm::atom(r[i+4], "N", sid), + r0, en, ed)); + register_atom_group(hb.back()->atom_groups[0]); + } } - } else { cvm::log("The hBondCoeff specified will disable the hydrogen bond terms.\n"); } @@ -290,41 +341,62 @@ int colvar::dihedPC::init(std::string const &conf) if (cvm::debug()) cvm::log("Initializing dihedral PC object.\n"); + bool b_use_index_groups = false; std::string segment_id; - get_keyval(conf, "psfSegID", segment_id, std::string("MAIN")); - std::vector residues; - { - std::string residues_conf = ""; - key_lookup(conf, "residueRange", &residues_conf); + size_t n_residues; + std::string residues_conf = ""; + std::string prefix; + cvm::atom_group group_CA, group_N, group_C; + + // residueRange is mandatory for the topology-based case + if (key_lookup(conf, "residueRange", &residues_conf)) { if (residues_conf.size()) { std::istringstream is(residues_conf); int initial, final; char dash; if ( (is >> initial) && (initial > 0) && - (is >> dash) && (dash == '-') && - (is >> final) && (final > 0) ) { + (is >> dash) && (dash == '-') && + (is >> final) && (final > 0) ) { for (int rnum = initial; rnum <= final; rnum++) { residues.push_back(rnum); } } } else { - error_code |= - cvm::error("Error: no residues defined in \"residueRange\".\n", COLVARS_INPUT_ERROR); + return cvm::error("Error: no residues defined in \"residueRange\".\n", COLVARS_INPUT_ERROR); } - } + n_residues = residues.size(); + get_keyval(conf, "psfSegID", segment_id, std::string("MAIN")); - if (residues.size() < 2) { + } else { + + b_use_index_groups = true; + get_keyval(conf, "prefix", prefix, "dihed_"); + + // All three groups are required + group_CA.add_index_group(prefix + "CA"); + group_N.add_index_group(prefix + "N"); + group_C.add_index_group(prefix + "C"); + int na = group_CA.size(); + int nn = group_N.size(); + int nc = group_C.size(); + if ((nn != na || na != nc)) { + return cvm::error("Error: atom groups " + prefix + "N, " + prefix + "CA, and " + prefix + + "C must have the same number of atoms.", COLVARS_INPUT_ERROR); + } + n_residues = nn; + } + if (n_residues < 2) { error_code |= - cvm::error("Error: dihedralPC requires at least two residues.\n", COLVARS_INPUT_ERROR); + cvm::error("Error: dihedralPC requires at least two residues.\n", COLVARS_INPUT_ERROR); } std::string const &sid = segment_id; std::vector const &r = residues; std::string vecFileName; - int vecNumber; if (get_keyval(conf, "vectorFile", vecFileName, vecFileName)) { + int vecNumber; get_keyval(conf, "vectorNumber", vecNumber, 0); if (vecNumber < 1) { error_code |= @@ -339,9 +411,8 @@ int colvar::dihedPC::init(std::string const &conf) } // TODO: adapt to different formats by setting this flag - bool eigenvectors_as_columns = true; - - if (eigenvectors_as_columns) { + // bool eigenvectors_as_columns = true; + // if (eigenvectors_as_columns) { // Carma-style dPCA file std::string line; cvm::real c; @@ -352,9 +423,7 @@ int colvar::dihedPC::init(std::string const &conf) for (int i=0; i> c; coeffs.push_back(c); } - } -/* TODO Uncomment this when different formats are recognized - else { + /* } else { // Uncomment this when different formats are recognized // Eigenvectors as lines // Skip to the right line for (int i = 1; iatom_groups[0]); register_atom_group(theta.back()->atom_groups[1]); register_atom_group(theta.back()->atom_groups[2]); register_atom_group(theta.back()->atom_groups[3]); // Phi (next res) - theta.push_back(new colvar::dihedral(cvm::atom(r[i ], "C", sid), - cvm::atom(r[i+1], "N", sid), - cvm::atom(r[i+1], "CA", sid), - cvm::atom(r[i+1], "C", sid))); + if (b_use_index_groups) { + theta.push_back(new colvar::dihedral(group_C[i], + group_N[i+1], + group_CA[i+1], + group_C[i+1])); + } else { + theta.push_back(new colvar::dihedral(cvm::atom(r[i ], "C", sid), + cvm::atom(r[i+1], "N", sid), + cvm::atom(r[i+1], "CA", sid), + cvm::atom(r[i+1], "C", sid))); + } register_atom_group(theta.back()->atom_groups[0]); register_atom_group(theta.back()->atom_groups[1]); register_atom_group(theta.back()->atom_groups[2]); diff --git a/lib/colvars/colvarcomp_rotations.cpp b/lib/colvars/colvarcomp_rotations.cpp index a04ace851a..766a0870d1 100644 --- a/lib/colvars/colvarcomp_rotations.cpp +++ b/lib/colvars/colvarcomp_rotations.cpp @@ -137,11 +137,14 @@ void colvar::orientation::apply_force(colvarvalue const &force) if (!atoms->noforce) { rot_deriv_impl->prepare_derivative(rotation_derivative_dldq::use_dq); cvm::vector1d dq0_2; + auto ag_force = atoms->get_group_force_object(); for (size_t ia = 0; ia < atoms->size(); ia++) { - rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); - for (size_t i = 0; i < 4; i++) { - (*atoms)[ia].apply_force(FQ[i] * dq0_2[i]); - } + rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); + const auto f_ia = FQ[0] * dq0_2[0] + + FQ[1] * dq0_2[1] + + FQ[2] * dq0_2[2] + + FQ[3] * dq0_2[3]; + ag_force.add_atom_force(ia, f_ia); } } } @@ -205,7 +208,7 @@ void colvar::orientation_angle::calc_gradients() rot_deriv_impl->prepare_derivative(rotation_derivative_dldq::use_dq); cvm::vector1d dq0_2; for (size_t ia = 0; ia < atoms->size(); ia++) { - rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); + rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); (*atoms)[ia].grad = (dxdq0 * dq0_2[0]); } } @@ -265,7 +268,7 @@ void colvar::orientation_proj::calc_gradients() rot_deriv_impl->prepare_derivative(rotation_derivative_dldq::use_dq); cvm::vector1d dq0_2; for (size_t ia = 0; ia < atoms->size(); ia++) { - rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); + rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); (*atoms)[ia].grad = (dxdq0 * dq0_2[0]); } } @@ -314,7 +317,7 @@ void colvar::tilt::calc_gradients() cvm::vector1d dq0_2; for (size_t ia = 0; ia < atoms->size(); ia++) { (*atoms)[ia].grad = cvm::rvector(0.0, 0.0, 0.0); - rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); + rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); for (size_t iq = 0; iq < 4; iq++) { (*atoms)[ia].grad += (dxdq[iq] * dq0_2[iq]); } @@ -351,7 +354,7 @@ void colvar::spin_angle::calc_gradients() cvm::vector1d dq0_2; for (size_t ia = 0; ia < atoms->size(); ia++) { (*atoms)[ia].grad = cvm::rvector(0.0, 0.0, 0.0); - rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); + rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); for (size_t iq = 0; iq < 4; iq++) { (*atoms)[ia].grad += (dxdq[iq] * dq0_2[iq]); } @@ -399,7 +402,7 @@ void colvar::euler_phi::calc_gradients() rot_deriv_impl->prepare_derivative(rotation_derivative_dldq::use_dq); cvm::vector1d dq0_2; for (size_t ia = 0; ia < atoms->size(); ia++) { - rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); + rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); (*atoms)[ia].grad = (dxdq0 * dq0_2[0]) + (dxdq1 * dq0_2[1]) + (dxdq2 * dq0_2[2]) + @@ -448,7 +451,7 @@ void colvar::euler_psi::calc_gradients() rot_deriv_impl->prepare_derivative(rotation_derivative_dldq::use_dq); cvm::vector1d dq0_2; for (size_t ia = 0; ia < atoms->size(); ia++) { - rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); + rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); (*atoms)[ia].grad = (dxdq0 * dq0_2[0]) + (dxdq1 * dq0_2[1]) + (dxdq2 * dq0_2[2]) + @@ -495,7 +498,7 @@ void colvar::euler_theta::calc_gradients() rot_deriv_impl->prepare_derivative(rotation_derivative_dldq::use_dq); cvm::vector1d dq0_2; for (size_t ia = 0; ia < atoms->size(); ia++) { - rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); + rot_deriv_impl->calc_derivative_wrt_group2(ia, nullptr, &dq0_2); (*atoms)[ia].grad = (dxdq0 * dq0_2[0]) + (dxdq1 * dq0_2[1]) + (dxdq2 * dq0_2[2]) + diff --git a/lib/colvars/colvarcomp_torchann.cpp b/lib/colvars/colvarcomp_torchann.cpp new file mode 100644 index 0000000000..7b83baf9b6 --- /dev/null +++ b/lib/colvars/colvarcomp_torchann.cpp @@ -0,0 +1,233 @@ +// -*- c++ -*- + +// This file is part of the Collective Variables module (Colvars). +// The original version of Colvars and its updates are located at: +// https://github.com/Colvars/colvars +// Please update all Colvars source files before making any changes. +// If you wish to distribute your changes, please submit them to the +// Colvars repository at GitHub. + +#include "colvar.h" +#include "colvarcomp.h" +#include "colvarmodule.h" +#include "colvarparse.h" +#include "colvarvalue.h" + +#include "colvarcomp_torchann.h" + + +#ifdef COLVARS_TORCH + +colvar::torchANN::torchANN() +{ + set_function_type("torchANN"); + provide(f_cvc_periodic); +} + +colvar::torchANN::~torchANN() {} + + +int colvar::torchANN::init(std::string const &conf) { + + int error_code = linearCombination::init(conf); + + std::string model_file ; + get_keyval(conf, "modelFile", model_file, std::string("")); + try { + nn = torch::jit::load(model_file); + nn.to(torch::kCPU); + cvm::log("torch model loaded.") ; + } catch (const std::exception & e) { + return cvm::error("Error: couldn't load libtorch model (see below).\n" + cvm::to_str(e.what()), + COLVARS_INPUT_ERROR); + } + + auto const legacy_keyword = get_keyval(conf, "m_output_index", m_output_index, m_output_index); + if (legacy_keyword) { + cvm::log("Warning: m_output_index is a deprecated keyword, please use output_component instead.\n"); + } + get_keyval(conf, "output_component", m_output_index, m_output_index); + + get_keyval(conf, "doubleInputTensor", use_double_input, use_double_input); + //get_keyval(conf, "useGPU", use_gpu, false); + + cvc_indices.resize(cv.size(),0); + + size_t num_inputs = 0; + // compute total number of inputs of neural network + for (size_t i_cv = 0; i_cv < cv.size(); ++i_cv) + { + num_inputs += cv[i_cv]->value().size() ; + if (i_cv < cv.size() - 1) + cvc_indices[i_cv+1] = num_inputs; + } + cvm::log("Input dimension of model: " + cvm::to_str(num_inputs)); + + // initialize the input tensor + auto options = torch::TensorOptions().dtype(torch::kFloat32).requires_grad(true); + + /* + if (use_gpu) { + if (torch::cuda::is_available()) { + try { + nn.to(torch::kCUDA); + } catch(const std::exception & e) { + cvm::error("Failed to move model to GPU."); + use_gpu = false; + } + } else { + use_gpu = false; + cvm::log("GPU not available."); + } + } + + if (use_gpu) { + options = options.device(torch::kCUDA); + if (use_double_input) { + cvm::log("Data type reset to Float for GPU computation!"); + use_double_input = false; + } + } + */ + + if (use_double_input) { // set type to double + options = options.dtype(torch::kFloat64); + nn.to(torch::kFloat64); + cvm::log("Model's dtype: kFloat64."); + } else { + cvm::log("Model's dtype: kFloat32."); + } + + input_tensor = torch::zeros({1,(long int) num_inputs}, options); + + try { // test the model + std::vector inputs={input_tensor}; + nn_outputs = nn.forward(inputs).toTensor()[0][m_output_index]; + cvm::log("Evaluating model with zero tensor succeeded."); + } catch (const std::exception & e) { + error_code |= cvm::error("Error: evaluating model with zero tensor failed (see below).\n" + + cvm::to_str(e.what()), + COLVARS_INPUT_ERROR); + } + + return error_code; +} + + +void colvar::torchANN::calc_value() { + + for (size_t i_cv = 0; i_cv < cv.size(); ++i_cv) + cv[i_cv]->calc_value(); + + /* + if (use_gpu) + input_tensor = input_tensor.to(torch::kCPU); + */ + + // set input tensor with no_grad + { + torch::NoGradGuard no_grad; + size_t l = 0; + for (size_t i_cv = 0; i_cv < cv.size(); ++i_cv) { + const colvarvalue& current_cv_value = cv[i_cv]->value(); + if (current_cv_value.type() == colvarvalue::type_scalar) { + input_tensor[0][l++] = cv[i_cv]->sup_coeff * (cvm::pow(current_cv_value.real_value, cv[i_cv]->sup_np)); + } else { + for (size_t j_elem = 0; j_elem < current_cv_value.size(); ++j_elem) + input_tensor[0][l++] = cv[i_cv]->sup_coeff * current_cv_value[j_elem]; + } + } + } + + /* + if (use_gpu) + input_tensor = input_tensor.to(torch::kCUDA); + */ + + std::vector inputs={input_tensor}; + + // evaluate the value of function + nn_outputs = nn.forward(inputs).toTensor()[0][m_output_index]; + + input_grad = torch::autograd::grad({nn_outputs}, {input_tensor})[0][0]; + + /* + if (use_gpu) + input_grad = input_grad.to(torch::kCPU); + */ + + x = nn_outputs.item() ; + + this->wrap(x); + +} + +void colvar::torchANN::calc_gradients() { + for (size_t i_cv = 0; i_cv < cv.size(); ++i_cv) { + cv[i_cv]->calc_gradients(); + if (cv[i_cv]->is_enabled(f_cvc_explicit_gradient)) { + const cvm::real factor_polynomial = getPolynomialFactorOfCVGradient(i_cv); + // get the initial index of this cvc + size_t l = cvc_indices[i_cv]; + for (size_t j_elem = 0; j_elem < cv[i_cv]->value().size(); ++j_elem) { + // get derivative of neural network wrt its input + const cvm::real factor = input_grad[l+j_elem].item(); + for (size_t k_ag = 0 ; k_ag < cv[i_cv]->atom_groups.size(); ++k_ag) { + for (size_t l_atom = 0; l_atom < (cv[i_cv]->atom_groups)[k_ag]->size(); ++l_atom) { + (*(cv[i_cv]->atom_groups)[k_ag])[l_atom].grad = factor_polynomial * factor * (*(cv[i_cv]->atom_groups)[k_ag])[l_atom].grad; + } + } + } + } + } +} + +void colvar::torchANN::apply_force(colvarvalue const &force) { + + for (size_t i_cv = 0; i_cv < cv.size(); ++i_cv) { + // If this CV uses explicit gradients, then atomic gradients is already calculated + // We can apply the force to atom groups directly + if (cv[i_cv]->is_enabled(f_cvc_explicit_gradient)) { + for (size_t k_ag = 0 ; k_ag < cv[i_cv]->atom_groups.size(); ++k_ag) { + (cv[i_cv]->atom_groups)[k_ag]->apply_colvar_force(force.real_value); + } + } else { + const colvarvalue& current_cv_value = cv[i_cv]->value(); + colvarvalue cv_force(current_cv_value); + cv_force.reset(); + const cvm::real factor_polynomial = getPolynomialFactorOfCVGradient(i_cv); + // get the initial index of this cvc + size_t l = cvc_indices[i_cv]; + for (size_t j_elem = 0; j_elem < current_cv_value.size(); ++j_elem) { + cv_force[j_elem] = factor_polynomial * input_grad[l+j_elem].item() * force.real_value; + } + cv[i_cv]->apply_force(cv_force); + } + } +} + + +#else + +colvar::torchANN::torchANN() +{ + set_function_type("torchANN"); +} + +colvar::torchANN::~torchANN() {} + +int colvar::torchANN::init(std::string const &conf) { + + return cvm::error( + "torchANN requires the libtorch library, but it is not enabled during compilation.\n" + "Please refer to the Compilation Notes section of the Colvars manual for more " + "information.\n", + COLVARS_NOT_IMPLEMENTED); + +} + +void colvar::torchANN::calc_value() +{ +} + +#endif diff --git a/lib/colvars/colvarcomp_torchann.h b/lib/colvars/colvarcomp_torchann.h new file mode 100644 index 0000000000..ae241edbcc --- /dev/null +++ b/lib/colvars/colvarcomp_torchann.h @@ -0,0 +1,63 @@ +// -*- c++ -*- + +// This file is part of the Collective Variables module (Colvars). +// The original version of Colvars and its updates are located at: +// https://github.com/Colvars/colvars +// Please update all Colvars source files before making any changes. +// If you wish to distribute your changes, please submit them to the +// Colvars repository at GitHub. +// +#ifndef COLVARCOMP_TORCH_H +#define COLVARCOMP_TORCH_H + +// Declaration of torchann + +#include + +#include "colvar.h" +#include "colvarcomp.h" +#include "colvarmodule.h" + +#ifdef COLVARS_TORCH + +#include +#include + +class colvar::torchANN + : public colvar::linearCombination +{ +protected: + torch::jit::script::Module nn; + /// the index of nn output component + size_t m_output_index = 0; + bool use_double_input = false; + //bool use_gpu; + // 1d tensor, concatenation of values of sub-cvcs + torch::Tensor input_tensor; + torch::Tensor nn_outputs; + torch::Tensor input_grad; + // record the initial index of of sub-cvcs in input_tensor + std::vector cvc_indices; +public: + torchANN(); + virtual ~torchANN(); + virtual int init(std::string const &conf); + virtual void calc_value(); + virtual void calc_gradients(); + virtual void apply_force(colvarvalue const &force); +}; + +#else + +class colvar::torchANN + : public colvar::cvc +{ +public: + torchANN(); + virtual ~torchANN(); + virtual int init(std::string const &conf); + virtual void calc_value(); +}; +#endif // COLVARS_TORCH checking + +#endif diff --git a/lib/colvars/colvardeps.cpp b/lib/colvars/colvardeps.cpp index 46b7917569..3ba3209000 100644 --- a/lib/colvars/colvardeps.cpp +++ b/lib/colvars/colvardeps.cpp @@ -92,6 +92,8 @@ void colvardeps::restore_children_deps() { void colvardeps::provide(int feature_id, bool truefalse) { feature_states[feature_id].available = truefalse; + // Make sure that we don't leave this feature enabled + if (!truefalse) disable(feature_id); } @@ -123,8 +125,9 @@ bool colvardeps::get_keyval_feature(colvarparse *cvp, int colvardeps::enable(int feature_id, - bool dry_run /* default: false */, - bool toplevel /* default: true */) + bool dry_run /* default: false */, + bool toplevel /* default: true */, + bool error /*default: false */) { int res; size_t i, j; @@ -137,9 +140,12 @@ int colvardeps::enable(int feature_id, feature *f = features()[feature_id]; feature_state *fs = &feature_states[feature_id]; + // dry_run can be true because parent object is not active, yet we are displaying an error message + // then error is set to true + if (cvm::debug()) { cvm::log("DEPS: " + description + - (dry_run ? " testing " : " enabling ") + + (dry_run ? " testing " : " enabling ") + (error ? " [error] " : "") + "\"" + f->description +"\"\n"); } @@ -159,7 +165,7 @@ int colvardeps::enable(int feature_id, (is_dynamic(feature_id) ? "Dynamic" : "User-controlled"); if (!fs->available) { - if (!dry_run) { + if (!dry_run || error) { if (toplevel) { cvm::error("Error: " + feature_type_descr + " feature unavailable: \"" + f->description + "\" in " + description + ".\n"); @@ -172,7 +178,7 @@ int colvardeps::enable(int feature_id, } if (!toplevel && !is_dynamic(feature_id)) { - if (!dry_run) { + if (!dry_run || error) { cvm::log(feature_type_descr + " feature \"" + f->description + "\" cannot be enabled automatically in " + description + ".\n"); if (is_user(feature_id)) { @@ -189,7 +195,7 @@ int colvardeps::enable(int feature_id, if (cvm::debug()) cvm::log(f->description + " requires exclude " + g->description + "\n"); if (is_enabled(f->requires_exclude[i])) { - if (!dry_run) { + if (!dry_run || error) { cvm::log("Feature \"" + f->description + "\" is incompatible with \"" + g->description + "\" in " + description + ".\n"); if (toplevel) { @@ -204,10 +210,14 @@ int colvardeps::enable(int feature_id, for (i=0; irequires_self.size(); i++) { if (cvm::debug()) cvm::log(f->description + " requires self " + features()[f->requires_self[i]]->description + "\n"); - res = enable(f->requires_self[i], dry_run, false); + res = enable(f->requires_self[i], dry_run, false, error); if (res != COLVARS_OK) { - if (!dry_run) { - cvm::log("...required by \"" + f->description + "\" in " + description + "\n"); + if (!dry_run || error) { + if (toplevel) { + cvm::log("Cannot enable \"" + f->description + "\" in " + description + "\n"); + } else { + cvm::log("...required by \"" + f->description + "\" in " + description + "\n"); + } if (toplevel) { cvm::error("Error: Failed dependency in " + description + ".\n"); } @@ -225,11 +235,11 @@ int colvardeps::enable(int feature_id, int g = f->requires_alt[i][j]; if (cvm::debug()) cvm::log(f->description + " requires alt " + features()[g]->description + "\n"); - res = enable(g, true, false); // see if available + res = enable(g, true, false, error); // see if available if (res == COLVARS_OK) { ok = true; - if (!dry_run) { - enable(g, false, false); // Require again, for real + if (!dry_run || error) { + enable(g, false, false, error); // Require again, for real fs->alternate_refs.push_back(g); // We remember we enabled this // so we can free it if this feature gets disabled } @@ -245,7 +255,7 @@ int colvardeps::enable(int feature_id, for (j=0; jrequires_alt[i].size(); j++) { int g = f->requires_alt[i][j]; cvm::log(cvm::to_str(j+1) + ". " + features()[g]->description + "\n"); - enable(g, false, false); // Just for printing error output + enable(g, false, false, true); // Just for printing error output } cvm::decrease_depth(); cvm::log("-----------------------------------------\n"); @@ -264,10 +274,14 @@ int colvardeps::enable(int feature_id, for (i=0; irequires_children.size(); i++) { int g = f->requires_children[i]; for (j=0; jenable(g, dry_run || !is_enabled(), false); + res = children[j]->enable(g, dry_run || !is_enabled(), false, error); if (res != COLVARS_OK) { - if (!dry_run) { - cvm::log("...required by \"" + f->description + "\" in " + description + "\n"); + if (!dry_run || error) { + if (toplevel) { + cvm::log("Cannot enable \"" + f->description + "\" in " + description + "\n"); + } else { + cvm::log("...required by \"" + f->description + "\" in " + description + "\n"); + } if (toplevel) { cvm::error("Error: Failed dependency in " + description + ".\n"); } diff --git a/lib/colvars/colvardeps.h b/lib/colvars/colvardeps.h index 1bd304b545..92e7a88326 100644 --- a/lib/colvars/colvardeps.h +++ b/lib/colvars/colvardeps.h @@ -198,7 +198,9 @@ public: /// \param toplevel False if this is called as part of a chain of dependency resolution. /// This is used to diagnose failed dependencies by displaying the full stack: /// only the toplevel dependency will throw a fatal error. - int enable(int f, bool dry_run = false, bool toplevel = true); + /// \param error Recursively enable, printing error messages along the way + /// Necessary when propagating errors across alternate dependencies + int enable(int f, bool dry_run = false, bool toplevel = true, bool error = false); /// Disable a feature, decrease the reference count of its dependencies /// and recursively disable them as applicable @@ -255,6 +257,8 @@ public: f_cvb_scale_biasing_force, /// \brief whether this bias is applied to one or more ext-Lagrangian colvars f_cvb_extended, + /// Process this bias's data in parallel over multiple CPU threads + f_cvb_smp, f_cvb_ntot }; @@ -263,8 +267,11 @@ public: f_cv_active, /// \brief Colvar is awake (active on its own accord) this timestep f_cv_awake, - /// \brief Gradients are calculated and temporarily stored, so - /// that external forces can be applied + /// \brief External force can be applied, either to atoms or to an + /// extended DOF + f_cv_apply_force, + /// \brief Gradients are calculated and temporarily stored, + /// so that external forces can be propagated to atoms f_cv_gradient, /// \brief Collect atomic gradient data from all cvcs into vector /// atomic_gradient @@ -277,7 +284,10 @@ public: /// forces on the inverse gradient f_cv_total_force, /// \brief Calculate total force from atomic forces + /// or get it from the back-end for an external parameter f_cv_total_force_calc, + /// \brief Total force is that of current time step + f_cv_total_force_current_step, /// \brief Subtract the applied force from the total force f_cv_subtract_applied_force, /// \brief Estimate Jacobian derivative @@ -289,8 +299,10 @@ public: /// center with fictitious mass; bias forces will be applied to /// the center f_cv_extended_Lagrangian, - /// \brief An extended variable that sets an external variable in the - /// back-end (eg. an alchemical coupling parameter for lambda-dynamics) + /// \brief A variable that constrains or follows an external parameter + /// in the back-end (eg. an alchemical coupling parameter for lambda-dynamics) + /// If extended Lagrangian, then we drive the external parameter + /// Otherwise we follow it /// Can have a single component f_cv_external, /// \brief The extended system coordinate undergoes Langevin dynamics diff --git a/lib/colvars/colvargrid.cpp b/lib/colvars/colvargrid.cpp index 11693a7587..ad42966943 100644 --- a/lib/colvars/colvargrid.cpp +++ b/lib/colvars/colvargrid.cpp @@ -24,15 +24,14 @@ colvar_grid_count::colvar_grid_count() mult = 1; } -colvar_grid_count::colvar_grid_count(std::vector const &nx_i, - size_t const &def_count) - : colvar_grid(nx_i, def_count, 1) +colvar_grid_count::colvar_grid_count(std::vector &colvars, + std::string config) + : colvar_grid(colvars, 0, 1, false, nullptr, config) {} colvar_grid_count::colvar_grid_count(std::vector &colvars, - size_t const &def_count, - bool margin) - : colvar_grid(colvars, def_count, 1, margin) + std::shared_ptr params) + : colvar_grid(colvars, 0, 1, false, params) {} std::string colvar_grid_count::get_state_params() const @@ -132,13 +131,17 @@ colvar_grid_scalar::colvar_grid_scalar(colvar_grid_scalar const &g) { } -colvar_grid_scalar::colvar_grid_scalar(std::vector const &nx_i) - : colvar_grid(nx_i, 0.0, 1), samples(NULL) +colvar_grid_scalar::colvar_grid_scalar(std::vector &colvars, + std::shared_ptr params, + bool add_extra_bin, + std::string config) + : colvar_grid(colvars, 0.0, 1, add_extra_bin, params, config), samples(NULL) { } -colvar_grid_scalar::colvar_grid_scalar(std::vector &colvars, bool margin) - : colvar_grid(colvars, 0.0, 1, margin), samples(NULL) +colvar_grid_scalar::colvar_grid_scalar(std::string const &filename) + : colvar_grid(filename, 1), + samples(nullptr) { } @@ -330,89 +333,37 @@ cvm::real colvar_grid_scalar::grid_rmsd(colvar_grid_scalar const &other_grid) co colvar_grid_gradient::colvar_grid_gradient() - : colvar_grid(), samples(NULL), full_samples(0), min_samples(0) + : colvar_grid(), samples(NULL) {} -colvar_grid_gradient::colvar_grid_gradient(std::vector const &nx_i) - : colvar_grid(nx_i, 0.0, nx_i.size()), samples(NULL), full_samples(0), min_samples(0) -{} +// colvar_grid_gradient::colvar_grid_gradient(std::vector &colvars, std::string config) +// : colvar_grid(colvars, 0.0, colvars.size(), false, nullptr, config), samples(NULL) +// {} +// colvar_grid_gradient::colvar_grid_gradient(std::vector &colvars, +// std::shared_ptr samples_in) +// : colvar_grid(colvars, 0.0, colvars.size(), false, samples_in), samples(samples_in) +// { +// if (samples_in) +// samples_in->has_parent_data = true; +// } -colvar_grid_gradient::colvar_grid_gradient(std::vector &colvars) - : colvar_grid(colvars, 0.0, colvars.size()), samples(NULL), full_samples(0), min_samples(0) -{} - - -colvar_grid_gradient::colvar_grid_gradient(std::vector &colvars, std::shared_ptr samples_in) - : colvar_grid(colvars, 0.0, colvars.size()), samples(samples_in), full_samples(0), min_samples(0) +colvar_grid_gradient::colvar_grid_gradient(std::vector &colvars, + std::shared_ptr samples_in, + std::shared_ptr params, + std::string config) + : colvar_grid(colvars, 0.0, colvars.size(), false, params, config), samples(samples_in) { - samples_in->has_parent_data = true; + if (samples_in) + samples_in->has_parent_data = true; } -colvar_grid_gradient::colvar_grid_gradient(std::string &filename) - : colvar_grid(), - samples(NULL) +colvar_grid_gradient::colvar_grid_gradient(std::string const &filename) + : colvar_grid(filename, 0), + samples(nullptr) { - std::istream &is = cvm::main()->proxy->input_stream(filename, - "gradient file"); - if (!is) { - return; - } - - // Data in the header: nColvars, then for each - // xiMin, dXi, nPoints, periodic flag - - std::string hash; - size_t i; - - if ( !(is >> hash) || (hash != "#") ) { - cvm::error("Error reading grid at position "+ - cvm::to_str(static_cast(is.tellg()))+ - " in stream(read \"" + hash + "\")\n"); - return; - } - - is >> nd; - - if (nd > 50) { - cvm::error("Error: excessive number of dimensions in file \""+ - filename+"\". Please ensure that the file is not corrupt.\n", - COLVARS_INPUT_ERROR); - return; - } - - mult = nd; - std::vector lower_in(nd), widths_in(nd); - std::vector nx_in(nd); - std::vector periodic_in(nd); - - for (i = 0; i < nd; i++ ) { - if ( !(is >> hash) || (hash != "#") ) { - cvm::error("Error reading grid at position "+ - cvm::to_str(static_cast(is.tellg()))+ - " in stream(read \"" + hash + "\")\n"); - return; - } - - is >> lower_in[i] >> widths_in[i] >> nx_in[i] >> periodic_in[i]; - } - - this->setup(nx_in, 0., mult); - - widths = widths_in; - - for (i = 0; i < nd; i++ ) { - lower_boundaries.push_back(colvarvalue(lower_in[i])); - periodic.push_back(static_cast(periodic_in[i])); - } - - // Reset the istream for read_multicol, which expects the whole file - is.clear(); - is.seekg(0); - read_multicol(is); - cvm::main()->proxy->close_input_stream(filename); } std::string colvar_grid_gradient::get_state_params() const @@ -586,12 +537,13 @@ cvm::real colvar_grid_gradient::grid_rmsd(colvar_grid_gradient const &other_grid } -integrate_potential::integrate_potential(std::vector &colvars, std::shared_ptr gradients) - : colvar_grid_scalar(colvars, true), +integrate_potential::integrate_potential(std::vector &colvars, + std::shared_ptr gradients) + : colvar_grid_scalar(colvars, gradients, true), b_smoothed(false), gradients(gradients) { - // parent class colvar_grid_scalar is constructed with margin option set to true + // parent class colvar_grid_scalar is constructed with add_extra_bin option set to true // hence PMF grid is wider than gradient grid if non-PBC if (nd > 1) { diff --git a/lib/colvars/colvargrid.h b/lib/colvars/colvargrid.h index 4cbbb10961..697b46a560 100644 --- a/lib/colvars/colvargrid.h +++ b/lib/colvars/colvargrid.h @@ -19,17 +19,13 @@ #include "colvarparse.h" -/// \brief Grid of values of a function of several collective -/// variables \param T The data type -/// -/// Only scalar colvars supported so far: vector colvars are treated as arrays -template class colvar_grid : public colvarparse { - - //protected: -public: // TODO create accessors for these after all instantiations work +/// \brief Unified base class for grid of values of a function of several collective +/// variables +class colvar_grid_params { +public: /// Number of dimensions - size_t nd; + size_t nd = 0; /// Number of points along each dimension std::vector nx; @@ -37,6 +33,27 @@ public: // TODO create accessors for these after all instantiations work /// Cumulative number of points along each dimension std::vector nxc; + /// Lower boundaries of the colvars in this grid + std::vector lower_boundaries; + + /// Upper boundaries of the colvars in this grid + std::vector upper_boundaries; + + /// Widths of the colvars in this grid + std::vector widths; +}; + + +/// \brief Grid of values of a function of several collective +/// variables \param T The data type +/// +/// Only scalar colvars supported so far: vector colvars are treated as arrays +/// All common, type-independent members are collected in the base class colvar_grid_base +template class colvar_grid : public colvar_grid_params, public colvarparse { + + //protected: +public: // TODO create accessors for these after all instantiations work + /// \brief Multiplicity of each datum (allow the binning of /// non-scalar types such as atomic gradients) size_t mult; @@ -73,13 +90,6 @@ public: // TODO create accessors for these after all instantiations work } public: - - /// Lower boundaries of the colvars in this grid - std::vector lower_boundaries; - - /// Upper boundaries of the colvars in this grid - std::vector upper_boundaries; - /// Whether some colvars are periodic std::vector periodic; @@ -89,9 +99,6 @@ public: /// Whether some colvars have hard upper boundaries std::vector hard_upper_boundaries; - /// Widths of the colvars in this grid - std::vector widths; - /// True if this is a count grid related to another grid of data bool has_parent_data; @@ -218,19 +225,15 @@ public: /// \brief "Almost copy-constructor": only copies configuration /// parameters from another grid, but doesn't reallocate stuff; /// setup() must be called after that; - colvar_grid(colvar_grid const &g) : colvarparse(), - nd(g.nd), - nx(g.nx), + colvar_grid(colvar_grid const &g) : colvar_grid_params(colvar_grid_params(g)), + colvarparse(), mult(g.mult), data(), cv(g.cv), use_actual_value(g.use_actual_value), - lower_boundaries(g.lower_boundaries), - upper_boundaries(g.upper_boundaries), periodic(g.periodic), hard_lower_boundaries(g.hard_lower_boundaries), hard_upper_boundaries(g.hard_upper_boundaries), - widths(g.widths), has_parent_data(false), has_data(false) {} @@ -247,22 +250,31 @@ public: this->setup(nx_i, t, mult_i); } - /// \brief Constructor from a vector of colvars + /// \brief Constructor from a vector of colvars or an optional grid config string /// \param add_extra_bin requests that non-periodic dimensions are extended /// by 1 bin to accommodate the integral (PMF) of another gridded quantity (gradient) colvar_grid(std::vector const &colvars, T const &t = T(), size_t mult_i = 1, - bool add_extra_bin = false) + bool add_extra_bin = false, + std::shared_ptr params = nullptr, + std::string config = std::string()) : has_parent_data(false), has_data(false) { (void) t; - this->init_from_colvars(colvars, mult_i, add_extra_bin); + this->init_from_colvars(colvars, mult_i, add_extra_bin, params, config); } + /// \brief Constructor from a multicol file + /// \param filename multicol file containing data to be read + /// \param multi_i multiplicity of the data - if 0, assume gradient multiplicity (mult = nd) + colvar_grid(std::string const &filename, size_t mult_i = 1); + int init_from_colvars(std::vector const &colvars, size_t mult_i = 1, - bool add_extra_bin = false) + bool add_extra_bin = false, + std::shared_ptr params = nullptr, + std::string config = std::string()) { if (cvm::debug()) { cvm::log("Reading grid configuration from collective variables.\n"); @@ -279,8 +291,7 @@ public: " collective variables, multiplicity = "+cvm::to_str(mult_i)+".\n"); } - for (i = 0; i < cv.size(); i++) { - + for (i = 0; i < nd; i++) { if (cv[i]->value().type() != colvarvalue::type_scalar) { cvm::error("Colvar grids can only be automatically " "constructed for scalar variables. " @@ -298,7 +309,6 @@ public: widths.push_back(cv[i]->width); hard_lower_boundaries.push_back(cv[i]->is_enabled(colvardeps::f_cv_hard_lower_boundary)); hard_upper_boundaries.push_back(cv[i]->is_enabled(colvardeps::f_cv_hard_upper_boundary)); - periodic.push_back(cv[i]->periodic_boundaries()); // By default, get reported colvar value (for extended Lagrangian colvars) use_actual_value.push_back(false); @@ -310,22 +320,55 @@ public: use_actual_value[i-1] = true; } + // This needs to work if the boundaries are undefined in the colvars + lower_boundaries.push_back(cv[i]->lower_boundary); + upper_boundaries.push_back(cv[i]->upper_boundary); + } + + // Replace widths and boundaries with optional custom configuration + if (!config.empty()) { + this->parse_params(config); + this->check_keywords(config, "grid"); + + if (params) { + cvm::error("Error: init_from_colvars was passed both a grid config and a template grid.", COLVARS_BUG_ERROR); + return COLVARS_BUG_ERROR; + } + } else if (params) { + // Match grid sizes with template + + if (params->nd != nd) { + cvm::error("Trying to initialize grid from template with wrong dimension (" + + cvm::to_str(params->nd) + " instead of " + + cvm::to_str(this->nd) + ")."); + return COLVARS_ERROR; + } + + widths =params->widths; + lower_boundaries =params->lower_boundaries; + upper_boundaries =params->upper_boundaries; + } + + // Only now can we determine periodicity + for (i = 0; i < nd; i++) { + periodic.push_back(cv[i]->periodic_boundaries(lower_boundaries[i].real_value, + upper_boundaries[i].real_value)); + if (add_extra_bin) { + // Shift the grid by half the bin width (values at edges instead of center of bins) + lower_boundaries[i] -= 0.5 * widths[i]; + if (periodic[i]) { - // Shift the grid by half the bin width (values at edges instead of center of bins) - lower_boundaries.push_back(cv[i]->lower_boundary.real_value - 0.5 * widths[i]); - upper_boundaries.push_back(cv[i]->upper_boundary.real_value - 0.5 * widths[i]); + // Just shift + upper_boundaries[i] -= 0.5 * widths[i]; } else { - // Make this grid larger by one bin width - lower_boundaries.push_back(cv[i]->lower_boundary.real_value - 0.5 * widths[i]); - upper_boundaries.push_back(cv[i]->upper_boundary.real_value + 0.5 * widths[i]); + // Widen grid by one bin width + upper_boundaries[i] += 0.5 * widths[i]; } - } else { - lower_boundaries.push_back(cv[i]->lower_boundary); - upper_boundaries.push_back(cv[i]->upper_boundary); } } + // Reset grid sizes based on widths and boundaries this->init_from_boundaries(); return this->setup(); } @@ -966,14 +1009,12 @@ public: virtual ~colvar_grid_count() {} - /// Constructor - colvar_grid_count(std::vector const &nx_i, - size_t const &def_count = 0); - - /// Constructor from a vector of colvars + /// Constructor from a vector of colvars or a config string colvar_grid_count(std::vector &colvars, - size_t const &def_count = 0, - bool add_extra_bin = false); + std::shared_ptr params = nullptr); + + colvar_grid_count(std::vector &colvars, + std::string config); /// Increment the counter at given position inline void incr_count(std::vector const &ix) @@ -1255,12 +1296,14 @@ public: /// Destructor virtual ~colvar_grid_scalar(); - /// Constructor from specific sizes arrays - colvar_grid_scalar(std::vector const &nx_i); - /// Constructor from a vector of colvars colvar_grid_scalar(std::vector &colvars, - bool add_extra_bin = false); + std::shared_ptr params = nullptr, + bool add_extra_bin = false, + std::string config = std::string()); + + /// Constructor from a multicol file + colvar_grid_scalar(std::string const &filename); /// Accumulate the value inline void acc_value(std::vector const &ix, @@ -1334,8 +1377,8 @@ public: /// \brief Return the gradient of the scalar field from finite differences /// Input coordinates are those of gradient grid, shifted wrt scalar grid - /// Should not be called on edges of scalar grid, provided the latter has margins - /// wrt gradient grid + /// Should not be called on edges of scalar grid, provided the latter has + /// margins (extra bins) wrt gradient grid inline void vector_gradient_finite_diff( const std::vector &ix0, std::vector &grad) { cvm::real A0, A1; @@ -1566,17 +1609,21 @@ public: virtual ~colvar_grid_gradient() {} - /// Constructor from specific sizes arrays - colvar_grid_gradient(std::vector const &nx_i); + // /// Constructor from specific sizes arrays + // colvar_grid_gradient(std::vector const &nx_i); - /// Constructor from a vector of colvars - colvar_grid_gradient(std::vector &colvars); + // /// Constructor from a vector of colvars + // colvar_grid_gradient(std::vector &colvars, + // std::string config = std::string()); /// Constructor from a multicol file - colvar_grid_gradient(std::string &filename); + colvar_grid_gradient(std::string const &filename); /// Constructor from a vector of colvars and a pointer to the count grid - colvar_grid_gradient(std::vector &colvars, std::shared_ptr samples_in); + colvar_grid_gradient(std::vector &colvars, + std::shared_ptr samples_in = nullptr, + std::shared_ptr params = nullptr, + std::string config = std::string()); /// Parameters for smoothing data with low sampling int full_samples; @@ -1829,7 +1876,8 @@ class integrate_potential : public colvar_grid_scalar {} /// Constructor from a vector of colvars + gradient grid - integrate_potential(std::vector &colvars, std::shared_ptr gradients); + integrate_potential(std::vector &colvars, + std::shared_ptr gradients); /// Constructor from a gradient grid (for processing grid files without a Colvars config) integrate_potential(std::shared_ptr gradients); diff --git a/lib/colvars/colvargrid_def.h b/lib/colvars/colvargrid_def.h index fa6531271b..96075e1ffe 100644 --- a/lib/colvars/colvargrid_def.h +++ b/lib/colvars/colvargrid_def.h @@ -22,6 +22,62 @@ #include "colvars_memstream.h" +template +colvar_grid::colvar_grid(std::string const &filename, size_t mult_i) +{ +std::istream &is = cvm::main()->proxy->input_stream(filename, "multicol grid file"); +if (!is) { + return; +} + +// Data in the header: nColvars, then for each +// xiMin, dXi, nPoints, periodic flag + +std::string hash; +size_t i; + +if ( !(is >> hash) || (hash != "#") ) { + cvm::error("Error reading grid at position "+ + cvm::to_str(static_cast(is.tellg()))+ + " in stream(read \"" + hash + "\")\n"); + return; +} + +is >> nd; +mult = (mult_i == 0) ? nd : mult_i; + +std::vector lower_in(nd), widths_in(nd); +std::vector nx_in(nd); +std::vector periodic_in(nd); + +for (i = 0; i < nd; i++ ) { + if ( !(is >> hash) || (hash != "#") ) { + cvm::error("Error reading grid at position "+ + cvm::to_str(static_cast(is.tellg()))+ + " in stream(read \"" + hash + "\")\n"); + return; + } + + is >> lower_in[i] >> widths_in[i] >> nx_in[i] >> periodic_in[i]; +} + +this->setup(nx_in, 0., mult); + +widths = widths_in; + +for (i = 0; i < nd; i++ ) { + lower_boundaries.push_back(colvarvalue(lower_in[i])); + periodic.push_back(static_cast(periodic_in[i])); +} + +// Reset the istream for read_multicol, which expects the whole file +is.clear(); +is.seekg(0); +read_multicol(is); +cvm::main()->proxy->close_input_stream(filename); +} + + template IST &read_restart_template_(colvar_grid &g, IST &is) { auto const start_pos = is.tellg(); @@ -203,14 +259,16 @@ template int colvar_grid::parse_params(std::string const &conf, lower_boundaries, lower_boundaries, colvarparse::parse_silent); colvarparse::get_keyval(conf, "upper_boundaries", upper_boundaries, upper_boundaries, colvarparse::parse_silent); + // plural form is used in state file + colvarparse::get_keyval(conf, "widths", widths, widths, colvarparse::parse_silent); // camel case keywords are used in config file - colvarparse::get_keyval(conf, "lowerBoundaries", + colvarparse::get_keyval(conf, "lowerBoundary", lower_boundaries, lower_boundaries, parse_mode); - colvarparse::get_keyval(conf, "upperBoundaries", + colvarparse::get_keyval(conf, "upperBoundary", upper_boundaries, upper_boundaries, parse_mode); - colvarparse::get_keyval(conf, "widths", widths, widths, parse_mode); + colvarparse::get_keyval(conf, "width", widths, widths, parse_mode); // only used in state file colvarparse::get_keyval(conf, "sizes", nx, nx, colvarparse::parse_silent); diff --git a/lib/colvars/colvarmodule.cpp b/lib/colvars/colvarmodule.cpp index 25b1efe209..34485d7883 100644 --- a/lib/colvars/colvarmodule.cpp +++ b/lib/colvars/colvarmodule.cpp @@ -24,6 +24,7 @@ #include "colvarbias_histogram_reweight_amd.h" #include "colvarbias_meta.h" #include "colvarbias_restraint.h" +#include "colvarbias_opes.h" #include "colvarscript.h" #include "colvaratoms.h" #include "colvarcomp.h" @@ -109,23 +110,23 @@ colvarmodule::colvarmodule(colvarproxy *proxy_in) " https://doi.org/10.1080/00268976.2013.813594\n" "as well as all other papers listed below for individual features used.\n"); -#if (__cplusplus >= 201103L) - cvm::log("This version was built with the C++11 standard or higher.\n"); -#else - cvm::log("This version was built without the C++11 standard: some features are disabled.\n" - "Please see the following link for details:\n" - " https://colvars.github.io/README-c++11.html\n"); -#endif - cvm::log("Summary of compile-time features available in this build:\n"); - if (proxy->check_smp_enabled() == COLVARS_NOT_IMPLEMENTED) { - cvm::log(" - SMP parallelism: not available\n"); + std::string cxx_lang_msg(" - C++ language version: " + cvm::to_str(__cplusplus)); +#if defined(_WIN32) && !defined(__CYGWIN__) + cxx_lang_msg += std::string(" (warning: may not be accurate for this build)"); +#endif + cxx_lang_msg += std::string("\n"); + cvm::log(cxx_lang_msg); + + if (proxy->check_replicas_enabled() == COLVARS_NOT_IMPLEMENTED) { + cvm::log(" - Multiple replicas: not available\n"); } else { - if (proxy->check_smp_enabled() == COLVARS_OK) { - cvm::log(" - SMP parallelism: enabled (num. threads = " + to_str(proxy->smp_num_threads()) + ")\n"); + if (proxy->check_replicas_enabled() == COLVARS_OK) { + cvm::log(" - Multiple replicas: enabled (replica number " + + to_str(proxy->replica_index() + 1) + " of " + to_str(proxy->num_replicas()) + ")\n"); } else { - cvm::log(" - SMP parallelism: available, but not enabled\n"); + cvm::log(" - Multiple replicas: available, but not (yet) enabled\n"); } } @@ -201,6 +202,20 @@ std::vector *colvarmodule::variables_active_smp_items() } +int colvarmodule::calc_component_smp(int i) +{ + colvar *x = (*(variables_active_smp()))[i]; + int x_item = (*(variables_active_smp_items()))[i]; + if (cvm::debug()) { + cvm::log("Thread "+cvm::to_str(proxy->smp_thread_id())+"/"+ + cvm::to_str(proxy->smp_num_threads())+ + ": calc_component_smp(), i = "+cvm::to_str(i)+", cv = "+ + x->name+", cvc = "+cvm::to_str(x_item)+"\n"); + } + return x->calc_cvcs(x_item, 1); +} + + std::vector *colvarmodule::biases_active() { return &(biases_active_); @@ -387,8 +402,26 @@ int colvarmodule::parse_global_params(std::string const &conf) } } - if (parse->get_keyval(conf, "smp", proxy->b_smp_active, proxy->b_smp_active)) { - if (proxy->b_smp_active == false) { + std::string smp; + if (parse->get_keyval(conf, "smp", smp, "cvcs")) { + if (smp == "cvcs" || smp == "on" || smp == "yes") { + if (proxy->set_smp_mode(colvarproxy_smp::smp_mode_t::cvcs) != COLVARS_OK) { + cvm::error("Colvars component-based parallelism is not implemented.\n"); + return COLVARS_INPUT_ERROR; + } else { + cvm::log("SMP parallelism will be applied to Colvars components.\n"); + cvm::log(" - SMP parallelism: enabled (num. threads = " + to_str(proxy->smp_num_threads()) + ")\n"); + } + } else if (smp == "inner_loop") { + if (proxy->set_smp_mode(colvarproxy_smp::smp_mode_t::inner_loop) != COLVARS_OK) { + cvm::error("SMP parallelism inside the calculation of Colvars components is not implemented.\n"); + return COLVARS_INPUT_ERROR; + } else { + cvm::log("SMP parallelism will be applied inside the Colvars components.\n"); + cvm::log(" - SMP parallelism: enabled (num. threads = " + to_str(proxy->smp_num_threads()) + ")\n"); + } + } else { + proxy->set_smp_mode(colvarproxy_smp::smp_mode_t::none); cvm::log("SMP parallelism has been disabled.\n"); } } @@ -589,6 +622,9 @@ int colvarmodule::parse_biases(std::string const &conf) /// initialize reweightaMD instances parse_biases_type(conf, "reweightaMD"); + /// initialize OPES instances + parse_biases_type(conf, "opes_metad"); + if (use_scripted_forces) { cvm::log(cvm::line_marker); cvm::increase_depth(); @@ -922,7 +958,7 @@ int colvarmodule::calc_colvars() } // if SMP support is available, split up the work - if (proxy->check_smp_enabled() == COLVARS_OK) { + if (proxy->get_smp_mode() == colvarproxy_smp::smp_mode_t::cvcs) { // first, calculate how much work (currently, how many active CVCs) each colvar has @@ -948,8 +984,10 @@ int colvarmodule::calc_colvars() } cvm::decrease_depth(); - // calculate colvar components in parallel - error_code |= proxy->smp_colvars_loop(); + // calculate active colvar components in parallel + error_code |= proxy->smp_loop(variables_active_smp()->size(), [](int i) { + return cvm::main()->calc_component_smp(i); + }); cvm::increase_depth(); for (cvi = variables_active()->begin(); cvi != variables_active()->end(); cvi++) { @@ -1013,7 +1051,7 @@ int colvarmodule::calc_biases() } // If SMP support is available, split up the work (unless biases need to use main thread's memory) - if (proxy->check_smp_enabled() == COLVARS_OK && !biases_need_main_thread) { + if (proxy->get_smp_mode() == colvarproxy::smp_mode_t::cvcs && !biases_need_main_thread) { if (use_scripted_forces && !scripting_after_biases) { // calculate biases and scripted forces in parallel @@ -1097,7 +1135,7 @@ int colvarmodule::update_colvar_forces() cvm::log("Communicating forces from the colvars to the atoms.\n"); cvm::increase_depth(); for (cvi = variables_active()->begin(); cvi != variables_active()->end(); cvi++) { - if ((*cvi)->is_enabled(colvardeps::f_cv_gradient)) { + if ((*cvi)->is_enabled(colvardeps::f_cv_apply_force)) { (*cvi)->communicate_forces(); if (cvm::get_error()) { return COLVARS_ERROR; @@ -1986,7 +2024,7 @@ size_t & colvarmodule::depth() { // NOTE: do not call log() or error() here, to avoid recursion colvarmodule *cv = cvm::main(); - if (proxy->check_smp_enabled() == COLVARS_OK) { + if (proxy->get_smp_mode() == colvarproxy::smp_mode_t::cvcs) { int const nt = proxy->smp_num_threads(); if (int(cv->depth_v.size()) != nt) { proxy->smp_lock(); diff --git a/lib/colvars/colvarmodule.h b/lib/colvars/colvarmodule.h index fa84b1ad75..5f042767dc 100644 --- a/lib/colvars/colvarmodule.h +++ b/lib/colvars/colvarmodule.h @@ -18,6 +18,11 @@ #define COLVARS_DEBUG false #endif +#if defined(__FAST_MATH__) +// NOTE: This is used for fixing https://github.com/Colvars/colvars/issues/767 +#define COLVARS_BOUNDED_INV_TRIGONOMETRIC_FUNC +#endif + /*! \mainpage Main page This is the Developer's documentation for the Collective Variables module (Colvars). @@ -147,17 +152,44 @@ public: return ::cos(static_cast(x)); } - /// Reimplemented to work around MS compiler issues - static inline real asin(real const &x) - { - return ::asin(static_cast(x)); - } +#ifndef PI +#define PI 3.14159265358979323846 +#endif +#ifndef PI_2 +#define PI_2 1.57079632679489661923 +#endif - /// Reimplemented to work around MS compiler issues - static inline real acos(real const &x) - { +/// Reimplemented to work around compiler issues; return hard-coded values for boundary conditions +static inline real asin(real const &x) +{ +#ifdef COLVARS_BOUNDED_INV_TRIGONOMETRIC_FUNC + if (x <= -1.0) { + return -PI_2; + } else if (x >= 1.0) { + return PI_2; + } else { + return ::asin(static_cast(x)); + } +#else + return ::asin(static_cast(x)); +#endif +} + +/// Reimplemented to work around compiler issues; return hard-coded values for boundary conditions +static inline real acos(real const &x) +{ +#ifdef COLVARS_BOUNDED_INV_TRIGONOMETRIC_FUNC + if (x <= -1.0) { + return PI; + } else if (x >= 1.0) { + return 0.0; + } else { + return ::acos(static_cast(x)); + } +#else return ::acos(static_cast(x)); - } +#endif +} /// Reimplemented to work around MS compiler issues static inline real atan2(real const &x, real const &y) @@ -307,6 +339,9 @@ public: /// Indexes of the items to calculate for each colvar std::vector *variables_active_smp_items(); + /// Calculate the value of the specified component (to be called in a SMP loop) + int calc_component_smp(int i); + /// Array of collective variable biases std::vector biases; diff --git a/lib/colvars/colvarmodule_refs.h b/lib/colvars/colvarmodule_refs.h index 2e9615e3b4..0317567502 100644 --- a/lib/colvars/colvarmodule_refs.h +++ b/lib/colvars/colvarmodule_refs.h @@ -129,6 +129,23 @@ " url = {https://doi.org/10.1002/jcc.26075}\n" "}\n"; + paper_count_[std::string("Fiorin2024")] = 0; + paper_url_[std::string("Fiorin2024")] = "https://doi.org/10.1021/acs.jpcb.4c05604"; + paper_bibtex_[std::string("Fiorin2024")] = + "\n" + "@article{Fiorin2024,\n" + " author = {Fiorin, Giacomo and Marinelli, Fabrizio and Forrest, Lucy R. and Chen, Haochuan and Chipot, Christophe and Kohlmeyer, Axel and Santuz, Hubert and H{\\'e}nin, J{\\'e}rĂ´me},\n" + " title = {Expanded Functionality and Portability for the Colvars Library},\n" + " journal = {J. Phys. Chem. {B}},\n" + " volume = {128},\n" + " number = {45},\n" + " pages = {11108--11123},\n" + " year = {2024},\n" + " doi = {10.1021/acs.jpcb.4c05604},\n" + " pmid = 39501453,\n" + " url = { https://doi.org/10.1021/acs.jpcb.4c05604}\n" + "}\n"; + paper_count_[std::string("Fu2016")] = 0; paper_url_[std::string("Fu2016")] = "https://doi.org/10.1021/acs.jctc.6b00447"; paper_bibtex_[std::string("Fu2016")] = @@ -227,6 +244,20 @@ " url = {https://doi.org/10.1016/0263-7855(96)00018-5}\n" "}\n"; + paper_count_[std::string("Lagardere2023")] = 0; + paper_url_[std::string("Lagardere2023")] = "https://arxiv.org/abs/2307.08006"; + paper_bibtex_[std::string("Lagardere2023")] = + "\n" + "@misc{Lagardere2023,\n" + " title={Lambda-ABF: Simplified, Accurate and Cost-effective Alchemical Free Energy Computations},\n" + " author={Louis Lagard\\`ere and Lise Maurin and Olivier Adjoua and Krystel El Hage and Pierre Monmarch\\'e and Jean-Philip Piquemal and J\\'er\\^ome H\\'enin},\n" + " year={2023},\n" + " eprint={2307.08006},\n" + " archivePrefix={arXiv},\n" + " primaryClass={physics.chem-ph},\n" + " url = {https://arxiv.org/abs/2307.08006}\n" + "}\n"; + paper_count_[std::string("Lesage2017")] = 0; paper_url_[std::string("Lesage2017")] = "https://doi.org/10.1021/acs.jpcb.6b10055"; paper_bibtex_[std::string("Lesage2017")] = @@ -344,6 +375,45 @@ " url = {https://doi.org/10.1021/ct500320c}\n" "}\n"; + paper_count_[std::string("Invernizzi2020")] = 0; + paper_url_[std::string("Invernizzi2020")] = "https://pubs.acs.org/doi/10.1021/acs.jpclett.0c00497"; + paper_bibtex_[std::string("Invernizzi2020")] = + "\n" + "@article{Invernizzi2020,\n" + " title = {Rethinking {Metadynamics}: {From} {Bias} {Potentials} to {Probability} {Distributions}},\n" + " volume = {11},\n" + " issn = {1948-7185, 1948-7185},\n" + " shorttitle = {Rethinking {Metadynamics}},\n" + " url = {https://pubs.acs.org/doi/10.1021/acs.jpclett.0c00497},\n" + " doi = {10.1021/acs.jpclett.0c00497},\n" + " number = {7},\n" + " urldate = {2020-09-30},\n" + " journal = {J. Phys. Chem. Lett.},\n" + " author = {Invernizzi, Michele and Parrinello, Michele},\n" + " month = apr,\n" + " year = {2020},\n" + " pages = {2731--2736},\n" + "}\n"; + + paper_count_[std::string("Invernizzi2022")] = 0; + paper_url_[std::string("Invernizzi2022")] = "https://doi.org/10.1021/acs.jctc.2c00152"; + paper_bibtex_[std::string("Invernizzi2022")] = + "\n" + "@article{Invernizzi2022,\n" + " title = {Exploration vs {Convergence} {Speed} in {Adaptive}-{Bias} {Enhanced} {Sampling}},\n" + " volume = {18},\n" + " issn = {1549-9618},\n" + " url = {https://doi.org/10.1021/acs.jctc.2c00152},\n" + " doi = {10.1021/acs.jctc.2c00152},\n" + " number = {6},\n" + " urldate = {2024-07-02},\n" + " journal = {J. Chem. Theory Comput.},\n" + " author = {Invernizzi, Michele and Parrinello, Michele},\n" + " month = jun,\n" + " year = {2022},\n" + " pages = {3988--3996},\n" + "}\n"; + paper_count_[std::string("n/a")] = 0; paper_url_[std::string("n/a")] = ""; paper_bibtex_[std::string("n/a")] = ""; @@ -489,6 +559,42 @@ feature_count_[std::string("Multi-Map collective variables")] = 0; feature_paper_map_[std::string("Multi-Map collective variables")] = "Fiorin2020"; + feature_count_[std::string("Colvars-GROMACS interface")] = 0; + feature_paper_map_[std::string("Colvars-GROMACS interface")] = "Fiorin2024"; + + feature_count_[std::string("gspath colvar component")] = 0; + feature_paper_map_[std::string("gspath colvar component")] = "Fiorin2024"; + + feature_count_[std::string("gzpath colvar component")] = 0; + feature_paper_map_[std::string("gzpath colvar component")] = "Fiorin2024"; + + feature_count_[std::string("linearCombination colvar component")] = 0; + feature_paper_map_[std::string("linearCombination colvar component")] = "Fiorin2024"; + + feature_count_[std::string("gspathCV colvar component")] = 0; + feature_paper_map_[std::string("gspathCV colvar component")] = "Fiorin2024"; + + feature_count_[std::string("gzpathCV colvar component")] = 0; + feature_paper_map_[std::string("gzpathCV colvar component")] = "Fiorin2024"; + + feature_count_[std::string("aspathCV colvar component")] = 0; + feature_paper_map_[std::string("aspathCV colvar component")] = "Fiorin2024"; + + feature_count_[std::string("azpathCV colvar component")] = 0; + feature_paper_map_[std::string("azpathCV colvar component")] = "Fiorin2024"; + + feature_count_[std::string("Custom functions (Lepton)")] = 0; + feature_paper_map_[std::string("Custom functions (Lepton)")] = "Fiorin2024"; + + feature_count_[std::string("Scripted functions (Tcl)")] = 0; + feature_paper_map_[std::string("Scripted functions (Tcl)")] = "Fiorin2024"; + + feature_count_[std::string("ABMD bias")] = 0; + feature_paper_map_[std::string("ABMD bias")] = "Fiorin2024"; + + feature_count_[std::string("Updated multiple-walker ABF implementation")] = 0; + feature_paper_map_[std::string("Updated multiple-walker ABF implementation")] = "Fiorin2024"; + feature_count_[std::string("Umbrella-integration eABF estimator")] = 0; feature_paper_map_[std::string("Umbrella-integration eABF estimator")] = "Fu2016"; @@ -525,6 +631,15 @@ feature_count_[std::string("VMD engine")] = 0; feature_paper_map_[std::string("VMD engine")] = "Humphrey1996"; + feature_count_[std::string("alchLambda colvar component")] = 0; + feature_paper_map_[std::string("alchLambda colvar component")] = "Lagardere2023"; + + feature_count_[std::string("alchFLambda colvar component")] = 0; + feature_paper_map_[std::string("alchFLambda colvar component")] = "Lagardere2023"; + + feature_count_[std::string("Tinker-HP interface")] = 0; + feature_paper_map_[std::string("Tinker-HP interface")] = "Lagardere2023"; + feature_count_[std::string("eABF implementation")] = 0; feature_paper_map_[std::string("eABF implementation")] = "Lesage2017"; @@ -555,38 +670,14 @@ feature_count_[std::string("ALB colvar bias implementation")] = 0; feature_paper_map_[std::string("ALB colvar bias implementation")] = "White2014"; - feature_count_[std::string("Colvars-GROMACS interface")] = 0; - feature_paper_map_[std::string("Colvars-GROMACS interface")] = "n/a"; + feature_count_[std::string("OPES")] = 0; + feature_paper_map_[std::string("OPES")] = "Invernizzi2020"; - feature_count_[std::string("gspath colvar component")] = 0; - feature_paper_map_[std::string("gspath colvar component")] = "n/a"; - - feature_count_[std::string("gzpath colvar component")] = 0; - feature_paper_map_[std::string("gzpath colvar component")] = "n/a"; - - feature_count_[std::string("linearCombination colvar component")] = 0; - feature_paper_map_[std::string("linearCombination colvar component")] = "n/a"; - - feature_count_[std::string("gspathCV colvar component")] = 0; - feature_paper_map_[std::string("gspathCV colvar component")] = "n/a"; - - feature_count_[std::string("gzpathCV colvar component")] = 0; - feature_paper_map_[std::string("gzpathCV colvar component")] = "n/a"; - - feature_count_[std::string("aspathCV colvar component")] = 0; - feature_paper_map_[std::string("aspathCV colvar component")] = "n/a"; - - feature_count_[std::string("azpathCV colvar component")] = 0; - feature_paper_map_[std::string("azpathCV colvar component")] = "n/a"; + feature_count_[std::string("OPES explore or adaptive kernels")] = 0; + feature_paper_map_[std::string("OPES explore or adaptive kernels")] = "Invernizzi2022"; feature_count_[std::string("coordNum pairlist")] = 0; feature_paper_map_[std::string("coordNum pairlist")] = "n/a"; - feature_count_[std::string("Custom functions (Lepton)")] = 0; - feature_paper_map_[std::string("Custom functions (Lepton)")] = "n/a"; - - feature_count_[std::string("Scripted functions (Tcl)")] = 0; - feature_paper_map_[std::string("Scripted functions (Tcl)")] = "n/a"; - - feature_count_[std::string("ABMD bias")] = 0; - feature_paper_map_[std::string("ABMD bias")] = "n/a"; + feature_count_[std::string("torchANN colvar component")] = 0; + feature_paper_map_[std::string("torchANN colvar component")] = "n/a"; diff --git a/lib/colvars/colvarparse.cpp b/lib/colvars/colvarparse.cpp index 76b5c694c1..cf3096ba50 100644 --- a/lib/colvars/colvarparse.cpp +++ b/lib/colvars/colvarparse.cpp @@ -592,7 +592,7 @@ int colvarparse::check_keywords(std::string &conf, char const *key) { if (cvm::debug()) cvm::log("Configuration string for \""+std::string(key)+ - "\": \"\n"+conf+"\".\n"); + "\":\n\""+conf+"\".\n"); strip_values(conf); // after stripping, the config string has either empty lines, or @@ -833,7 +833,8 @@ bool colvarparse::key_lookup(std::string const &conf, data_end) + 1; } - if (data != NULL) { + // data_end < data_begin means that the data or block contains only whitespace + if (data != NULL && data_end > data_begin) { data->append(line, data_begin, (data_end-data_begin)); if (cvm::debug()) { diff --git a/lib/colvars/colvarproxy.cpp b/lib/colvars/colvarproxy.cpp index 588b7c68d8..1ed7a55552 100644 --- a/lib/colvars/colvarproxy.cpp +++ b/lib/colvars/colvarproxy.cpp @@ -243,7 +243,7 @@ void colvarproxy_atom_groups::compute_max_atom_groups_applied_force() colvarproxy_smp::colvarproxy_smp() { - b_smp_active = true; // May be disabled by user option + smp_mode = smp_mode_t::cvcs; // May be disabled by user option omp_lock_state = NULL; #if defined(_OPENMP) if (omp_get_thread_num() == 0) { @@ -265,41 +265,45 @@ colvarproxy_smp::~colvarproxy_smp() #endif } - -int colvarproxy_smp::check_smp_enabled() -{ +colvarproxy::smp_mode_t colvarproxy_smp::get_smp_mode() const { #if defined(_OPENMP) - if (b_smp_active) { - return COLVARS_OK; - } - return COLVARS_ERROR; + return smp_mode; #else - return COLVARS_NOT_IMPLEMENTED; + return colvarproxy::smp_mode_t::none; +#endif +} + +int colvarproxy_smp::set_smp_mode(smp_mode_t mode) { +#if defined(_OPENMP) + smp_mode = mode; + return COLVARS_OK; +#else + if (mode != colvarproxy::smp_mode_t::none) { + return COLVARS_NOT_IMPLEMENTED; + } else { + smp_mode = colvarproxy::smp_mode_t::none; + } + return COLVARS_OK; #endif } -int colvarproxy_smp::smp_colvars_loop() +int colvarproxy_smp::smp_loop(int n_items, std::function const &worker) { + int error_code = COLVARS_OK; #if defined(_OPENMP) - colvarmodule *cv = cvm::main(); - colvarproxy *proxy = cv->proxy; + cvm::increase_depth(); #pragma omp parallel for - for (int i = 0; i < static_cast(cv->variables_active_smp()->size()); i++) { - colvar *x = (*(cv->variables_active_smp()))[i]; - int x_item = (*(cv->variables_active_smp_items()))[i]; - if (cvm::debug()) { - cvm::log("["+cvm::to_str(proxy->smp_thread_id())+"/"+ - cvm::to_str(proxy->smp_num_threads())+ - "]: calc_colvars_items_smp(), i = "+cvm::to_str(i)+", cv = "+ - x->name+", cvc = "+cvm::to_str(x_item)+"\n"); - } - x->calc_cvcs(x_item, 1); + for (int i = 0; i < n_items; i++) { + int const retcode = worker(i); +#pragma omp atomic + error_code |= retcode; } - return cvm::get_error(); + cvm::decrease_depth(); #else - return COLVARS_NOT_IMPLEMENTED; + error_code |= COLVARS_NOT_IMPLEMENTED; #endif + return error_code; } @@ -470,8 +474,8 @@ colvarproxy::~colvarproxy() bool colvarproxy::io_available() { - return (check_smp_enabled() == COLVARS_OK && smp_thread_id() == 0) || - (check_smp_enabled() != COLVARS_OK); + return ((get_smp_mode() != smp_mode_t::none) && smp_thread_id() == 0) || + (get_smp_mode() == smp_mode_t::none); } diff --git a/lib/colvars/colvarproxy.h b/lib/colvars/colvarproxy.h index 91db6011e9..353f354efa 100644 --- a/lib/colvars/colvarproxy.h +++ b/lib/colvars/colvarproxy.h @@ -10,9 +10,12 @@ #ifndef COLVARPROXY_H #define COLVARPROXY_H +#include + #include "colvarmodule.h" #include "colvartypes.h" #include "colvarproxy_io.h" +#include "colvarproxy_replicas.h" #include "colvarproxy_system.h" #include "colvarproxy_tcl.h" #include "colvarproxy_volmaps.h" @@ -447,21 +450,22 @@ class colvarproxy_smp { public: + enum class smp_mode_t {cvcs, inner_loop, none}; + /// Constructor colvarproxy_smp(); /// Destructor virtual ~colvarproxy_smp(); - /// Whether threaded parallelization should be used (TODO: make this a - /// cvm::deps feature) - bool b_smp_active; + /// Get the current SMP mode + virtual smp_mode_t get_smp_mode() const; - /// Whether threaded parallelization is available (TODO: make this a cvm::deps feature) - virtual int check_smp_enabled(); + /// Set the current SMP mode + virtual int set_smp_mode(smp_mode_t mode); - /// Distribute calculation of colvars (and their components) across threads - virtual int smp_colvars_loop(); + /// Distribute computation over threads using OpenMP, unless overridden in the backend (e.g. NAMD) + virtual int smp_loop(int n_items, std::function const &worker); /// Distribute calculation of biases across threads virtual int smp_biases_loop(); @@ -488,38 +492,10 @@ protected: /// Lock state for OpenMP omp_lock_t *omp_lock_state; -}; - - -/// \brief Methods for multiple-replica communication -class colvarproxy_replicas { - -public: - - /// Constructor - colvarproxy_replicas(); - - /// Destructor - virtual ~colvarproxy_replicas(); - - /// \brief Indicate if multi-replica support is available and active - virtual int replica_enabled(); - - /// \brief Index of this replica - virtual int replica_index(); - - /// \brief Total number of replicas - virtual int num_replicas(); - - /// \brief Synchronize replica with others - virtual void replica_comm_barrier(); - - /// \brief Receive data from other replica - virtual int replica_comm_recv(char* msg_data, int buf_len, int src_rep); - - /// \brief Send data to other replica - virtual int replica_comm_send(char* msg_data, int msg_len, int dest_rep); + /// Whether threaded parallelization should be used (TODO: make this a + /// cvm::deps feature) + smp_mode_t smp_mode; }; diff --git a/lib/colvars/colvarproxy_io.cpp b/lib/colvars/colvarproxy_io.cpp index 4cfdfeec26..0327ed36f0 100644 --- a/lib/colvars/colvarproxy_io.cpp +++ b/lib/colvars/colvarproxy_io.cpp @@ -7,10 +7,28 @@ // If you wish to distribute your changes, please submit them to the // Colvars repository at GitHub. + +#if defined(_WIN32) && !defined(__CYGWIN__) + // Using access() to check if a file exists (until we can assume C++14/17) -#if !defined(_WIN32) || defined(__CYGWIN__) -#include +#include + +#if defined(__has_include) +# if __has_include() +# include // MSVC only defines __cpp_lib_filesystem after include +# endif #endif + +#else + +#include + +#ifdef __cpp_lib_filesystem +#include +#endif + +#endif + #if defined(_WIN32) #include #endif @@ -64,6 +82,53 @@ int colvarproxy_io::set_frame(long int) } +std::string colvarproxy_io::get_current_work_dir() const +{ +#ifdef __cpp_lib_filesystem + + return std::filesystem::current_path().string(); + +#else + + // Legacy code + size_t constexpr buf_size = 3001; + char buf[buf_size]; + +#if defined(_WIN32) && !defined(__CYGWIN__) + char *getcwd_result = ::_getcwd(buf, buf_size); +#else + char *getcwd_result = ::getcwd(buf, buf_size); +#endif + + if (getcwd_result == nullptr) { + cvm::error("Error: cannot read the current working directory.\n", COLVARS_INPUT_ERROR); + return std::string(""); + } + + return std::string(getcwd_result); +#endif +} + + +std::string colvarproxy_io::join_paths(std::string const &path1, std::string const &path2) const +{ +#ifdef __cpp_lib_filesystem + + return (std::filesystem::path(path1) / std::filesystem::path(path2)).string(); + +#else + + // Legacy code +#if defined(_WIN32) && !defined(__CYGWIN__) + return (path1 + "\\" + path2); +#else + return (path1 + "/" + path2); +#endif + +#endif +} + + int colvarproxy_io::backup_file(char const *filename) { // Simplified version of NAMD_file_exists() diff --git a/lib/colvars/colvarproxy_io.h b/lib/colvars/colvarproxy_io.h index 726f915c97..eaf750366d 100644 --- a/lib/colvars/colvarproxy_io.h +++ b/lib/colvars/colvarproxy_io.h @@ -38,6 +38,12 @@ public: // Returns error code virtual int set_frame(long int); + /// Get the current working directory of this process + std::string get_current_work_dir() const; + + /// Join two paths using the operating system's path separation + std::string join_paths(std::string const &path1, std::string const &path2) const; + /// \brief Rename the given file, before overwriting it virtual int backup_file(char const *filename); diff --git a/lib/colvars/colvarproxy_replicas.cpp b/lib/colvars/colvarproxy_replicas.cpp index 1f336d3e44..ec7ffdd8d5 100644 --- a/lib/colvars/colvarproxy_replicas.cpp +++ b/lib/colvars/colvarproxy_replicas.cpp @@ -7,50 +7,103 @@ // If you wish to distribute your changes, please submit them to the // Colvars repository at GitHub. + #include "colvarmodule.h" -#include "colvarproxy.h" +#include "colvarproxy_replicas.h" -colvarproxy_replicas::colvarproxy_replicas() {} +colvarproxy_replicas::colvarproxy_replicas() +{ +#ifdef COLVARS_MPI + replicas_mpi_comm = MPI_COMM_NULL; +#endif +} colvarproxy_replicas::~colvarproxy_replicas() {} -int colvarproxy_replicas::replica_enabled() +void colvarproxy_replicas::set_replicas_mpi_communicator(replicas_mpi_comm_t comm) { + replicas_mpi_comm = comm; +#ifdef COLVARS_MPI + if (comm != MPI_COMM_NULL) { + MPI_Comm_rank(comm, &replicas_mpi_rank); + MPI_Comm_size(comm, &replicas_mpi_num); + cvm::log("Enabling multiple replicas: this is replica number " + + cvm::to_str(replica_index() + 1) + " of " + cvm::to_str(num_replicas()) + ".\n"); + } +#endif +} + + +int colvarproxy_replicas::check_replicas_enabled() +{ +#ifdef COLVARS_MPI + if (replicas_mpi_comm != MPI_COMM_NULL) { + return num_replicas() > 1 ? COLVARS_OK : COLVARS_ERROR; + } + return COLVARS_ERROR; +#else return COLVARS_NOT_IMPLEMENTED; +#endif } int colvarproxy_replicas::replica_index() { - return 0; + return replicas_mpi_rank; } int colvarproxy_replicas::num_replicas() { - return 1; + return replicas_mpi_num; } -void colvarproxy_replicas::replica_comm_barrier() {} - - -int colvarproxy_replicas::replica_comm_recv(char* /* msg_data */, - int /* buf_len */, - int /* src_rep */) +void colvarproxy_replicas::replica_comm_barrier() { - return COLVARS_NOT_IMPLEMENTED; +#ifdef COLVARS_MPI + MPI_Barrier(replicas_mpi_comm); +#endif } -int colvarproxy_replicas::replica_comm_send(char* /* msg_data */, - int /* msg_len */, - int /* dest_rep */) +int colvarproxy_replicas::replica_comm_recv(char *buffer, int buffer_length, int source_rank) { +#ifdef COLVARS_MPI + MPI_Status status; + int retval = MPI_Recv(buffer, buffer_length, MPI_CHAR, source_rank, 0, replicas_mpi_comm, &status); + if (retval == MPI_SUCCESS) { + MPI_Get_count(&status, MPI_CHAR, &retval); + } else { + retval = 0; + } + return retval; +#else + (void)buffer; + (void)buffer_length; + (void)source_rank; return COLVARS_NOT_IMPLEMENTED; +#endif } +int colvarproxy_replicas::replica_comm_send(char *buffer, int buffer_length, int destination_rank) +{ +#ifdef COLVARS_MPI + int retval = MPI_Send(buffer, buffer_length, MPI_CHAR, destination_rank, 0, replicas_mpi_comm); + if (retval == MPI_SUCCESS) { + retval = buffer_length; + } else { + retval = 0; + } + return retval; +#else + (void)buffer; + (void)buffer_length; + (void)destination_rank; + return COLVARS_NOT_IMPLEMENTED; +#endif +} diff --git a/lib/colvars/colvarproxy_replicas.h b/lib/colvars/colvarproxy_replicas.h new file mode 100644 index 0000000000..b58c80bf5e --- /dev/null +++ b/lib/colvars/colvarproxy_replicas.h @@ -0,0 +1,66 @@ +// -*- c++ -*- + +// This file is part of the Collective Variables module (Colvars). +// The original version of Colvars and its updates are located at: +// https://github.com/Colvars/colvars +// Please update all Colvars source files before making any changes. +// If you wish to distribute your changes, please submit them to the +// Colvars repository at GitHub. + +#ifndef COLVARPROXY_REPLICAS_H +#define COLVARPROXY_REPLICAS_H + + +#ifdef COLVARS_MPI +#include +typedef MPI_Comm replicas_mpi_comm_t; +#else +typedef void * replicas_mpi_comm_t; +#endif + + +/// \brief Methods for multiple-replica communication +class colvarproxy_replicas { + +public: + + /// Constructor + colvarproxy_replicas(); + + /// Destructor + virtual ~colvarproxy_replicas(); + + /// Set the multiple replicas communicator + virtual void set_replicas_mpi_communicator(replicas_mpi_comm_t comm); + + /// Indicate if multi-replica support is available and active + virtual int check_replicas_enabled(); + + /// Index of this replica + virtual int replica_index(); + + /// Total number of replicas + virtual int num_replicas(); + + /// Synchronize replica with others + virtual void replica_comm_barrier(); + + /// Receive data from other replica + virtual int replica_comm_recv(char* msg_data, int buf_len, int src_rep); + + /// Send data to other replica + virtual int replica_comm_send(char* msg_data, int msg_len, int dest_rep); + +protected: + + /// MPI communicator containint 1 root proc from each world + replicas_mpi_comm_t replicas_mpi_comm; + + /// Index (rank) of this replica in the MPI implementation + int replicas_mpi_rank = 0; + + /// Number of replicas in the MPI implementation + int replicas_mpi_num = 1; +}; + +#endif diff --git a/lib/colvars/colvarproxy_system.h b/lib/colvars/colvarproxy_system.h index 67d0938e54..bf2ad2ea8b 100644 --- a/lib/colvars/colvarproxy_system.h +++ b/lib/colvars/colvarproxy_system.h @@ -94,6 +94,7 @@ public: virtual bool total_forces_enabled() const; /// Are total forces from the current step available? + /// in which case they are really system forces virtual bool total_forces_same_step() const; /// Get the molecule ID when called in VMD; raise error otherwise @@ -109,6 +110,11 @@ public: /// Send cached value of alchemical lambda parameter to back-end (if available) virtual int send_alch_lambda(); + /// Request energy computation every freq steps (necessary for NAMD3, not all back-ends) + virtual int request_alch_energy_freq(int const freq) { + return COLVARS_OK; + } + /// Get energy derivative with respect to lambda (if available) virtual int get_dE_dlambda(cvm::real* dE_dlambda); diff --git a/lib/colvars/colvars_memstream.h b/lib/colvars/colvars_memstream.h index 0d80d2794d..c9564a3c41 100644 --- a/lib/colvars/colvars_memstream.h +++ b/lib/colvars/colvars_memstream.h @@ -108,6 +108,9 @@ public: /// Ignore formatting operators inline void setf(decltype(std::ios::fmtflags(0)), decltype(std::ios::floatfield)) {} + /// Ignore formatting operators + inline void setf(decltype(std::ios::fmtflags(0))) {} + /// Ignore formatting operators inline void flags(decltype(std::ios::fmtflags(0))) {} diff --git a/lib/colvars/colvars_version.h b/lib/colvars/colvars_version.h index d50a00fff5..02f949b517 100644 --- a/lib/colvars/colvars_version.h +++ b/lib/colvars/colvars_version.h @@ -1,3 +1,3 @@ #ifndef COLVARS_VERSION -#define COLVARS_VERSION "2024-06-04" +#define COLVARS_VERSION "2025-04-30" #endif diff --git a/lib/colvars/colvarscript_commands.h b/lib/colvars/colvarscript_commands.h index bdad74e433..191724dd0f 100644 --- a/lib/colvars/colvarscript_commands.h +++ b/lib/colvars/colvarscript_commands.h @@ -541,6 +541,15 @@ CVSCRIPT(cv_printframe, return COLVARS_OK; ) +CVSCRIPT(cv_patchversion, + "Get the Colvars patch version number (used for bugfixes only)\n" + "version : string - Colvars version", + 0, 0, + "", + script->set_result_int(cvm::main()->patch_version_number()); + return COLVARS_OK; + ) + CVSCRIPT(cv_printframelabels, "Return the labels that would be written to colvars.traj\n" "Labels : string - The labels", @@ -656,7 +665,7 @@ CVSCRIPT(cv_update, ) CVSCRIPT(cv_version, - "Get the Colvars Module version string\n" + "Get the Colvars version string\n" "version : string - Colvars version", 0, 0, "", @@ -665,7 +674,7 @@ CVSCRIPT(cv_version, ) // This guard allows compiling colvar and bias function bodies in their -// respecitve files instead of colvarscript_commands.o +// respective files instead of colvarscript_commands.o #ifndef COLVARSCRIPT_COMMANDS_GLOBAL #include "colvarscript_commands_colvar.h" #include "colvarscript_commands_bias.h" diff --git a/lib/colvars/colvarscript_commands_colvar.h b/lib/colvars/colvarscript_commands_colvar.h index f6bb6b8c98..c641b321d0 100644 --- a/lib/colvars/colvarscript_commands_colvar.h +++ b/lib/colvars/colvarscript_commands_colvar.h @@ -23,6 +23,7 @@ CVSCRIPT(colvar_addforce, script->add_error_msg("addforce : error parsing force value"); return COLVARSCRIPT_ERROR; } + this_colvar->enable(colvardeps::f_cv_apply_force); this_colvar->add_bias_force(force); script->set_result_colvarvalue(force); return COLVARS_OK; diff --git a/lib/colvars/colvartypes.cpp b/lib/colvars/colvartypes.cpp index f51791d015..6c65f1f5a6 100644 --- a/lib/colvars/colvartypes.cpp +++ b/lib/colvars/colvartypes.cpp @@ -137,71 +137,6 @@ std::istream & operator >> (std::istream &is, colvarmodule::quaternion &q) } -cvm::quaternion -cvm::quaternion::position_derivative_inner(cvm::rvector const &pos, - cvm::rvector const &vec) const -{ - cvm::quaternion result(0.0, 0.0, 0.0, 0.0); - - - result.q0 = 2.0 * pos.x * q0 * vec.x - +2.0 * pos.y * q0 * vec.y - +2.0 * pos.z * q0 * vec.z - - -2.0 * pos.y * q3 * vec.x - +2.0 * pos.z * q2 * vec.x - - +2.0 * pos.x * q3 * vec.y - -2.0 * pos.z * q1 * vec.y - - -2.0 * pos.x * q2 * vec.z - +2.0 * pos.y * q1 * vec.z; - - - result.q1 = +2.0 * pos.x * q1 * vec.x - -2.0 * pos.y * q1 * vec.y - -2.0 * pos.z * q1 * vec.z - - +2.0 * pos.y * q2 * vec.x - +2.0 * pos.z * q3 * vec.x - - +2.0 * pos.x * q2 * vec.y - -2.0 * pos.z * q0 * vec.y - - +2.0 * pos.x * q3 * vec.z - +2.0 * pos.y * q0 * vec.z; - - - result.q2 = -2.0 * pos.x * q2 * vec.x - +2.0 * pos.y * q2 * vec.y - -2.0 * pos.z * q2 * vec.z - - +2.0 * pos.y * q1 * vec.x - +2.0 * pos.z * q0 * vec.x - - +2.0 * pos.x * q1 * vec.y - +2.0 * pos.z * q3 * vec.y - - -2.0 * pos.x * q0 * vec.z - +2.0 * pos.y * q3 * vec.z; - - - result.q3 = -2.0 * pos.x * q3 * vec.x - -2.0 * pos.y * q3 * vec.y - +2.0 * pos.z * q3 * vec.z - - -2.0 * pos.y * q0 * vec.x - +2.0 * pos.z * q1 * vec.x - - +2.0 * pos.x * q0 * vec.y - +2.0 * pos.z * q2 * vec.y - - +2.0 * pos.x * q1 * vec.z - +2.0 * pos.y * q2 * vec.z; - - return result; -} - #ifdef COLVARS_LAMMPS namespace { inline void *new_Jacobi_solver(int size) { @@ -336,7 +271,7 @@ void colvarmodule::rotation::compute_overlap_matrix() #ifndef COLVARS_LAMMPS namespace NR { -void diagonalize_matrix(cvm::real m[4][4], +int diagonalize_matrix(cvm::real m[4][4], cvm::real eigval[4], cvm::real eigvec[4][4]) { @@ -347,9 +282,7 @@ void diagonalize_matrix(cvm::real m[4][4], int jac_nrot = 0; if (NR_Jacobi::jacobi(m, eigval, eigvec, &jac_nrot) != COLVARS_OK) { - cvm::error("Too many iterations in jacobi diagonalization.\n" - "This is usually the result of an ill-defined set of atoms for " - "rotational alignment (RMSD, rotateReference, etc).\n"); + return COLVARS_ERROR; } NR_Jacobi::eigsrt(eigval, eigvec); // jacobi saves eigenvectors by columns @@ -367,6 +300,7 @@ void diagonalize_matrix(cvm::real m[4][4], eigvec[ie][i] /= norm; } } + return COLVARS_OK; } } @@ -429,14 +363,25 @@ void colvarmodule::rotation::calc_optimal_rotation_impl() { cvm::real[4][4]> *>(jacobi); int ierror = ecalc->Diagonalize(S, S_eigval, S_eigvec); +#else + int ierror = NR::diagonalize_matrix(S, S_eigval, S_eigvec); +#endif if (ierror) { + cvm::log("Failed to diagonalize the following overlapping matrix:\n"); + for (size_t i = 0; i < 4; ++i) { + for (size_t j = 0; j < 4; ++j) { + cvm::log(cvm::to_str(S[i][j]) + " "); + } + cvm::log("\n"); + } + cvm::log("The corresponding correlation matrix is:\n"); + cvm::log(" " + cvm::to_str(C.xx) + " " + cvm::to_str(C.xy) + " " + cvm::to_str(C.xz)); + cvm::log(" " + cvm::to_str(C.yx) + " " + cvm::to_str(C.yy) + " " + cvm::to_str(C.yz)); + cvm::log(" " + cvm::to_str(C.zx) + " " + cvm::to_str(C.zy) + " " + cvm::to_str(C.zz) + "\n"); cvm::error("Too many iterations in jacobi diagonalization.\n" "This is usually the result of an ill-defined set of atoms for " "rotational alignment (RMSD, rotateReference, etc).\n"); } -#else - NR::diagonalize_matrix(S, S_eigval, S_eigvec); -#endif q = cvm::quaternion{S_eigvec[0][0], S_eigvec[0][1], S_eigvec[0][2], S_eigvec[0][3]}; if (cvm::rotation::monitor_crossings) { diff --git a/lib/colvars/colvartypes.h b/lib/colvars/colvartypes.h index 455e628f1b..db5827990b 100644 --- a/lib/colvars/colvartypes.h +++ b/lib/colvars/colvartypes.h @@ -20,10 +20,6 @@ #include "colvarmodule.h" -#ifndef PI -#define PI 3.14159265358979323846 -#endif - // ---------------------------------------------------------------------- /// Linear algebra functions and data types used in the collective /// variables implemented so far @@ -1221,8 +1217,57 @@ public: /// \brief Multiply the given vector by the derivative of the given /// (rotated) position with respect to the quaternion - cvm::quaternion position_derivative_inner(cvm::rvector const &pos, - cvm::rvector const &vec) const; + /// \param pos The position \f$\mathbf{x}\f$. + /// \param vec The vector \f$\mathbf{v}\f$. + /// \return A quaternion (see the detailed documentation below). + /// + /// This function is mainly used for projecting the gradients or forces on + /// the rotated atoms to the forces on quaternion. Assume this rotation can + /// be represented as \f$R(\mathbf{q})\f$, + /// where \f$\mathbf{q} := (q_0, q_1, q_2, q_3)\f$ + /// is the current quaternion, the function returns the following new + /// quaternion: + /// \f[ + /// \left(\mathbf{v}^\mathrm{T}\frac{\partial R(\mathbf{q})}{\partial q_0}\mathbf{x}, + /// \mathbf{v}^\mathrm{T}\frac{\partial R(\mathbf{q})}{\partial q_1}\mathbf{x}, + /// \mathbf{v}^\mathrm{T}\frac{\partial R(\mathbf{q})}{\partial q_2}\mathbf{x}, + /// \mathbf{v}^\mathrm{T}\frac{\partial R(\mathbf{q})}{\partial q_3}\mathbf{x}\right) + /// \f] + /// where \f$\mathbf{v}\f$ is usually the gradient of \f$\xi\f$ with respect to + /// the rotated frame \f$\tilde{\mathbf{X}}\f$, + /// \f$\partial \xi / \partial \tilde{\mathbf{X}}\f$, or the force acting on it + /// (\f$\mathbf{F}_{\tilde{\mathbf{X}}}\f$). + /// By using the following loop in pseudo C++ code, + /// either \f$\partial \xi / \partial \tilde{\mathbf{X}}\f$ + /// or \f$\mathbf{F}_{\tilde{\mathbf{X}}}\f$, can be projected to + /// \f$\partial \xi / \partial \mathbf{q}\f$ or \f$\mathbf{F}_q\f$ into `sum_dxdq`: + /// @code + /// cvm::real sum_dxdq[4] = {0, 0, 0, 0}; + /// for (size_t i = 0; i < main_group_size(); ++i) { + /// const cvm::rvector v = grad_or_force_on_rotated_main_group(i); + /// const cvm::rvector x = unrotated_main_group_positions(i); + /// cvm::quaternion const dxdq = position_derivative_inner(x, v); + /// sum_dxdq[0] += dxdq[0]; + /// sum_dxdq[1] += dxdq[1]; + /// sum_dxdq[2] += dxdq[2]; + /// sum_dxdq[3] += dxdq[3]; + /// } + /// @endcode + inline cvm::quaternion position_derivative_inner(cvm::rvector const &pos, + cvm::rvector const &vec) const { + return cvm::quaternion(2.0 * (vec.x * ( q0 * pos.x - q3 * pos.y + q2 * pos.z) + + vec.y * ( q3 * pos.x + q0 * pos.y - q1 * pos.z) + + vec.z * (-q2 * pos.x + q1 * pos.y + q0 * pos.z)), + 2.0 * (vec.x * ( q1 * pos.x + q2 * pos.y + q3 * pos.z) + + vec.y * ( q2 * pos.x - q1 * pos.y - q0 * pos.z) + + vec.z * ( q3 * pos.x + q0 * pos.y - q1 * pos.z)), + 2.0 * (vec.x * (-q2 * pos.x + q1 * pos.y + q0 * pos.z) + + vec.y * ( q1 * pos.x + q2 * pos.y + q3 * pos.z) + + vec.z * (-q0 * pos.x + q3 * pos.y - q2 * pos.z)), + 2.0 * (vec.x * (-q3 * pos.x - q0 * pos.y + q1 * pos.z) + + vec.y * ( q0 * pos.x - q3 * pos.y + q2 * pos.z) + + vec.z * ( q1 * pos.x + q2 * pos.y + q3 * pos.z))); + } /// \brief Return the cosine between the orientation frame @@ -1301,7 +1346,7 @@ public: #ifndef COLVARS_LAMMPS namespace NR { -void diagonalize_matrix(cvm::real m[4][4], +int diagonalize_matrix(cvm::real m[4][4], cvm::real eigval[4], cvm::real eigvec[4][4]); } diff --git a/lib/colvars/colvarvalue.cpp b/lib/colvars/colvarvalue.cpp index 3b8077d2e7..66baf35eeb 100644 --- a/lib/colvars/colvarvalue.cpp +++ b/lib/colvars/colvarvalue.cpp @@ -153,29 +153,6 @@ std::string const colvarvalue::type_keyword(Type t) } -size_t colvarvalue::num_df(Type t) -{ - switch (t) { - case colvarvalue::type_notset: - default: - return 0; break; - case colvarvalue::type_scalar: - return 1; break; - case colvarvalue::type_3vector: - return 3; break; - case colvarvalue::type_unit3vector: - case colvarvalue::type_unit3vectorderiv: - return 2; break; - case colvarvalue::type_quaternion: - case colvarvalue::type_quaternionderiv: - return 3; break; - case colvarvalue::type_vector: - // the size of a vector is unknown without its object - return 0; break; - } -} - - size_t colvarvalue::num_dimensions(Type t) { switch (t) { @@ -591,34 +568,132 @@ cvm::real operator * (colvarvalue const &x1, } +cvm::real colvarvalue::norm2() const +{ + switch (value_type) { + case colvarvalue::type_scalar: + return (this->real_value)*(this->real_value); + case colvarvalue::type_3vector: + case colvarvalue::type_unit3vector: + case colvarvalue::type_unit3vectorderiv: + return (this->rvector_value).norm2(); + case colvarvalue::type_quaternion: + case colvarvalue::type_quaternionderiv: + return (this->quaternion_value).norm2(); + case colvarvalue::type_vector: + if (elem_types.size() > 0) { + // if we have information about non-scalar types, use it + cvm::real result = 0.0; + size_t i; + for (i = 0; i < elem_types.size(); i++) { + result += (this->get_elem(i)).norm2(); + } + return result; + } else { + return vector1d_value.norm2(); + } + break; + case colvarvalue::type_notset: + default: + return 0.0; + } +} + + +cvm::real colvarvalue::sum() const +{ + switch (value_type) { + case colvarvalue::type_scalar: + return (this->real_value); + case colvarvalue::type_3vector: + case colvarvalue::type_unit3vector: + case colvarvalue::type_unit3vectorderiv: + return (this->rvector_value).x + (this->rvector_value).y + + (this->rvector_value).z; + case colvarvalue::type_quaternion: + case colvarvalue::type_quaternionderiv: + return (this->quaternion_value).q0 + (this->quaternion_value).q1 + + (this->quaternion_value).q2 + (this->quaternion_value).q3; + case colvarvalue::type_vector: + return (this->vector1d_value).sum(); + case colvarvalue::type_notset: + default: + return 0.0; + } +} + + +cvm::real colvarvalue::dist2(colvarvalue const &x2) const +{ + colvarvalue::check_types(*this, x2); + + switch (this->type()) { + case colvarvalue::type_scalar: + return (this->real_value - x2.real_value) * (this->real_value - x2.real_value); + case colvarvalue::type_3vector: + return (this->rvector_value - x2.rvector_value).norm2(); + case colvarvalue::type_unit3vector: { + cvm::rvector const &v1 = this->rvector_value; + cvm::rvector const &v2 = x2.rvector_value; + cvm::real const theta = cvm::acos(v1 * v2); + return theta * theta; + } + case colvarvalue::type_quaternion: + // angle between (*this) and x2 is the distance, the quaternion + // object has it implemented internally + return this->quaternion_value.dist2(x2.quaternion_value); + case colvarvalue::type_vector: + return (this->vector1d_value - x2.vector1d_value).norm2(); + case colvarvalue::type_unit3vectorderiv: + case colvarvalue::type_quaternionderiv: + cvm::error("Error: computing a squared-distance between two variables of type \"" + + type_desc(this->type()) + "\", for which it is not defined.\n", + COLVARS_BUG_ERROR); + case colvarvalue::type_notset: + default: + this->undef_op(); + return 0.0; + }; + + return 0.0; +} + + colvarvalue colvarvalue::dist2_grad(colvarvalue const &x2) const { colvarvalue::check_types(*this, x2); + // Compute derivative with respect to (*this) + switch (this->value_type) { case colvarvalue::type_scalar: return 2.0 * (this->real_value - x2.real_value); case colvarvalue::type_3vector: return 2.0 * (this->rvector_value - x2.rvector_value); - case colvarvalue::type_unit3vector: - case colvarvalue::type_unit3vectorderiv: - { - cvm::rvector const &v1 = this->rvector_value; - cvm::rvector const &v2 = x2.rvector_value; - cvm::real const cos_t = v1 * v2; - return colvarvalue(2.0 * (cos_t * v1 - v2), colvarvalue::type_unit3vectorderiv); - } + case colvarvalue::type_unit3vector: { + cvm::rvector const &v1 = this->rvector_value; + cvm::rvector const &v2 = x2.rvector_value; + cvm::real const cos_t = v1 * v2; + return colvarvalue(2.0 * cvm::acos(cos_t) * -1.0 / cvm::sqrt(1.0 - cos_t * cos_t) * v2, + colvarvalue::type_unit3vectorderiv); + } case colvarvalue::type_quaternion: - case colvarvalue::type_quaternionderiv: return this->quaternion_value.dist2_grad(x2.quaternion_value); case colvarvalue::type_vector: return colvarvalue(2.0 * (this->vector1d_value - x2.vector1d_value), colvarvalue::type_vector); break; + case colvarvalue::type_unit3vectorderiv: + case colvarvalue::type_quaternionderiv: + cvm::error("Error: computing a squared-distance gradient between two variables of type \"" + + type_desc(this->type()) + "\", for which it is not defined.\n", + COLVARS_BUG_ERROR); case colvarvalue::type_notset: default: this->undef_op(); return colvarvalue(colvarvalue::type_notset); }; + + return colvarvalue(colvarvalue::type_notset); } diff --git a/lib/colvars/colvarvalue.h b/lib/colvars/colvarvalue.h index e8a6a849d3..61f1bf718b 100644 --- a/lib/colvars/colvarvalue.h +++ b/lib/colvars/colvarvalue.h @@ -109,9 +109,6 @@ public: /// User keywords for specifying value types in the configuration static std::string const type_keyword(Type t); - /// Number of degrees of freedom for each supported type - static size_t num_df(Type t); - /// Number of dimensions for each supported type (used to allocate vector1d_value) static size_t num_dimensions(Type t); @@ -671,87 +668,4 @@ inline cvm::vector1d const colvarvalue::as_vector() const } -inline cvm::real colvarvalue::norm2() const -{ - switch (value_type) { - case colvarvalue::type_scalar: - return (this->real_value)*(this->real_value); - case colvarvalue::type_3vector: - case colvarvalue::type_unit3vector: - case colvarvalue::type_unit3vectorderiv: - return (this->rvector_value).norm2(); - case colvarvalue::type_quaternion: - case colvarvalue::type_quaternionderiv: - return (this->quaternion_value).norm2(); - case colvarvalue::type_vector: - if (elem_types.size() > 0) { - // if we have information about non-scalar types, use it - cvm::real result = 0.0; - size_t i; - for (i = 0; i < elem_types.size(); i++) { - result += (this->get_elem(i)).norm2(); - } - return result; - } else { - return vector1d_value.norm2(); - } - break; - case colvarvalue::type_notset: - default: - return 0.0; - } -} - - -inline cvm::real colvarvalue::sum() const -{ - switch (value_type) { - case colvarvalue::type_scalar: - return (this->real_value); - case colvarvalue::type_3vector: - case colvarvalue::type_unit3vector: - case colvarvalue::type_unit3vectorderiv: - return (this->rvector_value).x + (this->rvector_value).y + - (this->rvector_value).z; - case colvarvalue::type_quaternion: - case colvarvalue::type_quaternionderiv: - return (this->quaternion_value).q0 + (this->quaternion_value).q1 + - (this->quaternion_value).q2 + (this->quaternion_value).q3; - case colvarvalue::type_vector: - return (this->vector1d_value).sum(); - case colvarvalue::type_notset: - default: - return 0.0; - } -} - - -inline cvm::real colvarvalue::dist2(colvarvalue const &x2) const -{ - colvarvalue::check_types(*this, x2); - - switch (this->type()) { - case colvarvalue::type_scalar: - return (this->real_value - x2.real_value)*(this->real_value - x2.real_value); - case colvarvalue::type_3vector: - return (this->rvector_value - x2.rvector_value).norm2(); - case colvarvalue::type_unit3vector: - case colvarvalue::type_unit3vectorderiv: - // angle between (*this) and x2 is the distance - return cvm::acos(this->rvector_value * x2.rvector_value) * cvm::acos(this->rvector_value * x2.rvector_value); - case colvarvalue::type_quaternion: - case colvarvalue::type_quaternionderiv: - // angle between (*this) and x2 is the distance, the quaternion - // object has it implemented internally - return this->quaternion_value.dist2(x2.quaternion_value); - case colvarvalue::type_vector: - return (this->vector1d_value - x2.vector1d_value).norm2(); - case colvarvalue::type_notset: - default: - this->undef_op(); - return 0.0; - }; -} - - #endif diff --git a/src/COLVARS/colvarproxy_lammps.cpp b/src/COLVARS/colvarproxy_lammps.cpp index 265dc34f43..c92d835cf6 100644 --- a/src/COLVARS/colvarproxy_lammps.cpp +++ b/src/COLVARS/colvarproxy_lammps.cpp @@ -33,12 +33,9 @@ colvarproxy_lammps::colvarproxy_lammps(LAMMPS_NS::LAMMPS *lmp) : _lmp(lmp), _ra previous_step = -1; do_exit = false; - inter_me = 0; - inter_num = 1; bias_energy = 0.0; engine_ready_ = false; - inter_comm = MPI_COMM_NULL; } /* ---------------------------------------------------------------------- */ @@ -83,19 +80,6 @@ void colvarproxy_lammps::set_random_seed(int seed) _random = new LAMMPS_NS::RanPark(_lmp, seed); } -/* ---------------------------------------------------------------------- */ - -void colvarproxy_lammps::set_replicas_communicator(MPI_Comm root2root) -{ - inter_comm = root2root; - - // initialize multi-replica support, if available - if (replica_enabled() == COLVARS_OK) { - MPI_Comm_rank(inter_comm, &inter_me); - MPI_Comm_size(inter_comm, &inter_num); - } -} - /* ---------------------------------------------------------------------- re-initialize data where needed ------------------------------------------------------------------------- */ @@ -255,63 +239,7 @@ int colvarproxy_lammps::set_unit_system(std::string const &units_in, bool /*chec return COLVARS_OK; } -/* ---------------------------------------------------------------------- - multi-replica support -------------------------------------------------------------------------- */ -int colvarproxy_lammps::replica_enabled() -{ - return (inter_comm != MPI_COMM_NULL) ? COLVARS_OK : COLVARS_NOT_IMPLEMENTED; -} - -/* ---------------------------------------------------------------------- */ - -int colvarproxy_lammps::replica_index() -{ - return inter_me; -} - -/* ---------------------------------------------------------------------- */ - -int colvarproxy_lammps::num_replicas() -{ - return inter_num; -} - -/* ---------------------------------------------------------------------- */ - -void colvarproxy_lammps::replica_comm_barrier() -{ - MPI_Barrier(inter_comm); -} - -/* ---------------------------------------------------------------------- */ - -int colvarproxy_lammps::replica_comm_recv(char* msg_data, int buf_len, int src_rep) -{ - MPI_Status status; - int retval; - - retval = MPI_Recv(msg_data,buf_len,MPI_CHAR,src_rep,0,inter_comm,&status); - if (retval == MPI_SUCCESS) { - MPI_Get_count(&status, MPI_CHAR, &retval); - } else retval = 0; - return retval; -} - -/* ---------------------------------------------------------------------- */ - -int colvarproxy_lammps::replica_comm_send(char* msg_data, int msg_len, int dest_rep) -{ - int retval; - retval = MPI_Send(msg_data,msg_len,MPI_CHAR,dest_rep,0,inter_comm); - if (retval == MPI_SUCCESS) { - retval = msg_len; - } else retval = 0; - return retval; -} - -/* ---------------------------------------------------------------------- */ int colvarproxy_lammps::check_atom_id(int atom_number) { diff --git a/src/COLVARS/colvarproxy_lammps.h b/src/COLVARS/colvarproxy_lammps.h index d98be37b09..06bda4252e 100644 --- a/src/COLVARS/colvarproxy_lammps.h +++ b/src/COLVARS/colvarproxy_lammps.h @@ -45,9 +45,6 @@ class colvarproxy_lammps : public colvarproxy { std::vector atoms_types; - MPI_Comm inter_comm; // MPI comm with 1 root proc from each world - int inter_me, inter_num; // rank for the inter replica comm - public: friend class cvm::atom; @@ -59,9 +56,6 @@ class colvarproxy_lammps : public colvarproxy { /// Set the internal seed used by \link rand_gaussian() \endlink void set_random_seed(int seed); - /// Set the multiple replicas communicator - void set_replicas_communicator(MPI_Comm root2root); - int setup() override; // disable default and copy constructor @@ -72,7 +66,8 @@ class colvarproxy_lammps : public colvarproxy { // methods for lammps to move data or trigger actions in the proxy public: bool total_forces_enabled() const override { return total_force_requested; }; - bool total_forces_same_step() const override { return true; }; + // Total forces are saved at end of step, only processed at the next step + bool total_forces_same_step() const override { return false; }; bool want_exit() const { return do_exit; }; // perform colvars computation. returns biasing energy @@ -102,14 +97,6 @@ class colvarproxy_lammps : public colvarproxy { int check_atom_id(int atom_number) override; inline std::vector *modify_atom_types() { return &atoms_types; } - - int replica_enabled() override; - int replica_index() override; - int num_replicas() override; - - void replica_comm_barrier() override; - int replica_comm_recv(char *msg_data, int buf_len, int src_rep) override; - int replica_comm_send(char *msg_data, int msg_len, int dest_rep) override; }; #endif diff --git a/src/COLVARS/colvarproxy_lammps_version.h b/src/COLVARS/colvarproxy_lammps_version.h index 5901044b1e..1c16217679 100644 --- a/src/COLVARS/colvarproxy_lammps_version.h +++ b/src/COLVARS/colvarproxy_lammps_version.h @@ -1,3 +1,3 @@ #ifndef COLVARPROXY_VERSION -#define COLVARPROXY_VERSION "2024-07-05" +#define COLVARPROXY_VERSION "2025-03-31" #endif diff --git a/src/COLVARS/fix_colvars.cpp b/src/COLVARS/fix_colvars.cpp index d5a8806eed..247ad2ef8b 100644 --- a/src/COLVARS/fix_colvars.cpp +++ b/src/COLVARS/fix_colvars.cpp @@ -267,6 +267,7 @@ void FixColvars::init() if (init_flag) return; init_flag = 1; +#if defined(COLVARS_MPI) if (universe->nworlds > 1) { // create inter root communicator int color = 1; @@ -275,9 +276,10 @@ void FixColvars::init() } MPI_Comm_split(universe->uworld, color, universe->iworld, &root2root); if (me == 0) { - proxy->set_replicas_communicator(root2root); + proxy->set_replicas_mpi_communicator(root2root); } } +#endif }