From 5b3911d001fbadf313e2b9ab5fe449c35f3991ce Mon Sep 17 00:00:00 2001 From: "Josef M. Gallmetzer" <64498081+galjos@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:49:26 +0200 Subject: [PATCH] feat: add input validation for setup tools --- .cmake/slakos.cmake | 172 ++- CMakeLists.txt | 8 +- apps/CMakeLists.txt | 73 +- apps/PQ.cpp | 117 +- apps/capabilities.cpp | 285 ++++ apps/capabilities.hpp | 34 + apps/externalQMScripts.hpp | 145 ++ apps/validation.cpp | 546 +++++++ apps/validation.hpp | 75 + docs/sphinx/src/installation/installation.rst | 11 + docs/sphinx/src/userGuide/generalUsage.rst | 29 + docs/sphinx/src/userGuide/inputFile.rst | 5 +- external/progressbar/include/progressbar.hpp | 2 +- include/QM/external/externalQMRunner.hpp | 15 +- include/engine/optEngine.hpp | 4 +- include/exceptions/exceptions.hpp | 14 +- include/input/commandLineArgs.hpp | 33 +- .../inputFileParser/filesInputParser.hpp | 7 +- .../inputFileParser/generalInputParser.hpp | 8 +- include/input/inputFileReader.hpp | 8 +- .../parameterFileReader.hpp | 3 +- .../input/restartFileReader/atomSection.hpp | 8 +- include/output/output.hpp | 8 +- .../output/references/referenceFiles/3ob.ref | 26 + .../references/referenceFiles/3ob.ref.bib | 51 + .../references/referenceFiles/matsci.ref | 40 + .../references/referenceFiles/matsci.ref.bib | 78 + include/output/references/references.hpp | 4 +- .../output/references/referencesOutput.hpp | 4 +- include/settings/optimizerSettings.hpp | 7 + include/utilities/executablePath.hpp | 34 + include/utilities/stringUtilities.hpp | 4 +- src/QM/CMakeLists.txt | 2 +- src/QM/ase/CMakeLists.txt | 2 +- src/QM/external/CMakeLists.txt | 10 +- src/QM/external/dftbplusRunner.cpp | 36 +- src/QM/external/externalQMRunner.cpp | 120 +- src/QM/external/pyscfRunner.cpp | 22 +- src/QM/external/turbomoleRunner.cpp | 28 +- src/box/CMakeLists.txt | 2 +- src/connectivity/CMakeLists.txt | 2 +- src/constraints/CMakeLists.txt | 2 +- src/engine/CMakeLists.txt | 2 +- src/engine/hessianEngine.cpp | 87 +- src/engine/mdEngine.cpp | 2 +- src/engine/optEngine.cpp | 2 +- src/exceptions/CMakeLists.txt | 2 +- src/exceptions/exceptions.cpp | 40 +- src/forceField/CMakeLists.txt | 2 +- src/input/CMakeLists.txt | 4 +- src/input/commandLineArgs.cpp | 109 +- src/input/inputFileParser/CMakeLists.txt | 9 +- src/input/inputFileParser/QMInputParser.cpp | 41 +- .../inputFileParser/cellListInputParser.cpp | 20 +- .../constraintsInputParser.cpp | 27 +- .../convergenceInputParser.cpp | 172 ++- .../coulombLongRangeInputParser.cpp | 6 +- .../inputFileParser/filesInputParser.cpp | 58 +- .../inputFileParser/generalInputParser.cpp | 12 +- .../inputFileParser/hessianInputParser.cpp | 33 +- .../inputFileParser/hybridInputParser.cpp | 56 +- .../inputFileParser/manostatInputParser.cpp | 56 +- src/input/inputFileParser/optInputParser.cpp | 106 +- .../inputFileParser/outputInputParser.cpp | 2 +- .../resetKineticsInputParser.cpp | 17 +- .../ringPolymerInputParser.cpp | 15 +- .../simulationBoxInputParser.cpp | 25 +- .../inputFileParser/thermostatInputParser.cpp | 83 +- .../inputFileParser/timingsInputParser.cpp | 17 +- src/input/inputFileReader.cpp | 106 +- src/input/inputValidation.cpp | 323 +++- src/input/parameterFileReader/CMakeLists.txt | 2 +- .../parameterFileReader.cpp | 4 +- src/input/restartFileReader/CMakeLists.txt | 11 +- .../CMakeLists.txt | 2 +- src/input/topologyFileReader/CMakeLists.txt | 2 +- src/integrator/CMakeLists.txt | 2 +- src/intraNonBonded/CMakeLists.txt | 2 +- src/kernels/CMakeLists.txt | 2 +- src/linearAlgebra/CMakeLists.txt | 2 +- src/manostat/CMakeLists.txt | 2 +- src/maxwellBoltzmann/CMakeLists.txt | 2 +- src/mpi/CMakeLists.txt | 2 +- src/opt/CMakeLists.txt | 2 +- src/opt/convergence/CMakeLists.txt | 2 +- src/opt/evaluator/CMakeLists.txt | 2 +- src/opt/learningRateStrategy/CMakeLists.txt | 2 +- src/opt/optimizer/CMakeLists.txt | 2 +- src/output/CMakeLists.txt | 11 +- src/output/references/CMakeLists.txt | 9 +- src/output/references/referencesOutput.cpp | 85 +- src/physicalData/CMakeLists.txt | 2 +- src/potential/CMakeLists.txt | 2 +- src/potential/coulomb/CMakeLists.txt | 2 +- src/potential/coulomb_kokkos/CMakeLists.txt | 2 +- src/potential/nonCoulomb/CMakeLists.txt | 2 +- .../nonCoulomb_kokkos/CMakeLists.txt | 2 +- src/python/CMakeLists.txt | 2 +- src/randomNumberGenerator/CMakeLists.txt | 2 +- src/resetKinetics/CMakeLists.txt | 2 +- src/resetKinetics/resetKinetics.cpp | 28 +- src/settings/CMakeLists.txt | 2 +- src/settings/optimizerSettings.cpp | 63 +- src/settings/qmSettings.cpp | 44 +- src/setup/CMakeLists.txt | 9 +- src/setup/optimizerSetup.cpp | 41 +- src/setup/thermostatSetup.cpp | 19 +- src/simulationBox/CMakeLists.txt | 2 +- src/simulationBox/celllist.cpp | 20 +- src/thermostat/CMakeLists.txt | 2 +- src/thermostat/berendsenThermostat.cpp | 15 +- .../velocityRescalingThermostat.cpp | 18 +- src/timings/CMakeLists.txt | 2 +- src/utilities/CMakeLists.txt | 5 +- src/utilities/executablePath.cpp | 81 + src/utilities/stringUtilities.cpp | 71 +- src/virial/CMakeLists.txt | 2 +- tests/cmake/testPQCli.cmake | 220 +++ tests/cmake/testPQSlakosInstall.cmake | 204 +++ tests/cmake/testPQValidation.cmake | 1321 +++++++++++++++++ tests/src/exceptions/testExceptions.cpp | 16 +- .../src/input/inputFileParsing/CMakeLists.txt | 1 + .../testConstraintsParser.cpp | 42 + .../inputFileParsing/testHybridParser.cpp | 87 ++ .../inputFileParsing/testManostatParser.cpp | 33 +- .../input/inputFileParsing/testQMParser.cpp | 65 +- .../testSimulationBoxParser.cpp | 9 +- .../inputFileParsing/testThermostatParser.cpp | 68 +- .../inputFileParsing/testTimingsParser.cpp | 33 +- tests/src/input/testCommandLineArgs.cpp | 233 ++- tests/src/input/testInputValidation.cpp | 310 +++- tests/src/main/CMakeLists.txt | 4 - tests/src/output/testReferencesOutput.cpp | 12 +- tests/src/resetKinetics/testResetKinetics.cpp | 39 + tests/src/settings/testQMSettings.cpp | 23 - tests/src/setup/testThermostatSetup.cpp | 29 +- tests/src/simulationBox/testCelllist.cpp | 12 + tests/src/thermostat/testThermostat.cpp | 74 +- tests/src/utilities/CMakeLists.txt | 1 + tests/src/utilities/testExecutablePath.cpp | 36 + tests/src/utilities/testStringUtilities.cpp | 67 +- 141 files changed, 6172 insertions(+), 877 deletions(-) create mode 100644 apps/capabilities.cpp create mode 100644 apps/capabilities.hpp create mode 100644 apps/externalQMScripts.hpp create mode 100644 apps/validation.cpp create mode 100644 apps/validation.hpp create mode 100644 include/output/references/referenceFiles/3ob.ref create mode 100644 include/output/references/referenceFiles/3ob.ref.bib create mode 100644 include/output/references/referenceFiles/matsci.ref create mode 100644 include/output/references/referenceFiles/matsci.ref.bib create mode 100644 include/utilities/executablePath.hpp create mode 100644 src/utilities/executablePath.cpp create mode 100644 tests/cmake/testPQCli.cmake create mode 100644 tests/cmake/testPQSlakosInstall.cmake create mode 100644 tests/cmake/testPQValidation.cmake create mode 100644 tests/src/input/inputFileParsing/testHybridParser.cpp create mode 100644 tests/src/utilities/testExecutablePath.cpp diff --git a/.cmake/slakos.cmake b/.cmake/slakos.cmake index 98b88f0bc..fbec50af8 100644 --- a/.cmake/slakos.cmake +++ b/.cmake/slakos.cmake @@ -1,28 +1,166 @@ -include(FetchContent) +set( + PQ_SLAKOS_SOURCE_DIR + "" + CACHE PATH + "Path to preseeded 3ob and matsci directories for offline builds" +) -function(CloneRepository repositoryURL sourceDir) - #Commands are left empty so that we only checkout the source and no not perform any kind of build - if (EXISTS ${sourceDir}) - message("Directory ${sourceDir} already exists. Skipping cloning of ${repositoryURL}") - return() +function(ValidateSlakosSet source_dir set_name) + foreach(required_path IN ITEMS + LICENSE + README + RELEASE.md + CHANGELOG.md + ) + if(NOT EXISTS "${source_dir}/${required_path}") + message( + FATAL_ERROR + "${set_name} data is missing ${source_dir}/${required_path}" + ) + endif() + endforeach() + + if(NOT IS_DIRECTORY "${source_dir}/skfiles") + message( + FATAL_ERROR + "${set_name} data is missing ${source_dir}/skfiles" + ) + endif() + + file(GLOB slakos_files "${source_dir}/skfiles/*.skf") + if(NOT slakos_files) + message(FATAL_ERROR "${set_name} contains no Slater-Koster files") + endif() +endfunction() + +function(CheckoutRepository repository_url source_dir revision) + if(NOT EXISTS "${source_dir}/.git") + if(EXISTS "${source_dir}") + message( + FATAL_ERROR + "${source_dir} exists but is not a Git checkout" + ) + endif() + + execute_process( + COMMAND + "${GIT_EXECUTABLE}" clone --no-checkout + "${repository_url}" "${source_dir}" + RESULT_VARIABLE clone_result + ) + if(NOT clone_result EQUAL 0) + message(FATAL_ERROR "Failed to clone ${repository_url}") + endif() endif() - message("Starting to clone ${repositoryURL} into ${sourceDir}") execute_process( - COMMAND git clone ${repositoryURL} ${sourceDir} - RESULT_VARIABLE result + COMMAND + "${GIT_EXECUTABLE}" -C "${source_dir}" + cat-file -e "${revision}^{commit}" + RESULT_VARIABLE revision_available + OUTPUT_QUIET + ERROR_QUIET ) + if(NOT revision_available EQUAL 0) + execute_process( + COMMAND + "${GIT_EXECUTABLE}" -C "${source_dir}" + fetch --depth 1 origin "${revision}" + RESULT_VARIABLE fetch_result + ) + if(NOT fetch_result EQUAL 0) + message( + FATAL_ERROR + "Failed to fetch ${revision} from ${repository_url}" + ) + endif() + endif() - if(NOT ${result} EQUAL 0) - message(FATAL_ERROR "Failed to clone from ${repositoryURL}") + execute_process( + COMMAND + "${GIT_EXECUTABLE}" -C "${source_dir}" + checkout --detach "${revision}" + RESULT_VARIABLE checkout_result + OUTPUT_QUIET + ERROR_QUIET + ) + if(NOT checkout_result EQUAL 0) + message(FATAL_ERROR "Failed to checkout ${revision} in ${source_dir}") endif() -endfunction(CloneRepository) -# fetch 3ob slakos files -CloneRepository("https://github.com/dftbparams/3ob.git" "${CMAKE_BINARY_DIR}/external/slakos/3ob") + execute_process( + COMMAND "${GIT_EXECUTABLE}" -C "${source_dir}" rev-parse HEAD + RESULT_VARIABLE revision_result + OUTPUT_VARIABLE actual_revision + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(NOT revision_result EQUAL 0 OR + NOT "${actual_revision}" STREQUAL "${revision}") + message( + FATAL_ERROR + "Expected ${revision} in ${source_dir}, found ${actual_revision}" + ) + endif() -# fetch matsci files -CloneRepository("https://github.com/dftbparams/matsci.git" "${CMAKE_BINARY_DIR}/external/slakos/matsci") + execute_process( + COMMAND "${GIT_EXECUTABLE}" -C "${source_dir}" status --porcelain + RESULT_VARIABLE status_result + OUTPUT_VARIABLE checkout_status + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(NOT status_result EQUAL 0 OR NOT "${checkout_status}" STREQUAL "") + message( + FATAL_ERROR + "${source_dir} contains uncommitted or untracked files" + ) + endif() +endfunction() + +set(SLAKOS_3OB_REVISION "c5e165cb65f80f6b4e054c99e3f770ac3b8a4ecc") +set(SLAKOS_MATSCI_REVISION "57016b4363fd6180f6edff662e5fbaa95276b4bd") + +if(PQ_SLAKOS_SOURCE_DIR) + get_filename_component( + SLAKOS_SOURCE_DIR + "${PQ_SLAKOS_SOURCE_DIR}" + ABSOLUTE + BASE_DIR "${CMAKE_SOURCE_DIR}" + ) + message(STATUS "Using preseeded Slater-Koster data: ${SLAKOS_SOURCE_DIR}") +else() + find_package(Git REQUIRED) + set(SLAKOS_SOURCE_DIR "${CMAKE_BINARY_DIR}/external/slakos") + CheckoutRepository( + "https://github.com/dftbparams/3ob.git" + "${SLAKOS_SOURCE_DIR}/3ob" + "${SLAKOS_3OB_REVISION}" + ) + CheckoutRepository( + "https://github.com/dftbparams/matsci.git" + "${SLAKOS_SOURCE_DIR}/matsci" + "${SLAKOS_MATSCI_REVISION}" + ) +endif() + +ValidateSlakosSet("${SLAKOS_SOURCE_DIR}/3ob" "3ob") +ValidateSlakosSet("${SLAKOS_SOURCE_DIR}/matsci" "matsci") # define directory for 3ob and matsci for preprocessor -add_compile_definitions(__SLAKOS_DIR__="${CMAKE_BINARY_DIR}/external/slakos/") +add_compile_definitions(__SLAKOS_DIR__="${SLAKOS_SOURCE_DIR}/") + +foreach(slakos_set IN ITEMS 3ob matsci) + install( + DIRECTORY "${SLAKOS_SOURCE_DIR}/${slakos_set}/skfiles" + DESTINATION "share/PQ/slakos/${slakos_set}" + COMPONENT slakos + ) + install( + FILES + "${SLAKOS_SOURCE_DIR}/${slakos_set}/LICENSE" + "${SLAKOS_SOURCE_DIR}/${slakos_set}/README" + "${SLAKOS_SOURCE_DIR}/${slakos_set}/RELEASE.md" + "${SLAKOS_SOURCE_DIR}/${slakos_set}/CHANGELOG.md" + DESTINATION "share/PQ/slakos/${slakos_set}" + COMPONENT slakos + ) +endforeach() diff --git a/CMakeLists.txt b/CMakeLists.txt index dcb373d66..4f2112c95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,9 +48,11 @@ else() # ***************** # set install rpath # ***************** - SET(CMAKE_INSTALL_RPATH - "${CMAKE_INSTALL_PREFIX}/lib/;${CMAKE_INSTALL_PREFIX}/lib/tools/;" - ) + if(APPLE) + set(CMAKE_INSTALL_RPATH "@loader_path/../lib;@loader_path") + else() + set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib;$ORIGIN") + endif() set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) endif() diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index e294e552f..54a0c216f 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -1,11 +1,29 @@ add_executable(PQ PQ.cpp + capabilities.cpp + validation.cpp ) target_include_directories(PQ PUBLIC ${CODE_INCLUDE_DIR} ) +if(NOT DEFINED BUILD_WITH_PYBIND11) + set(BUILD_WITH_PYBIND11 OFF) +endif() + +target_compile_definitions(PQ + PRIVATE + PQ_BUILD_WITH_ASE=$ + PQ_BUILD_WITH_MPI=$ + PQ_BUILD_WITH_KOKKOS=$ + PQ_BUILD_WITH_PYTHON_BINDINGS=$ + PQ_BUILD_WITH_PYTHON_EMBEDDING=$ + PQ_BUILD_SHARED=$ + PQ_BUILD_STATIC=$> + PQ_BUILD_WITH_SINGULARITY=$ +) + target_link_libraries(PQ PUBLIC PQ_input @@ -30,5 +48,58 @@ if(BUILD_WITH_PYBIND11) endif() install(TARGETS PQ - DESTINATION ${CMAKE_INSTALL_PREFIX}/bin + DESTINATION bin ) + +if(BUILD_WITH_TESTS) + add_test( + NAME testPQCli + COMMAND + ${CMAKE_COMMAND} + -DPQ_EXECUTABLE=$ + -DEXPECTED_VERSION=${GIT_VERSION} + -DEXPECTED_ASE=${BUILD_WITH_ASE} + -DEXPECTED_MPI=${BUILD_WITH_MPI} + -DEXPECTED_KOKKOS=${BUILD_WITH_KOKKOS} + -DEXPECTED_PYTHON_BINDINGS=${BUILD_WITH_PYTHON_BINDINGS} + -DEXPECTED_PYTHON_EMBEDDING=${BUILD_WITH_PYBIND11} + -DEXPECTED_SHARED=${BUILD_SHARED_LIBS} + -DEXPECTED_SINGULARITY=${BUILD_WITH_SINGULARITY} + -P ${PROJECT_SOURCE_DIR}/tests/cmake/testPQCli.cmake + ) + set_property(TEST testPQCli PROPERTY LABELS input) + + add_test( + NAME testPQValidation + COMMAND + ${CMAKE_COMMAND} + -DPQ_EXECUTABLE=$ + -DEXPECTED_ASE=${BUILD_WITH_ASE} + -DEXPECTED_SHARED=${BUILD_SHARED_LIBS} + -DEXPECTED_SINGULARITY=${BUILD_WITH_SINGULARITY} + -DVALIDATION_FIXTURE_DIR=${PROJECT_SOURCE_DIR}/tests/regression/fixtures/mm-md-smoke + -DVALIDATION_WORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/pq-cli-validation + -P ${PROJECT_SOURCE_DIR}/tests/cmake/testPQValidation.cmake + ) + set_property(TEST testPQValidation PROPERTY LABELS input) + set_property( + TEST testPQValidation + PROPERTY ENVIRONMENT + "GMON_OUT_PREFIX=${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/pq-validation-gmon" + ) + + if(BUILD_WITH_ASE) + add_test( + NAME testPQSlakosInstall + COMMAND + ${CMAKE_COMMAND} + -DPQ_EXECUTABLE=$ + -DBUILD_DIR=${CMAKE_BINARY_DIR} + -DSLAKOS_SOURCE_DIR=${SLAKOS_SOURCE_DIR} + -DSTAGING_PREFIX=${CMAKE_CURRENT_BINARY_DIR}/pq-slakos-install + -DMM_FIXTURE_DIR=${PROJECT_SOURCE_DIR}/tests/regression/fixtures/mm-md-smoke + -P ${PROJECT_SOURCE_DIR}/tests/cmake/testPQSlakosInstall.cmake + ) + set_property(TEST testPQSlakosInstall PROPERTY LABELS input) + endif() +endif() diff --git a/apps/PQ.cpp b/apps/PQ.cpp index efccffcc9..fb22f2b4e 100644 --- a/apps/PQ.cpp +++ b/apps/PQ.cpp @@ -20,7 +20,7 @@ ******************************************************************************/ -#include // for EXIT_SUCCESS +#include // for EXIT_FAILURE, EXIT_SUCCESS #include // for exception #include // for remove_all #include // for operator<< @@ -28,10 +28,14 @@ #include // for string, char_traits #include // for vector +#include "capabilities.hpp" // for writeCapabilities #include "commandLineArgs.hpp" // for CommandLineArgs #include "engine.hpp" // for Engine +#include "exceptions.hpp" // for CustomException #include "inputFileReader.hpp" // for readJobType #include "setup.hpp" // for setupSimulation +#include "systemInfo.hpp" // for _VERSION_ +#include "validation.hpp" // for validation #ifdef WITH_MPI #include // for MPI_Abort, MPI_COMM_WORLD, MPI_Finalize @@ -43,15 +47,12 @@ #include // for scoped_interpreter #endif -static int PQ(int argc, const std::vector &arguments) +static int run(const std::string &inputFileName) { - auto commandLineArgs = CommandLineArgs(argc, arguments); - commandLineArgs.detectFlags(); - auto engine = std::unique_ptr(); - input::readJobType(commandLineArgs.getInputFileName(), engine); + input::readJobType(inputFileName, engine); - setup::setupRequestedJob(commandLineArgs.getInputFileName(), *engine); + setup::setupRequestedJob(inputFileName, *engine); /* HERE STARTS THE MAIN LOOP @@ -66,9 +67,94 @@ static int PQ(int argc, const std::vector &arguments) return EXIT_SUCCESS; } +static void printHelp() +{ + std::cout + << "Usage: PQ \n" + << " PQ --help\n" + << " PQ --version\n" + << " PQ --capabilities=json\n" + << " PQ --validate [--format=text|json] " + "[--scope=installed|portable]\n\n" + << "Run a PQ simulation from an input file.\n\n" + << "Options:\n" + << " -h, --help Show this help message.\n" + << " -V, --version Show the PQ version.\n" + << " --capabilities=json\n" + << " Show compiled capabilities as JSON.\n" + << " --validate \n" + << " Check input without running a simulation.\n" + << " --format=text Return readable validation (default).\n" + << " --format=json Return machine-readable validation.\n" + << " --scope=installed Check this build and referenced files " + "(default).\n" + << " --scope=portable Check portable input semantics only.\n"; +} + // main wrapper int main(int argc, char *argv[]) { + auto exitCode = EXIT_SUCCESS; + auto arguments = std::vector(argv, argv + argc); + auto commandLineArgs = CommandLineArgs(argc, arguments); + + try + { + commandLineArgs.parse(); + } + catch (const customException::CustomException &e) + { + std::cerr << "Error: " << e.getMessage() << '\n' << std::flush; + return EXIT_FAILURE; + } + catch (const std::exception &e) + { + std::cerr << "Error: " << e.what() << '\n' << std::flush; + return EXIT_FAILURE; + } + + if (CommandLineAction::HELP == commandLineArgs.getAction()) + { + printHelp(); + return EXIT_SUCCESS; + } + + if (CommandLineAction::VERSION == commandLineArgs.getAction()) + { + std::cout << "PQ " << sysinfo::_VERSION_ << '\n'; + return EXIT_SUCCESS; + } + + if (CommandLineAction::CAPABILITIES == commandLineArgs.getAction()) + { + cli::writeCapabilities(std::cout); + return EXIT_SUCCESS; + } + + if (CommandLineAction::VALIDATE == commandLineArgs.getAction()) + { + try + { + const auto result = cli::validateInputFile( + commandLineArgs.getInputFileName(), + commandLineArgs.getValidationScope() + ); + + if (CommandLineFormat::JSON == commandLineArgs.getFormat()) + cli::writeValidationJson(result, std::cout); + else + cli::writeValidationText(result, std::cout, std::cerr); + + return result.valid ? EXIT_SUCCESS : EXIT_FAILURE; + } + catch (const std::exception &e) + { + std::cerr << "Validation failed: " << e.what() << '\n' + << std::flush; + return 2; + } + } + #ifdef WITH_MPI mpi::MPI::init(&argc, &argv); #endif @@ -83,12 +169,21 @@ int main(int argc, char *argv[]) try { - auto arguments = std::vector(argv, argv + argc); - ::PQ(argc, arguments); + exitCode = run(commandLineArgs.getInputFileName()); + } + catch (const customException::CustomException &e) + { + std::cerr << "Error: " << e.getMessage() << '\n' << std::flush; + exitCode = EXIT_FAILURE; + +#ifdef WITH_MPI + ::MPI_Abort(MPI_COMM_WORLD, EXIT_FAILURE); +#endif } catch (const std::exception &e) { - std::cout << "Exception: " << e.what() << '\n' << std::flush; + std::cerr << "Error: " << e.what() << '\n' << std::flush; + exitCode = EXIT_FAILURE; #ifdef WITH_MPI ::MPI_Abort(MPI_COMM_WORLD, EXIT_FAILURE); @@ -103,5 +198,5 @@ int main(int argc, char *argv[]) mpi::MPI::finalize(); #endif - return EXIT_SUCCESS; + return exitCode; } diff --git a/apps/capabilities.cpp b/apps/capabilities.cpp new file mode 100644 index 000000000..ef7c5543a --- /dev/null +++ b/apps/capabilities.cpp @@ -0,0 +1,285 @@ +/***************************************************************************** + + + PQ + Copyright (C) 2023-now Jakob Gamper + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + +******************************************************************************/ + +#include "capabilities.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "constants/conversionFactors.hpp" +#include "defaults.hpp" +#include "externalQMScripts.hpp" +#include "systemInfo.hpp" + +namespace +{ + constexpr bool _WITH_ASE_ = PQ_BUILD_WITH_ASE; + constexpr bool _WITH_MPI_ = PQ_BUILD_WITH_MPI; + constexpr bool _WITH_KOKKOS_ = PQ_BUILD_WITH_KOKKOS; + constexpr bool _WITH_PYTHON_BINDINGS_ = PQ_BUILD_WITH_PYTHON_BINDINGS; + constexpr bool _WITH_PYTHON_EMBEDDING_ = PQ_BUILD_WITH_PYTHON_EMBEDDING; + constexpr bool _SHARED_ = PQ_BUILD_SHARED; + constexpr bool _STATIC_ = PQ_BUILD_STATIC; + constexpr bool _WITH_SINGULARITY_ = PQ_BUILD_WITH_SINGULARITY; + + void writeJsonString(std::ostream &output, const std::string_view value) + { + output << '"'; + for (const auto character : value) + { + if ('"' == character || '\\' == character) + output << '\\'; + output << character; + } + output << '"'; + } + + void writeExternalQMCapabilities(std::ostream &output) + { + output << " \"external_qm\": {\n" + << " \"script_mode\": \"" + << ((_STATIC_ || _WITH_SINGULARITY_) ? "full_path_only" + : "bundled_or_full_path") + << "\",\n" + << " \"programs\": {\n"; + + for (size_t methodIndex = 0; + methodIndex < cli::_EXTERNAL_QM_METHODS_.size(); + ++methodIndex) + { + const auto method = cli::_EXTERNAL_QM_METHODS_[methodIndex]; + output << " "; + writeJsonString(output, cli::externalQMProgramName(method)); + output << ": {\n" + << " \"recommended_script\": "; + + const auto recommended = cli::recommendedExternalQMScript(method); + if (recommended.empty()) + output << "null"; + else + writeJsonString(output, recommended); + + output << ",\n" + << " \"scripts\": [\n"; + + const auto scripts = cli::externalQMScripts(method); + for (size_t scriptIndex = 0; scriptIndex < scripts.size(); + ++scriptIndex) + { + const auto &script = scripts[scriptIndex]; + output << " {\"name\": "; + writeJsonString(output, script.name); + output << ", \"label\": "; + writeJsonString(output, script.label); + + if (!script.requiredFileKeyword.empty()) + { + output << ", \"required_file_keywords\": ["; + writeJsonString(output, script.requiredFileKeyword); + output << ']'; + } + + if (!script.requiredWorkingFile.empty()) + { + output << ", \"required_working_files\": ["; + writeJsonString(output, script.requiredWorkingFile); + output << ']'; + } + + output << '}' + << (scriptIndex + 1 == scripts.size() ? "\n" : ",\n"); + } + + output << " ]\n" + << " }" + << (methodIndex + 1 == cli::_EXTERNAL_QM_METHODS_.size() + ? "\n" + : ",\n"); + } + + output << " }\n" + << " }"; + } +} // namespace + +/** + * @brief Writes the versioned machine-readable PQ capabilities. + * + * @param output + */ +void cli::writeCapabilities(std::ostream &output) +{ + const auto flags = output.flags(); + const auto precision = output.precision(); + output << std::boolalpha + << std::setprecision(std::numeric_limits::max_digits10); + + output << "{\n" + << " \"schema\": \"pq.capabilities\",\n" + << " \"schema_version\": 1,\n" + << " \"version\": "; + writeJsonString(output, sysinfo::_VERSION_); + output << ",\n" + << " \"build\": {\n" + << " \"ase\": " << _WITH_ASE_ << ",\n" + << " \"mpi\": " << _WITH_MPI_ << ",\n" + << " \"kokkos\": " << _WITH_KOKKOS_ << ",\n" + << " \"python_bindings\": " << _WITH_PYTHON_BINDINGS_ << ",\n" + << " \"python_embedding\": " << _WITH_PYTHON_EMBEDDING_ << ",\n" + << " \"shared\": " << _SHARED_ << ",\n" + << " \"singularity\": " << _WITH_SINGULARITY_ << "\n" + << " },\n" + << " \"cli\": {\n" + << " \"input_validation\": {\n" + << " \"schema\": \"pq.validation\",\n" + << " \"schema_version\": 1,\n" + << " \"formats\": [\"text\", \"json\"],\n" + << " \"scopes\": [\"portable\", \"installed\"]\n" + << " }\n" + << " },\n" + << " \"input\": {\n" + << " \"job_types\": [\n" + << " \"mm-md\", \"mm-hessian\", \"mm-opt\", \"qm-md\", " + "\"qm-rpmd\"\n" + << " ],\n" + << " \"qm_programs\": [\n" + << " \"dftbplus\", \"pyscf\", \"turbomole\""; + if (_WITH_ASE_) + output << ", \"ase_dftbplus\", \"ase_xtb\", \"fennol\", \"mace\", " + "\"mace_mp\", \"mace_off\""; + output << "\n" + << " ],\n"; + writeExternalQMCapabilities(output); + output << ",\n" + << " \"thermostats\": [\n" + << " \"none\", \"berendsen\", \"velocity_rescaling\", " + "\"langevin\", \"nh-chain\"\n" + << " ],\n" + << " \"manostats\": [\n" + << " \"none\", \"berendsen\", \"stochastic_rescaling\"\n" + << " ],\n" + << " \"pressure_isotropies\": [\n" + << " \"isotropic\", \"xy\", \"xz\", \"yz\", " + "\"anisotropic\", \"full_anisotropic\"\n" + << " ],\n" + << " \"parameters\": {\n" + << " \"nstep\": {\n" + << " \"type\": \"integer\", \"minimum\": 1, \"maximum\": " + << INT_MAX << "\n" + << " },\n" + << " \"timestep\": {\n" + << " \"type\": \"number\", \"unit\": \"fs\", " + "\"exclusive_minimum\": 0\n" + << " },\n" + << " \"output_freq\": {\n" + << " \"type\": \"integer\", \"minimum\": 0, \"maximum\": " + << INT_MAX << "\n" + << " },\n" + << " \"random_seed\": {\n" + << " \"type\": \"integer\", \"minimum\": 0, " + "\"maximum\": " + << UINT32_MAX << "\n" + << " },\n" + << " \"temp\": {\n" + << " \"type\": \"number\", \"unit\": \"K\", " + "\"minimum\": 0\n" + << " },\n" + << " \"start_temp\": {\n" + << " \"type\": \"number\", \"unit\": \"K\", " + "\"minimum\": 0\n" + << " },\n" + << " \"end_temp\": {\n" + << " \"type\": \"number\", \"unit\": \"K\", " + "\"minimum\": 0\n" + << " },\n" + << " \"temp_ramp_steps\": {\n" + << " \"type\": \"integer\", \"minimum\": 0, \"maximum\": " + << INT_MAX << "\n" + << " },\n" + << " \"temp_ramp_frequency\": {\n" + << " \"type\": \"integer\", \"minimum\": 1, \"maximum\": " + << INT_MAX << "\n" + << " },\n" + << " \"t_relaxation\": {\n" + << " \"type\": \"number\", \"unit\": \"ps\", " + "\"exclusive_minimum\": 0, \"maximum\": " + << std::numeric_limits::max() / constants::_PS_TO_FS_ + << ", \"minimum_from\": {\"parameter\": \"timestep\", " + "\"factor\": 0.001}, \"default\": " + << defaults::_BERENDSEN_THERMOSTAT_RELAX_TIME_ << "\n" + << " },\n" + << " \"friction\": {\n" + << " \"type\": \"number\", \"unit\": \"ps^-1\", " + "\"minimum\": 0, \"maximum\": " + << std::numeric_limits::max() / 1.0e12 << ", \"default\": " + << defaults::_LANGEVIN_THERMOSTAT_FRICTION_ / 1.0e12 << "\n" + << " },\n" + << " \"nh-chain_length\": {\n" + << " \"type\": \"integer\", \"minimum\": 1, \"maximum\": " + << INT_MAX + << ", \"default\": " << defaults::_NH_CHAIN_LENGTH_DEFAULT_ << "\n" + << " },\n" + << " \"coupling_frequency\": {\n" + << " \"type\": \"number\", \"unit\": \"cm^-1\", " + "\"minimum\": 0, \"maximum\": " + << std::sqrt(std::numeric_limits::max()) / + constants::_PER_CM_TO_HZ_ + << ", \"default\": " << defaults::_NH_COUPLING_FREQ_ << "\n" + << " },\n" + << " \"pressure\": {\n" + << " \"type\": \"number\", \"unit\": \"bar\"\n" + << " },\n" + << " \"p_relaxation\": {\n" + << " \"type\": \"number\", \"unit\": \"ps\", " + "\"exclusive_minimum\": 0, \"maximum\": " + << std::numeric_limits::max() / constants::_PS_TO_FS_ + << ", \"minimum_from\": {\"parameter\": \"timestep\", " + "\"factor\": 0.001}, \"default\": " + << defaults::_BERENDSEN_MANOSTAT_RELAX_TIME_ << "\n" + << " },\n" + << " \"compressibility\": {\n" + << " \"type\": \"number\", \"unit\": \"bar^-1\", " + "\"minimum\": 0, \"default\": " + << defaults::_COMPRESSIBILITY_WATER_DEFAULT_ << "\n" + << " },\n" + << " \"density\": {\n" + << " \"type\": \"number\", \"unit\": \"kg/L\", " + "\"exclusive_minimum\": 0\n" + << " },\n" + << " \"rcoulomb\": {\n" + << " \"type\": \"number\", \"unit\": \"angstrom\", " + "\"minimum\": 0, " + "\"default\": " + << defaults::_COULOMB_CUT_OFF_DEFAULT_ << "\n" + << " }\n" + << " }\n" + << " }\n" + << "}\n"; + + output.flags(flags); + output.precision(precision); +} diff --git a/apps/capabilities.hpp b/apps/capabilities.hpp new file mode 100644 index 000000000..7537b5175 --- /dev/null +++ b/apps/capabilities.hpp @@ -0,0 +1,34 @@ +/***************************************************************************** + + + PQ + Copyright (C) 2023-now Jakob Gamper + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + +******************************************************************************/ + +#ifndef _CAPABILITIES_HPP_ + +#define _CAPABILITIES_HPP_ + +#include + +namespace cli +{ + void writeCapabilities(std::ostream &output); +} + +#endif // _CAPABILITIES_HPP_ diff --git a/apps/externalQMScripts.hpp b/apps/externalQMScripts.hpp new file mode 100644 index 000000000..076f6fb28 --- /dev/null +++ b/apps/externalQMScripts.hpp @@ -0,0 +1,145 @@ +/***************************************************************************** + + + PQ + Copyright (C) 2023-now Jakob Gamper + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + +******************************************************************************/ + +#ifndef _EXTERNAL_QM_SCRIPTS_HPP_ +#define _EXTERNAL_QM_SCRIPTS_HPP_ + +#include +#include +#include +#include + +#include "qmSettings.hpp" + +namespace cli +{ + struct ExternalQMScriptInfo + { + std::string_view name; + std::string_view label; + std::string_view requiredFileKeyword; + std::string_view requiredWorkingFile; + }; + + inline constexpr auto _DFTBPLUS_SCRIPTS_ = std::array{ExternalQMScriptInfo{ + "dftbplus_periodic_stress", + "DFTB+ periodic stress", + "dftb_file", + "" + }}; + inline constexpr auto _PYSCF_SCRIPTS_ = std::array{ + ExternalQMScriptInfo{"pyscf_hf.py", "UHF / STO-3G", "", ""}, + ExternalQMScriptInfo{"pyscf_mp2.py", "UMP2 / 6-311++G**", "", ""} + }; + inline constexpr auto _TURBOMOLE_SCRIPTS_ = std::array{ExternalQMScriptInfo{ + "turbomole_rimp2", + "RI-MP2", + "", + "tm_define.template" + }}; + + inline constexpr auto _EXTERNAL_QM_METHODS_ = std::array{ + settings::QMMethod::DFTBPLUS, + settings::QMMethod::PYSCF, + settings::QMMethod::TURBOMOLE + }; + + inline std::span externalQMScripts( + const settings::QMMethod method + ) + { + using enum settings::QMMethod; + + switch (method) + { + case DFTBPLUS: return _DFTBPLUS_SCRIPTS_; + case PYSCF: return _PYSCF_SCRIPTS_; + case TURBOMOLE: return _TURBOMOLE_SCRIPTS_; + + case NONE: + case ASEDFTBPLUS: + case ASEXTB: + case MACE: + case FENNOL: return {}; + } + + return {}; + } + + inline std::string_view externalQMProgramName( + const settings::QMMethod method + ) + { + using enum settings::QMMethod; + + switch (method) + { + case DFTBPLUS: return "dftbplus"; + case PYSCF: return "pyscf"; + case TURBOMOLE: return "turbomole"; + + case NONE: + case ASEDFTBPLUS: + case ASEXTB: + case MACE: + case FENNOL: return ""; + } + + return ""; + } + + inline std::string_view recommendedExternalQMScript( + const settings::QMMethod method + ) + { + using enum settings::QMMethod; + + switch (method) + { + case DFTBPLUS: return _DFTBPLUS_SCRIPTS_.front().name; + case TURBOMOLE: return _TURBOMOLE_SCRIPTS_.front().name; + + case NONE: + case ASEDFTBPLUS: + case ASEXTB: + case PYSCF: + case MACE: + case FENNOL: return ""; + } + + return ""; + } + + inline bool isExternalQMScript( + const settings::QMMethod method, + const std::string_view script + ) + { + const auto scripts = externalQMScripts(method); + return std::ranges::any_of( + scripts, + [script](const auto &candidate) { return candidate.name == script; } + ); + } +} // namespace cli + +#endif // _EXTERNAL_QM_SCRIPTS_HPP_ diff --git a/apps/validation.cpp b/apps/validation.cpp new file mode 100644 index 000000000..19b375231 --- /dev/null +++ b/apps/validation.cpp @@ -0,0 +1,546 @@ +/***************************************************************************** + + + PQ + Copyright (C) 2023-now Jakob Gamper + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + +******************************************************************************/ + +#include "validation.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "engine.hpp" +#include "exceptions.hpp" +#include "externalQMRunner.hpp" +#include "externalQMScripts.hpp" +#include "fileSettings.hpp" +#include "forceFieldSettings.hpp" +#include "inputFileReader.hpp" +#include "manostatSettings.hpp" +#include "qmSettings.hpp" +#include "settings.hpp" +#include "thermostatSettings.hpp" + +namespace +{ + class ScopedStreamRedirect + { + private: + std::ostream &_stream; + std::streambuf *_original; + + public: + ScopedStreamRedirect(std::ostream &stream, std::streambuf *replacement) + : _stream(stream), _original(stream.rdbuf(replacement)) + { + } + + ~ScopedStreamRedirect() { _stream.rdbuf(_original); } + }; + + cli::ValidationResult invalidResult( + const std::string_view inputFile, + const std::string_view message, + const std::optional lineNumber, + const ValidationScope scope + ) + { + return { + .valid = false, + .inputFile = std::string(inputFile), + .scope = scope, + .diagnostics = { + {cli::ValidationSeverity::ERROR, + std::string(message), + lineNumber} + } + }; + } + + void requireFile( + const std::string_view fileName, + const std::string_view description + ) + { + if (!std::filesystem::is_regular_file(fileName)) + throw customException::InputFileException( + std::format("{} \"{}\" does not exist", description, fileName) + ); + } + + void requireDirectory( + const std::string_view directoryName, + const std::string_view description + ) + { + if (!std::filesystem::is_directory(directoryName)) + throw customException::InputFileException( + std::format( + "{} \"{}\" does not exist or is not a directory", + description, + directoryName + ) + ); + } + + bool isRemoteResource(const std::string_view value) + { + return value.starts_with("https://") || value.starts_with("http://"); + } + + void validateExternalQMScriptSelection() + { + using settings::QMSettings; + using settings::Settings; + + if (!Settings::isQMActivated() || !QMSettings::isExternalQMRunner()) + return; + + const auto script = QMSettings::getQMScript(); + const auto fullPathScript = QMSettings::getQMScriptFullPath(); + + if (script.empty() && fullPathScript.empty()) + throw customException::InputFileException( + "No qm_script provided. Please provide a qm_script in the " + "input file." + ); + + if (!script.empty() && !fullPathScript.empty()) + throw customException::InputFileException( + "\"qm_script\" and \"qm_script_full_path\" are mutually " + "exclusive" + ); + + if (!script.empty() && + !cli::isExternalQMScript(QMSettings::getQMMethod(), script)) + throw customException::InputFileException( + std::format( + "Bundled QM script \"{}\" is not available for {}", + script, + cli::externalQMProgramName(QMSettings::getQMMethod()) + ) + ); + } + + void validateInstalledExternalQMScript() + { + using settings::QMSettings; + using settings::Settings; + + if (!Settings::isQMActivated() || !QMSettings::isExternalQMRunner()) + return; + + const auto script = QMSettings::getQMScript(); + const auto fullPathScript = QMSettings::getQMScriptFullPath(); + + if ((PQ_BUILD_STATIC || PQ_BUILD_WITH_SINGULARITY) && + fullPathScript.empty()) + throw customException::InputFileException( + "This PQ build requires \"qm_script_full_path\" for " + "external QM programs" + ); + + if (!fullPathScript.empty()) + requireFile(fullPathScript, "QM script"); + else + { + requireFile(QM::bundledQMScriptPath(script), "Bundled QM script"); + + const auto scripts = + cli::externalQMScripts(QMSettings::getQMMethod()); + const auto selected = std::ranges::find_if( + scripts, + [&script](const auto &candidate) + { return candidate.name == script; } + ); + if (selected != scripts.end() && + !selected->requiredWorkingFile.empty()) + requireFile( + selected->requiredWorkingFile, + "Required QM working file" + ); + } + } + + void validateInputDependencies(engine::Engine &engine) + { + using settings::FileSettings; + using settings::ForceFieldSettings; + + if (engine.isConstraintsActivated() || ForceFieldSettings::isActive()) + { + if (!FileSettings::isTopologyFileNameSet()) + throw customException::InputFileException( + "Topology file needed for requested simulation setup" + ); + } + + if (ForceFieldSettings::isActive() && + !FileSettings::isParameterFileNameSet()) + throw customException::InputFileException( + "Parameter file needed for requested simulation setup" + ); + + if (engine.getConstraints().isMShakeActive() && + FileSettings::getMShakeFileName().empty()) + throw customException::InputFileException( + "M-SHAKE file needed for requested simulation setup" + ); + + validateExternalQMScriptSelection(); + } + + void validateEffectiveFiles(engine::Engine &engine) + { + using settings::FileSettings; + using settings::ForceFieldSettings; + using settings::ManostatSettings; + using settings::ManostatType; + using settings::Settings; + + if (Settings::isMMActivated() || + ManostatSettings::getManostatType() != ManostatType::NONE) + requireFile( + FileSettings::getMolDescriptorFileName(), + "Moldescriptor file" + ); + + if (Settings::isMMActivated() && + !engine.isForceFieldNonCoulombicsActivated()) + requireFile(FileSettings::getGuffDatFileName(), "Guff file"); + + if (engine.isConstraintsActivated() || ForceFieldSettings::isActive()) + { + requireFile(FileSettings::getTopologyFileName(), "Topology file"); + } + + if (ForceFieldSettings::isActive()) + { + requireFile(FileSettings::getParameterFilename(), "Parameter file"); + } + + if (Settings::isQMActivated() && + settings::QMSettings::getQMMethod() == settings::QMMethod::DFTBPLUS) + requireFile(FileSettings::getDFTBFileName(), "DFTB setup file"); + + if (Settings::isQMActivated() && + settings::QMSettings::getQMMethod() == + settings::QMMethod::ASEDFTBPLUS && + settings::QMSettings::getSlakosType() != settings::SlakosType::NONE) + { + const auto description = settings::QMSettings::getSlakosType() == + settings::SlakosType::CUSTOM + ? "Slater-Koster directory" + : "Built-in Slater-Koster directory"; + requireDirectory( + settings::QMSettings::getSlakosPath(), + description + ); + } + + if (Settings::isQMActivated() && + settings::QMSettings::getQMMethod() == settings::QMMethod::FENNOL) + requireFile( + settings::QMSettings::getFennolModelPath(), + "FeNNol model file" + ); + + if (Settings::isQMActivated() && + settings::QMSettings::getQMMethod() == settings::QMMethod::MACE && + settings::QMSettings::getMaceModel() == settings::MaceModel::CUSTOM) + { + const auto modelPath = settings::QMSettings::getMaceModelPath(); + if (!isRemoteResource(modelPath)) + requireFile(modelPath, "MACE model file"); + } + + validateInstalledExternalQMScript(); + } + + void validateCompiledCapabilities() + { + if (PQ_BUILD_WITH_ASE || !settings::Settings::isQMActivated()) + return; + + const auto method = settings::QMSettings::getQMMethod(); + if (method == settings::QMMethod::ASEDFTBPLUS || + method == settings::QMMethod::ASEXTB || + method == settings::QMMethod::FENNOL || + method == settings::QMMethod::MACE) + throw customException::InputFileException( + std::format( + "QM method {} requires ASE support, but this PQ build " + "does not include it", + settings::string(method) + ) + ); + } + + void appendWarnings( + const input::InputFileReader &reader, + cli::ValidationResult &result + ) + { + using settings::ManostatSettings; + using settings::ManostatType; + using settings::ThermostatSettings; + using settings::ThermostatType; + + if (reader.getKeywordSet("mace_model_size")) + result.diagnostics.push_back( + {cli::ValidationSeverity::WARNING, + "\"mace_model_size\" is deprecated; use \"mace_model\"", + std::nullopt} + ); + + if (ThermostatSettings::getThermostatType() == + ThermostatType::NOSE_HOOVER && + ThermostatSettings::getNoseHooverCouplingFrequency() == 0.0) + result.diagnostics.push_back( + {cli::ValidationSeverity::WARNING, + "A zero Nose-Hoover coupling frequency disables thermostat " + "coupling", + std::nullopt} + ); + + if (ThermostatSettings::getThermostatType() == + ThermostatType::LANGEVIN && + ThermostatSettings::getFriction() == 0.0) + result.diagnostics.push_back( + {cli::ValidationSeverity::WARNING, + "A zero Langevin friction disables thermostat coupling", + std::nullopt} + ); + + if (ManostatSettings::getManostatType() != ManostatType::NONE && + ManostatSettings::getCompressibility() == 0.0) + result.diagnostics.push_back( + {cli::ValidationSeverity::WARNING, + "A zero compressibility disables cell response", + std::nullopt} + ); + } + + void writeJsonString(std::ostream &output, const std::string_view value) + { + output << '"'; + + for (const auto character : value) + { + switch (character) + { + case '"': output << "\\\""; break; + case '\\': output << "\\\\"; break; + case '\b': output << "\\b"; break; + case '\f': output << "\\f"; break; + case '\n': output << "\\n"; break; + case '\r': output << "\\r"; break; + case '\t': output << "\\t"; break; + default: + if (static_cast(character) < 0x20) + { + const auto flags = output.flags(); + const auto fill = output.fill(); + output << "\\u" << std::hex << std::setw(4) + << std::setfill('0') + << static_cast( + static_cast(character) + ); + output.flags(flags); + output.fill(fill); + } + else + output << character; + } + } + + output << '"'; + } + + std::string_view string(const cli::ValidationSeverity severity) + { + return severity == cli::ValidationSeverity::WARNING ? "warning" + : "error"; + } + + std::string_view string(const ValidationScope scope) + { + return scope == ValidationScope::PORTABLE ? "portable" : "installed"; + } +} // namespace + +/** + * @brief Parses and checks an input file without starting a simulation. + * + * @param inputFile + * @return ValidationResult + */ +cli::ValidationResult cli::validateInputFile( + const std::string_view inputFile, + const ValidationScope scope +) +{ + auto engine = std::unique_ptr(); + + std::ostringstream parserOutput; + ScopedStreamRedirect redirect(std::cout, parserOutput.rdbuf()); + + try + { + input::readJobType(std::string(inputFile), engine); + + input::InputFileReader reader( + inputFile, + *engine, + scope == ValidationScope::INSTALLED + ); + reader.read(); + reader.postProcess(); + reader.validateInputConfiguration(); + validateInputDependencies(*engine); + if (scope == ValidationScope::INSTALLED) + { + validateCompiledCapabilities(); + validateEffectiveFiles(*engine); + } + + auto result = ValidationResult{ + .valid = true, + .inputFile = std::string(inputFile), + .scope = scope + }; + appendWarnings(reader, result); + return result; + } + catch (const customException::CustomException &exception) + { + return invalidResult( + inputFile, + exception.getMessage(), + exception.getLineNumber(), + scope + ); + } +} + +/** + * @brief Writes a machine-readable validation result. + * + * @param result + * @param output + */ +void cli::writeValidationJson( + const ValidationResult &result, + std::ostream &output +) +{ + const auto flags = output.flags(); + + output << "{\n" + << " \"schema\": \"pq.validation\",\n" + << " \"schema_version\": 1,\n" + << " \"valid\": " << std::boolalpha << result.valid << ",\n" + << " \"input\": "; + writeJsonString(output, result.inputFile); + output << ",\n" + << " \"scope\": "; + writeJsonString(output, string(result.scope)); + output << ",\n" + << " \"diagnostics\": ["; + + if (result.diagnostics.empty()) + { + output << "]\n" + << "}\n"; + output.flags(flags); + return; + } + + for (size_t index = 0; index < result.diagnostics.size(); ++index) + { + const auto &diagnostic = result.diagnostics[index]; + output << (index == 0 ? "\n" : ",\n") << " {\n" + << " \"severity\": "; + writeJsonString(output, string(diagnostic.severity)); + output << ",\n" + << " \"message\": "; + writeJsonString(output, diagnostic.message); + output << ",\n" + << " \"file\": "; + writeJsonString(output, result.inputFile); + output << ",\n" + << " \"line\": "; + if (diagnostic.lineNumber.has_value()) + output << diagnostic.lineNumber.value(); + else + output << "null"; + output << "\n" + << " }"; + } + + output << '\n' + << " ]\n" + << "}\n"; + output.flags(flags); +} + +/** + * @brief Writes a concise validation result for interactive use. + * + * @param result + * @param output + * @param error + */ +void cli::writeValidationText( + const ValidationResult &result, + std::ostream &output, + std::ostream &error +) +{ + if (result.valid) + { + output << "Valid PQ input: " << result.inputFile << '\n'; + + for (const auto &diagnostic : result.diagnostics) + error << "Warning: " << diagnostic.message << '\n'; + + return; + } + + error << "Invalid PQ input: " << result.diagnostics.front().message; + if (result.diagnostics.front().lineNumber.has_value()) + { + const auto line = std::format( + "line {}", + result.diagnostics.front().lineNumber.value() + ); + if (!result.diagnostics.front().message.contains(line)) + error << " (" << line << ')'; + } + error << '\n'; +} diff --git a/apps/validation.hpp b/apps/validation.hpp new file mode 100644 index 000000000..d28af4da8 --- /dev/null +++ b/apps/validation.hpp @@ -0,0 +1,75 @@ +/***************************************************************************** + + + PQ + Copyright (C) 2023-now Jakob Gamper + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + +******************************************************************************/ + +#ifndef _CLI_VALIDATION_HPP_ + +#define _CLI_VALIDATION_HPP_ + +#include +#include +#include +#include +#include +#include + +#include "commandLineArgs.hpp" + +namespace cli +{ + enum class ValidationSeverity + { + WARNING, + ERROR + }; + + struct ValidationDiagnostic + { + ValidationSeverity severity = ValidationSeverity::ERROR; + std::string message; + std::optional lineNumber; + }; + + struct ValidationResult + { + bool valid = true; + std::string inputFile; + ValidationScope scope = ValidationScope::INSTALLED; + std::vector diagnostics; + }; + + [[nodiscard]] ValidationResult validateInputFile( + std::string_view inputFile, + ValidationScope scope = ValidationScope::INSTALLED + ); + + void writeValidationJson( + const ValidationResult &result, + std::ostream &output + ); + void writeValidationText( + const ValidationResult &result, + std::ostream &output, + std::ostream &error + ); +} // namespace cli + +#endif // _CLI_VALIDATION_HPP_ diff --git a/docs/sphinx/src/installation/installation.rst b/docs/sphinx/src/installation/installation.rst index 88958f6c6..1ace09c77 100644 --- a/docs/sphinx/src/installation/installation.rst +++ b/docs/sphinx/src/installation/installation.rst @@ -69,6 +69,9 @@ Common CMake options are listed below. Boolean options are set with ``On`` or * - ``BUILD_WITH_ASE`` - ``On`` - Build ASE-based QM runners and built-in Slater-Koster setup. + * - ``PQ_SLAKOS_SOURCE_DIR`` + - unset + - Use preseeded ``3ob`` and ``matsci`` directories instead of cloning them. * - ``BUILD_WITH_PYTHON_BINDINGS`` - ``Off`` - Build Python bindings. @@ -85,6 +88,14 @@ Example MPI build: $ cmake ../ -DCMAKE_BUILD_TYPE=Release -DBUILD_WITH_MPI=On +For a network-restricted ASE build, prepare a directory containing ``3ob`` and +``matsci`` checkouts, then configure with: + +.. code-block:: bash + + $ cmake ../ -DCMAKE_BUILD_TYPE=Release \ + -DPQ_SLAKOS_SOURCE_DIR=/path/to/slakos + .. _singularity: *********** diff --git a/docs/sphinx/src/userGuide/generalUsage.rst b/docs/sphinx/src/userGuide/generalUsage.rst index f91dbbeb7..236238712 100644 --- a/docs/sphinx/src/userGuide/generalUsage.rst +++ b/docs/sphinx/src/userGuide/generalUsage.rst @@ -17,3 +17,32 @@ model, output names and required setup files. For a first complete run, see :ref:`quickStart`. For the full input syntax and file formats, see :ref:`referenceManual`. + +Inspect and Validate +******************** + +Use the command-line metadata to check the installed PQ build: + +.. code-block:: bash + + $ PQ --version + $ PQ --capabilities=json + +Before starting a simulation, validate the input from its working directory: + +.. code-block:: bash + + $ PQ --validate PQ.in + $ PQ --validate PQ.in --format=json + $ PQ --validate PQ.in --scope=portable --format=json + +The default ``installed`` scope checks input syntax, setting dependencies, +compiled capabilities and required files. The ``portable`` scope checks syntax +and setting dependencies without requiring the target build or local files. +This is useful for input generators and setup packages. Validation does not +start a simulation or create output. Referenced file contents are checked when +the simulation starts. + +Exit status ``0`` means the input is valid, including inputs with warnings. +Status ``1`` means the input is invalid. Operational validation failures use +status ``2``. diff --git a/docs/sphinx/src/userGuide/inputFile.rst b/docs/sphinx/src/userGuide/inputFile.rst index 30e04b017..2a9d3570c 100644 --- a/docs/sphinx/src/userGuide/inputFile.rst +++ b/docs/sphinx/src/userGuide/inputFile.rst @@ -864,7 +864,7 @@ This keyword is used in combination with the Berendsen and velocity rescaling th With the ``t_relaxation`` keyword the relaxation time in ``ps`` (*i.e.* :math:`\tau`) of the Berendsen or stochastic velocity rescaling thermostat is set, see equations :eq:`BerendsenThermostatEquation` and :eq:`BussiDonadioParrinelloThermostatEquation`. -.. centered:: *default value* = 0.1 ps +.. centered:: *default value* = 1.0 ps .. _frictionKey: @@ -959,7 +959,7 @@ This keyword is used in combination with the Berendsen and stochastic cell resca .. admonition:: Key :class: tip - p_relaxation = {double} ps -> 0.1 ps + p_relaxation = {double} ps -> 1.0 ps With the ``p_relaxation`` keyword the relaxation time in ``ps`` (*i.e.* :math:`\tau`) of the Berendsen or stochastic cell rescaling manostat is set. @@ -1586,6 +1586,7 @@ ASE-DFTB+ Approach slakos = {string} With the ``slakos`` keyword the user can specify the type of the ``ase-dftbplus`` approach for DFTB+ calculations. +It is required when ``qm_prog = ase-dftbplus``. Possible options are: diff --git a/external/progressbar/include/progressbar.hpp b/external/progressbar/include/progressbar.hpp index 50ab6d089..eb154c66c 100644 --- a/external/progressbar/include/progressbar.hpp +++ b/external/progressbar/include/progressbar.hpp @@ -145,7 +145,7 @@ inline void progressbar::update() { int perc = 0; // compute percentage, if did not change, do nothing and return - perc = progress*100./(n_cycles-1); + perc = n_cycles == 1 ? 100 : progress*100./(n_cycles-1); if (perc < last_perc) return; // update percentage each unit diff --git a/include/QM/external/externalQMRunner.hpp b/include/QM/external/externalQMRunner.hpp index 0eb139e4a..8fdde2082 100644 --- a/include/QM/external/externalQMRunner.hpp +++ b/include/QM/external/externalQMRunner.hpp @@ -24,11 +24,16 @@ #define _EXTERNAL_QM_RUNNER_HPP_ +#include +#include + #include "qmRunner.hpp" #include "typeAliases.hpp" namespace QM { + [[nodiscard]] std::string bundledQMScriptPath(std::string_view script); + /** * @brief ExternalQMRunner inherits from QMRunner * @@ -40,6 +45,14 @@ namespace QM const std::string _singularity = SINGULARITY_; const std::string _staticBuild = STATIC_BUILD_; + [[nodiscard]] std::string resolveScriptPath( + std::string_view script + ) const; + void executeCommand( + std::string_view command, + std::string_view program + ) const; + public: ExternalQMRunner() = default; ~ExternalQMRunner() override = default; @@ -65,4 +78,4 @@ namespace QM }; } // namespace QM -#endif // _EXTERNAL_QM_RUNNER_HPP_ \ No newline at end of file +#endif // _EXTERNAL_QM_RUNNER_HPP_ diff --git a/include/engine/optEngine.hpp b/include/engine/optEngine.hpp index 8fbaebe2a..2319fa2b5 100644 --- a/include/engine/optEngine.hpp +++ b/include/engine/optEngine.hpp @@ -79,7 +79,7 @@ namespace engine [[nodiscard]] std::shared_ptr getSharedOptimizer(); [[nodiscard]] std::shared_ptr getSharedLearningRate(); [[nodiscard]] std::shared_ptr getSharedEvaluator(); - + [[nodiscard]] physicalData::PhysicalData &getPhysicalDataOld(); [[nodiscard]] std::shared_ptr getSharedPhysicalDataOld(); // clang-format on @@ -89,4 +89,4 @@ namespace engine } // namespace engine -#endif // _OPT_ENGINE_HPP_ \ No newline at end of file +#endif // _OPT_ENGINE_HPP_ diff --git a/include/exceptions/exceptions.hpp b/include/exceptions/exceptions.hpp index cea4831a1..8683d7e2c 100644 --- a/include/exceptions/exceptions.hpp +++ b/include/exceptions/exceptions.hpp @@ -26,6 +26,7 @@ #include #include +#include #include #include @@ -73,12 +74,19 @@ namespace customException class CustomException : public std::exception { protected: - std::string _message; + std::string _message; + std::optional _lineNumber; public: - explicit CustomException(const std::string_view message); + explicit CustomException( + const std::string_view message, + std::optional lineNumber = std::nullopt + ); void colorfulOutput(const Color::Code, const std::string_view) const; + void setLineNumber(const size_t lineNumber) noexcept; + [[nodiscard]] const std::string &getMessage() const noexcept; + [[nodiscard]] std::optional getLineNumber() const noexcept; }; /** @@ -377,4 +385,4 @@ namespace customException } // namespace customException -#endif // _EXCEPTIONS_HPP_ \ No newline at end of file +#endif // _EXCEPTIONS_HPP_ diff --git a/include/input/commandLineArgs.hpp b/include/input/commandLineArgs.hpp index 314d61f95..71f5b1a53 100644 --- a/include/input/commandLineArgs.hpp +++ b/include/input/commandLineArgs.hpp @@ -27,6 +27,27 @@ #include #include +enum class CommandLineAction +{ + RUN, + HELP, + VERSION, + CAPABILITIES, + VALIDATE +}; + +enum class CommandLineFormat +{ + TEXT, + JSON +}; + +enum class ValidationScope +{ + INSTALLED, + PORTABLE +}; + /** * @class CommandLineArgs * @@ -39,13 +60,19 @@ class CommandLineArgs int _argc; std::vector _argv; std::string _inputFileName; + CommandLineAction _action = CommandLineAction::RUN; + CommandLineFormat _format = CommandLineFormat::TEXT; + ValidationScope _validationScope = ValidationScope::INSTALLED; public: CommandLineArgs(const int argc, const std::vector &argv); - void detectFlags(); + void parse(); - std::string getInputFileName() const; + std::string getInputFileName() const; + CommandLineAction getAction() const; + CommandLineFormat getFormat() const; + ValidationScope getValidationScope() const; }; -#endif // _COMMAND_LINE_ARGS_HPP_ \ No newline at end of file +#endif // _COMMAND_LINE_ARGS_HPP_ diff --git a/include/input/inputFileParser/filesInputParser.hpp b/include/input/inputFileParser/filesInputParser.hpp index 1d0db4720..22bd43cea 100644 --- a/include/input/inputFileParser/filesInputParser.hpp +++ b/include/input/inputFileParser/filesInputParser.hpp @@ -39,8 +39,11 @@ namespace input */ class FilesInputParser : public InputFileParser { + private: + bool _validateFilePaths; + public: - explicit FilesInputParser(pq::Engine &); + explicit FilesInputParser(pq::Engine &, bool validateFilePaths = true); void parseIntraNonBondedFile(const pq::strings &, const size_t); void parseTopologyFilename(const pq::strings &, const size_t); @@ -56,4 +59,4 @@ namespace input } // namespace input -#endif // _FILES_INPUT_PARSER_HPP_ \ No newline at end of file +#endif // _FILES_INPUT_PARSER_HPP_ diff --git a/include/input/inputFileParser/generalInputParser.hpp b/include/input/inputFileParser/generalInputParser.hpp index 3ed1028d5..8c0bd0d58 100644 --- a/include/input/inputFileParser/generalInputParser.hpp +++ b/include/input/inputFileParser/generalInputParser.hpp @@ -47,9 +47,13 @@ namespace input void parseFloatingPointType(const pq::strings &, const size_t); void parseRandomSeed(const pq::strings &, const size_t); - void parseJobTypeForEngine(const pq::strings &, const size_t, pq::UniqueEngine &); + static void parseJobTypeForEngine( + const pq::strings &, + const size_t, + pq::UniqueEngine & + ); }; } // namespace input -#endif // _GENERAL_INPUT_PARSER_HPP_ \ No newline at end of file +#endif // _GENERAL_INPUT_PARSER_HPP_ diff --git a/include/input/inputFileReader.hpp b/include/input/inputFileReader.hpp index 74b04094d..48a5f6dcf 100644 --- a/include/input/inputFileReader.hpp +++ b/include/input/inputFileReader.hpp @@ -74,7 +74,11 @@ namespace input size_t _lineNumber = 1; public: - explicit InputFileReader(const std::string_view &, engine::Engine &); + explicit InputFileReader( + const std::string_view &, + engine::Engine &, + bool validateFilePaths = true + ); void read(); void addKeywords(); @@ -110,9 +114,11 @@ namespace input ******************************/ void validateTimings() const; + void validateOptimizer() const; void validateQM() const; void validateThermostat() const; void validateManostat() const; + void validateCellList() const; void validateReactionFieldCoulomb() const; void validateRingPolymer() const; }; diff --git a/include/input/parameterFileReader/parameterFileReader.hpp b/include/input/parameterFileReader/parameterFileReader.hpp index 257626e09..bab66a76e 100644 --- a/include/input/parameterFileReader/parameterFileReader.hpp +++ b/include/input/parameterFileReader/parameterFileReader.hpp @@ -52,6 +52,7 @@ namespace input::parameterFile public: ParameterFileReader(const std::string &filename, pq::Engine &engine); + ~ParameterFileReader(); void read(); void deleteSection(const pq::ParamFileSection *section); @@ -72,4 +73,4 @@ namespace input::parameterFile } // namespace input::parameterFile -#endif // _PARAMETER_FILE_READER_HPP_ \ No newline at end of file +#endif // _PARAMETER_FILE_READER_HPP_ diff --git a/include/input/restartFileReader/atomSection.hpp b/include/input/restartFileReader/atomSection.hpp index 0b71d4264..0fcfb8915 100644 --- a/include/input/restartFileReader/atomSection.hpp +++ b/include/input/restartFileReader/atomSection.hpp @@ -29,12 +29,8 @@ #include "restartFileSection.hpp" // for RestartFileSection #include "typeAliases.hpp" // for strings -#ifdef WITH_TESTS -#include // for FRIEND_TEST - class TestAtomSection_testProcessAtomLine_Test; // Friend test class class TestAtomSection_testProcessQMAtomLine_Test; // Friend test class -#endif namespace input::restartFile { @@ -54,8 +50,8 @@ namespace input::restartFile void setAtomPropertyVectors(pq::strings &, pq::SharedAtom &) const; #ifdef WITH_TESTS - FRIEND_TEST(::TestAtomSection, testProcessAtomLine); - FRIEND_TEST(::TestAtomSection, testProcessQMAtomLine); + friend class ::TestAtomSection_testProcessAtomLine_Test; + friend class ::TestAtomSection_testProcessQMAtomLine_Test; #endif public: diff --git a/include/output/output.hpp b/include/output/output.hpp index ec3522de8..2b92696cf 100644 --- a/include/output/output.hpp +++ b/include/output/output.hpp @@ -28,10 +28,6 @@ #include // for string #include // for string_view -#ifdef WITH_TESTS -#include // for FRIEND_TEST -#endif - class TestOutput_testSpecialSetFilename_Test; // Friend test class namespace output @@ -58,7 +54,7 @@ namespace output void close(); #ifdef WITH_TESTS - FRIEND_TEST(::TestOutput, testSpecialSetFilename); + friend class ::TestOutput_testSpecialSetFilename_Test; #endif /*************************** @@ -70,4 +66,4 @@ namespace output } // namespace output -#endif // _OUTPUT_HPP_ \ No newline at end of file +#endif // _OUTPUT_HPP_ diff --git a/include/output/references/referenceFiles/3ob.ref b/include/output/references/referenceFiles/3ob.ref new file mode 100644 index 000000000..a8235dec9 --- /dev/null +++ b/include/output/references/referenceFiles/3ob.ref @@ -0,0 +1,26 @@ +3ob Slater-Koster Parameter Set (3ob-3-1) + © 2017 Marcus Elstner, Karlsruhe Institute of Technology. + Licensed under CC BY-SA 4.0. + + M. Gaus, A. Goez, and M. Elstner + Parametrization and Benchmark of DFTB3 for Organic Molecules. + J. Chem. Theory Comput. 2013, 9, 338-354. + https://doi.org/10.1021/ct300849w + + M. Gaus, X. Lu, M. Elstner, and Q. Cui + Parameterization of DFTB3/3OB for Sulfur and Phosphorus for + Chemical and Biological Applications. + J. Chem. Theory Comput. 2014, 10, 1518-1537. + https://doi.org/10.1021/ct401002w + + X. Lu, M. Gaus, M. Elstner, and Q. Cui + Parametrization of DFTB3/3OB for Magnesium and Zinc for + Chemical and Biological Applications. + J. Phys. Chem. B 2015, 119, 1062-1082. + https://doi.org/10.1021/jp506557r + + M. Kubillus, T. Kubař, M. Gaus, J. Řezáč, and M. Elstner + Parameterization of the DFTB3 Method for Br, Ca, Cl, F, I, K, + and Na in Organic and Biological Systems. + J. Chem. Theory Comput. 2015, 11, 332-342. + https://doi.org/10.1021/ct5009137 diff --git a/include/output/references/referenceFiles/3ob.ref.bib b/include/output/references/referenceFiles/3ob.ref.bib new file mode 100644 index 000000000..e3813a848 --- /dev/null +++ b/include/output/references/referenceFiles/3ob.ref.bib @@ -0,0 +1,51 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% 3ob Slater-Koster parameters % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +@article{Gaus2013ThreeOB, + author = {Gaus, Michael and Goez, Albrecht and Elstner, Marcus}, + title = {Parametrization and Benchmark of DFTB3 for Organic Molecules}, + journal = {Journal of Chemical Theory and Computation}, + volume = {9}, + number = {1}, + pages = {338-354}, + year = {2013}, + doi = {10.1021/ct300849w}, + url = {https://doi.org/10.1021/ct300849w}, +} + +@article{Gaus2014ThreeOB, + author = {Gaus, Michael and Lu, Xiya and Elstner, Marcus and Cui, Qiang}, + title = {Parameterization of DFTB3/3OB for Sulfur and Phosphorus for Chemical and Biological Applications}, + journal = {Journal of Chemical Theory and Computation}, + volume = {10}, + number = {4}, + pages = {1518-1537}, + year = {2014}, + doi = {10.1021/ct401002w}, + url = {https://doi.org/10.1021/ct401002w}, +} + +@article{Lu2015ThreeOB, + author = {Lu, Xiya and Gaus, Michael and Elstner, Marcus and Cui, Qiang}, + title = {Parametrization of DFTB3/3OB for Magnesium and Zinc for Chemical and Biological Applications}, + journal = {The Journal of Physical Chemistry B}, + volume = {119}, + number = {3}, + pages = {1062-1082}, + year = {2015}, + doi = {10.1021/jp506557r}, + url = {https://doi.org/10.1021/jp506557r}, +} + +@article{Kubillus2015ThreeOB, + author = {Kubillus, Maximilian and Kubař, Tomáš and Gaus, Michael and Řezáč, Jan and Elstner, Marcus}, + title = {Parameterization of the DFTB3 Method for Br, Ca, Cl, F, I, K, and Na in Organic and Biological Systems}, + journal = {Journal of Chemical Theory and Computation}, + volume = {11}, + number = {1}, + pages = {332-342}, + year = {2015}, + doi = {10.1021/ct5009137}, + url = {https://doi.org/10.1021/ct5009137}, +} diff --git a/include/output/references/referenceFiles/matsci.ref b/include/output/references/referenceFiles/matsci.ref new file mode 100644 index 000000000..2ae8b354b --- /dev/null +++ b/include/output/references/referenceFiles/matsci.ref @@ -0,0 +1,40 @@ +matsci Slater-Koster Parameter Set (matsci-0-3) + © 2016 Gotthard Seifert, TU Dresden. + Licensed under CC BY-SA 4.0. + + J. Frenzel, A. F. Oliveira, N. Jardillier, T. Heine, and G. Seifert + Semi-relativistic, self-consistent charge Slater-Koster tables for + density-functional based tight-binding (DFTB) for materials science + simulations. + TU Dresden, 2004-2009. + + J. Frenzel, A. F. Oliveira, H. A. Duarte, T. Heine, and G. Seifert + Structural and Electronic Properties of Bulk Gibbsite and Gibbsite + Surfaces. Z. Anorg. Allg. Chem. 2005, 631, 1267-1271. + https://doi.org/10.1002/zaac.200500051 + + L. Guimarães, A. N. Enyashin, J. Frenzel, T. Heine, H. A. Duarte, + and G. Seifert + Imogolite Nanotubes: Stability, Electronic, and Mechanical Properties. + ACS Nano 2007, 1, 362-368. + https://doi.org/10.1021/nn700184k + + R. Luschtinetz, A. F. Oliveira, J. Frenzel, J.-O. Joswig, + G. Seifert, and H. A. Duarte + Adsorption of phosphonic and ethylphosphonic acid on aluminum oxide + surfaces. Surf. Sci. 2008, 602, 1347-1359. + https://doi.org/10.1016/j.susc.2008.01.035 + + R. Luschtinetz, J. Frenzel, T. Milek, and G. Seifert + Adsorption of Phosphonic Acid at the TiO2 Anatase (101) and + Rutile (110) Surfaces. J. Phys. Chem. C 2009, 113, 5730-5740. + https://doi.org/10.1021/jp8110343 + + S. Gemming, A. N. Enyashin, J. Frenzel, and G. Seifert + Adsorption of nucleotides on the rutile (110) surface. + Int. J. Mater. Res. 2010, 101, 758-764. + https://doi.org/10.3139/146.110337 + + N. Jardillier + PhD thesis, Université Montpellier II, 2006. + http://nicolas.jardillier.free.fr diff --git a/include/output/references/referenceFiles/matsci.ref.bib b/include/output/references/referenceFiles/matsci.ref.bib new file mode 100644 index 000000000..63ab9c505 --- /dev/null +++ b/include/output/references/referenceFiles/matsci.ref.bib @@ -0,0 +1,78 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matsci Slater-Koster parameters % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +@misc{Frenzel2009Matsci, + author = {Frenzel, Johannes and Oliveira, Augusto F. and Jardillier, Nicolas and Heine, Thomas and Seifert, Gotthard}, + title = {Semi-relativistic, self-consistent charge Slater-Koster tables for density-functional based tight-binding (DFTB) for materials science simulations}, + institution = {TU Dresden}, + year = {2009}, +} + +@article{Frenzel2005Matsci, + author = {Frenzel, Johannes and Oliveira, Augusto F. and Duarte, Helio A. and Heine, Thomas and Seifert, Gotthard}, + title = {Structural and Electronic Properties of Bulk Gibbsite and Gibbsite Surfaces}, + journal = {Zeitschrift für anorganische und allgemeine Chemie}, + volume = {631}, + number = {6-7}, + pages = {1267-1271}, + year = {2005}, + doi = {10.1002/zaac.200500051}, + url = {https://doi.org/10.1002/zaac.200500051}, +} + +@article{Guimaraes2007Matsci, + author = {Guimarães, Luciana and Enyashin, Andrey N. and Frenzel, Johannes and Heine, Thomas and Duarte, Hélio A. and Seifert, Gotthard}, + title = {Imogolite Nanotubes: Stability, Electronic, and Mechanical Properties}, + journal = {ACS Nano}, + volume = {1}, + number = {4}, + pages = {362-368}, + year = {2007}, + doi = {10.1021/nn700184k}, + url = {https://doi.org/10.1021/nn700184k}, +} + +@article{Luschtinetz2008Matsci, + author = {Luschtinetz, Regina and Oliveira, Augusto F. and Frenzel, Johannes and Joswig, Jan-Ole and Seifert, Gotthard and Duarte, Helio A.}, + title = {Adsorption of phosphonic and ethylphosphonic acid on aluminum oxide surfaces}, + journal = {Surface Science}, + volume = {602}, + number = {7}, + pages = {1347-1359}, + year = {2008}, + doi = {10.1016/j.susc.2008.01.035}, + url = {https://doi.org/10.1016/j.susc.2008.01.035}, +} + +@article{Luschtinetz2009Matsci, + author = {Luschtinetz, Regina and Frenzel, Johannes and Milek, Theodor and Seifert, Gotthard}, + title = {Adsorption of Phosphonic Acid at the TiO2 Anatase (101) and Rutile (110) Surfaces}, + journal = {The Journal of Physical Chemistry C}, + volume = {113}, + number = {14}, + pages = {5730-5740}, + year = {2009}, + doi = {10.1021/jp8110343}, + url = {https://doi.org/10.1021/jp8110343}, +} + +@article{Gemming2010Matsci, + author = {Gemming, Sibylle and Enyashin, Andrey N. and Frenzel, Johannes and Seifert, Gotthard}, + title = {Adsorption of nucleotides on the rutile (110) surface}, + journal = {International Journal of Materials Research}, + volume = {101}, + number = {6}, + pages = {758-764}, + year = {2010}, + doi = {10.3139/146.110337}, + url = {https://doi.org/10.3139/146.110337}, +} + +@phdthesis{Jardillier2006Matsci, + author = {Jardillier, Nicolas}, + title = {PhD thesis}, + school = {Université Montpellier II}, + year = {2006}, + url = {http://nicolas.jardillier.free.fr}, +} diff --git a/include/output/references/references.hpp b/include/output/references/references.hpp index 7f4fe5dd8..e97c3b71e 100644 --- a/include/output/references/references.hpp +++ b/include/output/references/references.hpp @@ -42,6 +42,8 @@ namespace references // QM Programs static constexpr char _DFTBPLUS_FILE_[] = "dftbplus.ref"; + static constexpr char _THREEOB_FILE_[] = "3ob.ref"; + static constexpr char _MATSCI_FILE_[] = "matsci.ref"; static constexpr char _GFN1_FILE_[] = "gfn1.ref"; static constexpr char _GFN2_FILE_[] = "gfn2.ref"; static constexpr char _IPEA1_FILE_[] = "ipea1.ref"; @@ -58,4 +60,4 @@ namespace references } // namespace references -#endif // _REFERENCES_HPP_ \ No newline at end of file +#endif // _REFERENCES_HPP_ diff --git a/include/output/references/referencesOutput.hpp b/include/output/references/referencesOutput.hpp index 852cadb76..694d98f18 100644 --- a/include/output/references/referencesOutput.hpp +++ b/include/output/references/referencesOutput.hpp @@ -41,8 +41,6 @@ namespace references class ReferencesOutput { private: - static inline std::string _referenceFilesPath = REFERENCES_PATH_; - static inline pq::stringSet _referenceFileNames = pq::stringSet(); static inline pq::stringSet _bibtexFileNames = pq::stringSet(); @@ -54,4 +52,4 @@ namespace references } // namespace references -#endif // _REFERENCES_OUTPUT_HPP_ \ No newline at end of file +#endif // _REFERENCES_OUTPUT_HPP_ diff --git a/include/settings/optimizerSettings.hpp b/include/settings/optimizerSettings.hpp index f4c89b35c..8e7b7721c 100644 --- a/include/settings/optimizerSettings.hpp +++ b/include/settings/optimizerSettings.hpp @@ -103,6 +103,13 @@ namespace settings static void setMaxLearningRate(const double); static void setMinLearningRate(const double); + /****************************** + * validation helper methods * + ******************************/ + + static void validateLearningRateStrategy(); + static void validateLearningRateBounds(); + /*************************** * standard getter methods * ***************************/ diff --git a/include/utilities/executablePath.hpp b/include/utilities/executablePath.hpp new file mode 100644 index 000000000..12540d962 --- /dev/null +++ b/include/utilities/executablePath.hpp @@ -0,0 +1,34 @@ +/***************************************************************************** + + + PQ + Copyright (C) 2023-now Jakob Gamper + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + +******************************************************************************/ + +#ifndef _EXECUTABLE_PATH_HPP_ + +#define _EXECUTABLE_PATH_HPP_ + +#include + +namespace utilities +{ + [[nodiscard]] std::filesystem::path executablePath(); +} // namespace utilities + +#endif // _EXECUTABLE_PATH_HPP_ diff --git a/include/utilities/stringUtilities.hpp b/include/utilities/stringUtilities.hpp index 1a009e767..dc4e30b07 100644 --- a/include/utilities/stringUtilities.hpp +++ b/include/utilities/stringUtilities.hpp @@ -46,10 +46,12 @@ namespace utilities std::string toLowerAndReplaceDashesCopy(std::string); std::string toLowerAndReplaceDashesCopy(std::string_view); std::string firstLetterToUpperCaseCopy(std::string); + std::string shellQuote(std::string_view); void addSpaces(std::string &, const std::string &, const size_t); std::uint_fast32_t stringToUintFast32t(const std::string &); + int stringToInt(const std::string &); double stringToFiniteDouble(const std::string &); bool fileExists(const std::string &); @@ -57,4 +59,4 @@ namespace utilities } // namespace utilities -#endif // _STRING_UTILITIES_HPP_ \ No newline at end of file +#endif // _STRING_UTILITIES_HPP_ diff --git a/src/QM/CMakeLists.txt b/src/QM/CMakeLists.txt index 5b6da0368..60f250056 100644 --- a/src/QM/CMakeLists.txt +++ b/src/QM/CMakeLists.txt @@ -24,7 +24,7 @@ else() endif() install(TARGETS QM - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) add_subdirectory(external) diff --git a/src/QM/ase/CMakeLists.txt b/src/QM/ase/CMakeLists.txt index af87f780f..016becc03 100644 --- a/src/QM/ase/CMakeLists.txt +++ b/src/QM/ase/CMakeLists.txt @@ -35,5 +35,5 @@ target_include_directories(aseQM ) install(TARGETS aseQM - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/QM/external/CMakeLists.txt b/src/QM/external/CMakeLists.txt index 98405e601..92f240505 100644 --- a/src/QM/external/CMakeLists.txt +++ b/src/QM/external/CMakeLists.txt @@ -43,5 +43,11 @@ add_custom_command(TARGET externalQM POST_BUILD ) install(TARGETS externalQM - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ -) \ No newline at end of file + DESTINATION lib +) + +install( + DIRECTORY ${CMAKE_SOURCE_DIR}/src/QM/scripts/ + DESTINATION share/PQ/scripts + USE_SOURCE_PERMISSIONS +) diff --git a/src/QM/external/dftbplusRunner.cpp b/src/QM/external/dftbplusRunner.cpp index ec0f0b8c1..e85ad5362 100644 --- a/src/QM/external/dftbplusRunner.cpp +++ b/src/QM/external/dftbplusRunner.cpp @@ -22,8 +22,8 @@ #include "dftbplusRunner.hpp" +#include // for isfinite #include // for size_t -#include // for system #include // for format #include // for ofstream #include // for string @@ -132,7 +132,7 @@ void DFTBPlusRunner::writeCoordsFile(SimulationBox &box) */ void DFTBPlusRunner::execute() { - const auto scriptFile = _scriptPath + QMSettings::getQMScript(); + const auto scriptFile = resolveScriptPath(QMSettings::getQMScript()); if (!fileExists(scriptFile)) throw InputFileException( @@ -143,11 +143,11 @@ void DFTBPlusRunner::execute() const auto command = std::format( "{} 0 {} 0 0 0 {}", - scriptFile, + shellQuote(scriptFile), reuseCharges, - FileSettings::getDFTBFileName() + shellQuote(FileSettings::getDFTBFileName()) ); - ::system(command.c_str()); + executeCommand(command, "DFTB+"); _isFirstExecution = false; } @@ -178,9 +178,27 @@ void DFTBPlusRunner::readStressTensor(Box &box, PhysicalData &data) StaticMatrix3x3 stress; - stressFile >> stress[0][0] >> stress[0][1] >> stress[0][2]; - stressFile >> stress[1][0] >> stress[1][1] >> stress[1][2]; - stressFile >> stress[2][0] >> stress[2][1] >> stress[2][2]; + if (!(stressFile >> stress[0][0] >> stress[0][1] >> stress[0][2] >> + stress[1][0] >> stress[1][1] >> stress[1][2] >> stress[2][0] >> + stress[2][1] >> stress[2][2])) + throw QMRunnerException( + std::format( + "Incomplete {} stress tensor \"{}\"", + string(QMSettings::getQMMethod()), + stressFileName + ) + ); + + for (size_t row = 0; row < 3; ++row) + for (size_t column = 0; column < 3; ++column) + if (!std::isfinite(stress[row][column])) + throw QMRunnerException( + std::format( + "Invalid value in {} stress tensor \"{}\"", + string(QMSettings::getQMMethod()), + stressFileName + ) + ); const auto conversion = _HARTREE_PER_BOHR3_TO_KCAL_PER_MOL_PER_ANGSTROM3_; stress = stress * conversion; @@ -190,4 +208,4 @@ void DFTBPlusRunner::readStressTensor(Box &box, PhysicalData &data) data.addVirial(virial); stressFile.close(); -} \ No newline at end of file +} diff --git a/src/QM/external/externalQMRunner.cpp b/src/QM/external/externalQMRunner.cpp index a6f9cf526..f9289f5d0 100644 --- a/src/QM/external/externalQMRunner.cpp +++ b/src/QM/external/externalQMRunner.cpp @@ -23,7 +23,10 @@ #include "externalQMRunner.hpp" #include // for __for_each_fn, for_each +#include // for array #include // for isnan, isinf +#include // for system +#include // for is_regular_file, path #include // for format #include // for ofstream #include // for string @@ -31,6 +34,7 @@ #include "constants/conversionFactors.hpp" // for _HARTREE_PER_BOHR_TO_KCAL_PER_MOL_PER_ANGSTROM_, _HARTREE_TO_KCAL_PER_MOL_ #include "exceptions.hpp" // for InputFileException +#include "executablePath.hpp" // for executablePath #include "fileSettings.hpp" // for FileSettings #include "physicalData.hpp" // for PhysicalData #include "qmSettings.hpp" // for QMSettings @@ -43,6 +47,21 @@ using namespace customException; using namespace settings; using namespace constants; +std::string QM::bundledQMScriptPath(const std::string_view script) +{ + const auto executable = utilities::executablePath(); + if (!executable.empty()) + { + const auto installedPath = executable.parent_path().parent_path() / + "share" / "PQ" / "scripts" / script; + if (std::filesystem::is_regular_file(installedPath)) + return installedPath.string(); + } + + const auto buildPath = std::filesystem::path(SCRIPT_PATH_) / script; + return buildPath.string(); +} + /** * @brief run the qm engine * @@ -52,6 +71,13 @@ void ExternalQMRunner::run(SimulationBox &simBox, PhysicalData &physicalData) { writeCoordsFile(simBox); + const auto resultFiles = std::array{ + FileSettings::getQMForcesTempFileName(), + FileSettings::getQMChargesTempFileName(), + FileSettings::getStressTensorTempFileName() + }; + for (const auto &file : resultFiles) std::filesystem::remove(file); + std::jthread timeoutThread{[this](const std::stop_token stopToken) { throwAfterTimeout(stopToken); }}; @@ -66,6 +92,31 @@ void ExternalQMRunner::run(SimulationBox &simBox, PhysicalData &physicalData) readStressTensor(simBox.getBox(), physicalData); } +std::string ExternalQMRunner::resolveScriptPath( + const std::string_view script +) const +{ + if (_scriptPath.empty()) + return std::string(script); + + if (_scriptPath == SCRIPT_PATH_) + return bundledQMScriptPath(script); + + return _scriptPath + std::string(script); +} + +void ExternalQMRunner::executeCommand( + const std::string_view command, + const std::string_view program +) const +{ + const auto status = std::system(std::string(command).c_str()); + if (status != EXIT_SUCCESS) + throw QMRunnerException( + std::format("{} command failed with status {}", program, status) + ); +} + /** * @brief reads the force file (including qm energy) and sets the forces of the * atoms @@ -106,14 +157,23 @@ void ExternalQMRunner::readForceFile( double energy = 0.0; - forceFile >> energy; + if (!(forceFile >> energy)) + throw QMRunnerException( + std::format( + "Cannot read QM energy from {} force file \"{}\"", + string(QMSettings::getQMMethod()), + forceFileName + ) + ); - if (std::isnan(energy) || std::isinf(energy)) - throw QMRunnerException(std::format( - "Invalid QM energy (NaN/Inf) in {} force file \"{}\"", - string(QMSettings::getQMMethod()), - forceFileName - )); + if (!std::isfinite(energy)) + throw QMRunnerException( + std::format( + "Invalid QM energy (NaN/Inf) in {} force file \"{}\"", + string(QMSettings::getQMMethod()), + forceFileName + ) + ); physicalData.setQMEnergy(energy * _HARTREE_TO_KCAL_PER_MOL_); @@ -121,16 +181,25 @@ void ExternalQMRunner::readForceFile( { auto grad = linearAlgebra::Vec3D(); - forceFile >> grad[0] >> grad[1] >> grad[2]; - - for (size_t i = 0; i < 3; ++i) - if (std::isnan(grad[i]) || std::isinf(grad[i])) - throw QMRunnerException(std::format( - "Invalid QM force component (NaN/Inf) in {} force file " - "\"{}\"", + if (!(forceFile >> grad[0] >> grad[1] >> grad[2])) + throw QMRunnerException( + std::format( + "Incomplete {} force file \"{}\"", string(QMSettings::getQMMethod()), forceFileName - )); + ) + ); + + for (size_t i = 0; i < 3; ++i) + if (!std::isfinite(grad[i])) + throw QMRunnerException( + std::format( + "Invalid QM force component (NaN/Inf) in {} force file " + "\"{}\"", + string(QMSettings::getQMMethod()), + forceFileName + ) + ); atom->setForce(-grad * _HARTREE_PER_BOHR_TO_KCAL_PER_MOL_PER_ANGSTROM_); }; @@ -178,12 +247,27 @@ void ExternalQMRunner::readChargeFile(SimulationBox &box) box.resetQMCharges(); - auto readCharges = [&chargeFile](auto &atom) + auto readCharges = [&chargeFile, &chargeFileName](auto &atom) { auto index = 0; // Read and discard the first column (index) auto charge = 0.0; // Read the second column (charge value) - chargeFile >> index >> charge; + if (!(chargeFile >> index >> charge)) + throw QMRunnerException( + std::format( + "Incomplete {} charge file \"{}\"", + string(QMSettings::getQMMethod()), + chargeFileName + ) + ); + if (!std::isfinite(charge)) + throw QMRunnerException( + std::format( + "Invalid value in {} charge file \"{}\"", + string(QMSettings::getQMMethod()), + chargeFileName + ) + ); atom->setQMCharge(charge); }; @@ -237,4 +321,4 @@ const std::string &ExternalQMRunner::getStaticBuild() const void ExternalQMRunner::setScriptPath(const std::string_view &scriptPath) { _scriptPath = scriptPath; -} \ No newline at end of file +} diff --git a/src/QM/external/pyscfRunner.cpp b/src/QM/external/pyscfRunner.cpp index 4edfe4c2f..848086dad 100644 --- a/src/QM/external/pyscfRunner.cpp +++ b/src/QM/external/pyscfRunner.cpp @@ -22,8 +22,7 @@ #include "pyscfRunner.hpp" -#include // for system, size_t - +#include // for size_t #include // for format #include // for ofstream, operator<<, basic_ostream #include // for allocator, string, operator+, operator<< @@ -76,15 +75,18 @@ void PySCFRunner::writeCoordsFile(SimulationBox &box) */ void PySCFRunner::execute() { - const auto scriptFileName = _scriptPath + QMSettings::getQMScript(); + const auto scriptFileName = resolveScriptPath(QMSettings::getQMScript()); if (!fileExists(scriptFileName)) - throw InputFileException(std::format( - "PySCF script file \"{}\" does not exist.", - scriptFileName - )); + throw InputFileException( + std::format( + "PySCF script file \"{}\" does not exist.", + scriptFileName + ) + ); - const auto command = std::format("python {} > pyscf.out", scriptFileName); + const auto command = + std::format("python {} > pyscf.out", shellQuote(scriptFileName)); - ::system(command.c_str()); -} \ No newline at end of file + executeCommand(command, "PySCF"); +} diff --git a/src/QM/external/turbomoleRunner.cpp b/src/QM/external/turbomoleRunner.cpp index f0c6ae21f..5ced16da9 100644 --- a/src/QM/external/turbomoleRunner.cpp +++ b/src/QM/external/turbomoleRunner.cpp @@ -22,11 +22,10 @@ #include "turbomoleRunner.hpp" -#include // for size_t -#include // for system -#include // for format -#include // for ofstream -#include // for string +#include // for size_t +#include // for format +#include // for ofstream +#include // for string #include "atom.hpp" // for Atom #include "constants.hpp" // for constants @@ -83,18 +82,21 @@ void TurbomoleRunner::writeCoordsFile(SimulationBox &simBox) */ void TurbomoleRunner::execute() { - const auto scriptFile = _scriptPath + QMSettings::getQMScript(); + const auto scriptFile = resolveScriptPath(QMSettings::getQMScript()); if (!fileExists(scriptFile)) - throw InputFileException(std::format( - "Turbomole script file \"{}\" does not exist.", - scriptFile - )); + throw InputFileException( + std::format( + "Turbomole script file \"{}\" does not exist.", + scriptFile + ) + ); const auto reuseCharges = _isFirstExecution ? 1 : 0; - const auto command = std::format("{} 0 {} 0 0 0", scriptFile, reuseCharges); - ::system(command.c_str()); + const auto command = + std::format("{} 0 {} 0 0 0", shellQuote(scriptFile), reuseCharges); + executeCommand(command, "Turbomole"); _isFirstExecution = false; -} \ No newline at end of file +} diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt index d7a12bad4..faf6b51a3 100644 --- a/src/box/CMakeLists.txt +++ b/src/box/CMakeLists.txt @@ -18,5 +18,5 @@ target_link_libraries(box ) install(TARGETS box - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/connectivity/CMakeLists.txt b/src/connectivity/CMakeLists.txt index d4676caa1..5f1d98762 100644 --- a/src/connectivity/CMakeLists.txt +++ b/src/connectivity/CMakeLists.txt @@ -16,5 +16,5 @@ target_link_libraries(connectivity ) install(TARGETS connectivity - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/constraints/CMakeLists.txt b/src/constraints/CMakeLists.txt index 7b65f28fc..ad2e15eb2 100644 --- a/src/constraints/CMakeLists.txt +++ b/src/constraints/CMakeLists.txt @@ -27,5 +27,5 @@ target_link_libraries(constraints ) install(TARGETS constraints - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index 7438d1dde..6e7d92b8a 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -92,5 +92,5 @@ if(BUILD_WITH_PYBIND11) endif() install(TARGETS engine - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) diff --git a/src/engine/hessianEngine.cpp b/src/engine/hessianEngine.cpp index 6c319680f..cc0998b59 100644 --- a/src/engine/hessianEngine.cpp +++ b/src/engine/hessianEngine.cpp @@ -44,10 +44,10 @@ #include "progressbar.hpp" #include "referencesOutput.hpp" #include "settings.hpp" -#include "steepestDescent.hpp" #include "stdoutOutput.hpp" -#include "timingsSettings.hpp" +#include "steepestDescent.hpp" #include "timingsOutput.hpp" +#include "timingsSettings.hpp" using namespace engine; using namespace opt; @@ -66,7 +66,7 @@ void HessianEngine::run() runOptimization(); } - auto builder = setupHessianBuilder(); + auto builder = setupHessianBuilder(); const auto hessian = builder->build(*evaluator, *_simulationBox); @@ -121,7 +121,7 @@ pq::SharedHessianBuilder HessianEngine::setupHessianBuilder() const void HessianEngine::setupOptimization(const pq::SharedEvaluator &evaluator) { - _evaluator = evaluator; + _evaluator = evaluator; _learningRateStrategy = setupLearningRateStrategy(); _optimizer = setupEmptyOptimizer(); @@ -162,10 +162,12 @@ void HessianEngine::runOptimization() } if (!_converged) - throw OptException(std::format( - "Optimizer did not converge after {} epochs.", - _optimizer->getNEpochs() - )); + throw OptException( + std::format( + "Optimizer did not converge after {} epochs.", + _optimizer->getNEpochs() + ) + ); if (_optStopped) { @@ -310,6 +312,8 @@ pq::SharedLearningRate HessianEngine::setupLearningRateStrategy() const auto alpha0 = OptimizerSettings::getInitialLearningRate(); const auto lrStrategy = OptimizerSettings::getLearningRateStrategy(); + OptimizerSettings::validateLearningRateStrategy(); + switch (lrStrategy) { using enum LREnum; @@ -318,48 +322,27 @@ pq::SharedLearningRate HessianEngine::setupLearningRateStrategy() case CONSTANT_DECAY: { - const auto alphaDecay = OptimizerSettings::getLearningRateDecay(); - - if (!alphaDecay.has_value()) - throw UserInputException( - "You need to specify a learning rate decay factor for the " - "constant decay learning rate strategy" - ); - const auto alphaFreq = OptimizerSettings::getLRUpdateFrequency(); return std::make_shared( alpha0, - alphaDecay.value(), + OptimizerSettings::getLearningRateDecay().value(), alphaFreq ); } case EXPONENTIAL_DECAY: { - const auto alphaDecay = OptimizerSettings::getLearningRateDecay(); - - if (!alphaDecay.has_value()) - throw UserInputException( - "You need to specify a learning rate decay factor for the " - "exponential decay learning rate strategy" - ); - const auto alphaFreq = OptimizerSettings::getLRUpdateFrequency(); return std::make_shared( alpha0, - alphaDecay.value(), + OptimizerSettings::getLearningRateDecay().value(), alphaFreq ); } case LINESEARCH_WOLFE: - throw UserInputException( - "The Wolfe line search learning rate strategy is not yet " - "implemented" - ); - case NONE: break; } @@ -419,13 +402,7 @@ void HessianEngine::setupMinMaxLearningRate( const auto minLR = OptimizerSettings::getMinLearningRate(); const auto maxLR = OptimizerSettings::getMaxLearningRate(); - if (maxLR.has_value() && minLR >= maxLR.value()) - throw UserInputException(std::format( - "The minimum learning rate {} is greater or equal to the " - "maximum learning rate {}, which is not allowed.", - minLR, - maxLR.value() - )); + OptimizerSettings::validateLearningRateBounds(); learningRate->setMinLearningRate(minLR); learningRate->setMaxLearningRate(maxLR); @@ -433,18 +410,24 @@ void HessianEngine::setupMinMaxLearningRate( void HessianEngine::writeOptimizationSetupInfo() { - _engineOutput.getLogOutput().writeSetupInfo(std::format( - "Optimize before Hessian: {}", - HessianSettings::optimizeBeforeHessian() ? "true" : "false" - )); - _engineOutput.getLogOutput().writeSetupInfo(std::format( - "Optimizer: {}", - string(OptimizerSettings::getOptimizer()) - )); - _engineOutput.getLogOutput().writeSetupInfo(std::format( - "Learning rate strategy: {}", - string(OptimizerSettings::getLearningRateStrategy()) - )); + _engineOutput.getLogOutput().writeSetupInfo( + std::format( + "Optimize before Hessian: {}", + HessianSettings::optimizeBeforeHessian() ? "true" : "false" + ) + ); + _engineOutput.getLogOutput().writeSetupInfo( + std::format( + "Optimizer: {}", + string(OptimizerSettings::getOptimizer()) + ) + ); + _engineOutput.getLogOutput().writeSetupInfo( + std::format( + "Learning rate strategy: {}", + string(OptimizerSettings::getLearningRateStrategy()) + ) + ); _engineOutput.getLogOutput().writeEmptyLine(); } @@ -453,9 +436,7 @@ void HessianEngine::writeHessian(const pq::HessianMatrix &hessian) const std::ofstream file(HessianSettings::getHessianFile()); if (file.fail()) - throw UserInputException( - "Could not open Hessian file for writing." - ); + throw UserInputException("Could not open Hessian file for writing."); file << std::scientific << std::setprecision(16); diff --git a/src/engine/mdEngine.cpp b/src/engine/mdEngine.cpp index 507501d2c..c0849c0d4 100644 --- a/src/engine/mdEngine.cpp +++ b/src/engine/mdEngine.cpp @@ -413,4 +413,4 @@ RingPolymerTrajectoryOutput &MDEngine::getRingPolymerChargeOutput() RingPolymerEnergyOutput &MDEngine::getRingPolymerEnergyOutput() { return _engineOutput.getRingPolymerEnergyOutput(); -} \ No newline at end of file +} diff --git a/src/engine/optEngine.cpp b/src/engine/optEngine.cpp index 8308dcdc5..cd719bdac 100644 --- a/src/engine/optEngine.cpp +++ b/src/engine/optEngine.cpp @@ -358,4 +358,4 @@ std::shared_ptr OptEngine::getSharedPhysicalDataOld() output::OptOutput &OptEngine::getOptOutput() { return _engineOutput.getOptOutput(); -} \ No newline at end of file +} diff --git a/src/exceptions/CMakeLists.txt b/src/exceptions/CMakeLists.txt index e895f61fa..b65a206f7 100644 --- a/src/exceptions/CMakeLists.txt +++ b/src/exceptions/CMakeLists.txt @@ -8,5 +8,5 @@ target_include_directories(exceptions ) install(TARGETS exceptions - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/exceptions/exceptions.cpp b/src/exceptions/exceptions.cpp index e276bb53a..30cccabcc 100644 --- a/src/exceptions/exceptions.cpp +++ b/src/exceptions/exceptions.cpp @@ -31,11 +31,45 @@ using namespace customException; * * @param message */ -CustomException::CustomException(const std::string_view message) - : _message(message) +CustomException::CustomException( + const std::string_view message, + std::optional lineNumber +) + : _message(message), _lineNumber(lineNumber) { } +/** + * @brief Adds a source line if the exception has no line yet. + * + * @param lineNumber + */ +void CustomException::setLineNumber(const size_t lineNumber) noexcept +{ + if (!_lineNumber.has_value()) + _lineNumber = lineNumber; +} + +/** + * @brief Returns the exception message without producing output. + * + * @return const std::string& + */ +const std::string &CustomException::getMessage() const noexcept +{ + return _message; +} + +/** + * @brief Returns the source line associated with the exception. + * + * @return std::optional + */ +std::optional CustomException::getLineNumber() const noexcept +{ + return _lineNumber; +} + /** * @brief Prints the exceptionMsg type in color. * @@ -293,4 +327,4 @@ const char *CompileTimeException::what() const throw() { colorfulOutput(Color::FG_RED, "CompileTimeError"); return _message.c_str(); -} \ No newline at end of file +} diff --git a/src/forceField/CMakeLists.txt b/src/forceField/CMakeLists.txt index 5050f7c17..29fbc33e0 100644 --- a/src/forceField/CMakeLists.txt +++ b/src/forceField/CMakeLists.txt @@ -30,5 +30,5 @@ target_link_libraries(forceField ) install(TARGETS forceField - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/input/CMakeLists.txt b/src/input/CMakeLists.txt index 77d6bde35..8a681ee00 100644 --- a/src/input/CMakeLists.txt +++ b/src/input/CMakeLists.txt @@ -21,11 +21,11 @@ target_link_libraries(PQ_input ) install(TARGETS PQ_input - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) add_subdirectory(inputFileParser) add_subdirectory(topologyFileReader) add_subdirectory(restartFileReader) add_subdirectory(parameterFileReader) -add_subdirectory(ringPolymerRestartFileReader) \ No newline at end of file +add_subdirectory(ringPolymerRestartFileReader) diff --git a/src/input/commandLineArgs.cpp b/src/input/commandLineArgs.cpp index 028a31ff8..d6c9531ec 100644 --- a/src/input/commandLineArgs.cpp +++ b/src/input/commandLineArgs.cpp @@ -22,8 +22,6 @@ #include "commandLineArgs.hpp" -#include // for string_view - #include "exceptions.hpp" // for UserInputException using namespace customException; @@ -38,29 +36,88 @@ CommandLineArgs::CommandLineArgs( const int argc, const std::vector &argv ) - : _argc(argc), _argv(argv){}; + : _argc(argc), _argv(argv) +{ +} /** - * @brief Detects flags in the command line arguments. First argument is the - * input file name. + * @brief Parses the command line arguments. * - * @throw UserInputException if a flag is detected (not yet implemented) - * @throw UserInputException if no input file is specified + * @throw UserInputException if the command line is invalid */ -void CommandLineArgs::detectFlags() +void CommandLineArgs::parse() { - for (const auto &arg : _argv) - if ('-' == arg[0]) + if (_argc < 2) + throw UserInputException( + "No input file specified. Usage: PQ " + ); + + const auto &argument = _argv[1]; + + if ("--validate" == argument) + { + _action = CommandLineAction::VALIDATE; + + if (_argc < 3 || _argv[2].starts_with('-')) throw UserInputException( - "Invalid flag: " + arg + " Flags are not yet implemented." + "No input file specified. Usage: PQ --validate " ); - if (_argc < 2) + _inputFileName = _argv[2]; + + auto formatSet = false; + auto scopeSet = false; + for (auto index = 3; index < _argc; ++index) + { + const auto &option = _argv[size_t(index)]; + + if ("--format=json" == option && !formatSet) + { + _format = CommandLineFormat::JSON; + formatSet = true; + } + else if ("--format=text" == option && !formatSet) + { + _format = CommandLineFormat::TEXT; + formatSet = true; + } + else if ("--scope=installed" == option && !scopeSet) + { + _validationScope = ValidationScope::INSTALLED; + scopeSet = true; + } + else if ("--scope=portable" == option && !scopeSet) + { + _validationScope = ValidationScope::PORTABLE; + scopeSet = true; + } + else + throw UserInputException( + "Unexpected argument: " + option + + ". Use PQ --help for usage." + ); + } + + return; + } + + if ("--help" == argument || "-h" == argument) + _action = CommandLineAction::HELP; + else if ("--version" == argument || "-V" == argument) + _action = CommandLineAction::VERSION; + else if ("--capabilities=json" == argument) + _action = CommandLineAction::CAPABILITIES; + else if (argument.starts_with('-')) throw UserInputException( - "No input file specified. Usage: PQ " + "Unknown option: " + argument + ". Use PQ --help for usage." ); + else + _inputFileName = argument; - _inputFileName = _argv[1]; + if (_argc > 2) + throw UserInputException( + "Unexpected argument: " + _argv[2] + ". Use PQ --help for usage." + ); } /** @@ -69,3 +126,27 @@ void CommandLineArgs::detectFlags() * @return std::string */ std::string CommandLineArgs::getInputFileName() const { return _inputFileName; } + +/** + * @brief get the requested command line action + * + * @return CommandLineAction + */ +CommandLineAction CommandLineArgs::getAction() const { return _action; } + +/** + * @brief get the requested output format + * + * @return CommandLineFormat + */ +CommandLineFormat CommandLineArgs::getFormat() const { return _format; } + +/** + * @brief get the requested validation scope + * + * @return ValidationScope + */ +ValidationScope CommandLineArgs::getValidationScope() const +{ + return _validationScope; +} diff --git a/src/input/inputFileParser/CMakeLists.txt b/src/input/inputFileParser/CMakeLists.txt index 5059d0611..a838f278a 100644 --- a/src/input/inputFileParser/CMakeLists.txt +++ b/src/input/inputFileParser/CMakeLists.txt @@ -41,13 +41,6 @@ target_link_libraries(inputFileParser references ) -if(BUILD_WITH_TESTS) - target_link_libraries(inputFileParser - PUBLIC - gtest - ) -endif() - install(TARGETS inputFileParser - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) diff --git a/src/input/inputFileParser/QMInputParser.cpp b/src/input/inputFileParser/QMInputParser.cpp index 96267a964..c82bf234d 100644 --- a/src/input/inputFileParser/QMInputParser.cpp +++ b/src/input/inputFileParser/QMInputParser.cpp @@ -25,6 +25,7 @@ #include // for format #include // for _Bind_front_t, bind_front #include // for stringstream +#include // for invalid_argument, out_of_range #include // for unordered_map #include "engine.hpp" // for Engine @@ -268,7 +269,7 @@ void QMInputParser::parseQMLoopTimeLimit( ) { checkCommand(lineElements, lineNumber); - QMSettings::setQMLoopTimeLimit(std::stod(lineElements[2])); + QMSettings::setQMLoopTimeLimit(stringToFiniteDouble(lineElements[2])); } /** @@ -477,11 +478,13 @@ void QMInputParser::parseSlakosType( { QMSettings::setSlakosType(THREEOB); QMSettings::setHubbardDerivs(hubbardDerivMap3ob); + ReferencesOutput::addReferenceFile(_THREEOB_FILE_); } else if ("matsci" == slakos) { QMSettings::setSlakosType(MATSCI); + ReferencesOutput::addReferenceFile(_MATSCI_FILE_); } else if ("custom" == slakos) @@ -554,14 +557,36 @@ void QMInputParser::parseHubbardDerivs( std::string item; while (std::getline(ss, item, ',')) { - std::stringstream pairStream(item); - std::string element; - double value; - if (std::getline(pairStream, element, ':') && pairStream >> value) + const auto separator = item.find(':'); + + if (separator == std::string::npos || 0 == separator || + separator + 1 == item.size() || + item.find(':', separator + 1) != std::string::npos) + { + throw InputFileException( + std::format( + "Invalid hubbard_derivs format \"{}\" in input file.", + derivs + ) + ); + } + + const auto element = item.substr(0, separator); + try { - hubbardDerivs[element] = value; + hubbardDerivs[element] = + stringToFiniteDouble(item.substr(separator + 1)); } - else + catch (const std::invalid_argument &) + { + throw InputFileException( + std::format( + "Invalid hubbard_derivs format \"{}\" in input file.", + derivs + ) + ); + } + catch (const std::out_of_range &) { throw InputFileException( std::format( @@ -641,4 +666,4 @@ void QMInputParser::parseGPUPreprocessing( { checkCommand(lineElements, lineNumber); QMSettings::setUseGPUPreprocessing(keywordToBool(lineElements)); -} \ No newline at end of file +} diff --git a/src/input/inputFileParser/cellListInputParser.cpp b/src/input/inputFileParser/cellListInputParser.cpp index d7d161722..ee4851d92 100644 --- a/src/input/inputFileParser/cellListInputParser.cpp +++ b/src/input/inputFileParser/cellListInputParser.cpp @@ -91,13 +91,15 @@ void CellListInputParser::parseCellListActivated( _engine.getCellList().deactivate(); else - throw InputFileException(std::format( - "Invalid cell-list keyword \"{}\" " - "at line {} in input file\n" - "Possible keywords are \"on\" and \"off\"", - lineElements[2], - lineNumber - )); + throw InputFileException( + std::format( + "Invalid cell-list keyword \"{}\" " + "at line {} in input file\n" + "Possible keywords are \"on\" and \"off\"", + lineElements[2], + lineNumber + ) + ); } /** @@ -117,7 +119,7 @@ void CellListInputParser::parseNumberOfCells( { checkCommand(lineElements, lineNumber); - const auto cellNumber = stoi(lineElements[2]); + const auto cellNumber = stringToInt(lineElements[2]); if (cellNumber <= 0) throw InputFileException( @@ -126,4 +128,4 @@ void CellListInputParser::parseNumberOfCells( ); _engine.getCellList().setNumberOfCells(size_t(cellNumber)); -} \ No newline at end of file +} diff --git a/src/input/inputFileParser/constraintsInputParser.cpp b/src/input/inputFileParser/constraintsInputParser.cpp index 53fd28405..17ccb6e42 100644 --- a/src/input/inputFileParser/constraintsInputParser.cpp +++ b/src/input/inputFileParser/constraintsInputParser.cpp @@ -33,6 +33,7 @@ #include "exceptions.hpp" // for InputFileException #include "references.hpp" // for ReferencesOutput #include "referencesOutput.hpp" // for ReferencesOutput +#include "stringUtilities.hpp" // for stringToFiniteDouble, stringToInt using namespace input; using namespace engine; @@ -168,9 +169,9 @@ void ConstraintsInputParser::parseShakeTolerance( { checkCommand(lineElements, lineNumber); - const auto tolerance = stod(lineElements[2]); + const auto tolerance = utilities::stringToFiniteDouble(lineElements[2]); - if (tolerance < 0.0) + if (tolerance <= 0.0) throw InputFileException("Shake tolerance must be positive"); ConstraintSettings::setShakeTolerance(tolerance); @@ -192,9 +193,9 @@ void ConstraintsInputParser::parseShakeIteration( { checkCommand(lineElements, lineNumber); - const auto iteration = stoi(lineElements[2]); + const auto iteration = utilities::stringToInt(lineElements[2]); - if (iteration < 0) + if (iteration <= 0) throw InputFileException("Maximum shake iterations must be positive"); ConstraintSettings::setShakeMaxIter(size_t(iteration)); @@ -216,9 +217,9 @@ void ConstraintsInputParser::parseRattleTolerance( { checkCommand(lineElements, lineNumber); - const auto tolerance = stod(lineElements[2]); + const auto tolerance = utilities::stringToFiniteDouble(lineElements[2]); - if (tolerance < 0.0) + if (tolerance <= 0.0) throw InputFileException("Rattle tolerance must be positive"); ConstraintSettings::setRattleTolerance(tolerance); @@ -240,9 +241,9 @@ void ConstraintsInputParser::parseRattleIteration( { checkCommand(lineElements, lineNumber); - const auto iteration = stoi(lineElements[2]); + const auto iteration = utilities::stringToInt(lineElements[2]); - if (iteration < 0) + if (iteration <= 0) throw InputFileException("Maximum rattle iterations must be positive"); ConstraintSettings::setRattleMaxIter(size_t(iteration)); @@ -264,9 +265,9 @@ void ConstraintsInputParser::parseMShakeTolerance( { checkCommand(lineElements, lineNumber); - const auto tolerance = stod(lineElements[2]); + const auto tolerance = utilities::stringToFiniteDouble(lineElements[2]); - if (tolerance < 0.0) + if (tolerance <= 0.0) throw InputFileException("MShake tolerance must be positive"); ConstraintSettings::setMShakeTolerance(tolerance); @@ -288,9 +289,9 @@ void ConstraintsInputParser::parseMShakeIteration( { checkCommand(lineElements, lineNumber); - const auto iteration = stoi(lineElements[2]); + const auto iteration = utilities::stringToInt(lineElements[2]); - if (iteration < 0) + if (iteration <= 0) throw InputFileException("Maximum MShake iterations must be positive"); ConstraintSettings::setMShakeMaxIter(size_t(iteration)); @@ -336,4 +337,4 @@ void ConstraintsInputParser::parseDistanceConstraintActivated( ); throw InputFileException(message); } -} \ No newline at end of file +} diff --git a/src/input/inputFileParser/convergenceInputParser.cpp b/src/input/inputFileParser/convergenceInputParser.cpp index 3b6ce9da9..e55a97435 100644 --- a/src/input/inputFileParser/convergenceInputParser.cpp +++ b/src/input/inputFileParser/convergenceInputParser.cpp @@ -155,13 +155,15 @@ void ConvInputParser::parseEnergyConvergenceStrategy( ConvSettings::setEnergyConvStrategy(RELATIVE); else - throw InputFileException(std::format( - "Unknown energy convergence strategy \"{}\" in input file " - "at line {}.\n" - "Possible options are: rigorous, loose, absolute, relative", - lineElements[2], - lineNumber - )); + throw InputFileException( + std::format( + "Unknown energy convergence strategy \"{}\" in input file " + "at line {}.\n" + "Possible options are: rigorous, loose, absolute, relative", + lineElements[2], + lineNumber + ) + ); } /** @@ -186,13 +188,15 @@ void ConvInputParser::parseUseEnergyConvergence( ConvSettings::setUseEnergyConv(false); else - throw InputFileException(std::format( - "Unknown option \"{}\" for use-energy-conv in input file " - "at line {}.\n" - "Possible options are: true, false", - lineElements[2], - lineNumber - )); + throw InputFileException( + std::format( + "Unknown option \"{}\" for use-energy-conv in input file " + "at line {}.\n" + "Possible options are: true, false", + lineElements[2], + lineNumber + ) + ); } /** @@ -217,13 +221,15 @@ void ConvInputParser::parseUseForceConvergence( ConvSettings::setUseForceConv(false); else - throw InputFileException(std::format( - "Unknown option \"{}\" for use-force-conv in input file " - "at line {}.\n" - "Possible options are: true, false", - lineElements[2], - lineNumber - )); + throw InputFileException( + std::format( + "Unknown option \"{}\" for use-force-conv in input file " + "at line {}.\n" + "Possible options are: true, false", + lineElements[2], + lineNumber + ) + ); } /** @@ -248,13 +254,15 @@ void ConvInputParser::parseUseMaxForceConvergence( ConvSettings::setUseMaxForceConv(false); else - throw InputFileException(std::format( - "Unknown option \"{}\" for use-max-force-conv in input file " - "at line {}.\n" - "Possible options are: true, false", - lineElements[2], - lineNumber - )); + throw InputFileException( + std::format( + "Unknown option \"{}\" for use-max-force-conv in input file " + "at line {}.\n" + "Possible options are: true, false", + lineElements[2], + lineNumber + ) + ); } /** @@ -279,13 +287,15 @@ void ConvInputParser::parseUseRMSForceConvergence( ConvSettings::setUseRMSForceConv(false); else - throw InputFileException(std::format( - "Unknown option \"{}\" for use-rms-force-conv in input file " - "at line {}.\n" - "Possible options are: true, false", - lineElements[2], - lineNumber - )); + throw InputFileException( + std::format( + "Unknown option \"{}\" for use-rms-force-conv in input file " + "at line {}.\n" + "Possible options are: true, false", + lineElements[2], + lineNumber + ) + ); } /** @@ -304,14 +314,16 @@ void ConvInputParser::parseEnergyConvergence( { checkCommandArray(lineElements, lineNumber); - const auto energyConvergence = std::stod(lineElements[2]); + const auto energyConvergence = stringToFiniteDouble(lineElements[2]); if (energyConvergence <= 0.0) - throw InputFileException(std::format( - "Energy convergence must be greater than 0.0 in input file " - "at line {}.", - lineNumber - )); + throw InputFileException( + std::format( + "Energy convergence must be greater than 0.0 in input file " + "at line {}.", + lineNumber + ) + ); ConvSettings::setEnergyConv(energyConvergence); } @@ -332,15 +344,18 @@ void ConvInputParser::parseRelativeEnergyConvergence( { checkCommandArray(lineElements, lineNumber); - const auto relativeEnergyConvergence = std::stod(lineElements[2]); + const auto relativeEnergyConvergence = + stringToFiniteDouble(lineElements[2]); if (relativeEnergyConvergence <= 0.0) - throw InputFileException(std::format( - "Relative energy convergence must be greater than 0.0 in input " - "file " - "at line {}.", - lineNumber - )); + throw InputFileException( + std::format( + "Relative energy convergence must be greater than 0.0 in input " + "file " + "at line {}.", + lineNumber + ) + ); ConvSettings::setRelEnergyConv(relativeEnergyConvergence); } @@ -361,15 +376,18 @@ void ConvInputParser::parseAbsoluteEnergyConvergence( { checkCommandArray(lineElements, lineNumber); - const auto absoluteEnergyConvergence = std::stod(lineElements[2]); + const auto absoluteEnergyConvergence = + stringToFiniteDouble(lineElements[2]); if (absoluteEnergyConvergence <= 0.0) - throw InputFileException(std::format( - "Absolute energy convergence must be greater than 0.0 in input " - "file " - "at line {}.", - lineNumber - )); + throw InputFileException( + std::format( + "Absolute energy convergence must be greater than 0.0 in input " + "file " + "at line {}.", + lineNumber + ) + ); ConvSettings::setAbsEnergyConv(absoluteEnergyConvergence); } @@ -390,14 +408,16 @@ void ConvInputParser::parseForceConvergence( { checkCommandArray(lineElements, lineNumber); - const auto forceConvergence = std::stod(lineElements[2]); + const auto forceConvergence = stringToFiniteDouble(lineElements[2]); if (forceConvergence <= 0.0) - throw InputFileException(std::format( - "Force convergence must be greater than 0.0 in input file " - "at line {}.", - lineNumber - )); + throw InputFileException( + std::format( + "Force convergence must be greater than 0.0 in input file " + "at line {}.", + lineNumber + ) + ); ConvSettings::setForceConv(forceConvergence); } @@ -418,14 +438,16 @@ void ConvInputParser::parseMaxForceConvergence( { checkCommandArray(lineElements, lineNumber); - const auto maxForceConvergence = std::stod(lineElements[2]); + const auto maxForceConvergence = stringToFiniteDouble(lineElements[2]); if (maxForceConvergence <= 0.0) - throw InputFileException(std::format( - "Max force convergence must be greater than 0.0 in input file " - "at line {}.", - lineNumber - )); + throw InputFileException( + std::format( + "Max force convergence must be greater than 0.0 in input file " + "at line {}.", + lineNumber + ) + ); ConvSettings::setMaxForceConv(maxForceConvergence); } @@ -446,14 +468,16 @@ void ConvInputParser::parseRMSForceConvergence( { checkCommandArray(lineElements, lineNumber); - const auto rmsForceConvergence = std::stod(lineElements[2]); + const auto rmsForceConvergence = stringToFiniteDouble(lineElements[2]); if (rmsForceConvergence <= 0.0) - throw InputFileException(std::format( - "RMS force convergence must be greater than 0.0 in input file " - "at line {}.", - lineNumber - )); + throw InputFileException( + std::format( + "RMS force convergence must be greater than 0.0 in input file " + "at line {}.", + lineNumber + ) + ); ConvSettings::setRMSForceConv(rmsForceConvergence); -} \ No newline at end of file +} diff --git a/src/input/inputFileParser/coulombLongRangeInputParser.cpp b/src/input/inputFileParser/coulombLongRangeInputParser.cpp index 71f776235..bd5d20f9d 100644 --- a/src/input/inputFileParser/coulombLongRangeInputParser.cpp +++ b/src/input/inputFileParser/coulombLongRangeInputParser.cpp @@ -131,7 +131,7 @@ void CoulombLongRangeInputParser::parseWolfParameter( { checkCommand(lineElements, lineNumber); - const auto wolfParameter = stod(lineElements[2]); + const auto wolfParameter = stringToFiniteDouble(lineElements[2]); if (wolfParameter < 0.0) throw InputFileException("Wolf parameter cannot be negative"); @@ -153,7 +153,7 @@ void CoulombLongRangeInputParser::parseReactionFieldEpsilon( { checkCommand(lineElements, lineNumber); - const auto epsilon = stod(lineElements[2]); + const auto epsilon = stringToFiniteDouble(lineElements[2]); if (epsilon < 1.0) throw InputFileException( @@ -162,4 +162,4 @@ void CoulombLongRangeInputParser::parseReactionFieldEpsilon( ); PotentialSettings::setReactionFieldEpsilon(epsilon); -} \ No newline at end of file +} diff --git a/src/input/inputFileParser/filesInputParser.cpp b/src/input/inputFileParser/filesInputParser.cpp index 354b58197..b9cf6b7fe 100644 --- a/src/input/inputFileParser/filesInputParser.cpp +++ b/src/input/inputFileParser/filesInputParser.cpp @@ -43,13 +43,14 @@ using namespace utilities; * @details following keywords are added to the _keywordFuncMap, * _keywordRequiredMap and _keywordCountMap: 1) intra-nonBonded_file 2) * topology_file 3) parameter_file 4) start_file - * (required) 5) rpmd_start_file 6) moldescriptor_file + * (required) 5) rpmd_start_file 6) moldescriptor_file * 7) guff_path (deprecated) 8) guff_file * 9) mshake_file 10) dftb_file * * @param engine */ -FilesInputParser::FilesInputParser(Engine &engine) : InputFileParser(engine) +FilesInputParser::FilesInputParser(Engine &engine, const bool validateFilePaths) + : InputFileParser(engine), _validateFilePaths(validateFilePaths) { addKeyword( std::string("intra-nonBonded_file"), @@ -131,7 +132,7 @@ void FilesInputParser::parseIntraNonBondedFile( const auto &fileName = lineElements[2]; - if (!fileExists(fileName)) + if (_validateFilePaths && !fileExists(fileName)) throw InputFileException( std::format("Intra non bonded file \"{}\" File not found", fileName) ); @@ -160,7 +161,7 @@ void FilesInputParser::parseTopologyFilename( const auto &filename = lineElements[2]; - if (!fileExists(filename)) + if (_validateFilePaths && !fileExists(filename)) throw InputFileException( std::format("Cannot open topology file - filename = {}", filename) ); @@ -187,7 +188,7 @@ void FilesInputParser::parseParameterFilename( const auto &filename = lineElements[2]; - if (!fileExists(filename)) + if (_validateFilePaths && !fileExists(filename)) throw InputFileException( std::format("Cannot open parameter file - filename = {}", filename) ); @@ -211,7 +212,7 @@ void FilesInputParser::parseStartFilename( const auto &filename = lineElements[2]; - if (!fileExists(filename)) + if (_validateFilePaths && !fileExists(filename)) throw InputFileException( std::format("Cannot open start file - filename = {}", filename) ); @@ -234,11 +235,13 @@ void FilesInputParser::parseRingPolymerStartFilename( const auto &filename = lineElements[2]; - if (!fileExists(filename)) - throw InputFileException(std::format( - "Cannot open ring polymer start file - filename = {}", - filename - )); + if (_validateFilePaths && !fileExists(filename)) + throw InputFileException( + std::format( + "Cannot open ring polymer start file - filename = {}", + filename + ) + ); FileSettings::setRingPolymerStartFileName(filename); FileSettings::setIsRingPolymerStartFileNameSet(); @@ -262,12 +265,14 @@ void FilesInputParser::parseMoldescriptorFilename( const auto &filename = lineElements[2]; - if (!fileExists(filename)) - throw InputFileException(std::format( - "Cannot open moldescriptor file - filename = \"{}\" - file not " - "found", - filename - )); + if (_validateFilePaths && !fileExists(filename)) + throw InputFileException( + std::format( + "Cannot open moldescriptor file - filename = \"{}\" - file not " + "found", + filename + ) + ); FileSettings::setMolDescriptorFileName(filename); } @@ -282,10 +287,13 @@ void FilesInputParser::parseGuffPath( const size_t ) { - throw InputFileException(std::format( - "The \"guff_path\" keyword id deprecated. Please use \"guffdat_file\" " - "instead." - )); + throw InputFileException( + std::format( + "The \"guff_path\" keyword id deprecated. Please use " + "\"guffdat_file\" " + "instead." + ) + ); } /** @@ -306,7 +314,7 @@ void FilesInputParser::parseGuffDatFilename( const auto &filename = lineElements[2]; - if (!fileExists(filename)) + if (_validateFilePaths && !fileExists(filename)) throw InputFileException( std::format("Cannot open guff file - filename = {}", filename) ); @@ -330,7 +338,7 @@ void FilesInputParser::parseMShakeFilename( const auto &filename = lineElements[2]; - if (!fileExists(filename)) + if (_validateFilePaths && !fileExists(filename)) throw InputFileException( std::format("Cannot open mshake file - filename = {}", filename) ); @@ -354,10 +362,10 @@ void FilesInputParser::parseDFTBFilename( const auto &filename = lineElements[2]; - if (!fileExists(filename)) + if (_validateFilePaths && !fileExists(filename)) throw InputFileException( std::format("Cannot open DFTB setup file - filename = {}", filename) ); FileSettings::setDFTBFileName(filename); -} \ No newline at end of file +} diff --git a/src/input/inputFileParser/generalInputParser.cpp b/src/input/inputFileParser/generalInputParser.cpp index d9c275b79..0c462d54b 100644 --- a/src/input/inputFileParser/generalInputParser.cpp +++ b/src/input/inputFileParser/generalInputParser.cpp @@ -28,12 +28,12 @@ #include // for _Bind_front_t, bind_front #include // for out_of_range and invalid_argument -#include "engine.hpp" // for Engine -#include "exceptions.hpp" // for InputFileException, customException +#include "engine.hpp" // for Engine +#include "exceptions.hpp" // for InputFileException, customException #include "hessianEngine.hpp" // for HessianEngine -#include "mmmdEngine.hpp" // for MMMDEngine -#include "optEngine.hpp" // for MMOptEngine -#include "qmmdEngine.hpp" // for QMMDEngine +#include "mmmdEngine.hpp" // for MMMDEngine +#include "optEngine.hpp" // for MMOptEngine +#include "qmmdEngine.hpp" // for QMMDEngine #include "ringPolymerqmmdEngine.hpp" // for RingPolymerQMMDEngine #include "settings.hpp" // for Settings #include "stringUtilities.hpp" // for toLowerCopy @@ -177,7 +177,7 @@ void GeneralInputParser::parseDimensionality( std::erase(dimensionalityString, 'd'); - const auto dimensionality = std::stoi(dimensionalityString); + const auto dimensionality = stringToInt(dimensionalityString); if (dimensionality == 3) Settings::setDimensionality(size_t(dimensionality)); diff --git a/src/input/inputFileParser/hessianInputParser.cpp b/src/input/inputFileParser/hessianInputParser.cpp index 824443ae9..c0cc07219 100644 --- a/src/input/inputFileParser/hessianInputParser.cpp +++ b/src/input/inputFileParser/hessianInputParser.cpp @@ -54,10 +54,7 @@ HessianInputParser::HessianInputParser(pq::Engine &engine) ); addKeyword( std::string("optimize_before_hessian"), - std::bind_front( - &HessianInputParser::parseOptimizeBeforeHessian, - this - ), + std::bind_front(&HessianInputParser::parseOptimizeBeforeHessian, this), false ); addKeyword( @@ -92,14 +89,16 @@ void HessianInputParser::parseDisplacement( { checkCommand(lineElements, lineNumber); - const auto displacement = std::stod(lineElements[2]); + const auto displacement = stringToFiniteDouble(lineElements[2]); if (displacement <= 0.0) - throw InputFileException(std::format( - "Hessian displacement must be greater than 0 in input file " - "at line {}", - lineNumber - )); + throw InputFileException( + std::format( + "Hessian displacement must be greater than 0 in input file " + "at line {}", + lineNumber + ) + ); HessianSettings::setDisplacement(displacement); } @@ -124,10 +123,12 @@ void HessianInputParser::parseBuilder( HessianSettings::setBuilder(lineElements[2]); if (HessianSettings::getBuilder() == NONE) - throw InputFileException(std::format( - "Invalid hessian_builder \"{}\" in input file at line {} - " - "possible values are: central, forward, five-point, analytic", - lineElements[2], - lineNumber - )); + throw InputFileException( + std::format( + "Invalid hessian_builder \"{}\" in input file at line {} - " + "possible values are: central, forward, five-point, analytic", + lineElements[2], + lineNumber + ) + ); } diff --git a/src/input/inputFileParser/hybridInputParser.cpp b/src/input/inputFileParser/hybridInputParser.cpp index 11e1bca1b..7138e74fc 100644 --- a/src/input/inputFileParser/hybridInputParser.cpp +++ b/src/input/inputFileParser/hybridInputParser.cpp @@ -149,11 +149,13 @@ void HybridInputParser::parseUseQMCharges( HybridSettings::setUseQMCharges(false); else - throw InputFileException(std::format( - "Invalid qm_charges \"{}\" in input file\n" - "Possible values are: qm, mm", - lineElements[2] - )); + throw InputFileException( + std::format( + "Invalid qm_charges \"{}\" in input file\n" + "Possible values are: qm, mm", + lineElements[2] + ) + ); throw UserInputException("Not implemented"); } @@ -171,14 +173,16 @@ void HybridInputParser::parseCoreRadius( { checkCommand(lineElements, lineNumber); - const auto coreRadius = std::stod(lineElements[2]); + const auto coreRadius = stringToFiniteDouble(lineElements[2]); if (coreRadius < 0.0) - throw InputFileException(std::format( - "Invalid {} {} in input file - must be a positive number", - lineElements[0], - lineElements[2] - )); + throw InputFileException( + std::format( + "Invalid {} {} in input file - must be a positive number", + lineElements[0], + lineElements[2] + ) + ); HybridSettings::setCoreRadius(coreRadius); @@ -198,14 +202,16 @@ void HybridInputParser::parseLayerRadius( { checkCommand(lineElements, lineNumber); - const auto layerRadius = std::stod(lineElements[2]); + const auto layerRadius = stringToFiniteDouble(lineElements[2]); if (layerRadius < 0.0) - throw InputFileException(std::format( - "Invalid {} {} in input file - must be a positive number", - lineElements[0], - lineElements[2] - )); + throw InputFileException( + std::format( + "Invalid {} {} in input file - must be a positive number", + lineElements[0], + lineElements[2] + ) + ); HybridSettings::setLayerRadius(layerRadius); @@ -225,16 +231,18 @@ void HybridInputParser::parseSmoothingRadius( { checkCommand(lineElements, lineNumber); - const auto smoothingRadius = std::stod(lineElements[2]); + const auto smoothingRadius = stringToFiniteDouble(lineElements[2]); if (smoothingRadius < 0.0) - throw InputFileException(std::format( - "Invalid {} {} in input file - must be a positive number", - lineElements[0], - lineElements[2] - )); + throw InputFileException( + std::format( + "Invalid {} {} in input file - must be a positive number", + lineElements[0], + lineElements[2] + ) + ); HybridSettings::setSmoothingRadius(smoothingRadius); throw UserInputException("Not implemented"); -} \ No newline at end of file +} diff --git a/src/input/inputFileParser/manostatInputParser.cpp b/src/input/inputFileParser/manostatInputParser.cpp index b9ac42181..377b4fab9 100644 --- a/src/input/inputFileParser/manostatInputParser.cpp +++ b/src/input/inputFileParser/manostatInputParser.cpp @@ -25,8 +25,10 @@ #include // for size_t #include // for format #include // for _Bind_front_t, bind_front +#include // for numeric_limits #include // for string_view +#include "constants/conversionFactors.hpp" #include "exceptions.hpp" // for InputFileException, customException #include "manostatSettings.hpp" // for ManostatSettings #include "references.hpp" // for ReferencesOutput @@ -39,6 +41,7 @@ using namespace settings; using namespace customException; using namespace references; using namespace utilities; +using namespace constants; /** * @brief Construct a new Input File Parser Manostat:: Input File Parser @@ -125,12 +128,15 @@ void ManostatInputParser::parseManostat( } else - throw InputFileException(std::format( - "Invalid manostat \"{}\" at line {} in input file.\n" - "Possible options are: berendsen, stochastic_rescaling and none", - lineElements[2], - lineNumber - )); + throw InputFileException( + std::format( + "Invalid manostat \"{}\" at line {} in input file.\n" + "Possible options are: berendsen, stochastic_rescaling and " + "none", + lineElements[2], + lineNumber + ) + ); } /** @@ -147,7 +153,9 @@ void ManostatInputParser::parsePressure( { checkCommand(lineElements, lineNumber); - ManostatSettings::setTargetPressure(stod(lineElements[2])); + const auto pressure = stringToFiniteDouble(lineElements[2]); + + ManostatSettings::setTargetPressure(pressure); } /** @@ -165,11 +173,17 @@ void ManostatInputParser::parseManostatRelaxationTime( ) { checkCommand(lineElements, lineNumber); - const auto relaxationTime = stod(lineElements[2]); + const auto relaxationTime = stringToFiniteDouble(lineElements[2]); + + if (relaxationTime <= 0.0) + throw InputFileException( + "Relaxation time of manostat must be finite and greater than zero" + ); - if (relaxationTime < 0) + if (relaxationTime > std::numeric_limits::max() / _PS_TO_FS_) throw InputFileException( - "Relaxation time of manostat cannot be negative" + "Relaxation time of manostat is too large to represent in " + "femtoseconds" ); ManostatSettings::setTauManostat(relaxationTime); @@ -191,10 +205,12 @@ void ManostatInputParser::parseCompressibility( ) { checkCommand(lineElements, lineNumber); - const auto compressibility = stod(lineElements[2]); + const auto compressibility = stringToFiniteDouble(lineElements[2]); if (compressibility < 0.0) - throw InputFileException("Compressibility cannot be negative"); + throw InputFileException( + "Compressibility must be finite and non-negative" + ); ManostatSettings::setCompressibility(compressibility); } @@ -254,11 +270,13 @@ void ManostatInputParser::parseIsotropy( ManostatSettings::setIsotropy(FULL_ANISOTROPIC); else - throw InputFileException(std::format( - "Invalid isotropy \"{}\" at line {} in input file.\n" - "Possible options are: isotropic, xy, xz, yz, " - "anisotropic and full_anisotropic", - lineElements[2], - lineNumber - )); + throw InputFileException( + std::format( + "Invalid isotropy \"{}\" at line {} in input file.\n" + "Possible options are: isotropic, xy, xz, yz, " + "anisotropic and full_anisotropic", + lineElements[2], + lineNumber + ) + ); } diff --git a/src/input/inputFileParser/optInputParser.cpp b/src/input/inputFileParser/optInputParser.cpp index b28c6bce8..1fc84a0b9 100644 --- a/src/input/inputFileParser/optInputParser.cpp +++ b/src/input/inputFileParser/optInputParser.cpp @@ -118,13 +118,15 @@ void OptInputParser::parseOptimizer( OptimizerSettings::setOptimizer(ADAM); else - throw InputFileException(std::format( - "Unknown optimizer method \"{}\" in input file " - "at line {}.\nPossible options are: steepest-descent, " - "adam", - lineElements[2], - lineNumber - )); + throw InputFileException( + std::format( + "Unknown optimizer method \"{}\" in input file " + "at line {}.\nPossible options are: steepest-descent, " + "adam", + lineElements[2], + lineNumber + ) + ); } /** @@ -162,13 +164,16 @@ void OptInputParser::parseLearningRateStrategy( OptimizerSettings::setLearningRateStrategy(LINESEARCH_WOLFE); else - throw InputFileException(std::format( - "Unknown learning rate strategy \"{}\" in input file " - "at line {}.\nPossible options are: constant, " - "constant-decay, exponential-decay, linesearch (linesearch-wolfe)", - lineElements[2], - lineNumber - )); + throw InputFileException( + std::format( + "Unknown learning rate strategy \"{}\" in input file " + "at line {}.\nPossible options are: constant, " + "constant-decay, exponential-decay, linesearch " + "(linesearch-wolfe)", + lineElements[2], + lineNumber + ) + ); } /** @@ -187,14 +192,16 @@ void OptInputParser::parseInitialLearningRate( { checkCommandArray(lineElements, lineNumber); - const auto initialLearningRate = std::stod(lineElements[2]); + const auto initialLearningRate = stringToFiniteDouble(lineElements[2]); if (initialLearningRate <= 0.0) - throw InputFileException(std::format( - "Initial learning rate must be greater than 0.0 in input file " - "at line {}.", - lineNumber - )); + throw InputFileException( + std::format( + "Initial learning rate must be greater than 0.0 in input file " + "at line {}.", + lineNumber + ) + ); OptimizerSettings::setInitialLearningRate(initialLearningRate); } @@ -215,14 +222,17 @@ void OptInputParser::parseLearningRateUpdateFreq( { checkCommandArray(lineElements, lineNumber); - const auto frequency = std::stoi(lineElements[2]); + const auto frequency = stringToInt(lineElements[2]); if (frequency <= 0) - throw InputFileException(std::format( - "Learning rate update frequency must be greater than 0 in input " - "file at line {}.", - lineNumber - )); + throw InputFileException( + std::format( + "Learning rate update frequency must be greater than 0 in " + "input " + "file at line {}.", + lineNumber + ) + ); OptimizerSettings::setLRUpdateFrequency(size_t(frequency)); } @@ -243,14 +253,16 @@ void OptInputParser::parseMinLearningRate( { checkCommandArray(lineElements, lineNumber); - const auto minLearningRate = std::stod(lineElements[2]); + const auto minLearningRate = stringToFiniteDouble(lineElements[2]); if (minLearningRate <= 0.0) - throw InputFileException(std::format( - "Minimum learning rate must be greater than 0.0 in input file " - "at line {}.", - lineNumber - )); + throw InputFileException( + std::format( + "Minimum learning rate must be greater than 0.0 in input file " + "at line {}.", + lineNumber + ) + ); OptimizerSettings::setMinLearningRate(minLearningRate); } @@ -271,14 +283,16 @@ void OptInputParser::parseMaxLearningRate( { checkCommandArray(lineElements, lineNumber); - const auto maxLearningRate = std::stod(lineElements[2]); + const auto maxLearningRate = stringToFiniteDouble(lineElements[2]); if (maxLearningRate <= 0.0) - throw InputFileException(std::format( - "Maximum learning rate must be greater than 0.0 in input file " - "at line {}.", - lineNumber - )); + throw InputFileException( + std::format( + "Maximum learning rate must be greater than 0.0 in input file " + "at line {}.", + lineNumber + ) + ); OptimizerSettings::setMaxLearningRate(maxLearningRate); } @@ -299,14 +313,16 @@ void OptInputParser::parseLearningRateDecay( { checkCommandArray(lineElements, lineNumber); - const auto decay = std::stod(lineElements[2]); + const auto decay = stringToFiniteDouble(lineElements[2]); if (decay <= 0.0) - throw InputFileException(std::format( - "Learning rate decay must be greater than 0.0 in input file " - "at line {}.", - lineNumber - )); + throw InputFileException( + std::format( + "Learning rate decay must be greater than 0.0 in input file " + "at line {}.", + lineNumber + ) + ); OptimizerSettings::setLearningRateDecay(decay); -} \ No newline at end of file +} diff --git a/src/input/inputFileParser/outputInputParser.cpp b/src/input/inputFileParser/outputInputParser.cpp index d0f636160..3d14b7cc0 100644 --- a/src/input/inputFileParser/outputInputParser.cpp +++ b/src/input/inputFileParser/outputInputParser.cpp @@ -219,7 +219,7 @@ void OutputInputParser::parseOutputFreq( { checkCommand(lineElements, lineNumber); - const auto outputFrequency = stoi(lineElements[2]); + const auto outputFrequency = stringToInt(lineElements[2]); if (outputFrequency < 0) throw InputFileException(format( "Output frequency cannot be negative - \"{}\" at line {} in input " diff --git a/src/input/inputFileParser/resetKineticsInputParser.cpp b/src/input/inputFileParser/resetKineticsInputParser.cpp index 7f74b4592..3cb4162f8 100644 --- a/src/input/inputFileParser/resetKineticsInputParser.cpp +++ b/src/input/inputFileParser/resetKineticsInputParser.cpp @@ -28,6 +28,7 @@ #include "exceptions.hpp" // for InputFileException, customException #include "resetKineticsSettings.hpp" // for ResetKineticsSettings +#include "stringUtilities.hpp" // for stringToInt using namespace input; using namespace engine; @@ -101,7 +102,7 @@ void ResetKineticsInputParser::parseNScale( { checkCommand(lineElements, lineNumber); - const auto nScale = stoi(lineElements[2]); + const auto nScale = utilities::stringToInt(lineElements[2]); if (nScale < 0) throw InputFileException("Nscale must be positive"); @@ -126,7 +127,7 @@ void ResetKineticsInputParser::parseFScale( { checkCommand(lineElements, lineNumber); - const auto fScale = stoi(lineElements[2]); + const auto fScale = utilities::stringToInt(lineElements[2]); if (fScale < 0) throw InputFileException("Fscale must be positive"); @@ -151,7 +152,7 @@ void ResetKineticsInputParser::parseNReset( { checkCommand(lineElements, lineNumber); - const auto nReset = stoi(lineElements[2]); + const auto nReset = utilities::stringToInt(lineElements[2]); if (nReset < 0) throw InputFileException("Nreset must be positive"); @@ -176,7 +177,7 @@ void ResetKineticsInputParser::parseFReset( { checkCommand(lineElements, lineNumber); - const auto fReset = stoi(lineElements[2]); + const auto fReset = utilities::stringToInt(lineElements[2]); if (fReset < 0) throw InputFileException("Freset must be positive"); @@ -201,7 +202,7 @@ void ResetKineticsInputParser::parseNResetAngular( { checkCommand(lineElements, lineNumber); - const auto nResetAngular = stoi(lineElements[2]); + const auto nResetAngular = utilities::stringToInt(lineElements[2]); if (nResetAngular < 0) throw InputFileException("Nreset_angular must be positive"); @@ -226,7 +227,7 @@ void ResetKineticsInputParser::parseFResetAngular( { checkCommand(lineElements, lineNumber); - const auto fResetAngular = stoi(lineElements[2]); + const auto fResetAngular = utilities::stringToInt(lineElements[2]); if (fResetAngular < 0) throw InputFileException("Freset_angular must be positive"); @@ -251,10 +252,10 @@ void ResetKineticsInputParser::parseFResetForces( { checkCommand(lineElements, lineNumber); - const auto fResetForces = stoi(lineElements[2]); + const auto fResetForces = utilities::stringToInt(lineElements[2]); if (fResetForces < 0) throw InputFileException("Freset_force must be positive"); ResetKineticsSettings::setFResetForces(static_cast(fResetForces)); -} \ No newline at end of file +} diff --git a/src/input/inputFileParser/ringPolymerInputParser.cpp b/src/input/inputFileParser/ringPolymerInputParser.cpp index 5aef34d6d..fd57942d9 100644 --- a/src/input/inputFileParser/ringPolymerInputParser.cpp +++ b/src/input/inputFileParser/ringPolymerInputParser.cpp @@ -27,6 +27,7 @@ #include "exceptions.hpp" // for InputFileException, customException #include "ringPolymerSettings.hpp" // for RingPolymerSettings +#include "stringUtilities.hpp" // for stringToInt using namespace input; using namespace engine; @@ -65,13 +66,15 @@ void RingPolymerInputParser::parseNumberOfBeads( { checkCommand(lineElements, lineNumber); - auto numberOfBeads = stoi(lineElements[2]); + auto numberOfBeads = utilities::stringToInt(lineElements[2]); if (numberOfBeads < 2) - throw InputFileException(std::format( - "Number of beads must be at least 2 - in input file in line {}", - lineNumber - )); + throw InputFileException( + std::format( + "Number of beads must be at least 2 - in input file in line {}", + lineNumber + ) + ); RingPolymerSettings::setNumberOfBeads(size_t(numberOfBeads)); -} \ No newline at end of file +} diff --git a/src/input/inputFileParser/simulationBoxInputParser.cpp b/src/input/inputFileParser/simulationBoxInputParser.cpp index c084e28a1..37c43f284 100644 --- a/src/input/inputFileParser/simulationBoxInputParser.cpp +++ b/src/input/inputFileParser/simulationBoxInputParser.cpp @@ -84,7 +84,7 @@ void SimulationBoxInputParser::parseCoulombRadius( { checkCommand(lineElements, lineNumber); - const auto cutOff = stod(lineElements[2]); + const auto cutOff = stringToFiniteDouble(lineElements[2]); if (cutOff < 0.0) throw InputFileException(format( @@ -114,9 +114,9 @@ void SimulationBoxInputParser::parseDensity( { checkCommand(lineElements, lineNumber); - const auto density = stod(lineElements[2]); + const auto density = stringToFiniteDouble(lineElements[2]); - if (density < 0.0) + if (density <= 0.0) throw InputFileException( std::format("Density must be positive - density = {}", density) ); @@ -157,11 +157,14 @@ void SimulationBoxInputParser::parseInitializeVelocities( SimulationBoxSettings::setInitializeVelocities(FORCE); else - throw InputFileException(std::format( - "Invalid value for initialize velocities - \"{}\" at line {} in " - "input file.\n" - "Possible options are: true, false, force", - lineElements[2], - lineNumber - )); -} \ No newline at end of file + throw InputFileException( + std::format( + "Invalid value for initialize velocities - \"{}\" at line {} " + "in " + "input file.\n" + "Possible options are: true, false, force", + lineElements[2], + lineNumber + ) + ); +} diff --git a/src/input/inputFileParser/thermostatInputParser.cpp b/src/input/inputFileParser/thermostatInputParser.cpp index ac609fbbf..4f77d627b 100644 --- a/src/input/inputFileParser/thermostatInputParser.cpp +++ b/src/input/inputFileParser/thermostatInputParser.cpp @@ -22,11 +22,14 @@ #include "thermostatInputParser.hpp" +#include // for sqrt #include // for size_t, std #include // for format #include // for _Bind_front_t, bind_front +#include // for numeric_limits #include // for string_view +#include "constants/conversionFactors.hpp" #include "exceptions.hpp" // for InputFileException, customException #include "references.hpp" // for References #include "referencesOutput.hpp" // for ReferencesOutput @@ -39,6 +42,7 @@ using namespace customException; using namespace settings; using namespace utilities; using namespace references; +using namespace constants; /** * @brief Construct a new Input File Parser Thermostat:: Input File Parser @@ -187,10 +191,10 @@ void ThermostatInputParser::parseTemperature( { checkCommand(lineElements, lineNumber); - const auto temperature = stod(lineElements[2]); + const auto temperature = stringToFiniteDouble(lineElements[2]); - if (temperature < 0) - throw InputFileException("Temperature cannot be negative"); + if (temperature < 0.0) + throw InputFileException("Temperature must be finite and non-negative"); ThermostatSettings::setTargetTemperature(temperature); } @@ -211,10 +215,12 @@ void ThermostatInputParser::parseStartTemperature( { checkCommand(lineElements, lineNumber); - const auto startTemperature = stod(lineElements[2]); + const auto startTemperature = stringToFiniteDouble(lineElements[2]); - if (startTemperature < 0) - throw InputFileException("Start temperature cannot be negative"); + if (startTemperature < 0.0) + throw InputFileException( + "Start temperature must be finite and non-negative" + ); ThermostatSettings::setStartTemperature(startTemperature); } @@ -235,10 +241,12 @@ void ThermostatInputParser::parseEndTemperature( { checkCommand(lineElements, lineNumber); - const auto endTemperature = stod(lineElements[2]); + const auto endTemperature = stringToFiniteDouble(lineElements[2]); - if (endTemperature < 0) - throw InputFileException("End temperature cannot be negative"); + if (endTemperature < 0.0) + throw InputFileException( + "End temperature must be finite and non-negative" + ); ThermostatSettings::setEndTemperature(endTemperature); } @@ -262,7 +270,7 @@ void ThermostatInputParser::parseTemperatureRampSteps( { checkCommand(lineElements, lineNumber); - const auto temperatureRampSteps = stoi(lineElements[2]); + const auto temperatureRampSteps = stringToInt(lineElements[2]); if (temperatureRampSteps < 0) throw InputFileException("Temperature ramp steps cannot be negative"); @@ -287,10 +295,11 @@ void ThermostatInputParser::parseTemperatureRampFrequency( { checkCommand(lineElements, lineNumber); - const auto tempRampFreq = stoi(lineElements[2]); + const auto tempRampFreq = stringToInt(lineElements[2]); - if (tempRampFreq < 0) - throw InputFileException("Temperature ramp frequency cannot be negative" + if (tempRampFreq < 1) + throw InputFileException( + "Temperature ramp frequency must be greater than zero" ); ThermostatSettings::setTemperatureRampFrequency(size_t(tempRampFreq)); @@ -312,11 +321,17 @@ void ThermostatInputParser::parseThermostatRelaxationTime( { checkCommand(lineElements, lineNumber); - const auto relaxationTime = stod(lineElements[2]); + const auto relaxationTime = stringToFiniteDouble(lineElements[2]); - if (relaxationTime < 0) + if (relaxationTime <= 0.0) throw InputFileException( - "Relaxation time of thermostat cannot be negative" + "Relaxation time of thermostat must be finite and greater than zero" + ); + + if (relaxationTime > std::numeric_limits::max() / _PS_TO_FS_) + throw InputFileException( + "Relaxation time of thermostat is too large to represent in " + "femtoseconds" ); ThermostatSettings::setRelaxationTime(relaxationTime); @@ -338,10 +353,18 @@ void ThermostatInputParser::parseThermostatFriction( { checkCommand(lineElements, lineNumber); - const auto friction = stod(lineElements[2]); + const auto friction = stringToFiniteDouble(lineElements[2]); + + if (friction < 0.0) + throw InputFileException( + "Friction of thermostat must be finite and non-negative" + ); - if (friction < 0) - throw InputFileException("Friction of thermostat cannot be negative"); + if (friction > std::numeric_limits::max() / 1.0e12) + throw InputFileException( + "Friction of thermostat is too large to represent in inverse " + "seconds" + ); ThermostatSettings::setFriction(friction * 1.0e12); } @@ -362,10 +385,11 @@ void ThermostatInputParser::parseThermostatChainLength( { checkCommand(lineElements, lineNumber); - const auto chainLength = stoi(lineElements[2]); + const auto chainLength = stringToInt(lineElements[2]); - if (chainLength < 0) - throw InputFileException("Chain length of thermostat cannot be negative" + if (chainLength < 1) + throw InputFileException( + "Chain length of thermostat must be greater than zero" ); ThermostatSettings::setNoseHooverChainLength(size_t(chainLength)); @@ -387,12 +411,19 @@ void ThermostatInputParser::parseThermostatCouplingFrequency( { checkCommand(lineElements, lineNumber); - const auto couplingFrequency = stod(lineElements[2]); + const auto couplingFrequency = stringToFiniteDouble(lineElements[2]); - if (couplingFrequency < 0) + if (couplingFrequency < 0.0) throw InputFileException( - "Coupling frequency of thermostat cannot be negative" + "Coupling frequency of thermostat must be finite and non-negative" + ); + + if (couplingFrequency > + std::sqrt(std::numeric_limits::max()) / _PER_CM_TO_HZ_) + throw InputFileException( + "Coupling frequency of thermostat is too large to represent in " + "hertz" ); ThermostatSettings::setNoseHooverCouplingFrequency(couplingFrequency); -} \ No newline at end of file +} diff --git a/src/input/inputFileParser/timingsInputParser.cpp b/src/input/inputFileParser/timingsInputParser.cpp index a9d3724d1..ac777e189 100644 --- a/src/input/inputFileParser/timingsInputParser.cpp +++ b/src/input/inputFileParser/timingsInputParser.cpp @@ -26,12 +26,14 @@ #include // for string_view #include "exceptions.hpp" // for InputFileException +#include "stringUtilities.hpp" // for stringToFiniteDouble, stringToInt #include "timingsSettings.hpp" // for TimingsSettings using namespace input; using namespace engine; using namespace customException; using namespace settings; +using namespace utilities; /** * @brief Construct a new Input File Parser Timings object @@ -68,7 +70,14 @@ void TimingsInputParser::parseTimeStep( ) { checkCommand(lineElements, lineNumber); - TimingsSettings::setTimeStep(stod(lineElements[2])); + + const auto timeStep = stringToFiniteDouble(lineElements[2]); + if (timeStep <= 0.0) + throw InputFileException( + "Time step must be finite and greater than zero" + ); + + TimingsSettings::setTimeStep(timeStep); } /** @@ -85,10 +94,10 @@ void TimingsInputParser::parseNumberOfSteps( { checkCommand(lineElements, lineNumber); - const auto numberOfSteps = stoi(lineElements[2]); + const auto numberOfSteps = stringToInt(lineElements[2]); - if (numberOfSteps < 0) - throw InputFileException("Number of steps cannot be negative"); + if (numberOfSteps < 1) + throw InputFileException("Number of steps must be greater than zero"); TimingsSettings::setNumberOfSteps(size_t(numberOfSteps)); } diff --git a/src/input/inputFileReader.cpp b/src/input/inputFileReader.cpp index ee83d2c59..db7cdfc64 100644 --- a/src/input/inputFileReader.cpp +++ b/src/input/inputFileReader.cpp @@ -26,6 +26,7 @@ #include // for format #include // for ifstream, basic_istream #include // for map, operator== +#include // for invalid_argument, out_of_range #include // for char_traits, string #include // for vector @@ -71,14 +72,17 @@ using std::make_unique; */ InputFileReader::InputFileReader( const std::string_view &fileName, - engine::Engine &engine + engine::Engine &engine, + const bool validateFilePaths ) : _fileName(fileName), _engine(engine) { _parsers.push_back(make_unique(_engine)); _parsers.push_back(make_unique(_engine)); _parsers.push_back(make_unique(_engine)); - _parsers.push_back(make_unique(_engine)); + _parsers.push_back( + make_unique(_engine, validateFilePaths) + ); _parsers.push_back(make_unique(_engine)); _parsers.push_back(make_unique(_engine)); _parsers.push_back(make_unique(_engine)); @@ -145,17 +149,50 @@ void InputFileReader::addKeywords() void InputFileReader::process(const std::vector &lineElements) { const auto original_keyword = lineElements[0]; - const auto keyword = toLowerAndReplaceDashesCopy(original_keyword); + const auto keyword = toLowerAndReplaceDashesCopy(original_keyword); if (!_keywordFuncMap.contains(keyword)) - throw InputFileException(std::format( - "Invalid keyword \"{}\" at line {}", - original_keyword, - _lineNumber - )); + throw InputFileException( + std::format( + "Invalid keyword \"{}\" at line {}", + original_keyword, + _lineNumber + ) + ); pq::ParseFunc parserFunc = _keywordFuncMap[keyword]; - parserFunc(lineElements, _lineNumber); + + try + { + parserFunc(lineElements, _lineNumber); + } + catch (CustomException &exception) + { + exception.setLineNumber(_lineNumber); + throw; + } + catch (const std::invalid_argument &) + { + throw InputFileException( + std::format( + "Invalid value \"{}\" for keyword \"{}\"", + lineElements[2], + original_keyword + ), + _lineNumber + ); + } + catch (const std::out_of_range &) + { + throw InputFileException( + std::format( + "Value \"{}\" for keyword \"{}\" is out of range", + lineElements[2], + original_keyword + ), + _lineNumber + ); + } ++_keywordCountMap[keyword]; _keywordSetMap[keyword] = true; @@ -201,10 +238,18 @@ void InputFileReader::read() process(lineElements); }; - std::ranges::for_each( - getLineCommands(line, _lineNumber), - processInputCommand - ); + try + { + std::ranges::for_each( + getLineCommands(line, _lineNumber), + processInputCommand + ); + } + catch (CustomException &exception) + { + exception.setLineNumber(_lineNumber); + throw; + } ++_lineNumber; } @@ -252,16 +297,27 @@ void input::readJobType( const auto lineElements = splitString(command); if (!lineElements.empty() && "jobtype" == lineElements[0]) { - auto parser = GeneralInputParser(*engine); - parser.parseJobTypeForEngine(lineElements, lineNumber, engine); + GeneralInputParser::parseJobTypeForEngine( + lineElements, + lineNumber, + engine + ); jobtypeFound = true; } }; - std::ranges::for_each( - getLineCommands(line, lineNumber), - processInputCommand - ); + try + { + std::ranges::for_each( + getLineCommands(line, lineNumber), + processInputCommand + ); + } + catch (CustomException &exception) + { + exception.setLineNumber(lineNumber); + throw; + } ++lineNumber; } @@ -335,11 +391,13 @@ void input::processEqualSign(std::string &command, const size_t lineNumber) command.replace(equalSignPos, 1, " = "); else - throw InputFileException(std::format( - "Missing equal sign in command \"{}\" in line {}", - command, - lineNumber - )); + throw InputFileException( + std::format( + "Missing equal sign in command \"{}\" in line {}", + command, + lineNumber + ) + ); } /*************************** diff --git a/src/input/inputValidation.cpp b/src/input/inputValidation.cpp index d31a318f1..36026f8a7 100644 --- a/src/input/inputValidation.cpp +++ b/src/input/inputValidation.cpp @@ -20,18 +20,23 @@ ******************************************************************************/ +#include // for max +#include // for isfinite +#include // for format + +#include "constants/conversionFactors.hpp" +#include "engine.hpp" +#include "exceptions.hpp" // for InputFileException +#include "hessianSettings.hpp" // for HessianSettings #include "inputFileReader.hpp" - -#include // for format - -#include "exceptions.hpp" // for InputFileException -#include "hessianSettings.hpp" // for HessianSettings -#include "manostatSettings.hpp" // for ManostatSettings -#include "potentialSettings.hpp" // for PotentialSettings -#include "qmSettings.hpp" // for QMSettings -#include "settings.hpp" // for Settings -#include "thermostatSettings.hpp" // for ThermostatSettings -#include "timingsSettings.hpp" // for TimingsSettings +#include "manostatSettings.hpp" // for ManostatSettings +#include "optimizerSettings.hpp" // for OptimizerSettings +#include "potentialSettings.hpp" // for PotentialSettings +#include "qmSettings.hpp" // for QMSettings +#include "settings.hpp" // for Settings +#include "simulationBoxSettings.hpp" +#include "thermostatSettings.hpp" // for ThermostatSettings +#include "timingsSettings.hpp" // for TimingsSettings using namespace input; using namespace settings; @@ -46,9 +51,11 @@ using namespace customException; void InputFileReader::validateInputConfiguration() const { validateTimings(); + validateOptimizer(); validateQM(); validateThermostat(); validateManostat(); + validateCellList(); validateReactionFieldCoulomb(); validateRingPolymer(); } @@ -69,17 +76,41 @@ void InputFileReader::validateTimings() const (jobType == MM_HESSIAN && HessianSettings::optimizeBeforeHessian()); if (requiresNumberOfSteps && !getKeywordSet("nstep")) - throw UserInputException(std::format( - "Job type {} selected. Please set nstep in the input file.", - string(jobType) - )); + throw UserInputException( + std::format( + "Job type {} selected. Please set nstep in the input file.", + string(jobType) + ) + ); if (Settings::isMDJobType() && !getKeywordSet("timestep")) - throw UserInputException(std::format( - "Molecular Dynamics job type {} selected. Please set the " - "time step in the input file.", - string(jobType) - )); + throw UserInputException( + std::format( + "Molecular Dynamics job type {} selected. Please set the " + "time step in the input file.", + string(jobType) + ) + ); +} + +/** + * @brief validates settings used by active optimization jobs + * + * @throws UserInputException if a decay strategy has no decay or the learning + * rate bounds overlap + */ +void InputFileReader::validateOptimizer() const +{ + const auto optimizerActive = + Settings::isOptJobType() || + (Settings::getJobtype() == JobType::MM_HESSIAN && + HessianSettings::optimizeBeforeHessian()); + + if (!optimizerActive) + return; + + OptimizerSettings::validateLearningRateStrategy(); + OptimizerSettings::validateLearningRateBounds(); } /** @@ -93,10 +124,27 @@ void InputFileReader::validateQM() const if (!Settings::isQMActivated()) return; + if (!getKeywordSet("qm_prog")) + throw InputFileException( + "QM job selected but the \"qm_prog\" keyword has not been set" + ); + const auto qmMethod = QMSettings::getQMMethod(); if (qmMethod == QMMethod::ASEDFTBPLUS) { + if (QMSettings::getSlakosType() == SlakosType::NONE) + throw InputFileException( + "ASE-DFTB+ requires slakos to be 3ob, matsci, or custom" + ); + + if (QMSettings::getSlakosType() == SlakosType::CUSTOM && + !getKeywordSet("slakos_path")) + throw InputFileException( + "Custom Slater-Koster parameters require the " + "\"slakos_path\" keyword" + ); + auto useThirdOrder = QMSettings::useThirdOrderDftb(); if (QMSettings::getSlakosType() == SlakosType::THREEOB && @@ -117,34 +165,38 @@ void InputFileReader::validateQM() const "invalid." ); - if (qmMethod != QMMethod::MACE) - return; - - const auto modelType = QMSettings::getMaceModelType(); - const auto model = QMSettings::getMaceModel(); - const auto modelPathSet = getKeywordSet("mace_model_path"); + if (qmMethod == QMMethod::MACE) + { + const auto modelType = QMSettings::getMaceModelType(); + const auto model = QMSettings::getMaceModel(); + const auto modelPathSet = getKeywordSet("mace_model_path"); - if (modelType != MaceModelType::MACE_MP && model != MaceModel::SMALL && - model != MaceModel::MEDIUM && model != MaceModel::LARGE) - throw InputFileException(std::format( - "The '{}' model size is only compatible with the '{}' model type.", - string(model), - string(MaceModelType::MACE_MP) - )); + if (modelType != MaceModelType::MACE_MP && model != MaceModel::SMALL && + model != MaceModel::MEDIUM && model != MaceModel::LARGE) + throw InputFileException( + std::format( + "The '{}' model size is only compatible with the '{}' " + "model " + "type.", + string(model), + string(MaceModelType::MACE_MP) + ) + ); - if (model == MaceModel::CUSTOM && !modelPathSet) - throw InputFileException( - "You have requested a custom MACE model but haven't provided a " - "MACE model path." - "This setup is invalid." - ); + if (model == MaceModel::CUSTOM && !modelPathSet) + throw InputFileException( + "You have requested a custom MACE model but haven't provided " + "a MACE model path." + "This setup is invalid." + ); - if (model != MaceModel::CUSTOM && modelPathSet) - throw InputFileException( - "You have set a custom MACE model path without requesting a custom " - "mace model size." - "This setup is invalid." - ); + if (model != MaceModel::CUSTOM && modelPathSet) + throw InputFileException( + "You have set a custom MACE model path without requesting a " + "custom mace model size." + "This setup is invalid." + ); + } } /** @@ -155,52 +207,155 @@ void InputFileReader::validateQM() const */ void InputFileReader::validateThermostat() const { - const auto thermostatType = ThermostatSettings::getThermostatType(); + const auto thermostatType = ThermostatSettings::getThermostatType(); + const auto targetTempDefined = getKeywordSet("temp"); + const auto startTempDefined = getKeywordSet("start_temp"); + const auto endTempDefined = getKeywordSet("end_temp"); if (thermostatType != ThermostatType::NONE) { - const auto targetTempDefined = getKeywordSet("temp"); - const auto endTempDefined = getKeywordSet("end_temp"); - if (!targetTempDefined && !endTempDefined) - throw InputFileException(std::format( - "Target or end temperature not set for {} thermostat", - string(thermostatType) - )); + throw InputFileException( + std::format( + "Target or end temperature not set for {} thermostat", + string(thermostatType) + ) + ); if (targetTempDefined && endTempDefined) - throw InputFileException(std::format( - "Both target and end temperature set for {} thermostat. They " - "are mutually exclusive as they are treated as synonyms", - string(thermostatType) - )); + throw InputFileException( + std::format( + "Both target and end temperature set for {} thermostat. " + "They " + "are mutually exclusive as they are treated as synonyms", + string(thermostatType) + ) + ); } - if (!getKeywordSet("start_temp")) + if (endTempDefined) + ThermostatSettings::setTargetTemperature( + ThermostatSettings::getEndTemperature() + ); + + if (startTempDefined) + ThermostatSettings::setActualTargetTemperature( + ThermostatSettings::getStartTemperature() + ); + + if (SimulationBoxSettings::getInitializeVelocities() != + InitVelocities::FALSE && + !targetTempDefined && !startTempDefined && !endTempDefined) + throw InputFileException( + "Initializing velocities requires temp, start_temp, or end_temp" + ); + + if (Settings::isMDJobType() && + (thermostatType == ThermostatType::BERENDSEN || + thermostatType == ThermostatType::VELOCITY_RESCALING)) + { + const auto timeStep = TimingsSettings::getTimeStep(); + const auto relaxationTime = + ThermostatSettings::getRelaxationTime() * constants::_PS_TO_FS_; + + if (timeStep > relaxationTime) + throw InputFileException( + "The timestep must not exceed the thermostat relaxation time" + ); + } + + if (thermostatType == ThermostatType::LANGEVIN) + { + const auto maxTemperature = std::max( + ThermostatSettings::getTargetTemperature(), + ThermostatSettings::getActualTargetTemperature() + ); + const auto unitConversion = constants::_M2_TO_ANGSTROM2_ * + constants::_KG_TO_GRAM_ / + constants::_FS_TO_S_; + const auto conversionFactor = + constants::_UNIVERSAL_GAS_CONSTANT_ * unitConversion; + const auto sigmaSquared = 4.0 * ThermostatSettings::getFriction() * + conversionFactor * maxTemperature / + TimingsSettings::getTimeStep(); + + if (!std::isfinite(sigmaSquared)) + throw InputFileException( + "Langevin thermostat parameters produce a non-finite " + "random-force scale" + ); + } + + if (thermostatType == ThermostatType::NOSE_HOOVER) + { + if (getKeywordSet("temp") && + ThermostatSettings::getTargetTemperature() <= 0.0) + throw InputFileException( + "Nose-Hoover target temperature must be greater than zero" + ); + + if (getKeywordSet("end_temp") && + ThermostatSettings::getEndTemperature() <= 0.0) + throw InputFileException( + "Nose-Hoover end temperature must be greater than zero" + ); + + if (getKeywordSet("start_temp") && + ThermostatSettings::getStartTemperature() <= 0.0) + throw InputFileException( + "Nose-Hoover start temperature must be greater than zero" + ); + } + + if (!startTempDefined) return; const auto totalSteps = TimingsSettings::getNumberOfSteps(); const auto rampSteps = ThermostatSettings::getTemperatureRampSteps(); if (rampSteps > totalSteps) - throw InputFileException(std::format( - "Number of total simulation steps {} is smaller than the " - "number of temperature ramping steps {}", - totalSteps, - rampSteps - )); + throw InputFileException( + std::format( + "Number of total simulation steps {} is smaller than the " + "number of temperature ramping steps {}", + totalSteps, + rampSteps + ) + ); const auto effectiveRampSteps = rampSteps == 0 ? totalSteps : rampSteps; - const auto frequency = - ThermostatSettings::getTemperatureRampFrequency(); + const auto frequency = ThermostatSettings::getTemperatureRampFrequency(); if (frequency > effectiveRampSteps) - throw InputFileException(std::format( - "Temperature ramp frequency {} is larger than the number of " - "ramping steps {}", - frequency, - effectiveRampSteps - )); + throw InputFileException( + std::format( + "Temperature ramp frequency {} is larger than the number of " + "ramping steps {}", + frequency, + effectiveRampSteps + ) + ); +} + +/** + * @brief validates cell-list dependencies + * + * @throws InputFileException if an active cell list has no positive cutoff + */ +void InputFileReader::validateCellList() const +{ + if (!_engine.isCellListActivated()) + return; + + if (Settings::isQMOnlyActivated()) + throw InputFileException( + "Cell lists are not available for pure QM simulations" + ); + + if (PotentialSettings::getCoulombRadiusCutOff() <= 0.0) + throw InputFileException( + "An active cell list requires rcoulomb to be greater than zero" + ); } /** @@ -212,11 +367,23 @@ void InputFileReader::validateManostat() const { const auto manostatType = ManostatSettings::getManostatType(); - if (manostatType != ManostatType::NONE && !getKeywordSet("pressure")) - throw InputFileException(std::format( - "Pressure not set for {} manostat", - string(manostatType) - )); + if (manostatType == ManostatType::NONE) + return; + + if (!getKeywordSet("pressure")) + throw InputFileException( + std::format( + "Pressure not set for {} manostat", + string(manostatType) + ) + ); + + const auto relaxationTime = + ManostatSettings::getTauManostat() * constants::_PS_TO_FS_; + if (TimingsSettings::getTimeStep() > relaxationTime) + throw InputFileException( + "The timestep must not exceed the manostat relaxation time" + ); } /** diff --git a/src/input/parameterFileReader/CMakeLists.txt b/src/input/parameterFileReader/CMakeLists.txt index e633cb527..a7eedfb8f 100644 --- a/src/input/parameterFileReader/CMakeLists.txt +++ b/src/input/parameterFileReader/CMakeLists.txt @@ -24,5 +24,5 @@ target_link_libraries(parameterFileReader ) install(TARGETS parameterFileReader - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/input/parameterFileReader/parameterFileReader.cpp b/src/input/parameterFileReader/parameterFileReader.cpp index 116132157..85e056730 100644 --- a/src/input/parameterFileReader/parameterFileReader.cpp +++ b/src/input/parameterFileReader/parameterFileReader.cpp @@ -68,6 +68,8 @@ ParameterFileReader::ParameterFileReader( _parameterFileSections.push_back(make_unique()); } +ParameterFileReader::~ParameterFileReader() = default; + /** * @brief determines which section of the parameter file the header line belongs * to @@ -219,4 +221,4 @@ std::vector> &ParameterFileReader:: const std::string &ParameterFileReader::getFilename() const { return _fileName; -} \ No newline at end of file +} diff --git a/src/input/restartFileReader/CMakeLists.txt b/src/input/restartFileReader/CMakeLists.txt index ef29fb85e..05b886c9f 100644 --- a/src/input/restartFileReader/CMakeLists.txt +++ b/src/input/restartFileReader/CMakeLists.txt @@ -20,13 +20,6 @@ target_link_libraries(restartFileReader linearAlgebra ) -if(BUILD_WITH_TESTS) - target_link_libraries(restartFileReader - PUBLIC - gtest - ) -endif() - install(TARGETS restartFileReader - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ -) \ No newline at end of file + DESTINATION lib +) diff --git a/src/input/ringPolymerRestartFileReader/CMakeLists.txt b/src/input/ringPolymerRestartFileReader/CMakeLists.txt index e592071a0..44a629cdc 100644 --- a/src/input/ringPolymerRestartFileReader/CMakeLists.txt +++ b/src/input/ringPolymerRestartFileReader/CMakeLists.txt @@ -17,5 +17,5 @@ target_link_libraries(ringPolymerRestartFileReader ) install(TARGETS ringPolymerRestartFileReader - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/input/topologyFileReader/CMakeLists.txt b/src/input/topologyFileReader/CMakeLists.txt index c8847c377..9a6d71741 100644 --- a/src/input/topologyFileReader/CMakeLists.txt +++ b/src/input/topologyFileReader/CMakeLists.txt @@ -24,5 +24,5 @@ target_link_libraries(topologyFileReader ) install(TARGETS topologyFileReader - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/integrator/CMakeLists.txt b/src/integrator/CMakeLists.txt index f3c5a6e79..3d4349f99 100644 --- a/src/integrator/CMakeLists.txt +++ b/src/integrator/CMakeLists.txt @@ -43,5 +43,5 @@ if(BUILD_WITH_KOKKOS) endif() install(TARGETS integrator - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/intraNonBonded/CMakeLists.txt b/src/intraNonBonded/CMakeLists.txt index 16fe6b43f..c39bc9861 100644 --- a/src/intraNonBonded/CMakeLists.txt +++ b/src/intraNonBonded/CMakeLists.txt @@ -22,5 +22,5 @@ target_link_libraries(intraNonBonded ) install(TARGETS intraNonBonded - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/kernels/CMakeLists.txt b/src/kernels/CMakeLists.txt index 8c5413598..a2f9b8799 100644 --- a/src/kernels/CMakeLists.txt +++ b/src/kernels/CMakeLists.txt @@ -15,5 +15,5 @@ target_link_libraries(kernels ) install(TARGETS kernels - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/linearAlgebra/CMakeLists.txt b/src/linearAlgebra/CMakeLists.txt index e28df9e5a..01ad4dc55 100644 --- a/src/linearAlgebra/CMakeLists.txt +++ b/src/linearAlgebra/CMakeLists.txt @@ -20,5 +20,5 @@ target_link_libraries(linearAlgebra ) install(TARGETS linearAlgebra - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/manostat/CMakeLists.txt b/src/manostat/CMakeLists.txt index f82185717..d4c1ca04b 100644 --- a/src/manostat/CMakeLists.txt +++ b/src/manostat/CMakeLists.txt @@ -22,5 +22,5 @@ target_link_libraries(manostat ) install(TARGETS manostat - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/maxwellBoltzmann/CMakeLists.txt b/src/maxwellBoltzmann/CMakeLists.txt index 1b0bc3ab5..80383277c 100644 --- a/src/maxwellBoltzmann/CMakeLists.txt +++ b/src/maxwellBoltzmann/CMakeLists.txt @@ -24,5 +24,5 @@ target_link_libraries(maxwellBoltzmann ) install(TARGETS maxwellBoltzmann - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/mpi/CMakeLists.txt b/src/mpi/CMakeLists.txt index 819bbd614..1da5316c0 100644 --- a/src/mpi/CMakeLists.txt +++ b/src/mpi/CMakeLists.txt @@ -15,5 +15,5 @@ if(BUILD_WITH_MPI) endif() install(TARGETS PQ_mpi - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/opt/CMakeLists.txt b/src/opt/CMakeLists.txt index 80ba455e7..cd0f369e1 100644 --- a/src/opt/CMakeLists.txt +++ b/src/opt/CMakeLists.txt @@ -18,7 +18,7 @@ target_link_libraries(optimization ) install(TARGETS optimization - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) add_subdirectory(learningRateStrategy) diff --git a/src/opt/convergence/CMakeLists.txt b/src/opt/convergence/CMakeLists.txt index 78c953af5..88d5eb649 100644 --- a/src/opt/convergence/CMakeLists.txt +++ b/src/opt/convergence/CMakeLists.txt @@ -17,5 +17,5 @@ target_link_libraries(convergence ) install(TARGETS convergence - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/opt/evaluator/CMakeLists.txt b/src/opt/evaluator/CMakeLists.txt index b6c50f13e..24c13dd8b 100644 --- a/src/opt/evaluator/CMakeLists.txt +++ b/src/opt/evaluator/CMakeLists.txt @@ -23,5 +23,5 @@ target_link_libraries(evaluator ) install(TARGETS evaluator - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) diff --git a/src/opt/learningRateStrategy/CMakeLists.txt b/src/opt/learningRateStrategy/CMakeLists.txt index 37bf82052..59e595b75 100644 --- a/src/opt/learningRateStrategy/CMakeLists.txt +++ b/src/opt/learningRateStrategy/CMakeLists.txt @@ -18,5 +18,5 @@ target_link_libraries(learningRateStrategy ) install(TARGETS learningRateStrategy - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/opt/optimizer/CMakeLists.txt b/src/opt/optimizer/CMakeLists.txt index 6a7b092ca..e098a94a8 100644 --- a/src/opt/optimizer/CMakeLists.txt +++ b/src/opt/optimizer/CMakeLists.txt @@ -25,5 +25,5 @@ target_link_libraries(optimizer ) install(TARGETS optimizer - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) diff --git a/src/output/CMakeLists.txt b/src/output/CMakeLists.txt index f9efd0f93..66bd682f9 100644 --- a/src/output/CMakeLists.txt +++ b/src/output/CMakeLists.txt @@ -39,15 +39,8 @@ target_link_libraries(output thermostat ) -if(BUILD_WITH_TESTS) - target_link_libraries(output - PUBLIC - gtest - ) -endif() - install(TARGETS output - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) -add_subdirectory(references) \ No newline at end of file +add_subdirectory(references) diff --git a/src/output/references/CMakeLists.txt b/src/output/references/CMakeLists.txt index 1834d03ef..b425d1b7c 100644 --- a/src/output/references/CMakeLists.txt +++ b/src/output/references/CMakeLists.txt @@ -23,5 +23,10 @@ add_custom_command(TARGET references POST_BUILD ${CMAKE_SOURCE_DIR}/include/output/references/referenceFiles $/referenceFiles) install(TARGETS references - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ -) \ No newline at end of file + DESTINATION lib +) + +install( + DIRECTORY ${CMAKE_SOURCE_DIR}/include/output/references/referenceFiles/ + DESTINATION share/PQ/references +) diff --git a/src/output/references/referencesOutput.cpp b/src/output/references/referencesOutput.cpp index 9feace5a6..1b91f6a60 100644 --- a/src/output/references/referencesOutput.cpp +++ b/src/output/references/referencesOutput.cpp @@ -22,16 +22,42 @@ #include "referencesOutput.hpp" -#include // for for_each -#include // for fstream -#include // for string - -#include "references.hpp" // for ReferencesOutput +#include // for for_each +#include // for path, is_directory, is_regular_file +#include // for format +#include // for fstream +#include // for runtime_error +#include // for string +#include // for vector + +#include "executablePath.hpp" // for executablePath #include "outputFileSettings.hpp" // for OutputFileSettings +#include "references.hpp" // for ReferencesOutput using references::ReferencesOutput; using namespace settings; +namespace +{ + std::filesystem::path referenceFilesPath() + { + const auto executable = utilities::executablePath(); + if (!executable.empty()) + { + const auto installedPath = executable.parent_path().parent_path() / + "share" / "PQ" / "references"; + if (std::filesystem::is_directory(installedPath)) + return installedPath; + } + + const auto buildPath = std::filesystem::path(REFERENCES_PATH_); + if (std::filesystem::is_directory(buildPath)) + return buildPath; + + throw std::runtime_error("PQ reference data could not be found"); + } +} // namespace + /** * @brief writes the references file * @@ -39,14 +65,46 @@ using namespace settings; */ void ReferencesOutput::writeReferencesFile() { - const auto filename = OutputFileSettings::getRefFileName(); + const auto sourceDirectory = referenceFilesPath(); + const auto filename = OutputFileSettings::getRefFileName(); + + auto referenceFileNames = std::vector{_PQ_FILE_}; + referenceFileNames.insert( + referenceFileNames.end(), + _referenceFileNames.begin(), + _referenceFileNames.end() + ); + referenceFileNames.emplace_back( + static_cast(_PQ_FILE_) + ".bib" + ); + referenceFileNames.insert( + referenceFileNames.end(), + _bibtexFileNames.begin(), + _bibtexFileNames.end() + ); + + for (const auto &referenceFileName : referenceFileNames) + { + const auto path = sourceDirectory / referenceFileName; + if (!std::filesystem::is_regular_file(path)) + throw std::runtime_error( + std::format( + "PQ reference file \"{}\" could not be found", + path.string() + ) + ); + } std::ofstream fp(filename); + if (!fp.is_open()) + throw std::runtime_error( + std::format("Could not open reference output file \"{}\"", filename) + ); - auto printReference = [&fp](const std::string &referenceFileName) + auto printReference = + [&fp, &sourceDirectory](const std::string &referenceFileName) { - const auto filepath = _referenceFilesPath + "/" + referenceFileName; - std::ifstream referenceFile(filepath); + std::ifstream referenceFile(sourceDirectory / referenceFileName); std::string line; while (getline(referenceFile, line)) fp << line << '\n'; @@ -81,6 +139,13 @@ void ReferencesOutput::writeReferencesFile() std::ranges::for_each(_bibtexFileNames, printReference); fp.close(); + if (!fp) + throw std::runtime_error( + std::format( + "Could not write reference output file \"{}\"", + filename + ) + ); } /** @@ -92,4 +157,4 @@ void ReferencesOutput::addReferenceFile(const std::string &referenceFileName) { _referenceFileNames.insert(referenceFileName); _bibtexFileNames.insert(referenceFileName + ".bib"); -} \ No newline at end of file +} diff --git a/src/physicalData/CMakeLists.txt b/src/physicalData/CMakeLists.txt index 23ef1618f..b7ced7991 100644 --- a/src/physicalData/CMakeLists.txt +++ b/src/physicalData/CMakeLists.txt @@ -18,5 +18,5 @@ target_link_libraries(physicalData ) install(TARGETS physicalData - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/potential/CMakeLists.txt b/src/potential/CMakeLists.txt index 481389b31..2a855f89a 100644 --- a/src/potential/CMakeLists.txt +++ b/src/potential/CMakeLists.txt @@ -50,7 +50,7 @@ if(BUILD_WITH_KOKKOS) endif() install(TARGETS potential - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) add_subdirectory(coulomb) diff --git a/src/potential/coulomb/CMakeLists.txt b/src/potential/coulomb/CMakeLists.txt index a902c029b..c7810b21b 100644 --- a/src/potential/coulomb/CMakeLists.txt +++ b/src/potential/coulomb/CMakeLists.txt @@ -12,5 +12,5 @@ target_include_directories(coulombPotential ) install(TARGETS coulombPotential - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/potential/coulomb_kokkos/CMakeLists.txt b/src/potential/coulomb_kokkos/CMakeLists.txt index 33e537a1d..d4bf9dff3 100644 --- a/src/potential/coulomb_kokkos/CMakeLists.txt +++ b/src/potential/coulomb_kokkos/CMakeLists.txt @@ -18,5 +18,5 @@ target_link_libraries(coulombPotential_kokkos ) install(TARGETS coulombPotential_kokkos - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/potential/nonCoulomb/CMakeLists.txt b/src/potential/nonCoulomb/CMakeLists.txt index 8966abb7d..96f1ab210 100644 --- a/src/potential/nonCoulomb/CMakeLists.txt +++ b/src/potential/nonCoulomb/CMakeLists.txt @@ -24,5 +24,5 @@ target_link_libraries(nonCoulombPotential ) install(TARGETS nonCoulombPotential - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/potential/nonCoulomb_kokkos/CMakeLists.txt b/src/potential/nonCoulomb_kokkos/CMakeLists.txt index a2dedaf06..2d116618f 100644 --- a/src/potential/nonCoulomb_kokkos/CMakeLists.txt +++ b/src/potential/nonCoulomb_kokkos/CMakeLists.txt @@ -19,5 +19,5 @@ target_link_libraries(nonCoulombPotential_kokkos ) install(TARGETS nonCoulombPotential_kokkos - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 2d83b1cac..967efbaa9 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -15,5 +15,5 @@ target_link_libraries(pq_python ) install(TARGETS pq_python - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/randomNumberGenerator/CMakeLists.txt b/src/randomNumberGenerator/CMakeLists.txt index 8c507f972..2f32e5cae 100644 --- a/src/randomNumberGenerator/CMakeLists.txt +++ b/src/randomNumberGenerator/CMakeLists.txt @@ -13,5 +13,5 @@ target_link_libraries(randomNumberGenerator ) install(TARGETS randomNumberGenerator - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/resetKinetics/CMakeLists.txt b/src/resetKinetics/CMakeLists.txt index 275f9db58..a46712a6c 100644 --- a/src/resetKinetics/CMakeLists.txt +++ b/src/resetKinetics/CMakeLists.txt @@ -17,5 +17,5 @@ target_link_libraries(resetKinetics ) install(TARGETS resetKinetics - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/resetKinetics/resetKinetics.cpp b/src/resetKinetics/resetKinetics.cpp index acc238269..6c609f85b 100644 --- a/src/resetKinetics/resetKinetics.cpp +++ b/src/resetKinetics/resetKinetics.cpp @@ -27,6 +27,8 @@ #include // for size_t #include "constants/conversionFactors.hpp" // for _FS_TO_S_, _S_TO_FS_ +#include "exceptions.hpp" // for UserInputException +#include "mathUtilities.hpp" // for isZero #include "physicalData.hpp" // for PhysicalData #include "simulationBox.hpp" // for SimulationBox #include "staticMatrix.hpp" // for operator*, operator+= @@ -38,7 +40,9 @@ using namespace linearAlgebra; using namespace physicalData; using namespace simulationBox; using namespace constants; +using namespace customException; using namespace settings; +using namespace utilities; /** * @brief Construct a new Reset Kinetics:: Reset Kinetics object @@ -129,7 +133,27 @@ void ResetKinetics::reset( void ResetKinetics::resetTemperature(SimulationBox &simBox) { const auto targetTemp = ThermostatSettings::getActualTargetTemperature(); - const auto lambda = ::sqrt(targetTemp / _temperature); + + if (isZero(targetTemp)) + { + std::ranges::for_each( + simBox.getAtoms(), + [](auto &atom) { atom->scaleVelocity(0.0); } + ); + + _temperature = simBox.calculateTemperature(); + _momentum = simBox.calculateMomentum(); + _angularMomentum = simBox.calculateAngularMomentum(_momentum); + return; + } + + if (isZero(_temperature)) + throw UserInputException( + "Cannot rescale a zero-temperature system to a positive target " + "temperature. Initialize velocities first." + ); + + const auto lambda = ::sqrt(targetTemp / _temperature); std::ranges::for_each( simBox.getAtoms(), @@ -323,4 +347,4 @@ size_t ResetKinetics::getFrequencyMomentumReset() const size_t ResetKinetics::getNStepsForcesReset() const { return _nStepsForcesReset; -} \ No newline at end of file +} diff --git a/src/settings/CMakeLists.txt b/src/settings/CMakeLists.txt index 31644c060..fca93b425 100644 --- a/src/settings/CMakeLists.txt +++ b/src/settings/CMakeLists.txt @@ -30,5 +30,5 @@ target_link_libraries(settings ) install(TARGETS settings - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) diff --git a/src/settings/optimizerSettings.cpp b/src/settings/optimizerSettings.cpp index 4eb78d42f..6d6fd538d 100644 --- a/src/settings/optimizerSettings.cpp +++ b/src/settings/optimizerSettings.cpp @@ -22,10 +22,14 @@ #include "optimizerSettings.hpp" +#include + +#include "exceptions.hpp" #include "stringUtilities.hpp" // for toLowerCopy using namespace settings; using namespace utilities; +using namespace customException; /** * @brief returns the optimizer as string @@ -204,6 +208,63 @@ void OptimizerSettings::setMaxLearningRate(const double maxLearningRate) _maxLearningRate = maxLearningRate; } +/***************************** + * * + * validation helper methods * + * * + *****************************/ + +/** + * @brief validates the selected learning rate strategy + */ +void OptimizerSettings::validateLearningRateStrategy() +{ + const auto strategy = getLearningRateStrategy(); + + if (strategy == LREnum::LINESEARCH_WOLFE) + throw UserInputException( + "The Wolfe line search learning rate strategy is not yet " + "implemented" + ); + + if (strategy == LREnum::NONE) + throw UserInputException( + "In order to run the optimizer, you need to specify a learning " + "rate strategy." + ); + + const auto needsDecay = strategy == LREnum::CONSTANT_DECAY || + strategy == LREnum::EXPONENTIAL_DECAY; + + if (needsDecay && !getLearningRateDecay().has_value()) + throw UserInputException( + std::format( + "The {} learning rate strategy requires learning-rate-decay.", + strategy == LREnum::CONSTANT_DECAY ? "constant-decay" + : "exponential-decay" + ) + ); +} + +/** + * @brief validates the configured learning rate bounds + */ +void OptimizerSettings::validateLearningRateBounds() +{ + const auto minLR = getMinLearningRate(); + const auto maxLR = getMaxLearningRate(); + + if (maxLR.has_value() && minLR >= maxLR.value()) + throw UserInputException( + std::format( + "The minimum learning rate {} is greater or equal to the " + "maximum learning rate {}, which is not allowed.", + minLR, + maxLR.value() + ) + ); +} + /*************************** * * * standard getter methods * @@ -273,4 +334,4 @@ std::optional OptimizerSettings::getLearningRateDecay() std::optional OptimizerSettings::getMaxLearningRate() { return _maxLearningRate; -} \ No newline at end of file +} diff --git a/src/settings/qmSettings.cpp b/src/settings/qmSettings.cpp index c8595f5ce..6415f9313 100644 --- a/src/settings/qmSettings.cpp +++ b/src/settings/qmSettings.cpp @@ -22,9 +22,11 @@ #include "qmSettings.hpp" +#include #include // for std::format -#include "exceptions.hpp" // for customException +#include "exceptions.hpp" // for customException +#include "executablePath.hpp" #include "stringUtilities.hpp" // for toLowerCopy using settings::MaceMode; @@ -156,19 +158,28 @@ std::string settings::string(const SlakosType slakos) /** * @brief builds the file path for a built-in SLAKOS set (3ob/matsci) * - * @details __SLAKOS_DIR__ is only defined when building with ASE support (see - * .cmake/slakos.cmake). In a build without ASE, requesting a built-in set is - * reported as a user input error instead of failing to compile. + * @details installed data next to the executable is preferred. The fetched + * build-tree data is used while running an uninstalled ASE build. */ static std::string builtinSlakosPath([[maybe_unused]] const SlakosType type) { #ifdef __SLAKOS_DIR__ - return __SLAKOS_DIR__ + settings::string(type) + "/skfiles/"; + const auto executable = utilities::executablePath(); + if (!executable.empty()) + { + const auto installedPath = executable.parent_path().parent_path() / + "share" / "PQ" / "slakos" / + settings::string(type) / "skfiles"; + if (std::filesystem::is_directory(installedPath)) + return installedPath.string() + + std::filesystem::path::preferred_separator; + } + + const auto buildPath = std::filesystem::path(__SLAKOS_DIR__) / + settings::string(type) / "skfiles"; + return buildPath.string() + std::filesystem::path::preferred_separator; #else - throw InputFileException( - "Built-in SLAKOS sets (3ob/matsci) require building PQ with " - "-DBUILD_WITH_ASE=On" - ); + return ""; #endif } @@ -392,11 +403,14 @@ void QMSettings::setMaceMode(const std::string_view &mode) _maceMode = FAST; else - throw UserInputException(std::format( - "Unknown mace_mode \"{}\". Valid values are \"accurate\" (exact " - "e3nn reference) or \"fast\" (cuequivariance-accelerated).", - mode - )); + throw UserInputException( + std::format( + "Unknown mace_mode \"{}\". Valid values are \"accurate\" " + "(exact " + "e3nn reference) or \"fast\" (cuequivariance-accelerated).", + mode + ) + ); } /** @@ -761,4 +775,4 @@ std::string QMSettings::getFennolModelPath() { return _fennolModelPath; } * * @return bool */ -bool QMSettings::useGPUPreprocessing() { return _useGPUPreprocessing; } \ No newline at end of file +bool QMSettings::useGPUPreprocessing() { return _useGPUPreprocessing; } diff --git a/src/setup/CMakeLists.txt b/src/setup/CMakeLists.txt index e13b3733d..ba335b1fa 100644 --- a/src/setup/CMakeLists.txt +++ b/src/setup/CMakeLists.txt @@ -52,13 +52,6 @@ target_link_libraries(setup optimization ) -if(BUILD_WITH_TESTS) - target_link_libraries(setup - PUBLIC - gtest - ) -endif() - if(BUILD_WITH_MPI) target_link_libraries(setup PRIVATE @@ -76,5 +69,5 @@ if(BUILD_WITH_PYTHON_BINDINGS) endif() install(TARGETS setup - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) diff --git a/src/setup/optimizerSetup.cpp b/src/setup/optimizerSetup.cpp index 6397aa6dc..2571374bc 100644 --- a/src/setup/optimizerSetup.cpp +++ b/src/setup/optimizerSetup.cpp @@ -154,6 +154,8 @@ pq::SharedLearningRate OptimizerSetup::setupLearningRateStrategy() const auto alpha_0 = OptimizerSettings::getInitialLearningRate(); const auto lrStrategy = OptimizerSettings::getLearningRateStrategy(); + OptimizerSettings::validateLearningRateStrategy(); + switch (lrStrategy) { using enum LREnum; @@ -162,15 +164,8 @@ pq::SharedLearningRate OptimizerSetup::setupLearningRateStrategy() case CONSTANT_DECAY: { - const auto alphaDecay = OptimizerSettings::getLearningRateDecay(); - - if (!alphaDecay.has_value()) - throw UserInputException( - "You need to specify a learning rate decay factor for the " - "constant decay learning rate strategy" - ); - - const auto alphaDecayValue = alphaDecay.value(); + const auto alphaDecayValue = + OptimizerSettings::getLearningRateDecay().value(); const auto alphaFreq = OptimizerSettings::getLRUpdateFrequency(); return std::make_shared( @@ -182,15 +177,8 @@ pq::SharedLearningRate OptimizerSetup::setupLearningRateStrategy() case EXPONENTIAL_DECAY: { - const auto alphaDecay = OptimizerSettings::getLearningRateDecay(); - - if (!alphaDecay.has_value()) - throw UserInputException( - "You need to specify a learning rate decay factor for the " - "constant decay learning rate strategy" - ); - - const auto alphaDecayValue = alphaDecay.value(); + const auto alphaDecayValue = + OptimizerSettings::getLearningRateDecay().value(); const auto alphaFreq = OptimizerSettings::getLRUpdateFrequency(); return std::make_shared( @@ -201,13 +189,6 @@ pq::SharedLearningRate OptimizerSetup::setupLearningRateStrategy() } case LINESEARCH_WOLFE: - { - throw UserInputException( - "The Wolfe line search learning rate strategy is not yet " - "implemented" - ); - } - case NONE: break; } @@ -227,13 +208,7 @@ void OptimizerSetup::setupMinMaxLR(pq::SharedLearningRate &lrStrategy) const auto minLR = OptimizerSettings::getMinLearningRate(); const auto maxLR = OptimizerSettings::getMaxLearningRate(); - if (maxLR.has_value() && minLR >= maxLR.value()) - throw UserInputException(std::format( - "The minimum learning rate {} is greater or equal to the " - "maximum learning rate {}, which is not allowed.", - minLR, - maxLR.value() - )); + OptimizerSettings::validateLearningRateBounds(); lrStrategy->setMinLearningRate(minLR); lrStrategy->setMaxLearningRate(maxLR); @@ -419,4 +394,4 @@ void OptimizerSetup::writeSetupInfo() const logOutput.writeSetupInfo(rmsForceMsg); logOutput.writeEmptyLine(); -} \ No newline at end of file +} diff --git a/src/setup/thermostatSetup.cpp b/src/setup/thermostatSetup.cpp index c9f4277c7..c2b8903a2 100644 --- a/src/setup/thermostatSetup.cpp +++ b/src/setup/thermostatSetup.cpp @@ -67,7 +67,7 @@ void setup::setupThermostat(Engine &engine) * * @param engine */ -ThermostatSetup::ThermostatSetup(MDEngine &engine) : _engine(engine){}; +ThermostatSetup::ThermostatSetup(MDEngine &engine) : _engine(engine) {} /** * @brief setup thermostat @@ -188,12 +188,14 @@ void ThermostatSetup::setupNoseHooverThermostat() auto fillChi = [&thermostat, nhChainLength](const auto pair) { if (pair.first > nhChainLength) - throw InputFileException(std::format( - "Chi index {} is larger than the number of nose hoover " - "chains {}", - pair.first, - nhChainLength - )); + throw InputFileException( + std::format( + "Chi index {} is larger than the number of nose hoover " + "chains {}", + pair.first, + nhChainLength + ) + ); thermostat.setChi(size_t(pair.first - 1), pair.second); }; @@ -251,7 +253,8 @@ void ThermostatSetup::setupTemperatureRamp() const auto targetTemp = ThermostatSettings::getTargetTemperature(); const auto tempDelta = targetTemp - startTemp; - const auto tempIncrease = tempDelta / double(steps) * frequency; + const auto updates = (steps + frequency - 1) / frequency; + const auto tempIncrease = tempDelta / double(updates); _engine.getThermostat().setTemperatureIncrease(tempIncrease); _engine.getThermostat().setTemperatureRampingFrequency(frequency); diff --git a/src/simulationBox/CMakeLists.txt b/src/simulationBox/CMakeLists.txt index 408308a05..0bb23b9ab 100644 --- a/src/simulationBox/CMakeLists.txt +++ b/src/simulationBox/CMakeLists.txt @@ -60,5 +60,5 @@ target_link_libraries(simulationBox ) install(TARGETS simulationBox - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) diff --git a/src/simulationBox/celllist.cpp b/src/simulationBox/celllist.cpp index e3c6d422b..a14b31ad5 100644 --- a/src/simulationBox/celllist.cpp +++ b/src/simulationBox/celllist.cpp @@ -313,7 +313,25 @@ Vec3Dul CellList::getCellIndexOfAtom( * @brief resize cells * */ -void CellList::resizeCells() { _cells.resize(prod(_nCells)); } +void CellList::resizeCells() +{ + auto numberOfCells = size_t{1}; + + for (size_t dimension = 0; dimension < 3; ++dimension) + { + if (0 == _nCells[dimension]) + throw CellListException("Number of cells must be positive"); + + if (_nCells[dimension] > _cells.max_size() / numberOfCells) + throw CellListException( + "Number of cells exceeds the supported size" + ); + + numberOfCells *= _nCells[dimension]; + } + + _cells.resize(numberOfCells); +} /** * @brief add cell to cell list diff --git a/src/thermostat/CMakeLists.txt b/src/thermostat/CMakeLists.txt index 7acf07989..53891332d 100644 --- a/src/thermostat/CMakeLists.txt +++ b/src/thermostat/CMakeLists.txt @@ -22,5 +22,5 @@ target_link_libraries(thermostat ) install(TARGETS thermostat - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/src/thermostat/berendsenThermostat.cpp b/src/thermostat/berendsenThermostat.cpp index 5f2c51361..2377d8f0a 100644 --- a/src/thermostat/berendsenThermostat.cpp +++ b/src/thermostat/berendsenThermostat.cpp @@ -24,6 +24,7 @@ #include // for sqrt +#include "exceptions.hpp" // for UserInputException #include "mathUtilities.hpp" // for isZero #include "physicalData.hpp" // for PhysicalData #include "simulationBox.hpp" // for SimulationBox @@ -31,6 +32,7 @@ #include "timingsSettings.hpp" // for TimingsSettings using thermostat::BerendsenThermostat; +using namespace customException; using namespace settings; using namespace simulationBox; using namespace physicalData; @@ -69,13 +71,16 @@ void BerendsenThermostat::applyThermostat( _temperature = data.getTemperature(); - // If the kinetic energy is (approximately) zero, there is nothing to - // thermostat: dividing by _temperature would NaN all velocities - // (1 / 0 -> Inf, then vel * Inf = NaN when vel is 0). Skip silently. if (isZero(_temperature)) { stopTimingsSection("Berendsen"); - return; + if (isZero(_targetTemperature)) + return; + + throw UserInputException( + "Cannot apply Berendsen coupling to a zero-temperature system " + "with a positive target temperature. Initialize velocities first." + ); } const auto dt = TimingsSettings::getTimeStep(); @@ -113,4 +118,4 @@ void BerendsenThermostat::setTau(const double tau) { _tau = tau; } ThermostatType BerendsenThermostat::getThermostatType() const { return ThermostatType::BERENDSEN; -} \ No newline at end of file +} diff --git a/src/thermostat/velocityRescalingThermostat.cpp b/src/thermostat/velocityRescalingThermostat.cpp index c81ac9cb5..59500a7de 100644 --- a/src/thermostat/velocityRescalingThermostat.cpp +++ b/src/thermostat/velocityRescalingThermostat.cpp @@ -24,15 +24,19 @@ #include // for sqrt +#include "exceptions.hpp" // for UserInputException +#include "mathUtilities.hpp" // for isZero #include "physicalData.hpp" // for PhysicalData #include "simulationBox.hpp" // for SimulationBox #include "thermostatSettings.hpp" // for ThermostatType #include "timingsSettings.hpp" // for TimingsSettings using thermostat::VelocityRescalingThermostat; +using namespace customException; using namespace settings; using namespace simulationBox; using namespace physicalData; +using namespace utilities; /** * @brief Construct a new Velocity Rescaling Thermostat:: Velocity Rescaling @@ -80,6 +84,18 @@ void VelocityRescalingThermostat::applyThermostat( _temperature = physicalData.getTemperature(); + if (isZero(_temperature)) + { + stopTimingsSection("Velocity Rescaling"); + if (isZero(_targetTemperature)) + return; + + throw UserInputException( + "Cannot apply velocity rescaling to a zero-temperature system " + "with a positive target temperature. Initialize velocities first." + ); + } + const auto timeStep = TimingsSettings::getTimeStep(); const auto tempRatio = _targetTemperature / _temperature; const auto dof = double(simulationBox.getDegreesOfFreedom()); @@ -136,4 +152,4 @@ void VelocityRescalingThermostat::setTau(const double tau) { _tau = tau; } ThermostatType VelocityRescalingThermostat::getThermostatType() const { return ThermostatType::VELOCITY_RESCALING; -} \ No newline at end of file +} diff --git a/src/timings/CMakeLists.txt b/src/timings/CMakeLists.txt index fd6ee64d3..84d924f2a 100644 --- a/src/timings/CMakeLists.txt +++ b/src/timings/CMakeLists.txt @@ -17,5 +17,5 @@ target_link_libraries(timings ) install(TARGETS timings - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) diff --git a/src/utilities/CMakeLists.txt b/src/utilities/CMakeLists.txt index 61a0f48ae..a38c11c64 100644 --- a/src/utilities/CMakeLists.txt +++ b/src/utilities/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(utilities + executablePath.cpp stringUtilities.cpp mathUtilities.cpp ) @@ -16,5 +17,5 @@ target_link_libraries(utilities ) install(TARGETS utilities - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ -) \ No newline at end of file + DESTINATION lib +) diff --git a/src/utilities/executablePath.cpp b/src/utilities/executablePath.cpp new file mode 100644 index 000000000..d773aef4a --- /dev/null +++ b/src/utilities/executablePath.cpp @@ -0,0 +1,81 @@ +/***************************************************************************** + + + PQ + Copyright (C) 2023-now Jakob Gamper + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + +******************************************************************************/ + +#include "executablePath.hpp" + +#include +#include +#include + +#if defined(_WIN32) +#include +#elif defined(__APPLE__) +#include +#elif defined(__linux__) +#include +#endif + +std::filesystem::path utilities::executablePath() +{ +#if defined(_WIN32) + auto buffer = std::vector(1024); + while (true) + { + const auto length = GetModuleFileNameW( + nullptr, + buffer.data(), + static_cast(buffer.size()) + ); + if (length == 0) + break; + if (length < buffer.size() - 1) + return std::filesystem::weakly_canonical( + std::filesystem::path(buffer.data()) + ); + buffer.resize(buffer.size() * 2); + } +#elif defined(__APPLE__) + auto size = uint32_t{0}; + _NSGetExecutablePath(nullptr, &size); + auto buffer = std::vector(size); + if (_NSGetExecutablePath(buffer.data(), &size) == 0) + return std::filesystem::weakly_canonical(buffer.data()); +#elif defined(__linux__) + auto buffer = std::vector(1024); + while (true) + { + const auto length = + readlink("/proc/self/exe", buffer.data(), buffer.size()); + if (length < 0) + break; + if (static_cast(length) < buffer.size()) + return std::filesystem::weakly_canonical( + std::filesystem::path( + std::string(buffer.data(), static_cast(length)) + ) + ); + buffer.resize(buffer.size() * 2); + } +#endif + + return {}; +} diff --git a/src/utilities/stringUtilities.cpp b/src/utilities/stringUtilities.cpp index ea91ef67f..c9a06ed5f 100644 --- a/src/utilities/stringUtilities.cpp +++ b/src/utilities/stringUtilities.cpp @@ -24,8 +24,9 @@ #include // for __for_each_fn #include // for isspace -#include // for isnan, isinf +#include // for isfinite #include // for uint_fast32_t and UINT32_MAX +#include // for is_regular_file #include // for format #include #include // for begin, end, operator|, views::split, views::transform @@ -197,6 +198,23 @@ std::string utilities::firstLetterToUpperCaseCopy(std::string myString) return myString; } +std::string utilities::shellQuote(const std::string_view argument) +{ + std::string quoted{"'"}; + quoted.reserve(argument.size() + 2); + + for (const auto character : argument) + { + if (character == '\'') + quoted += "'\"'\"'"; + else + quoted += character; + } + + quoted += '\''; + return quoted; +} + /** * @brief checks if a file exists and can be opened * @@ -206,6 +224,9 @@ std::string utilities::firstLetterToUpperCaseCopy(std::string myString) */ bool utilities::fileExists(const std::string &filename) { + if (!std::filesystem::is_regular_file(filename)) + return false; + std::ifstream file(filename); return file.good(); } @@ -317,6 +338,47 @@ std::uint_fast32_t utilities::stringToUintFast32t(const std::string &str) return static_cast(valueLL); } +/** + * @brief converts a complete string to an int + * + * @param str + * + * @throw invalid_argument if the complete string is not a valid integer + * @throw out_of_range if number is out of range for an int + */ +int utilities::stringToInt(const std::string &str) +{ + size_t parsedCharacters{}; + int value{}; + + try + { + value = std::stoi(str, &parsedCharacters); + } + catch (const std::invalid_argument &) + { + throw std::invalid_argument( + std::format("Invalid integer value '{}' encountered", str) + ); + } + catch (const std::out_of_range &) + { + throw std::out_of_range( + std::format( + "Integer value '{}' exceeds the representable range for an int", + str + ) + ); + } + + if (parsedCharacters != str.size()) + throw std::invalid_argument( + std::format("Invalid integer value '{}' encountered", str) + ); + + return value; +} + /** * @brief converts a string to a non-Nan and non-Inf double * @@ -327,11 +389,12 @@ std::uint_fast32_t utilities::stringToUintFast32t(const std::string &str) */ double utilities::stringToFiniteDouble(const std::string &str) { + size_t parsedCharacters{}; double value{}; try { - value = std::stod(str); + value = std::stod(str, &parsedCharacters); } catch (const std::invalid_argument &) { @@ -350,10 +413,10 @@ double utilities::stringToFiniteDouble(const std::string &str) ); } - if (std::isnan(value) || std::isinf(value)) + if (parsedCharacters != str.size() || !std::isfinite(value)) throw std::invalid_argument( std::format("Invalid floating-point value '{}' encountered", str) ); return value; -} \ No newline at end of file +} diff --git a/src/virial/CMakeLists.txt b/src/virial/CMakeLists.txt index 1152edc21..1d7400caa 100644 --- a/src/virial/CMakeLists.txt +++ b/src/virial/CMakeLists.txt @@ -19,5 +19,5 @@ target_link_libraries(virial ) install(TARGETS virial - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ + DESTINATION lib ) \ No newline at end of file diff --git a/tests/cmake/testPQCli.cmake b/tests/cmake/testPQCli.cmake new file mode 100644 index 000000000..99a455d1f --- /dev/null +++ b/tests/cmake/testPQCli.cmake @@ -0,0 +1,220 @@ +function(run_pq output_var error_var result_var) + execute_process( + COMMAND "${PQ_EXECUTABLE}" ${ARGN} + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE error + ) + set(${output_var} "${output}" PARENT_SCOPE) + set(${error_var} "${error}" PARENT_SCOPE) + set(${result_var} "${result}" PARENT_SCOPE) +endfunction() + +function(assert_input_array field expected_items) + string(JSON actual_count LENGTH "${output}" input "${field}") + list(LENGTH expected_items expected_count) + if(NOT actual_count EQUAL expected_count) + message( + FATAL_ERROR + "Unexpected ${field} count: ${actual_count}; expected ${expected_count}" + ) + endif() + + math(EXPR last_index "${expected_count} - 1") + foreach(index RANGE 0 ${last_index}) + string(JSON actual_item GET "${output}" input "${field}" ${index}) + list(GET expected_items ${index} expected_item) + if(NOT actual_item STREQUAL expected_item) + message( + FATAL_ERROR + "Unexpected ${field}[${index}]: ${actual_item}; expected ${expected_item}" + ) + endif() + endforeach() +endfunction() + +run_pq(output error result --help) +if(NOT result EQUAL 0) + message(FATAL_ERROR "PQ --help returned ${result}: ${error}") +endif() +if(NOT output MATCHES "^Usage: PQ ") + message(FATAL_ERROR "PQ --help did not print usage: ${output}") +endif() +if(NOT error STREQUAL "") + message(FATAL_ERROR "PQ --help wrote to stderr: ${error}") +endif() + +run_pq(output error result --version) +if(NOT result EQUAL 0) + message(FATAL_ERROR "PQ --version returned ${result}: ${error}") +endif() +if(NOT output STREQUAL "PQ ${EXPECTED_VERSION}\n") + message( + FATAL_ERROR + "PQ --version printed '${output}', expected 'PQ ${EXPECTED_VERSION}'" + ) +endif() +if(NOT error STREQUAL "") + message(FATAL_ERROR "PQ --version wrote to stderr: ${error}") +endif() + +run_pq(output error result --capabilities=json) +if(NOT result EQUAL 0) + message(FATAL_ERROR "PQ --capabilities=json returned ${result}: ${error}") +endif() +if(NOT error STREQUAL "") + message(FATAL_ERROR "PQ --capabilities=json wrote to stderr: ${error}") +endif() +string(JSON schema GET "${output}" schema) +string(JSON schema_version GET "${output}" schema_version) +string(JSON version GET "${output}" version) +string(JSON ase GET "${output}" build ase) +string(JSON mpi GET "${output}" build mpi) +string(JSON kokkos GET "${output}" build kokkos) +string(JSON python_bindings GET "${output}" build python_bindings) +string(JSON python_embedding GET "${output}" build python_embedding) +string(JSON shared GET "${output}" build shared) +string(JSON singularity GET "${output}" build singularity) +string(JSON validation_schema GET "${output}" cli input_validation schema) +string(JSON validation_schema_version GET "${output}" cli input_validation schema_version) +string(JSON validation_scope_count LENGTH "${output}" cli input_validation scopes) +string(JSON validation_scope_0 GET "${output}" cli input_validation scopes 0) +string(JSON validation_scope_1 GET "${output}" cli input_validation scopes 1) +string(JSON nstep_min GET "${output}" input parameters nstep minimum) +string(JSON nstep_max GET "${output}" input parameters nstep maximum) +string(JSON timestep_min GET "${output}" input parameters timestep exclusive_minimum) +string(JSON output_freq_min GET "${output}" input parameters output_freq minimum) +string(JSON t_relaxation GET "${output}" input parameters t_relaxation default) +string(JSON t_relaxation_min GET "${output}" input parameters t_relaxation exclusive_minimum) +string(JSON random_seed_max GET "${output}" input parameters random_seed maximum) +if(NOT schema STREQUAL "pq.capabilities") + message(FATAL_ERROR "Unexpected capabilities schema: ${schema}") +endif() +if(NOT schema_version EQUAL 1) + message(FATAL_ERROR "Unexpected capabilities schema version: ${schema_version}") +endif() +if(NOT version STREQUAL "${EXPECTED_VERSION}") + message(FATAL_ERROR "Unexpected capabilities PQ version: ${version}") +endif() +if(NOT ase STREQUAL "${EXPECTED_ASE}") + message(FATAL_ERROR "Unexpected ASE capability: ${ase}") +endif() +if(NOT mpi STREQUAL "${EXPECTED_MPI}") + message(FATAL_ERROR "Unexpected MPI capability: ${mpi}") +endif() +if(NOT kokkos STREQUAL "${EXPECTED_KOKKOS}") + message(FATAL_ERROR "Unexpected Kokkos capability: ${kokkos}") +endif() +if(NOT python_bindings STREQUAL "${EXPECTED_PYTHON_BINDINGS}") + message(FATAL_ERROR "Unexpected Python bindings capability: ${python_bindings}") +endif() +if(NOT python_embedding STREQUAL "${EXPECTED_PYTHON_EMBEDDING}") + message(FATAL_ERROR "Unexpected Python embedding capability: ${python_embedding}") +endif() +if(NOT shared STREQUAL "${EXPECTED_SHARED}") + message(FATAL_ERROR "Unexpected shared-build capability: ${shared}") +endif() +if(NOT singularity STREQUAL "${EXPECTED_SINGULARITY}") + message(FATAL_ERROR "Unexpected Singularity capability: ${singularity}") +endif() +if(NOT validation_schema STREQUAL "pq.validation") + message(FATAL_ERROR "Unexpected validation schema: ${validation_schema}") +endif() +if(NOT validation_schema_version EQUAL 1) + message(FATAL_ERROR "Unexpected validation schema version: ${validation_schema_version}") +endif() +if( + NOT validation_scope_count EQUAL 2 + OR NOT validation_scope_0 STREQUAL "portable" + OR NOT validation_scope_1 STREQUAL "installed" +) + message(FATAL_ERROR "Unexpected validation scopes: ${output}") +endif() + +assert_input_array( + job_types + "mm-md;mm-hessian;mm-opt;qm-md;qm-rpmd" +) +set(expected_qm_programs "dftbplus;pyscf;turbomole") +if(EXPECTED_ASE) + list( + APPEND expected_qm_programs + ase_dftbplus ase_xtb fennol mace mace_mp mace_off + ) +endif() +assert_input_array(qm_programs "${expected_qm_programs}") +assert_input_array( + thermostats + "none;berendsen;velocity_rescaling;langevin;nh-chain" +) +assert_input_array(manostats "none;berendsen;stochastic_rescaling") +assert_input_array( + pressure_isotropies + "isotropic;xy;xz;yz;anisotropic;full_anisotropic" +) + +if(NOT t_relaxation EQUAL 0.1) + message(FATAL_ERROR "Unexpected t_relaxation default: ${t_relaxation}") +endif() +if(NOT nstep_min EQUAL 1) + message(FATAL_ERROR "Unexpected nstep minimum: ${nstep_min}") +endif() +if(NOT nstep_max EQUAL 2147483647) + message(FATAL_ERROR "Unexpected nstep maximum: ${nstep_max}") +endif() +if(NOT timestep_min EQUAL 0) + message(FATAL_ERROR "Unexpected timestep minimum: ${timestep_min}") +endif() +if(NOT output_freq_min EQUAL 0) + message(FATAL_ERROR "Unexpected output frequency minimum: ${output_freq_min}") +endif() +if(NOT t_relaxation_min EQUAL 0) + message(FATAL_ERROR "Unexpected t_relaxation minimum: ${t_relaxation_min}") +endif() +if(NOT random_seed_max EQUAL 4294967295) + message(FATAL_ERROR "Unexpected random seed maximum: ${random_seed_max}") +endif() + +run_pq(output error result --unknown) +if(result EQUAL 0) + message(FATAL_ERROR "PQ --unknown returned success") +endif() +if(NOT output STREQUAL "") + message(FATAL_ERROR "PQ --unknown wrote to stdout: ${output}") +endif() +if(NOT error MATCHES "Unknown option: --unknown") + message(FATAL_ERROR "PQ --unknown did not report the option: ${error}") +endif() + +run_pq(output error result) +if(result EQUAL 0) + message(FATAL_ERROR "PQ without an input returned success") +endif() +if(NOT output STREQUAL "") + message(FATAL_ERROR "PQ without an input wrote to stdout: ${output}") +endif() +if(NOT error MATCHES "No input file specified") + message(FATAL_ERROR "PQ without an input did not report the error: ${error}") +endif() + +run_pq(output error result missing-input.in) +if(result EQUAL 0) + message(FATAL_ERROR "PQ with a missing input returned success") +endif() +if(NOT output STREQUAL "") + message(FATAL_ERROR "PQ with a missing input wrote to stdout: ${output}") +endif() +if(NOT error MATCHES "File not found") + message(FATAL_ERROR "PQ with a missing input did not report the error: ${error}") +endif() + +run_pq(output error result missing-input.in extra) +if(result EQUAL 0) + message(FATAL_ERROR "PQ with an extra argument returned success") +endif() +if(NOT output STREQUAL "") + message(FATAL_ERROR "PQ with an extra argument wrote to stdout: ${output}") +endif() +if(NOT error MATCHES "Unexpected argument: extra") + message(FATAL_ERROR "PQ did not report the extra argument: ${error}") +endif() diff --git a/tests/cmake/testPQSlakosInstall.cmake b/tests/cmake/testPQSlakosInstall.cmake new file mode 100644 index 000000000..3937d91a3 --- /dev/null +++ b/tests/cmake/testPQSlakosInstall.cmake @@ -0,0 +1,204 @@ +if(NOT DEFINED STAGING_PREFIX OR STAGING_PREFIX STREQUAL "" OR + STAGING_PREFIX STREQUAL "/") + message(FATAL_ERROR "A safe staging prefix is required") +endif() + +function(run_staged_validation input_file) + execute_process( + COMMAND "${STAGED_EXECUTABLE}" + --validate "${input_file}" --format=json + WORKING_DIRECTORY "${STAGING_PREFIX}/work" + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE error + ) + if(NOT result EQUAL 0) + message( + FATAL_ERROR + "Staged PQ rejected ${input_file}: ${output} ${error}" + ) + endif() + string(JSON valid GET "${output}" valid) + if(NOT valid) + message(FATAL_ERROR "Staged validation reported invalid: ${output}") + endif() +endfunction() + +file(REMOVE_RECURSE "${STAGING_PREFIX}") +file(MAKE_DIRECTORY "${STAGING_PREFIX}/work") + +execute_process( + COMMAND + "${CMAKE_COMMAND}" --install "${BUILD_DIR}" + --prefix "${STAGING_PREFIX}" + RESULT_VARIABLE install_result + OUTPUT_VARIABLE install_output + ERROR_VARIABLE install_error +) +if(NOT install_result EQUAL 0) + message( + FATAL_ERROR + "Could not stage PQ: ${install_output} ${install_error}" + ) +endif() + +get_filename_component(pq_executable_name "${PQ_EXECUTABLE}" NAME) +set(STAGED_EXECUTABLE "${STAGING_PREFIX}/bin/${pq_executable_name}") +if(NOT EXISTS "${STAGED_EXECUTABLE}") + message(FATAL_ERROR "PQ executable was not installed") +endif() + +foreach(script IN ITEMS + dftbplus_periodic_stress + pyscf_hf.py + pyscf_mp2.py + turbomole_rimp2) + if(NOT EXISTS "${STAGING_PREFIX}/share/PQ/scripts/${script}") + message(FATAL_ERROR "QM script ${script} was not installed") + endif() +endforeach() + +foreach(reference IN ITEMS + pq.ref + pq.ref.bib + 3ob.ref + 3ob.ref.bib + matsci.ref + matsci.ref.bib) + if(NOT EXISTS "${STAGING_PREFIX}/share/PQ/references/${reference}") + message(FATAL_ERROR "Reference data ${reference} was not installed") + endif() +endforeach() + +file(READ "${STAGING_PREFIX}/share/PQ/references/3ob.ref" threeob_references) +foreach(doi IN ITEMS + "10.1021/ct300849w" + "10.1021/ct401002w" + "10.1021/jp506557r" + "10.1021/ct5009137") + if(NOT threeob_references MATCHES "${doi}") + message(FATAL_ERROR "3ob reference data is missing ${doi}") + endif() +endforeach() + +file(READ "${STAGING_PREFIX}/share/PQ/references/matsci.ref" matsci_references) +foreach(marker IN ITEMS + "TU Dresden" + "10.1002/zaac.200500051" + "10.1021/nn700184k" + "10.1016/j.susc.2008.01.035" + "10.1021/jp8110343" + "10.3139/146.110337" + "Jardillier") + if(NOT matsci_references MATCHES "${marker}") + message(FATAL_ERROR "matsci reference data is missing ${marker}") + endif() +endforeach() + +file(GLOB installed_libraries "${STAGING_PREFIX}/lib/*") +if(NOT installed_libraries) + message(FATAL_ERROR "PQ libraries were not installed") +endif() + +foreach(slakos_set IN ITEMS 3ob matsci) + set(slakos_root "${STAGING_PREFIX}/share/PQ/slakos/${slakos_set}") + foreach(metadata IN ITEMS LICENSE README RELEASE.md CHANGELOG.md) + if(NOT EXISTS "${slakos_root}/${metadata}") + message( + FATAL_ERROR + "${slakos_set} metadata ${metadata} was not installed" + ) + endif() + endforeach() + foreach(pair IN ITEMS H-H C-C O-O) + if(NOT EXISTS "${slakos_root}/skfiles/${pair}.skf") + message( + FATAL_ERROR + "${slakos_set} parameter ${pair}.skf was not installed" + ) + endif() + endforeach() + + file( + GLOB source_parameters + "${SLAKOS_SOURCE_DIR}/${slakos_set}/skfiles/*.skf" + ) + file(GLOB installed_parameters "${slakos_root}/skfiles/*.skf") + list(LENGTH source_parameters source_parameter_count) + list(LENGTH installed_parameters installed_parameter_count) + if(NOT installed_parameter_count EQUAL source_parameter_count) + message( + FATAL_ERROR + "${slakos_set} installed ${installed_parameter_count} of " + "${source_parameter_count} parameter files" + ) + endif() +endforeach() + +execute_process( + COMMAND "${STAGED_EXECUTABLE}" --capabilities=json + RESULT_VARIABLE capabilities_result + OUTPUT_VARIABLE capabilities_output + ERROR_VARIABLE capabilities_error +) +if(NOT capabilities_result EQUAL 0) + message( + FATAL_ERROR + "Installed PQ capabilities failed: " + "${capabilities_output} ${capabilities_error}" + ) +endif() + +file( + COPY + "${MM_FIXTURE_DIR}/run.in" + "${MM_FIXTURE_DIR}/start.rst" + "${MM_FIXTURE_DIR}/moldescriptor.dat" + "${MM_FIXTURE_DIR}/guff.dat" + DESTINATION "${STAGING_PREFIX}/work" +) + +foreach(slakos_set IN ITEMS 3ob matsci) + file( + WRITE "${STAGING_PREFIX}/work/${slakos_set}.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = ase-dftbplus;\n" + "slakos = ${slakos_set};\n" + "start_file = start.rst;\n" + ) + run_staged_validation("${slakos_set}.in") +endforeach() + +execute_process( + COMMAND "${STAGED_EXECUTABLE}" run.in + WORKING_DIRECTORY "${STAGING_PREFIX}/work" + TIMEOUT 30 + RESULT_VARIABLE simulation_result + OUTPUT_VARIABLE simulation_output + ERROR_VARIABLE simulation_error +) +if(NOT simulation_result EQUAL 0) + message( + FATAL_ERROR + "Installed PQ simulation failed: " + "${simulation_output} ${simulation_error}" + ) +endif() +if(NOT simulation_output MATCHES "PQ ended normally") + message(FATAL_ERROR "Installed PQ did not report normal completion") +endif() + +set(reference_output "${STAGING_PREFIX}/work/smoke.ref") +if(NOT EXISTS "${reference_output}") + message(FATAL_ERROR "Installed PQ did not write smoke.ref") +endif() +file(READ "${reference_output}" reference_contents) +foreach(expected IN ITEMS "PQ Software" "BIBTEX ENTRIES") + if(NOT reference_contents MATCHES "${expected}") + message(FATAL_ERROR "smoke.ref is missing ${expected}") + endif() +endforeach() + +file(REMOVE_RECURSE "${STAGING_PREFIX}") diff --git a/tests/cmake/testPQValidation.cmake b/tests/cmake/testPQValidation.cmake new file mode 100644 index 000000000..509d1f7bb --- /dev/null +++ b/tests/cmake/testPQValidation.cmake @@ -0,0 +1,1321 @@ +function(run_pq_in working_directory output_var error_var result_var) + execute_process( + COMMAND "${PQ_EXECUTABLE}" ${ARGN} + WORKING_DIRECTORY "${working_directory}" + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE error + ) + set(${output_var} "${output}" PARENT_SCOPE) + set(${error_var} "${error}" PARENT_SCOPE) + set(${result_var} "${result}" PARENT_SCOPE) +endfunction() + +function(snapshot_directory directory output_var) + file(GLOB_RECURSE relative_paths RELATIVE "${directory}" "${directory}/*") + list(SORT relative_paths) + + set(snapshot "") + foreach(relative_path IN LISTS relative_paths) + set(path "${directory}/${relative_path}") + if(NOT IS_DIRECTORY "${path}") + file(SHA256 "${path}" sha256) + string(APPEND snapshot "${relative_path}:${sha256}\n") + endif() + endforeach() + + set(${output_var} "${snapshot}" PARENT_SCOPE) +endfunction() + +file(REMOVE_RECURSE "${VALIDATION_WORK_DIR}") +file(MAKE_DIRECTORY "${VALIDATION_WORK_DIR}") +file( + COPY + "${VALIDATION_FIXTURE_DIR}/run.in" + "${VALIDATION_FIXTURE_DIR}/start.rst" + "${VALIDATION_FIXTURE_DIR}/moldescriptor.dat" + "${VALIDATION_FIXTURE_DIR}/guff.dat" + DESTINATION "${VALIDATION_WORK_DIR}" +) + +snapshot_directory("${VALIDATION_WORK_DIR}" before_validation) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate run.in --format=json +) +snapshot_directory("${VALIDATION_WORK_DIR}" after_validation) +if(NOT result EQUAL 0) + message(FATAL_ERROR "PQ rejected a valid input: ${output} ${error}") +endif() +if(NOT error STREQUAL "") + message(FATAL_ERROR "JSON validation wrote to stderr: ${error}") +endif() +if(NOT before_validation STREQUAL after_validation) + message( + FATAL_ERROR + "Input validation changed files\n" + "Before:\n${before_validation}" + "After:\n${after_validation}" + ) +endif() +string(JSON validation_schema GET "${output}" schema) +string(JSON validation_valid GET "${output}" valid) +string(JSON validation_scope GET "${output}" scope) +string(JSON diagnostic_count LENGTH "${output}" diagnostics) +if(NOT validation_schema STREQUAL "pq.validation") + message(FATAL_ERROR "Unexpected validation schema: ${validation_schema}") +endif() +if(NOT validation_valid) + message(FATAL_ERROR "Valid input reported invalid: ${output}") +endif() +if(NOT validation_scope STREQUAL "installed") + message(FATAL_ERROR "Unexpected default validation scope: ${output}") +endif() +if(NOT diagnostic_count EQUAL 0) + message(FATAL_ERROR "Valid input produced diagnostics: ${output}") +endif() + +file(READ "${VALIDATION_FIXTURE_DIR}/run.in" zero_output_frequency_input) +string( + REPLACE "output_freq = 1;" "output_freq = 0;" + zero_output_frequency_input "${zero_output_frequency_input}" +) +file( + WRITE "${VALIDATION_WORK_DIR}/zero-output-frequency.in" + "${zero_output_frequency_input}" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate zero-output-frequency.in --format=json +) +if(NOT result EQUAL 0) + message(FATAL_ERROR "PQ rejected a zero output frequency: ${output}") +endif() +string(JSON zero_output_valid GET "${output}" valid) +string(JSON zero_output_diagnostic_count LENGTH "${output}" diagnostics) +if(NOT zero_output_valid OR NOT zero_output_diagnostic_count EQUAL 0) + message(FATAL_ERROR "Unexpected zero output frequency result: ${output}") +endif() +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + zero-output-frequency.in +) +if(NOT result EQUAL 0 OR NOT error STREQUAL "") + message(FATAL_ERROR "PQ dry run failed: ${output} ${error}") +endif() +if(NOT output MATCHES "PQ ended normally") + message(FATAL_ERROR "PQ dry run did not finish normally: ${output}") +endif() + +run_pq_in( + "${VALIDATION_WORK_DIR}" + capabilities_output capabilities_error capabilities_result + --capabilities=json +) +if(NOT capabilities_result EQUAL 0 OR NOT capabilities_error STREQUAL "") + message( + FATAL_ERROR + "Could not read PQ capabilities: ${capabilities_error}" + ) +endif() +string( + JSON t_relaxation_max + GET "${capabilities_output}" input parameters t_relaxation maximum +) +string( + JSON friction_max + GET "${capabilities_output}" input parameters friction maximum +) +string( + JSON coupling_frequency_max + GET "${capabilities_output}" input parameters coupling_frequency maximum +) +string( + JSON p_relaxation_max + GET "${capabilities_output}" input parameters p_relaxation maximum +) +string( + JSON dftbplus_recommended_script + GET "${capabilities_output}" input external_qm programs dftbplus recommended_script +) +string( + JSON pyscf_script_count + LENGTH "${capabilities_output}" input external_qm programs pyscf scripts +) +string( + JSON pyscf_recommended_type + TYPE "${capabilities_output}" input external_qm programs pyscf recommended_script +) +if(NOT dftbplus_recommended_script STREQUAL "dftbplus_periodic_stress") + message(FATAL_ERROR "Unexpected DFTB+ recommendation: ${capabilities_output}") +endif() +if(NOT pyscf_script_count EQUAL 2) + message(FATAL_ERROR "Unexpected PySCF script list: ${capabilities_output}") +endif() +if(NOT pyscf_recommended_type STREQUAL "NULL") + message(FATAL_ERROR "PySCF must not select a method implicitly: ${capabilities_output}") +endif() +file(READ "${VALIDATION_FIXTURE_DIR}/run.in" maximum_input) +string( + APPEND maximum_input + "\nt_relaxation = ${t_relaxation_max};\n" + "friction = ${friction_max};\n" + "coupling_frequency = ${coupling_frequency_max};\n" + "p_relaxation = ${p_relaxation_max};\n" +) +file( + WRITE "${VALIDATION_WORK_DIR}/advertised-maxima.in" + "${maximum_input}" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate advertised-maxima.in --format=json +) +if(NOT result EQUAL 0) + message(FATAL_ERROR "PQ rejected an advertised maximum: ${output}") +endif() + +file(READ "${VALIDATION_FIXTURE_DIR}/run.in" unsafe_thermostat_input) +string( + APPEND unsafe_thermostat_input + "\nthermostat = berendsen;\n" + "temp = 300;\n" + "t_relaxation = 0.0001;\n" +) +file( + WRITE "${VALIDATION_WORK_DIR}/unsafe-thermostat.in" + "${unsafe_thermostat_input}" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate unsafe-thermostat.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a timestep above the thermostat relaxation time") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "timestep must not exceed") + message(FATAL_ERROR "Unexpected thermostat stability diagnostic: ${output}") +endif() + +file(READ "${VALIDATION_FIXTURE_DIR}/run.in" unsafe_langevin_input) +string( + APPEND unsafe_langevin_input + "\nthermostat = langevin;\n" + "temp = 300;\n" + "friction = 1e290;\n" +) +file( + WRITE "${VALIDATION_WORK_DIR}/unsafe-langevin.in" + "${unsafe_langevin_input}" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate unsafe-langevin.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a non-finite Langevin scale") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "non-finite random-force scale") + message(FATAL_ERROR "Unexpected Langevin diagnostic: ${output}") +endif() + +file(READ "${VALIDATION_FIXTURE_DIR}/run.in" unsafe_manostat_input) +string( + APPEND unsafe_manostat_input + "\nmanostat = berendsen;\n" + "pressure = 1;\n" + "p_relaxation = 0.0001;\n" +) +file( + WRITE "${VALIDATION_WORK_DIR}/unsafe-manostat.in" + "${unsafe_manostat_input}" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate unsafe-manostat.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a timestep above the manostat relaxation time") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "timestep must not exceed") + message(FATAL_ERROR "Unexpected manostat stability diagnostic: ${output}") +endif() + +file(READ "${VALIDATION_FIXTURE_DIR}/run.in" missing_initial_temp_input) +string(APPEND missing_initial_temp_input "\ninit_velocities = force;\n") +file( + WRITE "${VALIDATION_WORK_DIR}/missing-initial-temp.in" + "${missing_initial_temp_input}" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate missing-initial-temp.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted velocity initialization without a temperature") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "Initializing velocities requires") + message(FATAL_ERROR "Unexpected initialization diagnostic: ${output}") +endif() + +run_pq_in("${VALIDATION_WORK_DIR}" output error result --validate run.in) +if(NOT result EQUAL 0) + message(FATAL_ERROR "Text validation rejected a valid input: ${error}") +endif() +if(NOT output STREQUAL "Valid PQ input: run.in\n") + message(FATAL_ERROR "Unexpected text validation output: ${output}") +endif() +if(NOT error STREQUAL "") + message(FATAL_ERROR "Valid text validation wrote to stderr: ${error}") +endif() + +file( + READ "${VALIDATION_FIXTURE_DIR}/run.in" + mm_with_unused_qm +) +string( + APPEND mm_with_unused_qm + "\nqm_prog = dftbplus;\n" + "mace_model_size = small;\n" +) +file( + WRITE + "${VALIDATION_WORK_DIR}/mm-with-unused-qm.in" + "${mm_with_unused_qm}" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate mm-with-unused-qm.in --format=json +) +if(NOT result EQUAL 0) + message(FATAL_ERROR "PQ rejected unused QM settings in an MM input: ${output}") +endif() +if(NOT error STREQUAL "") + message(FATAL_ERROR "Warning validation wrote to stderr: ${error}") +endif() +string(JSON validation_valid GET "${output}" valid) +string(JSON diagnostic_count LENGTH "${output}" diagnostics) +string(JSON diagnostic_severity GET "${output}" diagnostics 0 severity) +if(NOT validation_valid OR NOT diagnostic_count EQUAL 1) + message(FATAL_ERROR "Unexpected warning validation result: ${output}") +endif() +if(NOT diagnostic_severity STREQUAL "warning") + message(FATAL_ERROR "Unexpected diagnostic severity: ${output}") +endif() + +set(qm_without_descriptor_dir "${VALIDATION_WORK_DIR}/qm-no-descriptor") +file(MAKE_DIRECTORY "${qm_without_descriptor_dir}") +file( + COPY + "${VALIDATION_FIXTURE_DIR}/start.rst" + DESTINATION "${qm_without_descriptor_dir}" +) +set(failing_qm_dir "${qm_without_descriptor_dir}/failing-command") +file(MAKE_DIRECTORY "${failing_qm_dir}") +file( + COPY + "${VALIDATION_FIXTURE_DIR}/start.rst" + "${VALIDATION_FIXTURE_DIR}/moldescriptor.dat" + DESTINATION "${failing_qm_dir}" +) +set(failing_qm_script "${failing_qm_dir}/failing-qm") +file(WRITE "${failing_qm_script}" "#!/bin/sh\nexit 17\n") +file( + CHMOD "${failing_qm_script}" + PERMISSIONS + OWNER_READ OWNER_WRITE OWNER_EXECUTE + GROUP_READ GROUP_EXECUTE + WORLD_READ WORLD_EXECUTE +) +file(WRITE "${failing_qm_dir}/dftb.in" "") +file(WRITE "${failing_qm_dir}/qm_forces" "0\n0 0 0\n0 0 0\n") +file(WRITE "${failing_qm_dir}/qm_charges" "1 0\n2 0\n") +file(WRITE "${failing_qm_dir}/stress_tensor" "0 0 0\n0 0 0\n0 0 0\n") +file( + WRITE "${failing_qm_dir}/failing-qm.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "output_freq = 0;\n" + "init_velocities = false;\n" + "virial = atomic;\n" + "qm_prog = dftbplus;\n" + "qm_script_full_path = ${failing_qm_script};\n" + "dftb_file = dftb.in;\n" + "start_file = start.rst;\n" + "file_prefix = failing-qm;\n" +) +run_pq_in( + "${failing_qm_dir}" + output error result + failing-qm.in +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ ignored an external QM command failure") +endif() +if(NOT error MATCHES "DFTB\\+ command failed") + message(FATAL_ERROR "Unexpected external QM failure: ${output} ${error}") +endif() +if( + EXISTS "${failing_qm_dir}/qm_forces" + OR EXISTS "${failing_qm_dir}/qm_charges" + OR EXISTS "${failing_qm_dir}/stress_tensor" +) + message(FATAL_ERROR "PQ reused stale external QM result files") +endif() +set(incomplete_qm_script "${failing_qm_dir}/incomplete-qm") +file( + WRITE "${incomplete_qm_script}" + "#!/bin/sh\n" + "printf '0\\n0 0 0\\n' > qm_forces\n" + "printf '1 0\\n2 0\\n' > qm_charges\n" + "printf '0 0 0\\n0 0 0\\n0 0 0\\n' > stress_tensor\n" +) +file( + CHMOD "${incomplete_qm_script}" + PERMISSIONS + OWNER_READ OWNER_WRITE OWNER_EXECUTE + GROUP_READ GROUP_EXECUTE + WORLD_READ WORLD_EXECUTE +) +file( + WRITE "${failing_qm_dir}/incomplete-qm.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "output_freq = 0;\n" + "init_velocities = false;\n" + "virial = atomic;\n" + "qm_prog = dftbplus;\n" + "qm_script_full_path = ${incomplete_qm_script};\n" + "dftb_file = dftb.in;\n" + "start_file = start.rst;\n" + "file_prefix = incomplete-qm;\n" +) +run_pq_in( + "${failing_qm_dir}" + output error result + incomplete-qm.in +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted incomplete external QM results") +endif() +if(NOT error MATCHES "Incomplete .* force file") + message(FATAL_ERROR "Unexpected incomplete QM result error: ${output} ${error}") +endif() + +if(EXPECTED_SHARED AND NOT EXPECTED_SINGULARITY) + set(qm_script_setting "qm_script = dftbplus_periodic_stress;") +else() + file(WRITE "${qm_without_descriptor_dir}/qm-runner" "") + set( + qm_script_setting + "qm_script_full_path = ${qm_without_descriptor_dir}/qm-runner;" + ) +endif() +file( + WRITE "${qm_without_descriptor_dir}/run.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = dftbplus;\n" + "${qm_script_setting}\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate run.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted direct DFTB+ without a setup file") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "DFTB setup file.*does not exist") + message(FATAL_ERROR "Unexpected DFTB setup diagnostic: ${output}") +endif() +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate run.in --format=json --scope=portable +) +if(NOT result EQUAL 0) + message(FATAL_ERROR "Portable validation required a local DFTB setup: ${output}") +endif() +string(JSON validation_scope GET "${output}" scope) +if(NOT validation_scope STREQUAL "portable") + message(FATAL_ERROR "Portable validation reported the wrong scope: ${output}") +endif() +file(WRITE "${qm_without_descriptor_dir}/dftb_in.template" "") +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate run.in --format=json +) +if(NOT result EQUAL 0) + message(FATAL_ERROR "PQ required a moldescriptor for pure QM: ${output}") +endif() + +file( + WRITE "${qm_without_descriptor_dir}/custom-slakos.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = ase-dftbplus;\n" + "slakos = custom;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate custom-slakos.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted custom Slater-Koster parameters without a path") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "require.*slakos_path") + message(FATAL_ERROR "Unexpected custom Slater-Koster diagnostic: ${output}") +endif() + +file( + WRITE "${qm_without_descriptor_dir}/missing-slakos.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = ase-dftbplus;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate missing-slakos.in --scope=portable --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted ASE-DFTB+ without a Slater-Koster set") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "requires slakos") + message(FATAL_ERROR "Unexpected missing Slater-Koster diagnostic: ${output}") +endif() + +if(EXPECTED_ASE) + file( + WRITE "${qm_without_descriptor_dir}/custom-slakos.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = ase-dftbplus;\n" + "slakos = custom;\n" + "slakos_path = custom-slakos;\n" + "start_file = start.rst;\n" + ) + run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate custom-slakos.in --format=json + ) + if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a missing Slater-Koster directory") + endif() + string(JSON diagnostic_message GET "${output}" diagnostics 0 message) + if(NOT diagnostic_message MATCHES "Slater-Koster directory.*does not exist") + message(FATAL_ERROR "Unexpected Slater-Koster path diagnostic: ${output}") + endif() + run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate custom-slakos.in --scope=portable --format=json + ) + if(NOT result EQUAL 0) + message(FATAL_ERROR "Portable validation required local Slater-Koster files: ${output}") + endif() + file(MAKE_DIRECTORY "${qm_without_descriptor_dir}/custom-slakos") + run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate custom-slakos.in --format=json + ) + if(NOT result EQUAL 0) + message(FATAL_ERROR "PQ rejected an existing Slater-Koster directory: ${output}") + endif() +endif() + +file( + WRITE "${qm_without_descriptor_dir}/cell-list.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = dftbplus;\n" + "${qm_script_setting}\n" + "cell-list = on;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate cell-list.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a cell list for a pure QM simulation") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "Cell lists are not available for pure QM") + message(FATAL_ERROR "Unexpected pure-QM cell-list diagnostic: ${output}") +endif() + +file( + WRITE "${qm_without_descriptor_dir}/bad-hubbard.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = ase-dftbplus;\n" + "hubbard_derivs = H:0.1junk;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate bad-hubbard.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a Hubbard derivative with trailing text") +endif() +string(JSON diagnostic_line GET "${output}" diagnostics 0 line) +if(NOT diagnostic_line EQUAL 5) + message(FATAL_ERROR "Hubbard derivative diagnostic lost its line: ${output}") +endif() + +if(NOT EXPECTED_SHARED OR EXPECTED_SINGULARITY) + file( + WRITE "${qm_without_descriptor_dir}/name-only-script.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = dftbplus;\n" + "qm_script = dftbplus_periodic_stress;\n" + "start_file = start.rst;\n" + ) + run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate name-only-script.in --format=json + ) + if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted qm_script in this build mode") + endif() + string(JSON diagnostic_message GET "${output}" diagnostics 0 message) + if(NOT diagnostic_message MATCHES "requires.*qm_script_full_path") + message(FATAL_ERROR "Unexpected build-specific script diagnostic: ${output}") + endif() +endif() + +file( + WRITE "${qm_without_descriptor_dir}/npt.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = dftbplus;\n" + "${qm_script_setting}\n" + "start_file = start.rst;\n" + "manostat = berendsen;\n" + "pressure = 1.0;\n" +) +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate npt.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted pressure coupling without a moldescriptor") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "Moldescriptor file.*does not exist") + message(FATAL_ERROR "Unexpected QM NPT descriptor diagnostic: ${output}") +endif() +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate npt.in --scope=portable --format=json +) +if(NOT result EQUAL 0) + message(FATAL_ERROR "Portable validation required a local moldescriptor: ${output}") +endif() +file( + COPY + "${VALIDATION_FIXTURE_DIR}/moldescriptor.dat" + DESTINATION "${qm_without_descriptor_dir}" +) +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate npt.in --format=json +) +if(NOT result EQUAL 0) + message(FATAL_ERROR "PQ required guff.dat for pure-QM NPT: ${output}") +endif() + +set(missing_default_dir "${VALIDATION_WORK_DIR}/missing-default") +file(MAKE_DIRECTORY "${missing_default_dir}") +file( + COPY + "${VALIDATION_FIXTURE_DIR}/run.in" + "${VALIDATION_FIXTURE_DIR}/start.rst" + DESTINATION "${missing_default_dir}" +) +run_pq_in( + "${missing_default_dir}" + output error result + --validate run.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a missing default input file") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "Moldescriptor file.*does not exist") + message(FATAL_ERROR "Unexpected default file diagnostic: ${output}") +endif() + +file( + WRITE "${VALIDATION_WORK_DIR}/portable-missing-reference.in" + "jobtype = mm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "start_file = missing-start.rst;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate portable-missing-reference.in --format=json --scope=portable +) +if(NOT result EQUAL 0) + message(FATAL_ERROR "Portable validation required a referenced file: ${output}") +endif() + +file( + WRITE "${qm_without_descriptor_dir}/ase-xtb.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = ase-xtb;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate ase-xtb.in --scope=portable --format=json +) +if(NOT result EQUAL 0) + message(FATAL_ERROR "Portable validation rejected an ASE input: ${output}") +endif() +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate ase-xtb.in --format=json +) +if(EXPECTED_ASE) + if(NOT result EQUAL 0) + message(FATAL_ERROR "ASE build rejected an ASE input: ${output}") + endif() +else() + if(result EQUAL 0) + message(FATAL_ERROR "Non-ASE build accepted an installed ASE input") + endif() + string(JSON diagnostic_message GET "${output}" diagnostics 0 message) + if(NOT diagnostic_message MATCHES "requires ASE support") + message(FATAL_ERROR "Unexpected ASE capability diagnostic: ${output}") + endif() +endif() + +file( + WRITE "${qm_without_descriptor_dir}/fennol-model.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = fennol;\n" + "fennol_model_path = missing.fnx;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate fennol-model.in --scope=portable --format=json +) +if(NOT result EQUAL 0) + message(FATAL_ERROR "Portable validation required a local FeNNol model: ${output}") +endif() +if(EXPECTED_ASE) + run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate fennol-model.in --format=json + ) + if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a missing FeNNol model file") + endif() + string(JSON diagnostic_message GET "${output}" diagnostics 0 message) + if(NOT diagnostic_message MATCHES "FeNNol model file.*does not exist") + message(FATAL_ERROR "Unexpected FeNNol model diagnostic: ${output}") + endif() +endif() + +file( + WRITE "${qm_without_descriptor_dir}/mace-model.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = mace;\n" + "mace_model = custom;\n" + "mace_model_path = missing.model;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate mace-model.in --scope=portable --format=json +) +if(NOT result EQUAL 0) + message(FATAL_ERROR "Portable validation required a local MACE model: ${output}") +endif() +if(EXPECTED_ASE) + run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate mace-model.in --format=json + ) + if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a missing local MACE model file") + endif() + string(JSON diagnostic_message GET "${output}" diagnostics 0 message) + if(NOT diagnostic_message MATCHES "MACE model file.*does not exist") + message(FATAL_ERROR "Unexpected MACE model diagnostic: ${output}") + endif() + + file( + WRITE "${qm_without_descriptor_dir}/mace-model-url.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = mace;\n" + "mace_model = custom;\n" + "mace_model_path = https://example.org/model.model;\n" + "start_file = start.rst;\n" + ) + run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate mace-model-url.in --format=json + ) + if(NOT result EQUAL 0) + message(FATAL_ERROR "PQ rejected a remote MACE model URL: ${output}") + endif() +endif() + +file( + WRITE "${qm_without_descriptor_dir}/portable-threeob.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = ase-dftbplus;\n" + "slakos = 3ob;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate portable-threeob.in --scope=portable --format=json +) +if(NOT result EQUAL 0) + message(FATAL_ERROR "Portable validation rejected built-in Slater-Koster input: ${output}") +endif() +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate portable-threeob.in --format=json +) +if(EXPECTED_ASE) + if(NOT result EQUAL 0) + message(FATAL_ERROR "ASE build could not resolve built-in 3ob data: ${output}") + endif() +else() + if(result EQUAL 0) + message(FATAL_ERROR "Non-ASE build accepted installed 3ob validation") + endif() +endif() + +file( + WRITE "${qm_without_descriptor_dir}/contradictory-script.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = dftbplus;\n" + "qm_script = dftbplus_periodic_stress;\n" + "qm_script_full_path = runner;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate contradictory-script.in --scope=portable --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "Portable validation accepted contradictory QM scripts") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "mutually exclusive") + message(FATAL_ERROR "Unexpected QM script diagnostic: ${output}") +endif() + +file( + WRITE "${qm_without_descriptor_dir}/missing-script.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = dftbplus;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate missing-script.in --scope=portable --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "Portable validation accepted a missing QM script") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "No qm_script provided") + message(FATAL_ERROR "Unexpected missing QM script diagnostic: ${output}") +endif() + +file( + WRITE "${qm_without_descriptor_dir}/wrong-script.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = pyscf;\n" + "qm_script = dftbplus_periodic_stress;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate wrong-script.in --scope=portable --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a bundled script for the wrong QM program") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "not available for pyscf") + message(FATAL_ERROR "Unexpected QM script compatibility diagnostic: ${output}") +endif() + +if(EXPECTED_SHARED AND NOT EXPECTED_SINGULARITY) + file( + WRITE "${qm_without_descriptor_dir}/turbomole-template.in" + "jobtype = qm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "qm_prog = turbomole;\n" + "qm_script = turbomole_rimp2;\n" + "start_file = start.rst;\n" + ) + run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate turbomole-template.in --scope=portable --format=json + ) + if(NOT result EQUAL 0) + message(FATAL_ERROR "Portable validation required a Turbomole template: ${output}") + endif() + run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate turbomole-template.in --format=json + ) + if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted Turbomole without tm_define.template") + endif() + string(JSON diagnostic_message GET "${output}" diagnostics 0 message) + if(NOT diagnostic_message MATCHES "Required QM working file.*tm_define.template") + message(FATAL_ERROR "Unexpected Turbomole template diagnostic: ${output}") + endif() + file(WRITE "${qm_without_descriptor_dir}/tm_define.template" "") + run_pq_in( + "${qm_without_descriptor_dir}" + output error result + --validate turbomole-template.in --format=json + ) + if(NOT result EQUAL 0) + message(FATAL_ERROR "PQ rejected a complete Turbomole setup: ${output}") + endif() +endif() + +file( + WRITE "${VALIDATION_WORK_DIR}/portable-missing-topology.in" + "jobtype = mm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "force-field = on;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate portable-missing-topology.in --scope=portable --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "Portable validation accepted force-field input without topology") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "Topology file needed") + message(FATAL_ERROR "Unexpected topology dependency diagnostic: ${output}") +endif() + +file( + WRITE "${VALIDATION_WORK_DIR}/portable-missing-mshake.in" + "jobtype = mm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "force-field = off;\n" + "shake = mshake;\n" + "topology_file = missing.top;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate portable-missing-mshake.in --scope=portable --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "Portable validation accepted M-SHAKE without its file") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "M-SHAKE file needed") + message(FATAL_ERROR "Unexpected M-SHAKE dependency diagnostic: ${output}") +endif() + +file( + WRITE "${VALIDATION_WORK_DIR}/bad-keyword.in" + "jobtype = mm-md;\n" + "nstep = 1;\n" + "not_a_keyword = 1;\n" + "timestep = 0.5;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate bad-keyword.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted an unknown keyword") +endif() +if(NOT error STREQUAL "") + message(FATAL_ERROR "Invalid JSON validation wrote to stderr: ${error}") +endif() +string(JSON validation_valid GET "${output}" valid) +string(JSON diagnostic_line GET "${output}" diagnostics 0 line) +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(validation_valid) + message(FATAL_ERROR "Invalid input reported valid: ${output}") +endif() +if(NOT diagnostic_line EQUAL 3) + message(FATAL_ERROR "Unexpected diagnostic line: ${diagnostic_line}") +endif() +if(NOT diagnostic_message MATCHES "Invalid keyword") + message(FATAL_ERROR "Unexpected validation diagnostic: ${diagnostic_message}") +endif() + +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate bad-keyword.in +) +if(result EQUAL 0 OR NOT output STREQUAL "") + message(FATAL_ERROR "Text validation did not reject the bad keyword") +endif() +string(REGEX MATCHALL "line 3" line_mentions "${error}") +list(LENGTH line_mentions line_mention_count) +if(NOT line_mention_count EQUAL 1) + message(FATAL_ERROR "Text diagnostic repeated its line number: ${error}") +endif() + +file( + WRITE "${VALIDATION_WORK_DIR}/bad-number.in" + "jobtype = mm-md;\n" + "nstep = 1;\n" + "timestep = nope;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate bad-number.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a malformed number") +endif() +string(JSON diagnostic_line GET "${output}" diagnostics 0 line) +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_line EQUAL 3) + message(FATAL_ERROR "Malformed number lost its line: ${output}") +endif() +if(NOT diagnostic_message MATCHES "Invalid value.*timestep") + message(FATAL_ERROR "Unexpected numeric diagnostic: ${diagnostic_message}") +endif() + +file( + WRITE "${VALIDATION_WORK_DIR}/trailing-unit.in" + "jobtype = mm-md;\n" + "nstep = 1;\n" + "timestep = 0.5fs;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate trailing-unit.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a numeric token with trailing text") +endif() +string(JSON diagnostic_line GET "${output}" diagnostics 0 line) +if(NOT diagnostic_line EQUAL 3) + message(FATAL_ERROR "Trailing-text diagnostic lost its line: ${output}") +endif() + +file( + WRITE "${VALIDATION_WORK_DIR}/fractional-integer.in" + "jobtype = mm-md;\n" + "nstep = 1.5;\n" + "timestep = 0.5;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate fractional-integer.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a fractional integer") +endif() +string(JSON diagnostic_line GET "${output}" diagnostics 0 line) +if(NOT diagnostic_line EQUAL 2) + message(FATAL_ERROR "Fractional-integer diagnostic lost its line: ${output}") +endif() + +file( + WRITE "${VALIDATION_WORK_DIR}/unsafe-zero.in" + "jobtype = mm-md;\n" + "nstep = 1;\n" + "timestep = 0;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate unsafe-zero.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a zero timestep") +endif() +string(JSON diagnostic_line GET "${output}" diagnostics 0 line) +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_line EQUAL 3) + message(FATAL_ERROR "Unsafe value lost its line: ${output}") +endif() +if(NOT diagnostic_message MATCHES "Time step must be finite") + message(FATAL_ERROR "Unexpected unsafe value diagnostic: ${output}") +endif() + +file( + WRITE "${VALIDATION_WORK_DIR}/zero-density.in" + "jobtype = mm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "density = 0;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate zero-density.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a zero density") +endif() +string(JSON diagnostic_line GET "${output}" diagnostics 0 line) +if(NOT diagnostic_line EQUAL 4) + message(FATAL_ERROR "Zero-density diagnostic lost its line: ${output}") +endif() + +file( + WRITE "${VALIDATION_WORK_DIR}/zero-constraint-iterations.in" + "jobtype = mm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "shake-iter = 0;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate zero-constraint-iterations.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted zero maximum constraint iterations") +endif() +string(JSON diagnostic_line GET "${output}" diagnostics 0 line) +if(NOT diagnostic_line EQUAL 4) + message(FATAL_ERROR "Constraint-iteration diagnostic lost its line: ${output}") +endif() + +file( + WRITE "${VALIDATION_WORK_DIR}/missing-timestep.in" + "jobtype = mm-md;\n" + "nstep = 1;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate missing-timestep.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted an MD input without timestep") +endif() +string(JSON diagnostic_line_type TYPE "${output}" diagnostics 0 line) +if(NOT diagnostic_line_type STREQUAL "NULL") + message(FATAL_ERROR "Semantic diagnostic invented a line: ${output}") +endif() + +foreach(decay_strategy IN ITEMS constant-decay exponential-decay) + file( + WRITE "${VALIDATION_WORK_DIR}/missing-${decay_strategy}.in" + "jobtype = mm-opt;\n" + "nstep = 1;\n" + "learning-rate-strategy = ${decay_strategy};\n" + "force-field = off;\n" + "start_file = start.rst;\n" + ) + run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate missing-${decay_strategy}.in --format=json + ) + if(result EQUAL 0) + message( + FATAL_ERROR + "PQ accepted ${decay_strategy} without learning-rate-decay" + ) + endif() + string(JSON diagnostic_message GET "${output}" diagnostics 0 message) + if(NOT diagnostic_message MATCHES "requires learning-rate-decay") + message( + FATAL_ERROR + "Unexpected learning-rate decay diagnostic: ${output}" + ) + endif() +endforeach() + +file( + WRITE "${VALIDATION_WORK_DIR}/unimplemented-line-search.in" + "jobtype = mm-opt;\n" + "nstep = 1;\n" + "learning-rate-strategy = linesearch-wolfe;\n" + "force-field = off;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate unimplemented-line-search.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted the unimplemented line search") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "not yet implemented") + message(FATAL_ERROR "Unexpected line search diagnostic: ${output}") +endif() + +file( + WRITE "${VALIDATION_WORK_DIR}/overlapping-learning-rate-bounds.in" + "jobtype = mm-opt;\n" + "nstep = 1;\n" + "learning-rate-strategy = constant;\n" + "min-learning-rate = 0.5;\n" + "max-learning-rate = 0.5;\n" + "force-field = off;\n" + "start_file = start.rst;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate overlapping-learning-rate-bounds.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted overlapping learning-rate bounds") +endif() +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_message MATCHES "minimum learning rate.*greater or equal") + message(FATAL_ERROR "Unexpected learning-rate bounds diagnostic: ${output}") +endif() + +file( + WRITE "${VALIDATION_WORK_DIR}/missing-reference.in" + "jobtype = mm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "start_file = missing\\\"start.rst;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate missing-reference.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a missing referenced file") +endif() +string(JSON diagnostic_line GET "${output}" diagnostics 0 line) +string(JSON diagnostic_message GET "${output}" diagnostics 0 message) +if(NOT diagnostic_line EQUAL 4) + message(FATAL_ERROR "Missing reference lost its line: ${output}") +endif() +if(NOT diagnostic_message MATCHES "missing.*start.rst") + message(FATAL_ERROR "JSON escaping changed the diagnostic: ${output}") +endif() + +file(MAKE_DIRECTORY "${VALIDATION_WORK_DIR}/start-directory") +file( + WRITE "${VALIDATION_WORK_DIR}/directory-reference.in" + "jobtype = mm-md;\n" + "nstep = 1;\n" + "timestep = 0.5;\n" + "start_file = start-directory;\n" +) +run_pq_in( + "${VALIDATION_WORK_DIR}" + output error result + --validate directory-reference.in --format=json +) +if(result EQUAL 0) + message(FATAL_ERROR "PQ accepted a directory as a referenced file") +endif() +string(JSON diagnostic_line GET "${output}" diagnostics 0 line) +if(NOT diagnostic_line EQUAL 4) + message(FATAL_ERROR "Directory reference lost its line: ${output}") +endif() diff --git a/tests/src/exceptions/testExceptions.cpp b/tests/src/exceptions/testExceptions.cpp index fee4fae34..9c58870d8 100644 --- a/tests/src/exceptions/testExceptions.cpp +++ b/tests/src/exceptions/testExceptions.cpp @@ -22,6 +22,7 @@ #include // for Test, TestInfo (ptr only), TEST, InitGoogleTest, RUN_ALL_TESTS +#include // for optional #include // for string_view #include "exceptions.hpp" // for GuffDatException, InputFileException @@ -41,6 +42,19 @@ TEST(TestExceptions, inputFileException) ); } +/** + * @brief tests structured source line metadata + */ +TEST(TestExceptions, sourceLine) +{ + auto exception = customException::InputFileException("test", 12); + + EXPECT_EQ(exception.getLineNumber(), std::optional(12)); + + exception.setLineNumber(13); + EXPECT_EQ(exception.getLineNumber(), std::optional(12)); +} + /** * @brief tests throwing restart file exception * @@ -208,4 +222,4 @@ TEST(TestExceptions, qmRunnerException) customException::QMRunnerException, "test" ); -} \ No newline at end of file +} diff --git a/tests/src/input/inputFileParsing/CMakeLists.txt b/tests/src/input/inputFileParsing/CMakeLists.txt index cdb9c8268..3307c44e8 100644 --- a/tests/src/input/inputFileParsing/CMakeLists.txt +++ b/tests/src/input/inputFileParsing/CMakeLists.txt @@ -10,6 +10,7 @@ set(source_files testForceFieldParser.cpp testGeneralParser.cpp testHessianParser.cpp + testHybridParser.cpp testIntegratorParser.cpp testManostatParser.cpp testNonCoulombParser.cpp diff --git a/tests/src/input/inputFileParsing/testConstraintsParser.cpp b/tests/src/input/inputFileParsing/testConstraintsParser.cpp index a21f99fc1..b6bec1d27 100644 --- a/tests/src/input/inputFileParsing/testConstraintsParser.cpp +++ b/tests/src/input/inputFileParsing/testConstraintsParser.cpp @@ -106,6 +106,13 @@ TEST_F(TestInputFileReader, testParseShakeTolerance) customException::InputFileException, "Shake tolerance must be positive" ); + + lineElements = {"shake-tolerance", "=", "0"}; + EXPECT_THROW_MSG( + parser.parseShakeTolerance(lineElements, 0), + customException::InputFileException, + "Shake tolerance must be positive" + ); } /** @@ -127,6 +134,13 @@ TEST_F(TestInputFileReader, testParseShakeIteration) customException::InputFileException, "Maximum shake iterations must be positive" ); + + lineElements = {"shake-iter", "=", "0"}; + EXPECT_THROW_MSG( + parser.parseShakeIteration(lineElements, 0), + customException::InputFileException, + "Maximum shake iterations must be positive" + ); } /** @@ -148,6 +162,13 @@ TEST_F(TestInputFileReader, testParseRattleTolerance) customException::InputFileException, "Rattle tolerance must be positive" ); + + lineElements = {"rattle-tolerance", "=", "0"}; + EXPECT_THROW_MSG( + parser.parseRattleTolerance(lineElements, 0), + customException::InputFileException, + "Rattle tolerance must be positive" + ); } /** @@ -169,6 +190,13 @@ TEST_F(TestInputFileReader, testParseRattleIteration) customException::InputFileException, "Maximum rattle iterations must be positive" ); + + lineElements = {"rattle-iter", "=", "0"}; + EXPECT_THROW_MSG( + parser.parseRattleIteration(lineElements, 0), + customException::InputFileException, + "Maximum rattle iterations must be positive" + ); } /** @@ -190,6 +218,13 @@ TEST_F(TestInputFileReader, testParseMShakeTolerance) customException::InputFileException, "MShake tolerance must be positive" ); + + lineElements = {"mshake-tolerance", "=", "0"}; + EXPECT_THROW_MSG( + parser.parseMShakeTolerance(lineElements, 0), + customException::InputFileException, + "MShake tolerance must be positive" + ); } /** @@ -211,6 +246,13 @@ TEST_F(TestInputFileReader, testParseMShakeIteration) customException::InputFileException, "Maximum MShake iterations must be positive" ); + + lineElements = {"mshake-iter", "=", "0"}; + EXPECT_THROW_MSG( + parser.parseMShakeIteration(lineElements, 0), + customException::InputFileException, + "Maximum MShake iterations must be positive" + ); } /** diff --git a/tests/src/input/inputFileParsing/testHybridParser.cpp b/tests/src/input/inputFileParsing/testHybridParser.cpp new file mode 100644 index 000000000..0ad220ec9 --- /dev/null +++ b/tests/src/input/inputFileParsing/testHybridParser.cpp @@ -0,0 +1,87 @@ +/***************************************************************************** + + + PQ + Copyright (C) 2023-now Jakob Gamper + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + +******************************************************************************/ + +#include + +#include + +#include "exceptions.hpp" +#include "hybridInputParser.hpp" +#include "testInputFileReader.hpp" +#include "throwWithMessage.hpp" + +using namespace customException; +using namespace input; + +TEST_F(TestInputFileReader, parseInvalidQMCharges) +{ + auto parser = HybridInputParser(*_engine); + + EXPECT_THROW_MSG( + parser.parseUseQMCharges({"qm_charges", "=", "invalid"}, 0), + InputFileException, + "Invalid qm_charges \"invalid\" in input file\n" + "Possible values are: qm, mm" + ); +} + +TEST_F(TestInputFileReader, parseNegativeHybridRadii) +{ + auto parser = HybridInputParser(*_engine); + + EXPECT_THROW_MSG( + parser.parseCoreRadius({"core_radius", "=", "-1"}, 0), + InputFileException, + "Invalid core_radius -1 in input file - must be a positive number" + ); + EXPECT_THROW_MSG( + parser.parseLayerRadius({"layer_radius", "=", "-1"}, 0), + InputFileException, + "Invalid layer_radius -1 in input file - must be a positive number" + ); + EXPECT_THROW_MSG( + parser.parseSmoothingRadius({"smoothing_radius", "=", "-1"}, 0), + InputFileException, + "Invalid smoothing_radius -1 in input file - must be a positive number" + ); +} + +TEST_F(TestInputFileReader, parseNonFiniteHybridRadii) +{ + auto parser = HybridInputParser(*_engine); + + EXPECT_THROW_MSG( + parser.parseCoreRadius({"core_radius", "=", "nan"}, 0), + std::invalid_argument, + "Invalid floating-point value 'nan' encountered" + ); + EXPECT_THROW_MSG( + parser.parseLayerRadius({"layer_radius", "=", "inf"}, 0), + std::invalid_argument, + "Invalid floating-point value 'inf' encountered" + ); + EXPECT_THROW_MSG( + parser.parseSmoothingRadius({"smoothing_radius", "=", "-inf"}, 0), + std::invalid_argument, + "Invalid floating-point value '-inf' encountered" + ); +} diff --git a/tests/src/input/inputFileParsing/testManostatParser.cpp b/tests/src/input/inputFileParsing/testManostatParser.cpp index 4aa71c2ba..aa4b69c1d 100644 --- a/tests/src/input/inputFileParsing/testManostatParser.cpp +++ b/tests/src/input/inputFileParsing/testManostatParser.cpp @@ -22,6 +22,7 @@ #include // for TestInfo (ptr only), EXPECT_EQ +#include #include // for string, allocator, basic_string #include // for vector @@ -45,6 +46,13 @@ TEST_F(TestInputFileReader, ParsePressure) parser.parsePressure(lineElements, 0); EXPECT_EQ(settings::ManostatSettings::getTargetPressure(), 300.0); + + lineElements = {"pressure", "=", "nan"}; + EXPECT_THROW_MSG( + parser.parsePressure(lineElements, 0), + std::invalid_argument, + "Invalid floating-point value 'nan' encountered" + ); } /** @@ -65,7 +73,21 @@ TEST_F(TestInputFileReader, ParseRelaxationTimeManostat) EXPECT_THROW_MSG( parser.parseManostatRelaxationTime(lineElements, 0), customException::InputFileException, - "Relaxation time of manostat cannot be negative" + "Relaxation time of manostat must be finite and greater than zero" + ); + + lineElements = {"p_relaxation", "=", "0"}; + EXPECT_THROW_MSG( + parser.parseManostatRelaxationTime(lineElements, 0), + customException::InputFileException, + "Relaxation time of manostat must be finite and greater than zero" + ); + + lineElements = {"p_relaxation", "=", "1e308"}; + EXPECT_THROW_MSG( + parser.parseManostatRelaxationTime(lineElements, 0), + customException::InputFileException, + "Relaxation time of manostat is too large to represent in femtoseconds" ); } @@ -126,7 +148,14 @@ TEST_F(TestInputFileReader, ParseCompressibility) EXPECT_THROW_MSG( parser.parseCompressibility(lineElements, 0), customException::InputFileException, - "Compressibility cannot be negative" + "Compressibility must be finite and non-negative" + ); + + lineElements = {"compressibility", "=", "inf"}; + EXPECT_THROW_MSG( + parser.parseCompressibility(lineElements, 0), + std::invalid_argument, + "Invalid floating-point value 'inf' encountered" ); } diff --git a/tests/src/input/inputFileParsing/testQMParser.cpp b/tests/src/input/inputFileParsing/testQMParser.cpp index 48cba9c6f..0b48891fb 100644 --- a/tests/src/input/inputFileParsing/testQMParser.cpp +++ b/tests/src/input/inputFileParsing/testQMParser.cpp @@ -22,18 +22,24 @@ #include // for TEST_F, EXPECT_EQ, RUN_ALL_TESTS -#include // for string, allocator +#include +#include +#include +#include #include "QMInputParser.hpp" // for InputFileParserQM #include "exceptions.hpp" // for InputFileException, customException #include "gtest/gtest.h" // for Message, TestPartResult +#include "outputFileSettings.hpp" #include "qmSettings.hpp" // for QMSettings +#include "referencesOutput.hpp" #include "testInputFileReader.hpp" // for TestInputFileReader #include "throwWithMessage.hpp" // for ASSERT_THROW_MSG using namespace input; using namespace settings; using namespace customException; +using references::ReferencesOutput; TEST_F(TestInputFileReader, parseQMMethod) { @@ -289,31 +295,44 @@ TEST_F(TestInputFileReader, parseMaceModelPath) TEST_F(TestInputFileReader, parseSlakosType) { - using enum QMMethod; - auto parser = QMInputParser(*_engine); -#ifdef WITH_ASE parser.parseSlakosType({"slakos", "=", "3ob"}, 0); EXPECT_EQ(QMSettings::getSlakosType(), SlakosType::THREEOB); parser.parseSlakosType({"slakos", "=", "matsci"}, 0); EXPECT_EQ(QMSettings::getSlakosType(), SlakosType::MATSCI); -#else - ASSERT_THROW_MSG( - parser.parseSlakosType({"slakos", "=", "3ob"}, 0), - InputFileException, - "Built-in SLAKOS sets (3ob/matsci) require building PQ with " - "-DBUILD_WITH_ASE=On" - ); - ASSERT_THROW_MSG( - parser.parseSlakosType({"slakos", "=", "matsci"}, 0), - InputFileException, - "Built-in SLAKOS sets (3ob/matsci) require building PQ with " - "-DBUILD_WITH_ASE=On" + const std::string referencePath = "slakos.refs.test"; + OutputFileSettings::setRefFileName(referencePath); + ReferencesOutput::writeReferencesFile(); + + std::ifstream referenceFile(referencePath); + std::stringstream referenceBuffer; + referenceBuffer << referenceFile.rdbuf(); + const auto references = referenceBuffer.str(); + EXPECT_NE( + references.find("3ob Slater-Koster Parameter Set"), + std::string::npos ); -#endif + EXPECT_NE( + references.find("matsci Slater-Koster Parameter Set"), + std::string::npos + ); + for (const auto reference : { + "10.1021/ct300849w", + "10.1021/ct401002w", + "10.1021/jp506557r", + "10.1021/ct5009137", + "10.1002/zaac.200500051", + "10.1021/nn700184k", + "10.1016/j.susc.2008.01.035", + "10.1021/jp8110343", + "10.3139/146.110337", + "Jardillier2006Matsci", + }) + EXPECT_NE(references.find(reference), std::string::npos); + ::remove(referencePath.c_str()); parser.parseSlakosType({"slakos", "=", "custom"}, 0); EXPECT_EQ(QMSettings::getSlakosType(), SlakosType::CUSTOM); @@ -405,6 +424,18 @@ TEST_F(TestInputFileReader, parseHubbardDerivs) InputFileException, "Invalid hubbard_derivs format \"H:1.0,He\" in input file." ) + + ASSERT_THROW_MSG( + parser.parseHubbardDerivs({"hubbard_derivs", "=", "H:0.1junk"}, 0), + InputFileException, + "Invalid hubbard_derivs format \"H:0.1junk\" in input file." + ) + + ASSERT_THROW_MSG( + parser.parseHubbardDerivs({"hubbard_derivs", "=", "H:nan"}, 0), + InputFileException, + "Invalid hubbard_derivs format \"H:nan\" in input file." + ) } TEST_F(TestInputFileReader, parseXtbMethod) diff --git a/tests/src/input/inputFileParsing/testSimulationBoxParser.cpp b/tests/src/input/inputFileParsing/testSimulationBoxParser.cpp index 90adb1ed6..fc15efdcb 100644 --- a/tests/src/input/inputFileParsing/testSimulationBoxParser.cpp +++ b/tests/src/input/inputFileParsing/testSimulationBoxParser.cpp @@ -55,6 +55,13 @@ TEST_F(TestInputFileReader, parseDensity) customException::InputFileException, "Density must be positive - density = -1" ); + + const std::vector zeroDensity = {"density", "=", "0"}; + EXPECT_THROW_MSG( + parser.parseDensity(zeroDensity, 0), + customException::InputFileException, + "Density must be positive - density = 0" + ); } /** @@ -128,4 +135,4 @@ TEST_F(TestInputFileReader, parseInitVelocities) "in input file.\n" "Possible options are: true, false, force" ); -} \ No newline at end of file +} diff --git a/tests/src/input/inputFileParsing/testThermostatParser.cpp b/tests/src/input/inputFileParsing/testThermostatParser.cpp index 4c5efab14..6dcdad208 100644 --- a/tests/src/input/inputFileParsing/testThermostatParser.cpp +++ b/tests/src/input/inputFileParsing/testThermostatParser.cpp @@ -55,8 +55,11 @@ TEST_F(TestInputFileReader, testParseTemperature) EXPECT_THROW_MSG( parser.parseTemperature(lineElements, 0), customException::InputFileException, - "Temperature cannot be negative" + "Temperature must be finite and non-negative" ); + + lineElements = {"temp", "=", "0"}; + EXPECT_NO_THROW(parser.parseTemperature(lineElements, 0)); } /** @@ -77,7 +80,22 @@ TEST_F(TestInputFileReader, testParseRelaxationTime) EXPECT_THROW_MSG( parser.parseThermostatRelaxationTime(lineElements, 0), customException::InputFileException, - "Relaxation time of thermostat cannot be negative" + "Relaxation time of thermostat must be finite and greater than zero" + ); + + lineElements = {"t_relaxation", "=", "1e308"}; + EXPECT_THROW_MSG( + parser.parseThermostatRelaxationTime(lineElements, 0), + customException::InputFileException, + "Relaxation time of thermostat is too large to represent in " + "femtoseconds" + ); + + lineElements = {"t_relaxation", "=", "0"}; + EXPECT_THROW_MSG( + parser.parseThermostatRelaxationTime(lineElements, 0), + customException::InputFileException, + "Relaxation time of thermostat must be finite and greater than zero" ); } @@ -158,7 +176,14 @@ TEST_F(TestInputFileReader, testParseFriction) EXPECT_THROW_MSG( parser.parseThermostatFriction(lineElements, 0), customException::InputFileException, - "Friction of thermostat cannot be negative" + "Friction of thermostat must be finite and non-negative" + ); + + lineElements = {"friction", "=", "1e308"}; + EXPECT_THROW_MSG( + parser.parseThermostatFriction(lineElements, 0), + customException::InputFileException, + "Friction of thermostat is too large to represent in inverse seconds" ); } @@ -179,7 +204,14 @@ TEST_F(TestInputFileReader, testParseChainLength) EXPECT_THROW_MSG( parser.parseThermostatChainLength(lineElements, 0), customException::InputFileException, - "Chain length of thermostat cannot be negative" + "Chain length of thermostat must be greater than zero" + ); + + lineElements = {"nh-chain-length", "=", "0"}; + EXPECT_THROW_MSG( + parser.parseThermostatChainLength(lineElements, 0), + customException::InputFileException, + "Chain length of thermostat must be greater than zero" ); } @@ -201,7 +233,14 @@ TEST_F(TestInputFileReader, testParseCouplingFrequency) EXPECT_THROW_MSG( parser.parseThermostatCouplingFrequency(lineElements, 0), customException::InputFileException, - "Coupling frequency of thermostat cannot be negative" + "Coupling frequency of thermostat must be finite and non-negative" + ); + + lineElements = {"coupling_frequency", "=", "1e308"}; + EXPECT_THROW_MSG( + parser.parseThermostatCouplingFrequency(lineElements, 0), + customException::InputFileException, + "Coupling frequency of thermostat is too large to represent in hertz" ); } @@ -243,7 +282,14 @@ TEST_F(TestInputFileReader, testParseTemperatureRampFrequency) EXPECT_THROW_MSG( parser.parseTemperatureRampFrequency(lineElements, 0), customException::InputFileException, - "Temperature ramp frequency cannot be negative" + "Temperature ramp frequency must be greater than zero" + ); + + lineElements = {"temp_ramp_frequency", "=", "0"}; + EXPECT_THROW_MSG( + parser.parseTemperatureRampFrequency(lineElements, 0), + customException::InputFileException, + "Temperature ramp frequency must be greater than zero" ); } @@ -264,8 +310,11 @@ TEST_F(TestInputFileReader, testParseStartTemperature) EXPECT_THROW_MSG( parser.parseStartTemperature(lineElements, 0), customException::InputFileException, - "Start temperature cannot be negative" + "Start temperature must be finite and non-negative" ); + + lineElements = {"start_temp", "=", "0"}; + EXPECT_NO_THROW(parser.parseStartTemperature(lineElements, 0)); } /** @@ -285,6 +334,9 @@ TEST_F(TestInputFileReader, testParseEndTemperature) EXPECT_THROW_MSG( parser.parseEndTemperature(lineElements, 0), customException::InputFileException, - "End temperature cannot be negative" + "End temperature must be finite and non-negative" ); + + lineElements = {"end_temp", "=", "0"}; + EXPECT_NO_THROW(parser.parseEndTemperature(lineElements, 0)); } diff --git a/tests/src/input/inputFileParsing/testTimingsParser.cpp b/tests/src/input/inputFileParsing/testTimingsParser.cpp index e45aed2d6..76d25e1cb 100644 --- a/tests/src/input/inputFileParsing/testTimingsParser.cpp +++ b/tests/src/input/inputFileParsing/testTimingsParser.cpp @@ -22,6 +22,8 @@ #include // for TestInfo (ptr only), InitGoogleTest, RUN_ALL_TESTS, EXPECT_EQ +#include // for format +#include #include // for string, allocator, basic_string #include // for vector @@ -46,6 +48,26 @@ TEST_F(TestInputFileReader, testParseTimestep) vector lineElements = {"timestep", "=", "1"}; parser.parseTimeStep(lineElements, 0); EXPECT_EQ(settings::TimingsSettings::getTimeStep(), 1.0); + + lineElements = {"timestep", "=", "0"}; + EXPECT_THROW_MSG( + parser.parseTimeStep(lineElements, 0), + customException::InputFileException, + "Time step must be finite and greater than zero" + ); + + for (const auto &invalid : {"nan", "inf"}) + { + lineElements = {"timestep", "=", invalid}; + EXPECT_THROW_MSG( + parser.parseTimeStep(lineElements, 0), + std::invalid_argument, + std::format( + "Invalid floating-point value '{}' encountered", + invalid + ) + ); + } } /** @@ -65,6 +87,13 @@ TEST_F(TestInputFileReader, testParseNumberOfSteps) EXPECT_THROW_MSG( parser.parseNumberOfSteps(lineElements, 0), customException::InputFileException, - "Number of steps cannot be negative" + "Number of steps must be greater than zero" ); -} \ No newline at end of file + + lineElements = {"nsteps", "=", "0"}; + EXPECT_THROW_MSG( + parser.parseNumberOfSteps(lineElements, 0), + customException::InputFileException, + "Number of steps must be greater than zero" + ); +} diff --git a/tests/src/input/testCommandLineArgs.cpp b/tests/src/input/testCommandLineArgs.cpp index 09f44eaff..48a478b21 100644 --- a/tests/src/input/testCommandLineArgs.cpp +++ b/tests/src/input/testCommandLineArgs.cpp @@ -31,32 +31,222 @@ #include "throwWithMessage.hpp" // for EXPECT_THROW_MSG /** - * @brief tests detecting flags and input file name via console input - * + * @brief tests parsing an input file name */ -TEST(TestCommandLineArgs, detectFlags) +TEST(TestCommandLineArgs, parse_input_file) { std::vector args = {"program", "input.in"}; auto commandLineArgs = CommandLineArgs(int(args.size()), args); - commandLineArgs.detectFlags(); + commandLineArgs.parse(); EXPECT_EQ("input.in", commandLineArgs.getInputFileName()); + EXPECT_EQ(CommandLineAction::RUN, commandLineArgs.getAction()); } /** - * @brief tests detecting flags and input file name via console input - * - * @TODO: no flags implemented at the moment + * @brief tests parsing the help option + */ +TEST(TestCommandLineArgs, parse_help) +{ + for (const auto &option : {"-h", "--help"}) + { + std::vector args = {"program", option}; + auto commandLineArgs = CommandLineArgs(int(args.size()), args); + + commandLineArgs.parse(); + EXPECT_EQ(CommandLineAction::HELP, commandLineArgs.getAction()); + } +} + +/** + * @brief tests parsing the version option + */ +TEST(TestCommandLineArgs, parse_version) +{ + for (const auto &option : {"-V", "--version"}) + { + std::vector args = {"program", option}; + auto commandLineArgs = CommandLineArgs(int(args.size()), args); + + commandLineArgs.parse(); + EXPECT_EQ(CommandLineAction::VERSION, commandLineArgs.getAction()); + } +} + +/** + * @brief tests parsing the machine-readable capabilities option + */ +TEST(TestCommandLineArgs, parse_capabilities) +{ + std::vector args = {"program", "--capabilities=json"}; + auto commandLineArgs = CommandLineArgs(int(args.size()), args); + + commandLineArgs.parse(); + EXPECT_EQ(CommandLineAction::CAPABILITIES, commandLineArgs.getAction()); +} + +/** + * @brief tests parsing input validation + */ +TEST(TestCommandLineArgs, parse_validation) +{ + std::vector args = {"program", "--validate", "input.in"}; + auto commandLineArgs = CommandLineArgs(int(args.size()), args); + + commandLineArgs.parse(); + EXPECT_EQ(CommandLineAction::VALIDATE, commandLineArgs.getAction()); + EXPECT_EQ(CommandLineFormat::TEXT, commandLineArgs.getFormat()); + EXPECT_EQ(ValidationScope::INSTALLED, commandLineArgs.getValidationScope()); + EXPECT_EQ("input.in", commandLineArgs.getInputFileName()); +} + +TEST(TestCommandLineArgs, parse_portable_json_validation) +{ + std::vector args = { + "program", + "--validate", + "input.in", + "--scope=portable", + "--format=json" + }; + auto commandLineArgs = CommandLineArgs(int(args.size()), args); + + commandLineArgs.parse(); + EXPECT_EQ(CommandLineFormat::JSON, commandLineArgs.getFormat()); + EXPECT_EQ(ValidationScope::PORTABLE, commandLineArgs.getValidationScope()); +} + +TEST(TestCommandLineArgs, parse_validation_options_in_either_order) +{ + std::vector args = { + "program", + "--validate", + "input.in", + "--format=json", + "--scope=portable" + }; + auto commandLineArgs = CommandLineArgs(int(args.size()), args); + + EXPECT_NO_THROW(commandLineArgs.parse()); + EXPECT_EQ(CommandLineFormat::JSON, commandLineArgs.getFormat()); + EXPECT_EQ(ValidationScope::PORTABLE, commandLineArgs.getValidationScope()); +} + +/** + * @brief tests parsing machine-readable input validation + */ +TEST(TestCommandLineArgs, parse_json_validation) +{ + std::vector args = + {"program", "--validate", "input.in", "--format=json"}; + auto commandLineArgs = CommandLineArgs(int(args.size()), args); + + commandLineArgs.parse(); + EXPECT_EQ(CommandLineAction::VALIDATE, commandLineArgs.getAction()); + EXPECT_EQ(CommandLineFormat::JSON, commandLineArgs.getFormat()); + EXPECT_EQ("input.in", commandLineArgs.getInputFileName()); +} + +TEST(TestCommandLineArgs, parse_explicit_text_validation) +{ + std::vector args = { + "program", + "--validate", + "input.in", + "--format=text", + "--scope=installed" + }; + auto commandLineArgs = CommandLineArgs(int(args.size()), args); + + commandLineArgs.parse(); + EXPECT_EQ(CommandLineFormat::TEXT, commandLineArgs.getFormat()); + EXPECT_EQ(ValidationScope::INSTALLED, commandLineArgs.getValidationScope()); +} + +TEST(TestCommandLineArgs, reject_duplicate_validation_format) +{ + std::vector args = + {"program", "--validate", "input.in", "--format=text", "--format=json"}; + auto commandLineArgs = CommandLineArgs(int(args.size()), args); + + EXPECT_THROW_MSG( + commandLineArgs.parse(), + customException::UserInputException, + "Unexpected argument: --format=json. Use PQ --help for usage." + ); +} + +/** + * @brief tests rejecting validation without an input file + */ +TEST(TestCommandLineArgs, parse_validation_without_input) +{ + std::vector args = {"program", "--validate"}; + auto commandLineArgs = CommandLineArgs(int(args.size()), args); + + EXPECT_THROW_MSG( + commandLineArgs.parse(), + customException::UserInputException, + "No input file specified. Usage: PQ --validate " + ); +} + +/** + * @brief tests rejecting a validation format without an input file */ -TEST(TestCommandLineArgs, detectFlags_flag_given) +TEST(TestCommandLineArgs, parse_validation_format_without_input) { - std::vector args = {"program", "-i", "input.in"}; + std::vector args = {"program", "--validate", "--format=json"}; auto commandLineArgs = CommandLineArgs(int(args.size()), args); EXPECT_THROW_MSG( - commandLineArgs.detectFlags(), + commandLineArgs.parse(), customException::UserInputException, - "Invalid flag: " + args[1] + " Flags are not yet implemented." + "No input file specified. Usage: PQ --validate " + ); +} + +/** + * @brief tests rejecting unsupported validation formats + */ +TEST(TestCommandLineArgs, parse_validation_unknown_format) +{ + std::vector args = + {"program", "--validate", "input.in", "--format=yaml"}; + auto commandLineArgs = CommandLineArgs(int(args.size()), args); + + EXPECT_THROW_MSG( + commandLineArgs.parse(), + customException::UserInputException, + "Unexpected argument: --format=yaml. Use PQ --help for usage." + ); +} + +TEST(TestCommandLineArgs, parse_validation_unknown_scope) +{ + std::vector args = + {"program", "--validate", "input.in", "--scope=project"}; + auto commandLineArgs = CommandLineArgs(int(args.size()), args); + + EXPECT_THROW_MSG( + commandLineArgs.parse(), + customException::UserInputException, + "Unexpected argument: --scope=project. Use PQ --help for usage." + ); +} + +/** + * @brief tests rejecting an unknown option + */ +TEST(TestCommandLineArgs, parse_unknown_option) +{ + std::vector args = {"program", "--unknown"}; + auto commandLineArgs = CommandLineArgs(int(args.size()), args); + + EXPECT_THROW_MSG( + commandLineArgs.parse(), + customException::UserInputException, + "Unknown option: --unknown. Use PQ --help for usage." ); } @@ -64,14 +254,29 @@ TEST(TestCommandLineArgs, detectFlags_flag_given) * @brief tests throwing exception if no input file name is given * */ -TEST(TestCommandLineArgs, detectFlags_missing_input_file) +TEST(TestCommandLineArgs, parse_missing_input_file) { std::vector args = {"program"}; auto commandLineArgs = CommandLineArgs(int(args.size()), args); EXPECT_THROW_MSG( - commandLineArgs.detectFlags(), + commandLineArgs.parse(), customException::UserInputException, "No input file specified. Usage: PQ " ); -} \ No newline at end of file +} + +/** + * @brief tests rejecting extra positional arguments + */ +TEST(TestCommandLineArgs, parse_extra_argument) +{ + std::vector args = {"program", "input.in", "extra"}; + auto commandLineArgs = CommandLineArgs(int(args.size()), args); + + EXPECT_THROW_MSG( + commandLineArgs.parse(), + customException::UserInputException, + "Unexpected argument: extra. Use PQ --help for usage." + ); +} diff --git a/tests/src/input/testInputValidation.cpp b/tests/src/input/testInputValidation.cpp index 4e56d31ca..3b0fa8dd1 100644 --- a/tests/src/input/testInputValidation.cpp +++ b/tests/src/input/testInputValidation.cpp @@ -22,20 +22,25 @@ #include +#include // for numeric_limits #include // for make_unique, unique_ptr #include // for string -#include "exceptions.hpp" // for InputFileException -#include "hessianSettings.hpp" // for HessianSettings -#include "inputFileReader.hpp" // for InputFileReader -#include "manostatSettings.hpp" // for ManostatSettings -#include "optEngine.hpp" // for OptEngine -#include "potentialSettings.hpp" // for PotentialSettings -#include "qmSettings.hpp" // for QMSettings -#include "settings.hpp" // for Settings -#include "thermostatSettings.hpp" // for ThermostatSettings -#include "throwWithMessage.hpp" // for ASSERT_THROW_MSG -#include "timingsSettings.hpp" // for TimingsSettings +#include "celllist.hpp" // for CellList +#include "defaults.hpp" // for manostat defaults +#include "exceptions.hpp" // for InputFileException +#include "hessianSettings.hpp" // for HessianSettings +#include "inputFileReader.hpp" // for InputFileReader +#include "manostatSettings.hpp" // for ManostatSettings +#include "optEngine.hpp" // for OptEngine +#include "optimizerSettings.hpp" // for OptimizerSettings +#include "potentialSettings.hpp" // for PotentialSettings +#include "qmSettings.hpp" // for QMSettings +#include "settings.hpp" // for Settings +#include "simulationBoxSettings.hpp" +#include "thermostatSettings.hpp" // for ThermostatSettings +#include "throwWithMessage.hpp" // for ASSERT_THROW_MSG +#include "timingsSettings.hpp" // for TimingsSettings using namespace customException; using namespace input; @@ -49,7 +54,14 @@ class TestInputValidation : public ::testing::Test Settings::setJobtype(JobType::NONE); HessianSettings::setOptimizeBeforeHessian(false); + OptimizerSettings::setLearningRateStrategy(LREnum::CONSTANT); + OptimizerSettings::setMinLearningRate(1.0e-15); + OptimizerSettings::setMaxLearningRate(1.0); + ManostatSettings::setManostatType(ManostatType::NONE); + ManostatSettings::setTauManostat( + defaults::_BERENDSEN_MANOSTAT_RELAX_TIME_ + ); ThermostatSettings::setThermostatType(ThermostatType::NONE); ThermostatSettings::setTemperatureSet(false); @@ -57,10 +69,17 @@ class TestInputValidation : public ::testing::Test ThermostatSettings::setEndTemperatureSet(false); ThermostatSettings::setTemperatureRampSteps(0); ThermostatSettings::setTemperatureRampFrequency(1); + ThermostatSettings::setRelaxationTime(0.1); + ThermostatSettings::setFriction( + defaults::_LANGEVIN_THERMOSTAT_FRICTION_ + ); + SimulationBoxSettings::setInitializeVelocities(InitVelocities::FALSE); PotentialSettings::setCoulombLongRangeType( CoulombLongRangeType::SHIFTED ); + PotentialSettings::setCoulombRadiusCutOff(12.5); + TimingsSettings::setTimeStep(0.5); QMSettings::setQMMethod(QMMethod::NONE); QMSettings::setMaceModel(MaceModel::MEDIUM); @@ -87,6 +106,8 @@ class TestInputValidation : public ::testing::Test TimingsSettings::setNumberOfSteps(100); setKeyword("nstep"); setKeyword("timestep"); + if (jobType == JobType::QM_MD || jobType == JobType::RING_POLYMER_QM_MD) + setKeyword("qm_prog"); } std::unique_ptr _engine; @@ -159,6 +180,33 @@ TEST_F(TestInputValidation, requiresPressureForManostat) ); } +TEST_F(TestInputValidation, rejectsUnstableManostatRelaxationTime) +{ + configureMDJob(JobType::MM_MD); + ManostatSettings::setManostatType(ManostatType::BERENDSEN); + ManostatSettings::setTauManostat(0.0001); + setKeyword("pressure"); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + InputFileException, + "The timestep must not exceed the manostat relaxation time" + ); +} + +TEST_F(TestInputValidation, requiresQMProgramForQMJob) +{ + Settings::setJobtype(JobType::QM_MD); + setKeyword("nstep"); + setKeyword("timestep"); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + InputFileException, + "QM job selected but the \"qm_prog\" keyword has not been set" + ); +} + TEST_F(TestInputValidation, requiresTemperatureForThermostat) { ThermostatSettings::setThermostatType(ThermostatType::BERENDSEN); @@ -188,8 +236,132 @@ TEST_F(TestInputValidation, acceptsEndTemperatureForThermostat) { configureMDJob(JobType::MM_MD); ThermostatSettings::setThermostatType(ThermostatType::BERENDSEN); + ThermostatSettings::setEndTemperature(300.0); setKeyword("end_temp"); + EXPECT_NO_THROW(_reader->validateInputConfiguration()); + EXPECT_DOUBLE_EQ(ThermostatSettings::getTargetTemperature(), 300.0); + EXPECT_DOUBLE_EQ(ThermostatSettings::getActualTargetTemperature(), 300.0); +} + +TEST_F(TestInputValidation, usesStartTemperatureForVelocityInitialization) +{ + configureMDJob(JobType::MM_MD); + ThermostatSettings::setThermostatType(ThermostatType::BERENDSEN); + ThermostatSettings::setTargetTemperature(300.0); + ThermostatSettings::setStartTemperature(200.0); + setKeyword("temp"); + setKeyword("start_temp"); + + EXPECT_NO_THROW(_reader->validateInputConfiguration()); + EXPECT_DOUBLE_EQ(ThermostatSettings::getActualTargetTemperature(), 200.0); +} + +TEST_F(TestInputValidation, requiresTemperatureForVelocityInitialization) +{ + configureMDJob(JobType::MM_MD); + SimulationBoxSettings::setInitializeVelocities(InitVelocities::FORCE); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + InputFileException, + "Initializing velocities requires temp, start_temp, or end_temp" + ); +} + +TEST_F(TestInputValidation, rejectsUnstableThermostatRelaxationTime) +{ + configureMDJob(JobType::MM_MD); + ThermostatSettings::setThermostatType(ThermostatType::VELOCITY_RESCALING); + ThermostatSettings::setTargetTemperature(300.0); + ThermostatSettings::setRelaxationTime(0.0001); + setKeyword("temp"); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + InputFileException, + "The timestep must not exceed the thermostat relaxation time" + ); +} + +TEST_F(TestInputValidation, rejectsNonFiniteLangevinScale) +{ + configureMDJob(JobType::MM_MD); + ThermostatSettings::setThermostatType(ThermostatType::LANGEVIN); + ThermostatSettings::setTargetTemperature(300.0); + ThermostatSettings::setFriction(std::numeric_limits::max()); + setKeyword("temp"); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + InputFileException, + "Langevin thermostat parameters produce a non-finite random-force " + "scale" + ); +} + +TEST_F(TestInputValidation, rejectsNonFiniteLangevinRampScale) +{ + configureMDJob(JobType::MM_MD); + ThermostatSettings::setThermostatType(ThermostatType::LANGEVIN); + ThermostatSettings::setTargetTemperature(300.0); + ThermostatSettings::setStartTemperature(std::numeric_limits::max()); + setKeyword("temp"); + setKeyword("start_temp"); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + InputFileException, + "Langevin thermostat parameters produce a non-finite random-force " + "scale" + ); +} + +TEST_F(TestInputValidation, rejectsCellListWithoutCoulombCutoff) +{ + configureMDJob(JobType::MM_MD); + _engine->getCellList().activate(); + PotentialSettings::setCoulombRadiusCutOff(0.0); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + InputFileException, + "An active cell list requires rcoulomb to be greater than zero" + ); +} + +TEST_F(TestInputValidation, rejectsCellListForPureQM) +{ + configureMDJob(JobType::QM_MD); + QMSettings::setQMMethod(QMMethod::DFTBPLUS); + _engine->getCellList().activate(); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + InputFileException, + "Cell lists are not available for pure QM simulations" + ); +} + +TEST_F(TestInputValidation, rejectsZeroTemperatureForNoseHoover) +{ + ThermostatSettings::setThermostatType(ThermostatType::NOSE_HOOVER); + ThermostatSettings::setTargetTemperature(0.0); + setKeyword("temp"); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + InputFileException, + "Nose-Hoover target temperature must be greater than zero" + ); +} + +TEST_F(TestInputValidation, acceptsZeroTemperatureForBerendsen) +{ + ThermostatSettings::setThermostatType(ThermostatType::BERENDSEN); + ThermostatSettings::setTargetTemperature(0.0); + setKeyword("temp"); + EXPECT_NO_THROW(_reader->validateInputConfiguration()); } @@ -262,6 +434,7 @@ TEST_F(TestInputValidation, rejectsHubbardDerivativesWithoutThirdOrder) QMSettings::setQMMethod(QMMethod::ASEDFTBPLUS); QMSettings::setSlakosType(SlakosType::CUSTOM); QMSettings::setUseThirdOrderDftb(false); + setKeyword("slakos_path"); setKeyword("third_order"); setKeyword("hubbard_derivs"); @@ -273,19 +446,45 @@ TEST_F(TestInputValidation, rejectsHubbardDerivativesWithoutThirdOrder) ); } +TEST_F(TestInputValidation, requiresSlaterKosterSetForAseDftbPlus) +{ + configureMDJob(JobType::QM_MD); + QMSettings::setQMMethod(QMMethod::ASEDFTBPLUS); + QMSettings::setSlakosType(SlakosType::NONE); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + InputFileException, + "ASE-DFTB+ requires slakos to be 3ob, matsci, or custom" + ); +} + TEST_F(TestInputValidation, acceptsHubbardDerivativesWithThirdOrder) { configureMDJob(JobType::QM_MD); QMSettings::setQMMethod(QMMethod::ASEDFTBPLUS); QMSettings::setSlakosType(SlakosType::CUSTOM); QMSettings::setUseThirdOrderDftb(true); + setKeyword("slakos_path"); setKeyword("third_order"); setKeyword("hubbard_derivs"); EXPECT_NO_THROW(_reader->validateInputConfiguration()); } -#ifdef WITH_ASE +TEST_F(TestInputValidation, requiresPathForCustomSlaterKosterParameters) +{ + configureMDJob(JobType::QM_MD); + QMSettings::setQMMethod(QMMethod::ASEDFTBPLUS); + QMSettings::setSlakosType(SlakosType::CUSTOM); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + InputFileException, + "Custom Slater-Koster parameters require the \"slakos_path\" keyword" + ); +} + TEST_F(TestInputValidation, rejectsExplicitlyDisabledThreeObThirdOrder) { configureMDJob(JobType::QM_MD); @@ -313,7 +512,6 @@ TEST_F(TestInputValidation, acceptsThreeObDefaultThirdOrder) EXPECT_NO_THROW(_reader->validateInputConfiguration()); } -#endif TEST_F(TestInputValidation, requiresFennolModelPath) { @@ -404,3 +602,89 @@ TEST_F(TestInputValidation, acceptsValidConditionalKeywords) EXPECT_NO_THROW(_reader->validateInputConfiguration()); } + +TEST_F(TestInputValidation, requiresDecayForConstantDecayOptimization) +{ + Settings::setJobtype(JobType::MM_OPT); + TimingsSettings::setNumberOfSteps(100); + OptimizerSettings::setLearningRateStrategy(LREnum::CONSTANT_DECAY); + setKeyword("nstep"); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + UserInputException, + "The constant-decay learning rate strategy requires " + "learning-rate-decay." + ); +} + +TEST_F(TestInputValidation, requiresDecayForExponentialDecayOptimization) +{ + Settings::setJobtype(JobType::MM_OPT); + TimingsSettings::setNumberOfSteps(100); + OptimizerSettings::setLearningRateStrategy(LREnum::EXPONENTIAL_DECAY); + setKeyword("nstep"); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + UserInputException, + "The exponential-decay learning rate strategy requires " + "learning-rate-decay." + ); +} + +TEST_F(TestInputValidation, acceptsConstantOptimizationWithoutDecay) +{ + Settings::setJobtype(JobType::MM_OPT); + TimingsSettings::setNumberOfSteps(100); + OptimizerSettings::setLearningRateStrategy(LREnum::CONSTANT); + setKeyword("nstep"); + + EXPECT_NO_THROW(_reader->validateInputConfiguration()); +} + +TEST_F(TestInputValidation, rejectsUnimplementedLineSearchOptimization) +{ + Settings::setJobtype(JobType::MM_OPT); + TimingsSettings::setNumberOfSteps(100); + OptimizerSettings::setLearningRateStrategy(LREnum::LINESEARCH_WOLFE); + setKeyword("nstep"); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + UserInputException, + "The Wolfe line search learning rate strategy is not yet implemented" + ); +} + +TEST_F(TestInputValidation, rejectsMissingLearningRateStrategy) +{ + Settings::setJobtype(JobType::MM_OPT); + TimingsSettings::setNumberOfSteps(100); + OptimizerSettings::setLearningRateStrategy(LREnum::NONE); + setKeyword("nstep"); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + UserInputException, + "In order to run the optimizer, you need to specify a learning rate " + "strategy." + ); +} + +TEST_F(TestInputValidation, rejectsOverlappingLearningRateBounds) +{ + Settings::setJobtype(JobType::MM_OPT); + TimingsSettings::setNumberOfSteps(100); + OptimizerSettings::setLearningRateStrategy(LREnum::CONSTANT); + OptimizerSettings::setMinLearningRate(0.5); + OptimizerSettings::setMaxLearningRate(0.5); + setKeyword("nstep"); + + ASSERT_THROW_MSG( + _reader->validateInputConfiguration(), + UserInputException, + "The minimum learning rate 0.5 is greater or equal to the maximum " + "learning rate 0.5, which is not allowed." + ); +} diff --git a/tests/src/main/CMakeLists.txt b/tests/src/main/CMakeLists.txt index 89fa86b50..ee01cae99 100644 --- a/tests/src/main/CMakeLists.txt +++ b/tests/src/main/CMakeLists.txt @@ -19,7 +19,3 @@ if(BUILD_WITH_KOKKOS) ${Kokkos_INCLUDE_DIRS_RET} ) endif() - -install(TARGETS pq_test_main - DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/ -) diff --git a/tests/src/output/testReferencesOutput.cpp b/tests/src/output/testReferencesOutput.cpp index d6887b5e5..75e35a377 100644 --- a/tests/src/output/testReferencesOutput.cpp +++ b/tests/src/output/testReferencesOutput.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include "outputFileSettings.hpp" @@ -67,21 +68,12 @@ TEST(TestReferencesOutput, writeReferencesFileEmitsHeaderAndBibtexBanner) TEST(TestReferencesOutput, addReferenceFileExtendsBothReferenceAndBibtexLists) { - // Sanity-check the static accessor is exposed at all; we can only observe - // the side effect through the rendered output file, which won't contain - // the new entry's body (the .ref file doesn't exist on disk) but the call - // itself must not throw and must remain idempotent for duplicates. EXPECT_NO_THROW(ReferencesOutput::addReferenceFile("nonexistent.ref")); EXPECT_NO_THROW(ReferencesOutput::addReferenceFile("nonexistent.ref")); const std::string path = "default.refs.test"; OutputFileSettings::setRefFileName(path); - ReferencesOutput::writeReferencesFile(); - - // Even with a non-existent reference file in the registered set, the - // overall write succeeds and the file exists with at least the headers. - const auto content = slurp(path); - EXPECT_FALSE(content.empty()); + EXPECT_THROW(ReferencesOutput::writeReferencesFile(), std::runtime_error); ::remove(path.c_str()); } diff --git a/tests/src/resetKinetics/testResetKinetics.cpp b/tests/src/resetKinetics/testResetKinetics.cpp index 76b9d68dd..37e543b87 100644 --- a/tests/src/resetKinetics/testResetKinetics.cpp +++ b/tests/src/resetKinetics/testResetKinetics.cpp @@ -26,6 +26,7 @@ #include #include "atom.hpp" +#include "exceptions.hpp" #include "gtest/gtest.h" #include "molecule.hpp" #include "physicalData.hpp" @@ -124,6 +125,44 @@ TEST(TestResetKinetics, resetTemperatureRescalesVelocitiesAndStaysFinite) delete box; } +TEST(TestResetKinetics, resetTemperatureSupportsZeroKelvin) +{ + auto *box = makeBox(); + resetKinetics::ResetKinetics resetKinetics; + + settings::ThermostatSettings::setTargetTemperature(0.0); + + auto data = physicalData::PhysicalData(); + data.calculateTemperature(*box); + resetKinetics.setTemperature(data.getTemperature()); + resetKinetics.resetTemperature(*box); + + data.calculateTemperature(*box); + EXPECT_DOUBLE_EQ(data.getTemperature(), 0.0); + for (const auto &atom : box->getAtoms()) + EXPECT_EQ(atom->getVelocity(), linearAlgebra::Vec3D(0.0, 0.0, 0.0)); + + delete box; +} + +TEST(TestResetKinetics, rejectsPositiveTargetFromZeroTemperature) +{ + auto *box = makeBox(); + resetKinetics::ResetKinetics resetKinetics; + + for (const auto &atom : box->getAtoms()) atom->setVelocity({0.0, 0.0, 0.0}); + + settings::ThermostatSettings::setTargetTemperature(300.0); + resetKinetics.setTemperature(0.0); + + EXPECT_THROW( + resetKinetics.resetTemperature(*box), + customException::UserInputException + ); + + delete box; +} + TEST(TestResetKinetics, resetMomentumZerosTotalLinearMomentum) { auto *box = makeBox(); diff --git a/tests/src/settings/testQMSettings.cpp b/tests/src/settings/testQMSettings.cpp index 4f692ed43..4bf15cebc 100644 --- a/tests/src/settings/testQMSettings.cpp +++ b/tests/src/settings/testQMSettings.cpp @@ -143,7 +143,6 @@ TEST(QMSettingsTest, SetMaceModelTypeTest) TEST(QMSettingsTest, SetSlakosTypeTest) { -#ifdef WITH_ASE QMSettings::setSlakosType("3ob"); EXPECT_EQ(QMSettings::getSlakosType(), SlakosType::THREEOB); @@ -155,7 +154,6 @@ TEST(QMSettingsTest, SetSlakosTypeTest) QMSettings::setSlakosType(SlakosType::MATSCI); EXPECT_EQ(QMSettings::getSlakosType(), SlakosType::MATSCI); -#endif QMSettings::setSlakosType("custom"); EXPECT_EQ(QMSettings::getSlakosType(), SlakosType::CUSTOM); @@ -176,27 +174,6 @@ TEST(QMSettingsTest, SetSlakosTypeTest) ); } -#ifndef WITH_ASE -TEST(QMSettingsTest, SetBuiltInSlakosTypeRequiresAse) -{ - ASSERT_THROW_MSG( - QMSettings::setSlakosType("3ob"), - InputFileException, - "Built-in SLAKOS sets (3ob/matsci) require building PQ with " - "-DBUILD_WITH_ASE=On" - ); - - ASSERT_THROW_MSG( - QMSettings::setSlakosType("matsci"), - InputFileException, - "Built-in SLAKOS sets (3ob/matsci) require building PQ with " - "-DBUILD_WITH_ASE=On" - ); - - QMSettings::setSlakosType("none"); -} -#endif - TEST(QMSettingsTest, SetSlakosPathTest) { QMSettings::setSlakosType("none"); diff --git a/tests/src/setup/testThermostatSetup.cpp b/tests/src/setup/testThermostatSetup.cpp index 16e377f0b..fb9d4d8ea 100644 --- a/tests/src/setup/testThermostatSetup.cpp +++ b/tests/src/setup/testThermostatSetup.cpp @@ -99,7 +99,33 @@ TEST_F(TestSetup, setupThermostat_temp_ramping) thermostatSetup.getEngine().getThermostat().getRampingFrequency(), 2 ); +} + +TEST_F(TestSetup, temperatureRampReachesEndWithPartialFinalInterval) +{ + ThermostatSetup thermostatSetup(*_mdEngine); + + settings::TimingsSettings::setNumberOfSteps(10); + settings::ThermostatSettings::setThermostatType("berendsen"); + settings::ThermostatSettings::setTargetTemperature(300); + settings::ThermostatSettings::setStartTemperature(200); + settings::ThermostatSettings::setTemperatureRampSteps(10); + settings::ThermostatSettings::setTemperatureRampFrequency(3); + thermostatSetup.setup(); + + EXPECT_DOUBLE_EQ( + thermostatSetup.getEngine().getThermostat().getTemperatureIncrease(), + 25.0 + ); + + for (size_t step = 0; step < 10; ++step) + thermostatSetup.getEngine().getThermostat().applyTemperatureRamping(); + + EXPECT_DOUBLE_EQ( + thermostatSetup.getEngine().getThermostat().getTargetTemperature(), + 300.0 + ); } TEST_F(TestSetup, setupThermostat_only_end_temp_defined) @@ -188,8 +214,7 @@ TEST_F(TestSetup, setupThermostat_langevin) EXPECT_EQ(langevinThermostat.getFriction(), 1.0e11); const auto conversionFactor = - constants::_UNIVERSAL_GAS_CONSTANT_ * - constants::_M2_TO_ANGSTROM2_ * + constants::_UNIVERSAL_GAS_CONSTANT_ * constants::_M2_TO_ANGSTROM2_ * constants::_KG_TO_GRAM_ / constants::_FS_TO_S_; const auto sigma = std::sqrt( 4.0 * langevinThermostat.getFriction() * conversionFactor * diff --git a/tests/src/simulationBox/testCelllist.cpp b/tests/src/simulationBox/testCelllist.cpp index 56e929b69..f2b202ee4 100644 --- a/tests/src/simulationBox/testCelllist.cpp +++ b/tests/src/simulationBox/testCelllist.cpp @@ -22,6 +22,7 @@ #include "testCelllist.hpp" +#include // for numeric_limits #include // for make_shared, __shared_ptr_access #include // for allocator, basic_string #include // for vector @@ -265,6 +266,17 @@ TEST_F(TestCellList, activateDeactivateToggles_isActive) EXPECT_TRUE(_cellList->isActive()); } +TEST_F(TestCellList, resizeCellsRejectsOverflow) +{ + _cellList->setNumberOfCells(std::numeric_limits::max()); + + EXPECT_THROW_MSG( + _cellList->resizeCells(), + customException::CellListException, + "Number of cells exceeds the supported size" + ); +} + /* ---------- clone() copies the configured cell counts ---------- */ TEST_F(TestCellList, clone_preservesNumberOfCellsAndNeighbourCells) diff --git a/tests/src/thermostat/testThermostat.cpp b/tests/src/thermostat/testThermostat.cpp index ab0303b7d..f18b51073 100644 --- a/tests/src/thermostat/testThermostat.cpp +++ b/tests/src/thermostat/testThermostat.cpp @@ -24,16 +24,17 @@ #include // for sqrt -#include "berendsenThermostat.hpp" // for BerendsenThermostat -#include "constants/internalConversionFactors.hpp" // for _TEMPERATURE_FACTOR_ -#include "gtest/gtest.h" // for InitGoogleTest -#include "langevinThermostat.hpp" // for LangevinThermostat -#include "noseHooverThermostat.hpp" // for NoseHooverThermostat -#include "physicalData.hpp" // for PhysicalData -#include "simulationBox.hpp" // for SimulationBox -#include "thermostatSettings.hpp" // for ThermostatType -#include "timingsSettings.hpp" // for TimingsSettings -#include "velocityRescalingThermostat.hpp" // for VelocityRescalingThermostat +#include "berendsenThermostat.hpp" // for BerendsenThermostat +#include "constants/internalConversionFactors.hpp" // for _TEMPERATURE_FACTOR_ +#include "exceptions.hpp" // for UserInputException +#include "gtest/gtest.h" // for InitGoogleTest +#include "langevinThermostat.hpp" // for LangevinThermostat +#include "noseHooverThermostat.hpp" // for NoseHooverThermostat +#include "physicalData.hpp" // for PhysicalData +#include "simulationBox.hpp" // for SimulationBox +#include "thermostatSettings.hpp" // for ThermostatType +#include "timingsSettings.hpp" // for TimingsSettings +#include "velocityRescalingThermostat.hpp" // for VelocityRescalingThermostat TEST_F(TestThermostat, calculateTemperature) { @@ -174,7 +175,10 @@ TEST_F(TestThermostat, velocityRescaling_tauSetterGetter) TEST_F(TestThermostat, velocityRescaling_thermostatType) { auto vr = thermostat::VelocityRescalingThermostat(300.0, 100.0); - EXPECT_EQ(vr.getThermostatType(), settings::ThermostatType::VELOCITY_RESCALING); + EXPECT_EQ( + vr.getThermostatType(), + settings::ThermostatType::VELOCITY_RESCALING + ); } TEST_F(TestThermostat, velocityRescaling_applyDoesNotNaN) @@ -195,14 +199,10 @@ TEST_F(TestThermostat, velocityRescaling_applyDoesNotNaN) } } -// Regression test: starting from zero kinetic energy (T == 0) used to -// produce NaN velocities, because tempRatio = T_target / 0 = Inf and -// the velocity scaling 0 * Inf = NaN. The guard skips the scaling and -// leaves velocities at zero. -TEST_F(TestThermostat, applyBerendsen_zeroTemperatureNoNaN) +TEST_F(TestThermostat, velocityRescalingZeroTemperatureDoesNotNaN) { delete _thermostat; - _thermostat = new thermostat::BerendsenThermostat(300.0, 100.0); + _thermostat = new thermostat::VelocityRescalingThermostat(0.0, 100.0); settings::TimingsSettings::setTimeStep(0.1); for (auto &atom : _simulationBox->getAtoms()) @@ -210,14 +210,40 @@ TEST_F(TestThermostat, applyBerendsen_zeroTemperatureNoNaN) _thermostat->applyThermostat(*_simulationBox, *_data); - EXPECT_FALSE(std::isnan(_data->getTemperature())); - EXPECT_FALSE(std::isinf(_data->getTemperature())); + EXPECT_TRUE(std::isfinite(_data->getTemperature())); for (const auto &atom : _simulationBox->getAtoms()) - for (size_t i = 0; i < 3; ++i) - { - EXPECT_FALSE(std::isnan(atom->getVelocity()[i])); - EXPECT_FALSE(std::isinf(atom->getVelocity()[i])); - } + for (size_t dimension = 0; dimension < 3; ++dimension) + EXPECT_TRUE(std::isfinite(atom->getVelocity()[dimension])); +} + +TEST_F(TestThermostat, velocityRescalingRejectsPositiveTargetFromZero) +{ + delete _thermostat; + _thermostat = new thermostat::VelocityRescalingThermostat(300.0, 100.0); + settings::TimingsSettings::setTimeStep(0.1); + + for (auto &atom : _simulationBox->getAtoms()) + atom->setVelocity({0.0, 0.0, 0.0}); + + EXPECT_THROW( + _thermostat->applyThermostat(*_simulationBox, *_data), + customException::UserInputException + ); +} + +TEST_F(TestThermostat, berendsenRejectsPositiveTargetFromZero) +{ + delete _thermostat; + _thermostat = new thermostat::BerendsenThermostat(300.0, 100.0); + settings::TimingsSettings::setTimeStep(0.1); + + for (auto &atom : _simulationBox->getAtoms()) + atom->setVelocity({0.0, 0.0, 0.0}); + + EXPECT_THROW( + _thermostat->applyThermostat(*_simulationBox, *_data), + customException::UserInputException + ); } /* ---------- LangevinThermostat ---------- */ diff --git a/tests/src/utilities/CMakeLists.txt b/tests/src/utilities/CMakeLists.txt index cbfd29b18..e33d93cca 100644 --- a/tests/src/utilities/CMakeLists.txt +++ b/tests/src/utilities/CMakeLists.txt @@ -1,4 +1,5 @@ set(source_files + testExecutablePath.cpp testStringUtilities.cpp testMathUtilities.cpp testCollectionUtilities.cpp diff --git a/tests/src/utilities/testExecutablePath.cpp b/tests/src/utilities/testExecutablePath.cpp new file mode 100644 index 000000000..f2a52a472 --- /dev/null +++ b/tests/src/utilities/testExecutablePath.cpp @@ -0,0 +1,36 @@ +/***************************************************************************** + + + PQ + Copyright (C) 2023-now Jakob Gamper + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + +******************************************************************************/ + +#include + +#include + +#include "executablePath.hpp" + +TEST(ExecutablePathTest, resolvesRunningExecutable) +{ + const auto executable = utilities::executablePath(); + + EXPECT_FALSE(executable.empty()); + EXPECT_TRUE(std::filesystem::is_regular_file(executable)); + EXPECT_EQ(executable.stem(), "testExecutablePath"); +} diff --git a/tests/src/utilities/testStringUtilities.cpp b/tests/src/utilities/testStringUtilities.cpp index 71d826879..04382e246 100644 --- a/tests/src/utilities/testStringUtilities.cpp +++ b/tests/src/utilities/testStringUtilities.cpp @@ -22,11 +22,13 @@ #include // for Test, TestInfo (ptr only), EXPECT_EQ -#include // for UINT32_MAX -#include // for remove -#include // for ofstream -#include // for out_of_range and invalid_argument -#include // for string, allocator +#include // for INT_MAX, INT_MIN +#include // for UINT32_MAX +#include // for remove +#include // for create_directory +#include // for ofstream +#include // for out_of_range and invalid_argument +#include // for string, allocator #include "exceptions.hpp" // for InputFileException #include "gmock/gmock.h" // for ElementsAre, MakePredicateFormatter @@ -164,18 +166,32 @@ TEST(TestStringUtilities, firstLetterToUpperCaseCopy) EXPECT_EQ("Test", utilities::firstLetterToUpperCaseCopy(line)); } +TEST(TestStringUtilities, shellQuote) +{ + EXPECT_EQ("''", utilities::shellQuote("")); + EXPECT_EQ("'path with spaces'", utilities::shellQuote("path with spaces")); + EXPECT_EQ( + "'a'\"'\"'b; touch nope'", + utilities::shellQuote("a'b; touch nope") + ); +} + /** * @brief test check if file exists * */ TEST(TestStringUtilities, fileExists) { - std::string file = "testFile.txt"; + std::string file = "testFile.txt"; + std::string directory = "testDirectory"; std::ofstream out(file); out.close(); + std::filesystem::create_directory(directory); EXPECT_TRUE(utilities::fileExists(file)); EXPECT_FALSE(utilities::fileExists("testFile2.txt")); + EXPECT_FALSE(utilities::fileExists(directory)); std::remove(file.c_str()); + std::filesystem::remove(directory); } /** @@ -260,6 +276,36 @@ TEST(TestStringUtilities, stringToUintFast32t) ); } +/** + * @brief test stringToInt function + * + */ +TEST(TestStringUtilities, stringToInt) +{ + EXPECT_EQ(0, utilities::stringToInt("0")); + EXPECT_EQ(43, utilities::stringToInt("+43")); + EXPECT_EQ(-43, utilities::stringToInt("-43")); + EXPECT_EQ(INT_MAX, utilities::stringToInt(std::to_string(INT_MAX))); + EXPECT_EQ(INT_MIN, utilities::stringToInt(std::to_string(INT_MIN))); + + for (const std::string invalid : {"", "3.14", "12steps", "1e3"}) + EXPECT_THROW_MSG( + utilities::stringToInt(invalid), + std::invalid_argument, + std::format("Invalid integer value '{}' encountered", invalid) + ); + + const auto outOfRange = std::to_string(static_cast(INT_MAX) + 1); + EXPECT_THROW_MSG( + utilities::stringToInt(outOfRange), + std::out_of_range, + std::format( + "Integer value '{}' exceeds the representable range for an int", + outOfRange + ) + ); +} + /** * @brief test stringToFiniteDouble function * @@ -290,6 +336,13 @@ TEST(TestStringUtilities, stringToFiniteDouble) std::format("Invalid floating-point value '{}' encountered", str) ); + str = "1.5fs"; + EXPECT_THROW_MSG( + utilities::stringToFiniteDouble(str), + std::invalid_argument, + std::format("Invalid floating-point value '{}' encountered", str) + ); + str = "-nan"; EXPECT_THROW_MSG( utilities::stringToFiniteDouble(str), @@ -321,4 +374,4 @@ TEST(TestStringUtilities, stringToFiniteDouble) str ) ); -} \ No newline at end of file +}