From 2fae47678d7019176b1c450990f1c4b90e7a1586 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Fri, 10 Jan 2025 13:30:54 +0100 Subject: [PATCH 01/61] allow building wihtout wxwidget --- CMakeLists.txt | 12 ++++++---- cmake/DebInstall.cmake | 51 ++++++++++++++++++++++------------------- cmake/MacSpecific.cmake | 35 ++++++++++++++-------------- cmake/Options.cmake | 6 +++++ cmake/Resources.cmake | 47 ++++++++++++++++++++++--------------- cmake/Sources.cmake | 25 +++++++++++++------- 6 files changed, 105 insertions(+), 71 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 083cfeee..515446d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,9 +31,11 @@ if(MOLOVOL_OSX_FAT_FILE) endif() # wxWidgets -set(wxWidgets_USE_STATIC=ON) -find_package(wxWidgets REQUIRED core base OPTIONAL_COMPONENTS net) -include(${wxWidgets_USE_FILE}) +if(MOLOVOL_BUILD_GUI) + set(wxWidgets_USE_STATIC=ON) + find_package(wxWidgets REQUIRED core base OPTIONAL_COMPONENTS net) + include(${wxWidgets_USE_FILE}) +endif() #find_package(OpenMP) @@ -76,7 +78,9 @@ add_executable(${EXE_NAME} ${SOURCES} ${OSX_RESOURCE_FILES}) # XCode, app bundle and libtiff include(MacSpecific) -target_link_libraries(${EXE_NAME} ${wxWidgets_LIBRARIES}) +if(MOLOVOL_BUILD_GUI) + target_link_libraries(${EXE_NAME} ${wxWidgets_LIBRARIES}) +endif() # Add custom flag if(MOLOVOL_ABS_RESOURCE_PATH) target_compile_definitions(${EXE_NAME} PUBLIC -DABS_PATH) diff --git a/cmake/DebInstall.cmake b/cmake/DebInstall.cmake index eaee6db1..9fa50b10 100644 --- a/cmake/DebInstall.cmake +++ b/cmake/DebInstall.cmake @@ -1,10 +1,8 @@ - include(GNUInstallDirs) -# Compress changelog +# Compress changelog and man pages (needed for both GUI and non-GUI) set(DEB_CHANGELOG_COMPRESSED "${CMAKE_CURRENT_BINARY_DIR}/changelog.gz") set(DEB_MAN_COMPRESSED "${CMAKE_CURRENT_BINARY_DIR}/molovol.1.gz") -set(HICOLOR_DIR "${CMAKE_CURRENT_BINARY_DIR}/hicolor") add_custom_command( OUTPUT ${DEB_CHANGELOG_COMPRESSED} ${DEB_MAN_COMPRESSED} @@ -15,23 +13,24 @@ add_custom_command( COMMENT "Compressing changelog and manual file" ) -add_custom_command( - OUTPUT ${HICOLOR_DIR} - COMMAND bash ${DEB_RES_DIR}/shell/resize_icon ${DEB_ICON} ./ - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - DEPENDS ${DEB_ICON} - COMMENT "Resizing icon to supported sizes" -) - add_custom_target(compress ALL DEPENDS ${DEB_CHANGELOG_COMPRESSED} ${DEB_MAN_COMPRESSED}) -add_custom_target(resize_icon ALL DEPENDS ${HICOLOR_DIR}) - -# -execute_process(COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE LINUX_ARCHITECTURE) +if(MOLOVOL_BUILD_GUI) + # GUI-specific icon processing + set(HICOLOR_DIR "${CMAKE_CURRENT_BINARY_DIR}/hicolor") + + add_custom_command( + OUTPUT ${HICOLOR_DIR} + COMMAND bash ${DEB_RES_DIR}/shell/resize_icon ${DEB_ICON} ./ + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS ${DEB_ICON} + COMMENT "Resizing icon to supported sizes" + ) + + add_custom_target(resize_icon ALL DEPENDS ${HICOLOR_DIR}) +endif() # Set directory permissions to 0755 -# Must come before install commands set( CPACK_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE @@ -39,15 +38,21 @@ set( WORLD_READ WORLD_EXECUTE ) -# Install commands so that these files get added to the deb file +# Install commands for both GUI and non-GUI install(TARGETS ${EXE_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES ${ELEM_FILE} ${SPACEGROUP_FILE} DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${EXE_NAME}) install(FILES ${DEB_COPYRIGHT_FILE} ${DEB_CHANGELOG_COMPRESSED} DESTINATION ${CMAKE_INSTALL_DATADIR}/doc/${EXE_NAME}) -install(FILES ${DEB_DESKTOP_FILE} - DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications) -install(FILES ${DEB_MAN_COMPRESSED} DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) -install(FILES ${DEB_ICON} DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pixmaps) -install(DIRECTORY ${HICOLOR_DIR} DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons) - +install(FILES ${DEB_MAN_COMPRESSED} + DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) + +# GUI-specific install commands +if(MOLOVOL_BUILD_GUI) + install(FILES ${DEB_DESKTOP_FILE} + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications) + install(FILES ${DEB_ICON} + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pixmaps) + install(DIRECTORY ${HICOLOR_DIR} + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons) +endif() \ No newline at end of file diff --git a/cmake/MacSpecific.cmake b/cmake/MacSpecific.cmake index b5ca7fde..8464f0fd 100644 --- a/cmake/MacSpecific.cmake +++ b/cmake/MacSpecific.cmake @@ -1,19 +1,20 @@ +if(MOLOVOL_BUILD_GUI) + # XCode compatibility + set_target_properties(${EXE_NAME} PROPERTIES + XCODE_GENERATE_SCHEME TRUE + XCODE_SCHEME_WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) -# XCode compatibility -set_target_properties(${EXE_NAME} PROPERTIES - XCODE_GENERATE_SCHEME TRUE - XCODE_SCHEME_WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} -) + # macOS Bundle + set_target_properties(${EXE_NAME} PROPERTIES + MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME} + MACOSX_BUNDLE_EXECUTABLE_NAME ${PROJECT_NAME} + MACOSX_BUNDLE_BUNDLE_VERSION ${CMAKE_PROJECT_VERSION} + MACOSX_BUNDLE_ICON_FILE icon + ) -# macOS Bundle -set_target_properties(${EXE_NAME} PROPERTIES - MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME} - MACOSX_BUNDLE_EXECUTABLE_NAME ${PROJECT_NAME} - MACOSX_BUNDLE_BUNDLE_VERSION ${CMAKE_PROJECT_VERSION} - MACOSX_BUNDLE_ICON_FILE icon -) - -# Library shenanigans -if(APPLE) - string(REPLACE "-ltiff" "/usr/local/opt/libtiff/lib/libtiff.a" wxWidgets_LIBRARIES "${wxWidgets_LIBRARIES}") -endif() + # Library shenanigans for wxWidgets + if(APPLE) + string(REPLACE "-ltiff" "/usr/local/opt/libtiff/lib/libtiff.a" wxWidgets_LIBRARIES "${wxWidgets_LIBRARIES}") + endif() +endif() \ No newline at end of file diff --git a/cmake/Options.cmake b/cmake/Options.cmake index 533be022..758ac5e0 100644 --- a/cmake/Options.cmake +++ b/cmake/Options.cmake @@ -20,3 +20,9 @@ if (CMAKE_MACOSX_BUNDLE AND NOT MOLOVOL_ABS_RESOURCE_PATH) message(WARNING "The executable inside a macOS application bundle is always compiled with MOLOVOL_ABS_RESOURCE_PATH enabled") set(MOLOVOL_ABS_RESOURCE_PATH TRUE) endif() + +option( + MOLOVOL_BUILD_GUI + "Build MoloVol with GUI support (requires wxWidgets)" + ON +) \ No newline at end of file diff --git a/cmake/Resources.cmake b/cmake/Resources.cmake index 79810b25..4445559e 100644 --- a/cmake/Resources.cmake +++ b/cmake/Resources.cmake @@ -1,26 +1,35 @@ # RESOURCE FILES -# Universal resource files +# Universal resource files (needed for both GUI and non-GUI) set(ELEM_FILE "${CMAKE_CURRENT_SOURCE_DIR}/inputfile/elements.txt") set(SPACEGROUP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/inputfile/space_groups.txt") set(LICENSE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE") set(README_FILE "${CMAKE_CURRENT_SOURCE_DIR}/README.md") -# Resource files for macOS Bundle -set(OSX_RES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/res/macOS") -set(OSX_ICON_FILE "${OSX_RES_DIR}/icon.icns") -set(OSX_LICENSE_RTF "${OSX_RES_DIR}/LICENSE.rtf") -set(OSX_DMG_BACKGROUND "${OSX_RES_DIR}/background.png") -set(OSX_DMG_DSSTORE "${OSX_RES_DIR}/DS_Store/.DS_Store") -set_source_files_properties(${OSX_ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") -set_source_files_properties(${ELEM_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") -set_source_files_properties(${SPACEGROUP_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") -set(OSX_RESOURCE_FILES ${OSX_ICON_FILE} ${ELEM_FILE} ${SPACEGROUP_FILE}) +if(MOLOVOL_BUILD_GUI) + # Resource files for macOS Bundle (GUI only) + set(OSX_RES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/res/macOS") + set(OSX_ICON_FILE "${OSX_RES_DIR}/icon.icns") + set(OSX_LICENSE_RTF "${OSX_RES_DIR}/LICENSE.rtf") + set(OSX_DMG_BACKGROUND "${OSX_RES_DIR}/background.png") + set(OSX_DMG_DSSTORE "${OSX_RES_DIR}/DS_Store/.DS_Store") + + if(APPLE) + set_source_files_properties(${OSX_ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") + set_source_files_properties(${ELEM_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") + set_source_files_properties(${SPACEGROUP_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") + set(OSX_RESOURCE_FILES ${OSX_ICON_FILE} ${ELEM_FILE} ${SPACEGROUP_FILE}) + endif() -# Resource files for Debian package -set(DEB_RES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/res/linux") -set(DEB_COPYRIGHT_FILE "${DEB_RES_DIR}/copyright") -set(DEB_CHANGELOG_FILE "${DEB_RES_DIR}/changelog") -file(STRINGS ${DEB_RES_DIR}/MoloVol.desktop DEB_DESKTOP_FILE) -set(DEB_DESKTOP_FILE ${DEB_RES_DIR}/MoloVol.desktop) -set(DEB_MAN_FILE ${DEB_RES_DIR}/molovol.1) -set(DEB_ICON ${DEB_RES_DIR}/molovol.png) + # GUI-specific Debian package resources + set(DEB_RES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/res/linux") + set(DEB_DESKTOP_FILE ${DEB_RES_DIR}/MoloVol.desktop) + set(DEB_ICON ${DEB_RES_DIR}/molovol.png) +endif() + +# Debian package resources needed for both GUI and non-GUI +if(UNIX AND NOT APPLE) + set(DEB_RES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/res/linux") + set(DEB_COPYRIGHT_FILE "${DEB_RES_DIR}/copyright") + set(DEB_CHANGELOG_FILE "${DEB_RES_DIR}/changelog") + set(DEB_MAN_FILE "${DEB_RES_DIR}/molovol.1") +endif() \ No newline at end of file diff --git a/cmake/Sources.cmake b/cmake/Sources.cmake index fd263b0d..ce56d5e7 100644 --- a/cmake/Sources.cmake +++ b/cmake/Sources.cmake @@ -1,16 +1,10 @@ - -# List of source files -set(SOURCES +# Base sources (non-GUI) +set(BASE_SOURCES src/atom.cpp src/atomtree.cpp - src/base_guicontrol.cpp src/base_cmdline.cpp src/base_constr.cpp - src/base_event.cpp - src/base_guicontrol.cpp - src/base_init.cpp src/cavity.cpp - src/controller.cpp src/crystallographer.cpp src/griddata.cpp src/importmanager.cpp @@ -23,3 +17,18 @@ set(SOURCES src/vector.cpp src/voxel.cpp ) + +# GUI-specific sources +set(GUI_SOURCES + src/base_guicontrol.cpp + src/base_event.cpp + src/base_init.cpp + src/controller.cpp +) + +# Combine sources based on GUI option +if(MOLOVOL_BUILD_GUI) + set(SOURCES ${BASE_SOURCES} ${GUI_SOURCES}) +else() + set(SOURCES ${BASE_SOURCES}) +endif() \ No newline at end of file From 6df246f805ecfc3b4b46a0503be189e652f6cdc3 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Fri, 10 Jan 2025 13:58:28 +0100 Subject: [PATCH 02/61] adapt code to work without GUI --- cmake/Sources.cmake | 16 +- include/controller.h | 33 ++-- include/special_chars.h | 3 +- src/base_cmdline.cpp | 423 ++++++++++++++++++++-------------------- src/controller.cpp | 94 +++++---- src/special_chars.cpp | 50 +++-- 6 files changed, 325 insertions(+), 294 deletions(-) diff --git a/cmake/Sources.cmake b/cmake/Sources.cmake index ce56d5e7..b17d795d 100644 --- a/cmake/Sources.cmake +++ b/cmake/Sources.cmake @@ -3,7 +3,6 @@ set(BASE_SOURCES src/atom.cpp src/atomtree.cpp src/base_cmdline.cpp - src/base_constr.cpp src/cavity.cpp src/crystallographer.cpp src/griddata.cpp @@ -16,19 +15,24 @@ set(BASE_SOURCES src/special_chars.cpp src/vector.cpp src/voxel.cpp + src/controller.cpp ) # GUI-specific sources set(GUI_SOURCES src/base_guicontrol.cpp + src/base_constr.cpp src/base_event.cpp src/base_init.cpp - src/controller.cpp ) -# Combine sources based on GUI option +set(CLI_SOURCES + src/base_cmdline.cpp +) + if(MOLOVOL_BUILD_GUI) - set(SOURCES ${BASE_SOURCES} ${GUI_SOURCES}) + set(SOURCES ${BASE_SOURCES} ${GUI_SOURCES}) else() - set(SOURCES ${BASE_SOURCES}) -endif() \ No newline at end of file + set(SOURCES ${BASE_SOURCES} ${CLI_SOURCES}) +endif() + diff --git a/include/controller.h b/include/controller.h index 27e83568..c06173b2 100644 --- a/include/controller.h +++ b/include/controller.h @@ -1,16 +1,19 @@ #ifndef CONTROLLER_H - #define CONTROLLER_H #include "flags.h" #include #include + +#ifdef MOLOVOL_GUI #include +class MainFrame; +#endif struct CalcReportBundle; class Model; -class MainFrame; -class Ctrl{ + +class Ctrl { public: static Ctrl* getInstance(); @@ -24,11 +27,19 @@ class Ctrl{ static std::string getDefaultElemPath(); static std::string getVersion(); +#ifdef MOLOVOL_GUI bool loadElementsFile(); bool loadAtomFile(); - bool runCalculation(); - bool runCalculation(const double, const double, const double, const std::string&, const std::string&, const std::string&, const int, const bool, const bool, const bool, const bool, const bool, const bool, const bool, const unsigned); + bool runCalculation(); // GUI version void registerView(MainFrame* inp_gui); +#endif + + // CLI version + bool runCalculation(const double, const double, const double, + const std::string&, const std::string&, const std::string&, + const int, const bool, const bool, const bool, const bool, + const bool, const bool, const bool, const unsigned); + void clearOutput(); void notifyUser(std::string); void notifyUser(std::wstring); @@ -50,16 +61,16 @@ class Ctrl{ void displayErrorMessage(const int, const std::vector& =std::vector()); private: - // consider making static pointer for model Model* _current_calculation; - // static attributes to ensure there is only one of each static Ctrl* s_instance; +#ifdef MOLOVOL_GUI static MainFrame* s_gui; +#endif - bool _abort_calculation; // variable for main thread to signal stopping the calculation + bool _abort_calculation; bool _calculation_finished; - bool _to_gui = true; // determines whether to print to console or to GUI - bool _quiet = true; // silences all non-result command line outputs + bool _to_gui = true; + bool _quiet = true; void displayInput(CalcReportBundle&, const unsigned=mvOUT_ALL); void displayResults(CalcReportBundle&, const unsigned=mvOUT_ALL); @@ -70,4 +81,4 @@ class Ctrl{ inline static const std::string s_elem_file = "elements.txt"; }; -#endif +#endif \ No newline at end of file diff --git a/include/special_chars.h b/include/special_chars.h index 1aea4487..b2653d26 100644 --- a/include/special_chars.h +++ b/include/special_chars.h @@ -3,8 +3,9 @@ #define SPECIAL_CHARS_H #include +#ifdef MOLOVOL_GUI #include - +#endif class Symbol{ public: static std::wstring angstrom(); diff --git a/src/base_cmdline.cpp b/src/base_cmdline.cpp index be7605c2..46951275 100644 --- a/src/base_cmdline.cpp +++ b/src/base_cmdline.cpp @@ -1,235 +1,226 @@ -#include - -#ifndef WX_PRECOMP -# include -#endif - -#include "base.h" +#include +#include +#include +#include +#include +#include #include "controller.h" #include "misc.h" -#include "flags.h" #include "special_chars.h" -#include -#include -// contains all command line options -static const wxCmdLineEntryDesc s_cmd_line_desc[] = -{ - { wxCMD_LINE_SWITCH, "h", "help", "Display help for command line interface", wxCMD_LINE_VAL_NONE, wxCMD_LINE_OPTION_HELP}, - // required - { wxCMD_LINE_OPTION, "r", "radius", "Probe radius", wxCMD_LINE_VAL_DOUBLE}, - { wxCMD_LINE_OPTION, "g", "grid", "Spatial resolution of the underlying grid", wxCMD_LINE_VAL_DOUBLE}, - { wxCMD_LINE_OPTION, "fs", "file-structure", "Path to the structure file", wxCMD_LINE_VAL_STRING}, - // optional - { wxCMD_LINE_OPTION, "fe", "file-elements", "Path to the elements file", wxCMD_LINE_VAL_STRING}, - { wxCMD_LINE_OPTION, "do", "dir-output", "Path to the output directory", wxCMD_LINE_VAL_STRING}, - { wxCMD_LINE_OPTION, "r2", "radius2", "Large probe radius (for two-probe mode)", wxCMD_LINE_VAL_DOUBLE}, - { wxCMD_LINE_OPTION, "d", "depth", "Octree depth", wxCMD_LINE_VAL_NUMBER}, - { wxCMD_LINE_SWITCH, "ht", "hetatm", "Include HETATM from pdb file", wxCMD_LINE_VAL_NONE, 0}, - { wxCMD_LINE_SWITCH, "uc", "unitcell", "Evaluate unit cell", wxCMD_LINE_VAL_NONE, 0}, - { wxCMD_LINE_SWITCH, "sf", "surface", "Calculate surfaces", wxCMD_LINE_VAL_NONE, 0}, - { wxCMD_LINE_SWITCH, "xr", "export-report", "Export report (requires:-do)", wxCMD_LINE_VAL_NONE, 0}, - { wxCMD_LINE_SWITCH, "xt", "export-total", "Export total surface map (requires:-do)", wxCMD_LINE_VAL_NONE, 0}, - { wxCMD_LINE_SWITCH, "xc", "export-cavities", "Export surface maps for all cavities (requires:-do)", wxCMD_LINE_VAL_NONE, 0}, - { wxCMD_LINE_OPTION, "o", "output", "Control what parts of the output to display (default:all)", wxCMD_LINE_VAL_STRING}, - { wxCMD_LINE_SWITCH, "q", "quiet", "Silence progress reporting", wxCMD_LINE_VAL_NONE, 0}, - { wxCMD_LINE_SWITCH, "un", "unicode", "Allow unicode output", wxCMD_LINE_VAL_NONE}, - { wxCMD_LINE_SWITCH, "v", "version", "Display the app version", wxCMD_LINE_VAL_NONE}, - { wxCMD_LINE_NONE } +struct CommandLineOption { + std::string shortName; + std::string longName; + std::string description; + bool isSwitch; // true for flags, false for options with values + bool isRequired; }; -static const std::vector s_required_args = {"radius", "grid", "file-structure"}; - -bool validateProbes(const double, const double, const bool); -bool validateExport(const std::string, const std::vector); -bool validatePdb(const std::string, const bool, const bool); -unsigned evalDisplayOptions(const std::string); - -// return true to supress GUI, return false to open GUI -void MainApp::evalCmdLine(){ - // if there are no cmd line arguments, open app normally - silenceGUI(true); - wxCmdLineParser parser = wxCmdLineParser(argc,argv); - parser.SetDesc(s_cmd_line_desc); - // if something is wrong with the cmd line args, stop - if(parser.Parse() != 0){return;} - // ascii - if(parser.Found("un")){Symbol::allow_unicode();} - else{Symbol::limit2ascii();} - // version - if(parser.Found("v")){ - Ctrl::getInstance()->version(); - return; - } - - // Check if all required arguments are available - std::vector missing_args; - - for (const std::string& arg_name : s_required_args){ - if (!parser.Found(arg_name)){ - missing_args.push_back(arg_name); - } - } - - switch (missing_args.size()){ - case (0): - break; - case (1): - Ctrl::getInstance()->displayErrorMessage(911, missing_args); - return; - case (2): - Ctrl::getInstance()->displayErrorMessage(912, missing_args); - return; - case (3): - Ctrl::getInstance()->displayErrorMessage(913, missing_args); - return; - default: - Ctrl::getInstance()->displayErrorMessage(914, missing_args); - return; - } - // All required arguments are available - - Ctrl::getInstance()->hush(parser.Found("q")); - - // minimum required arguments for calculation - double probe_radius_s; - double grid_resolution; - wxString structure_file_path; - - parser.Found("r",&probe_radius_s); - parser.Found("g",&grid_resolution); - parser.Found("fs",&structure_file_path); - - // optional arguments with default values - wxString elements_file_path = Ctrl::getDefaultElemPath(); - wxString output_dir_path = ""; - wxString output = "all"; - double probe_radius_l = 0; - long tree_depth = 4; - bool opt_include_hetatm = false; - bool opt_unit_cell = false; - bool opt_surface_area = false; - bool opt_probe_mode = false; - bool exp_report = false; - bool exp_total_map = false; - bool exp_cavity_maps = false; - - parser.Found("fe",&elements_file_path); - parser.Found("do",&output_dir_path); - parser.Found("o",&output); - parser.Found("r2",&probe_radius_l); - parser.Found("d",&tree_depth); - opt_include_hetatm = parser.Found("ht"); - opt_unit_cell = parser.Found("uc"); - opt_surface_area = parser.Found("sf"); - opt_probe_mode = parser.Found("r") && parser.Found("r2"); - exp_report = parser.Found("xr"); - exp_total_map = parser.Found("xt"); - exp_cavity_maps = parser.Found("xc"); - - if(!validateProbes(probe_radius_s, probe_radius_l, opt_probe_mode) - || !validateExport(output_dir_path.ToStdString(), {exp_report, exp_total_map, exp_cavity_maps}) - || !validatePdb(structure_file_path.ToStdString(), opt_include_hetatm, opt_unit_cell)){ - return; - } - - unsigned display_flag = evalDisplayOptions(output.ToStdString()); - - // run calculation - Ctrl::getInstance()->runCalculation( - probe_radius_s, - probe_radius_l, - grid_resolution, - structure_file_path.ToStdString(), - elements_file_path.ToStdString(), - output_dir_path.ToStdString(), - (int)tree_depth, - opt_include_hetatm, - opt_unit_cell, - opt_surface_area, - opt_probe_mode, - exp_report, - exp_total_map, - exp_cavity_maps, - display_flag); -} +class CommandLineParser { +private: + const std::vector options = { + {"h", "help", "Display help for command line interface", true, false}, + {"r", "radius", "Probe radius", false, true}, + {"g", "grid", "Spatial resolution of the underlying grid", false, true}, + {"fs", "file-structure", "Path to the structure file", false, true}, + {"fe", "file-elements", "Path to the elements file", false, false}, + {"do", "dir-output", "Path to the output directory", false, false}, + {"r2", "radius2", "Large probe radius (for two-probe mode)", false, false}, + {"d", "depth", "Octree depth", false, false}, + {"ht", "hetatm", "Include HETATM from pdb file", true, false}, + {"uc", "unitcell", "Evaluate unit cell", true, false}, + {"sf", "surface", "Calculate surfaces", true, false}, + {"xr", "export-report", "Export report (requires:-do)", true, false}, + {"xt", "export-total", "Export total surface map (requires:-do)", true, false}, + {"xc", "export-cavities", "Export surface maps for all cavities (requires:-do)", true, false}, + {"o", "output", "Control what parts of the output to display (default:all)", false, false}, + {"q", "quiet", "Silence progress reporting", true, false}, + {"un", "unicode", "Allow unicode output", true, false}, + {"v", "version", "Display the app version", true, false} + }; + + std::map parsedOptions; + std::map parsedFlags; + +public: + bool parse(int argc, char* argv[]) { + for (int i = 1; i < argc; i++) { + std::string arg = argv[i]; + if (arg[0] != '-') continue; + + std::string optName = arg.substr(arg[1] == '-' ? 2 : 1); + auto option = findOption(optName); + if (!option) { + std::cerr << "Unknown option: " << arg << std::endl; + return false; + } + + if (option->isSwitch) { + parsedFlags[option->longName] = true; + } else if (i + 1 < argc) { + parsedOptions[option->longName] = argv[++i]; + } else { + std::cerr << "Missing value for option: " << arg << std::endl; + return false; + } + } + + return validateRequiredOptions(); + } + + bool found(const std::string& name) const { + return parsedFlags.count(name) > 0 || parsedOptions.count(name) > 0; + } + + std::optional getValue(const std::string& name) const { + auto it = parsedOptions.find(name); + return it != parsedOptions.end() ? std::optional(it->second) : std::nullopt; + } + + void displayHelp() const { + std::cout << "Usage:\n"; + for (const auto& opt : options) { + std::cout << " -" << opt.shortName << ", --" << opt.longName + << (opt.isRequired ? " (required)" : "") << "\n " + << opt.description << "\n"; + } + } + +private: + std::optional findOption(const std::string& name) const { + for (const auto& opt : options) { + if (opt.shortName == name || opt.longName == name) { + return opt; + } + } + return std::nullopt; + } + + bool validateRequiredOptions() const { + std::vector missing; + for (const auto& opt : options) { + if (opt.isRequired && !found(opt.longName)) { + missing.push_back(opt.longName); + } + } + + if (!missing.empty()) { + Ctrl::getInstance()->displayErrorMessage(910 + missing.size(), missing); + return false; + } + return true; + } +}; -bool validateProbes(const double r1, const double r2, const bool pm){ - if(pm && r2 < r1){ - Ctrl::getInstance()->displayErrorMessage(104); - return false; - } - return true; +// Validation functions combined into single-line expressions +bool validateProbes(double r1, double r2, bool pm) { + return !(pm && r2 < r1 && (Ctrl::getInstance()->displayErrorMessage(104), true)); } -bool validateExport(const std::string out_dir, const std::vector exp_options){ - bool any_option_on = isIncluded(true,exp_options); - if (any_option_on && out_dir.empty()){ - Ctrl::getInstance()->displayErrorMessage(302); - return false; - } - return true; +bool validateExport(const std::string& out_dir, const std::vector& exp_options) { + return !(isIncluded(true, exp_options) && out_dir.empty() && (Ctrl::getInstance()->displayErrorMessage(302), true)); } -bool validatePdb(const std::string file, const bool hetatm, const bool unitcell){ - if ((fileExtension(file) != "pdb" && fileExtension(file) != "cif") && (hetatm || unitcell)){ - Ctrl::getInstance()->displayErrorMessage(115); - return false; - } - return true; +bool validatePdb(const std::string& file, bool hetatm, bool unitcell) { + return !((fileExtension(file) != "pdb" && fileExtension(file) != "cif") && (hetatm || unitcell) && + (Ctrl::getInstance()->displayErrorMessage(115), true)); } -static std::map s_display_map { - {"none", mvOUT_NONE}, - {"inputfile", mvOUT_STRUCTURE}, - {"resolution", mvOUT_RESOLUTION}, - {"depth", mvOUT_DEPTH}, - {"radius_small", mvOUT_RADIUS_S}, - {"radius_large", mvOUT_RADIUS_L}, - {"input", mvOUT_INP}, - {"hetatm", mvOUT_OPT_HETATM}, - {"unitcell", mvOUT_OPT_UNITCELL}, - {"probemode", mvOUT_OPT_PROBEMODE}, - {"surface", mvOUT_OPT_SURFACE}, - {"options", mvOUT_OPT}, - {"formula", mvOUT_FORMULA}, - {"time", mvOUT_TIME}, - {"vol_vdw", mvOUT_VOL_VDW}, - {"vol_inaccessible", mvOUT_VOL_INACCESSIBLE}, - {"vol_core_s", mvOUT_VOL_CORE_S}, - {"vol_shell_s", mvOUT_VOL_SHELL_S}, - {"vol_core_l", mvOUT_VOL_CORE_L}, - {"vol_shell_l", mvOUT_VOL_SHELL_L}, - {"vol_mol", mvOUT_VOL_MOL}, - {"vol", mvOUT_VOL}, - {"surf_vdw", mvOUT_SURF_VDW}, - {"surf_mol", mvOUT_SURF_MOL}, - {"surf_excluded_s", mvOUT_SURF_EXCLUDED_S}, - {"surf_accessible_s", mvOUT_SURF_ACCESSIBLE_S}, - {"surf", mvOUT_SURF}, - {"cavities", mvOUT_CAVITIES}, - {"all", mvOUT_ALL} +// Display options map +static const std::map DISPLAY_OPTIONS = { + {"none", mvOUT_NONE}, {"inputfile", mvOUT_STRUCTURE}, {"resolution", mvOUT_RESOLUTION}, + {"depth", mvOUT_DEPTH}, {"radius_small", mvOUT_RADIUS_S}, {"radius_large", mvOUT_RADIUS_L}, + {"input", mvOUT_INP}, {"hetatm", mvOUT_OPT_HETATM}, {"unitcell", mvOUT_OPT_UNITCELL}, + {"probemode", mvOUT_OPT_PROBEMODE}, {"surface", mvOUT_OPT_SURFACE}, {"options", mvOUT_OPT}, + {"formula", mvOUT_FORMULA}, {"time", mvOUT_TIME}, {"vol_vdw", mvOUT_VOL_VDW}, + {"vol_inaccessible", mvOUT_VOL_INACCESSIBLE}, {"vol_core_s", mvOUT_VOL_CORE_S}, + {"vol_shell_s", mvOUT_VOL_SHELL_S}, {"vol_core_l", mvOUT_VOL_CORE_L}, + {"vol_shell_l", mvOUT_VOL_SHELL_L}, {"vol_mol", mvOUT_VOL_MOL}, {"vol", mvOUT_VOL}, + {"surf_vdw", mvOUT_SURF_VDW}, {"surf_mol", mvOUT_SURF_MOL}, + {"surf_excluded_s", mvOUT_SURF_EXCLUDED_S}, {"surf_accessible_s", mvOUT_SURF_ACCESSIBLE_S}, + {"surf", mvOUT_SURF}, {"cavities", mvOUT_CAVITIES}, {"all", mvOUT_ALL} }; -unsigned evalDisplayOptions(const std::string output){ - std::stringstream ss(output); - std::vector display_options; - while(ss.good()){ - std::string substr; - getline(ss, substr, ','); - display_options.push_back(substr); - } - unsigned display_flag = 0; - bool unknown_flag = false; - for (std::string& elem : display_options){ - if (s_display_map.find(elem) == s_display_map.end()){ - unknown_flag = true; +unsigned evalDisplayOptions(const std::string& output) { + std::stringstream ss(output); + std::string option; + unsigned display_flag = 0; + bool unknown_flag = false; + + while (std::getline(ss, option, ',')) { + auto it = DISPLAY_OPTIONS.find(option); + if (it != DISPLAY_OPTIONS.end()) { + display_flag |= it->second; + } else { + unknown_flag = true; + } + } + + if (unknown_flag) { + Ctrl::getInstance()->displayErrorMessage(902); + } + return display_flag; +} + +bool evalCmdLine(int argc, char* argv[]) { + CommandLineParser parser; + if (!parser.parse(argc, argv)) return false; + + if (parser.found("help")) { + parser.displayHelp(); + return true; } - else { - display_flag |= s_display_map.at(elem); + + parser.found("unicode") ? Symbol::allow_unicode() : Symbol::limit2ascii(); + + if (parser.found("version")) { + Ctrl::getInstance()->version(); + return true; + } + + Ctrl::getInstance()->hush(parser.found("quiet")); + + // Parse required options + auto probe_radius_s = std::stod(parser.getValue("radius").value()); + auto grid_resolution = std::stod(parser.getValue("grid").value()); + auto structure_file_path = parser.getValue("file-structure").value(); + + // Optional parameters with defaults + auto elements_file_path = parser.getValue("file-elements").value_or(Ctrl::getDefaultElemPath()); + auto output_dir_path = parser.getValue("dir-output").value_or(""); + auto output = parser.getValue("output").value_or("all"); + auto probe_radius_l = parser.getValue("radius2") ? std::stod(parser.getValue("radius2").value()) : 0.0; + auto tree_depth = parser.getValue("depth") ? std::stol(parser.getValue("depth").value()) : 4; + + bool opt_include_hetatm = parser.found("hetatm"); + bool opt_unit_cell = parser.found("unitcell"); + bool opt_surface_area = parser.found("surface"); + bool opt_probe_mode = parser.found("radius") && parser.found("radius2"); + bool exp_report = parser.found("export-report"); + bool exp_total_map = parser.found("export-total"); + bool exp_cavity_maps = parser.found("export-cavities"); + + if (!validateProbes(probe_radius_s, probe_radius_l, opt_probe_mode) || + !validateExport(output_dir_path, {exp_report, exp_total_map, exp_cavity_maps}) || + !validatePdb(structure_file_path, opt_include_hetatm, opt_unit_cell)) { + return false; } - } - if (unknown_flag){Ctrl::getInstance()->displayErrorMessage(902);} - return display_flag; + + unsigned display_flag = evalDisplayOptions(output); + + return Ctrl::getInstance()->runCalculation( + probe_radius_s, probe_radius_l, grid_resolution, structure_file_path, + elements_file_path, output_dir_path, tree_depth, opt_include_hetatm, + opt_unit_cell, opt_surface_area, opt_probe_mode, exp_report, + exp_total_map, exp_cavity_maps, display_flag); } + +int main(int argc, char* argv[]) { + Ctrl::getInstance()->disableGUI(); + if (argc < 2) { + std::cout << "Use --help for usage information\n"; + return 1; + } + return evalCmdLine(argc, argv) ? 0 : 1; +} \ No newline at end of file diff --git a/src/controller.cpp b/src/controller.cpp index 7680edd7..f85281ae 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -1,7 +1,5 @@ - #include "controller.h" -#include "base.h" -#include "atom.h" // i don't know why +#include "atom.h" #include "model.h" #include "misc.h" #include "exception.h" @@ -11,12 +9,18 @@ #include #include +#ifdef MOLOVOL_GUI +#include "base.h" +#endif + /////////////////////// // STATIC ATTRIBUTES // /////////////////////// Ctrl* Ctrl::s_instance = NULL; +#ifdef MOLOVOL_GUI MainFrame* Ctrl::s_gui = NULL; +#endif /////////////////////////// // STATIC MEMBERS ACCESS // @@ -51,7 +55,11 @@ void Ctrl::disableGUI(){ } void Ctrl::enableGUI(){ +#ifdef MOLOVOL_GUI _to_gui = true; +#else + _to_gui = false; +#endif } bool Ctrl::isGUIEnabled(){ @@ -62,10 +70,6 @@ bool Ctrl::isGUIEnabled(){ // METHODS // ///////////// -void Ctrl::registerView(MainFrame* inp_gui){ - s_gui = inp_gui; -} - Ctrl* Ctrl::getInstance(){ if(s_instance == NULL){ s_instance = new Ctrl(); @@ -73,44 +77,39 @@ Ctrl* Ctrl::getInstance(){ return s_instance; } +#ifdef MOLOVOL_GUI +void Ctrl::registerView(MainFrame* inp_gui){ + s_gui = inp_gui; +} + bool Ctrl::loadElementsFile(){ - // create an instance of the model class - // ensures, that there is only ever one instance of the model class + // GUI-specific implementation... if(_current_calculation == NULL){ _current_calculation = new Model(); } std::string elements_filepath = s_gui->getElementsFilepath(); - // even if there is no valid radii file, the program can be used - // by manually setting radii in the GUI after loading a structure if(!_current_calculation->importElemFile(elements_filepath)){ displayErrorMessage(101); } - // refresh atom list using new radius map s_gui->displayAtomList(_current_calculation->generateAtomList()); return true; } bool Ctrl::loadAtomFile(){ - // create an instance of the model class - // ensures, that there is only ever one instance of the model class + // GUI-specific implementation... if(_current_calculation == NULL){ _current_calculation = new Model(); } - bool successful_import; - successful_import = _current_calculation->readAtomsFromFile(s_gui->getAtomFilepath(), s_gui->getIncludeHetatm()); - + bool successful_import = _current_calculation->readAtomsFromFile( + s_gui->getAtomFilepath(), + s_gui->getIncludeHetatm() + ); s_gui->displayAtomList(_current_calculation->generateAtomList()); - return successful_import; } -///////////////// -// CALCULATION // -///////////////// - -// default function call: transfer data from GUI to Model bool Ctrl::runCalculation(){ // reset abort flag setAbortFlag(false); @@ -166,8 +165,9 @@ bool Ctrl::runCalculation(){ return data.success; } +#endif -// for starting a calculation from the command line +// CLI version bool Ctrl::runCalculation( const double probe_radius_s, const double probe_radius_l, @@ -245,10 +245,12 @@ bool Ctrl::runCalculation( //////////////////////// void Ctrl::clearOutput(){ +#ifdef MOLOVOL_GUI if (_to_gui) { s_gui->extClearOutputText(); s_gui->extClearOutputGrid(); } +#endif } void Ctrl::displayInput(CalcReportBundle& data, const unsigned display_flag){ @@ -455,8 +457,11 @@ void Ctrl::displayCavityList(CalcReportBundle& data, const unsigned display_flag } // display data + if (_to_gui){ + #ifdef MOLOVOL_GUI s_gui->extDisplayCavityList(table); + #endif } else{ table.print(); @@ -464,41 +469,47 @@ void Ctrl::displayCavityList(CalcReportBundle& data, const unsigned display_flag } void Ctrl::notifyUser(std::string str){ +#ifdef MOLOVOL_GUI if (_to_gui){ s_gui->extAppendOutput(str); + return; } - else { - std::cout << str; - } +#endif + std::cout << str; } void Ctrl::notifyUser(std::wstring wstr){ +#ifdef MOLOVOL_GUI if (_to_gui){ s_gui->extAppendOutputW(wstr); + return; } - else { - std::cout << wstr; - } +#endif + std::wcout << wstr; } void Ctrl::updateStatus(const std::string str){ +#ifdef MOLOVOL_GUI if (_to_gui) { s_gui->extSetStatus(str); + return; } - else if(_quiet) {} - else{ +#endif + if (!_quiet) { std::cout << str << std::endl; } } void Ctrl::updateProgressBar(const int percentage){ assert (percentage <= 100); +#ifdef MOLOVOL_GUI if (_to_gui) { s_gui->extSetProgressBar(percentage); + return; } - else if(_quiet) {} - else { - std::cout << std::to_string(percentage) + "\%" << std::endl; +#endif + if (!_quiet) { + std::cout << percentage << "%" << std::endl; } } @@ -565,9 +576,11 @@ bool Ctrl::getAbortFlag(){ // checks whether worker thread has received a signal to stop the calculation and // updates the progress of the calculation void Ctrl::updateCalculationStatus(){ +#ifdef MOLOVOL_GUI if (_to_gui){ setAbortFlag(s_gui->receivedAbortCommand()); } + #endif } //////////////////// @@ -624,14 +637,14 @@ void Ctrl::displayErrorMessage(const int error_code, const std::vectorextOpenErrorDialog(error_code, msg); + return; } - else{ - // Print to console - std::cout << error_code << ": " << msg << std::endl; - } +#endif + // Print to console + std::cout << error_code << ": " << msg << std::endl; } std::string Ctrl::getErrorMessage(const int error_code){ @@ -642,3 +655,4 @@ std::string Ctrl::getErrorMessage(const int error_code){ return s_error_codes.find(error_code)->second; } } + diff --git a/src/special_chars.cpp b/src/special_chars.cpp index 3720f87f..5ff4689f 100644 --- a/src/special_chars.cpp +++ b/src/special_chars.cpp @@ -1,52 +1,62 @@ - #include "special_chars.h" #include #include +#include +#include +#include + +#ifdef MOLOVOL_GUI +#include +#endif bool Symbol::s_ascii = false; -void Symbol::limit2ascii(){ +void Symbol::limit2ascii() { Symbol::s_ascii = true; } -void Symbol::allow_unicode(){ +void Symbol::allow_unicode() { Symbol::s_ascii = false; } -std::wstring Symbol::angstrom(){ - return Symbol::s_ascii? L"A" : L"\u212B"; +std::wstring Symbol::angstrom() { + return Symbol::s_ascii ? L"A" : L"\u212B"; } -std::wstring Symbol::squared(){ - return Symbol::s_ascii? L"^2" : L"\u00B2"; +std::wstring Symbol::squared() { + return Symbol::s_ascii ? L"^2" : L"\u00B2"; } -std::wstring Symbol::cubed(){ - return Symbol::s_ascii? L"^3": L"\u00B3"; +std::wstring Symbol::cubed() { + return Symbol::s_ascii ? L"^3" : L"\u00B3"; } // from a numeric char, return the unicode encoded subscripts -wchar_t Symbol::digitSubscript(char digit){ - if (Symbol::s_ascii){return digit;} +wchar_t Symbol::digitSubscript(char digit) { + if (Symbol::s_ascii) { + return digit; + } wchar_t subscript = 0x2080; // unicode encoding of subscript "0" int value = digit - '0'; // convert numeric char to corresponding int subscript = subscript + value; // adding a digit to subscript "0" return subscript of that digit return subscript; } -std::wstring Symbol::generateChemicalFormulaUnicode(std::string chemical_formula){ +std::wstring Symbol::generateChemicalFormulaUnicode(std::string chemical_formula) { std::wstring chemical_formula_unicode; - for (size_t i = 0; i < chemical_formula.size(); i++){ - if (isalpha(chemical_formula[i])){ - // it is much simpler to convert string to wxString to wstring than directly from string to wstring + for (size_t i = 0; i < chemical_formula.size(); i++) { + if (isalpha(chemical_formula[i])) { +#ifdef MOLOVOL_GUI + // Use wxString for GUI builds wxString buffer(chemical_formula[i]); chemical_formula_unicode.append(buffer.wxString::ToStdWstring()); - } - else { +#else + // Direct conversion for non-GUI builds + chemical_formula_unicode.push_back(static_cast(chemical_formula[i])); +#endif + } else { chemical_formula_unicode += digitSubscript(chemical_formula[i]); } } return chemical_formula_unicode; -} - - +} \ No newline at end of file From 9feba9a11dcb0acb0e0394055b64281a3ebb95be Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Fri, 10 Jan 2025 14:21:39 +0100 Subject: [PATCH 03/61] fix imports --- include/controller.h | 2 +- src/importmanager.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/controller.h b/include/controller.h index c06173b2..41dca8a0 100644 --- a/include/controller.h +++ b/include/controller.h @@ -4,7 +4,7 @@ #include "flags.h" #include #include - +#include #ifdef MOLOVOL_GUI #include class MainFrame; diff --git a/src/importmanager.cpp b/src/importmanager.cpp index 5df31bc2..0efe6864 100644 --- a/src/importmanager.cpp +++ b/src/importmanager.cpp @@ -1,7 +1,7 @@ #include "importmanager.h" #include "crystallographer.h" #include "misc.h" - +#include // This is a temporary fix so that we can wite unit tests for sections of the code #ifndef LIBRARY_BUILD #include "controller.h" From efaddeac4a6874f8a30f0a5bf911f039dda70463 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Fri, 10 Jan 2025 14:22:35 +0100 Subject: [PATCH 04/61] remove headless file --- launch_headless.sh | 5 ----- webserver/app.py | 5 +++-- 2 files changed, 3 insertions(+), 7 deletions(-) delete mode 100755 launch_headless.sh diff --git a/launch_headless.sh b/launch_headless.sh deleted file mode 100755 index 21194a97..00000000 --- a/launch_headless.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -# This script launches the headless version of the application by creating fake display so that wxwidgets is happy. -export DISPLAY=:1.0 -Xvfb :1 -screen 0 1024x768x16 &> xvfb.log & -molovol "$@" diff --git a/webserver/app.py b/webserver/app.py index d01cbb0b..9ab748c3 100644 --- a/webserver/app.py +++ b/webserver/app.py @@ -15,6 +15,7 @@ app = Flask(__name__) UPLOAD_FOLDER = './userupload/' +EXECUTABLE_CMD = "./molovol" app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER # check if upload folder exists and create if missing @@ -334,7 +335,7 @@ def io(): print(f"Starting process with args: {args}\n") try: - mlvl_out = subprocess.check_output(["./launch_headless.sh"] + args, stderr=subprocess.STDOUT).decode( + mlvl_out = subprocess.check_output([EXECUTABLE_CMD] + args, stderr=subprocess.STDOUT).decode( "utf-8") except Exception as e: out = "Exception: " + str(e) @@ -422,7 +423,7 @@ def get_entry_size(entry_path): # Request the executable's version. If the executable is not found, then the web page crashes def app_version(): - return subprocess.check_output(["./launch_headless.sh", "-v"], stderr=subprocess.STDOUT).decode("utf-8") + return subprocess.check_output([EXECUTABLE_CMD, "-v"], stderr=subprocess.STDOUT).decode("utf-8") def is_nonzero_numeric(value): From 5b72112add2b7e29720354522dd5fcf9443cf077 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Fri, 10 Jan 2025 14:22:41 +0100 Subject: [PATCH 05/61] slimmer build --- container/Dockerfile | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/container/Dockerfile b/container/Dockerfile index 2e8855b6..4a4cceb3 100644 --- a/container/Dockerfile +++ b/container/Dockerfile @@ -1,11 +1,11 @@ # builds molovol and installs the webapp -FROM bsvogler/wxwidgets -# hack to create a headless x server, cmake for building -RUN apt update && apt upgrade -y -RUN apt-get install xvfb cmake -y -# does not work when set in dockerfile? -ENV DISPLAY=:1.0 - +FROM alpine:latest +RUN apk add --no-cache \ + cmake \ + make \ + gcc \ + g++ \ + musl-dev # compile molovol, for some reason some less important files like the readme are needed for cmake WORKDIR /build COPY cmake/ cmake/ @@ -16,24 +16,26 @@ COPY include/ include/ COPY LICENSE ./ COPY README.md ./ COPY inputfile/ inputfile/ -RUN cmake . -DCMAKE_BUILD_TYPE=RELEASE -DMOLOVOL_ABS_RESOURCE_PATH=ON && make +RUN cmake . -DCMAKE_BUILD_TYPE=RELEASE -DMOLOVOL_ABS_RESOURCE_PATH=ON -DMOLOVOL_BUILD_GUI=OFF && make +RUN apk add --no-cache \ + file \ + dpkg RUN cpack -G DEB && dpkg -i MoloVol_*.deb WORKDIR / # add flask webserver -#RUN apt install software-properties-common -y -RUN apt install python3.11 curl -y && apt-get clean && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache python3 py3-pip curl + # install poetry -RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/usr/local/ python3 - --version 1.6.1 +RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/usr/local/ python3 - --version 1.8.1 RUN poetry config virtualenvs.create false RUN poetry config virtualenvs.options.system-site-packages true -ENV PYTHONPATH=$PYTHONPATH:/usr/local/lib/python3.12/site-packages/ + # install dependencies COPY webserver/ ./ RUN poetry install --no-root --only main + WORKDIR / -COPY launch_headless.sh /launch_headless.sh -RUN chmod +x launch_headless.sh ENV FLASK_APP=/webserver/app.py -CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0"] +CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0"] \ No newline at end of file From 3d0c48492fc82eb39c2c0c1fa80965a375dd2b8a Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Fri, 10 Jan 2025 14:33:50 +0100 Subject: [PATCH 06/61] fix parsing of help and version commands --- src/base_cmdline.cpp | 49 ++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/src/base_cmdline.cpp b/src/base_cmdline.cpp index 46951275..6847d991 100644 --- a/src/base_cmdline.cpp +++ b/src/base_cmdline.cpp @@ -44,28 +44,33 @@ class CommandLineParser { public: bool parse(int argc, char* argv[]) { - for (int i = 1; i < argc; i++) { - std::string arg = argv[i]; - if (arg[0] != '-') continue; - - std::string optName = arg.substr(arg[1] == '-' ? 2 : 1); - auto option = findOption(optName); - if (!option) { - std::cerr << "Unknown option: " << arg << std::endl; - return false; - } - - if (option->isSwitch) { - parsedFlags[option->longName] = true; - } else if (i + 1 < argc) { - parsedOptions[option->longName] = argv[++i]; - } else { - std::cerr << "Missing value for option: " << arg << std::endl; - return false; - } - } - - return validateRequiredOptions(); + for (int i = 1; i < argc; i++) { + std::string arg = argv[i]; + if (arg[0] != '-') continue; + + std::string optName = arg.substr(arg[1] == '-' ? 2 : 1); + auto option = findOption(optName); + if (!option) { + std::cerr << "Unknown option: " << arg << std::endl; + return false; + } + + if (option->isSwitch) { + parsedFlags[option->longName] = true; + } else if (i + 1 < argc) { + parsedOptions[option->longName] = argv[++i]; + } else { + std::cerr << "Missing value for option: " << arg << std::endl; + return false; + } + } + + // Don't validate required options for help or version flags + if (parsedFlags.count("help") > 0 || parsedFlags.count("version") > 0) { + return true; + } + + return validateRequiredOptions(); } bool found(const std::string& name) const { From d8c252b60bd37672b40f69d6b8b4d15053985b86 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Fri, 10 Jan 2025 14:34:45 +0100 Subject: [PATCH 07/61] remove headless launcher --- container/alpine_wip.Dockerfile | 9 --------- container/headless.Dockerfile | 32 -------------------------------- launch_headless.sh | 5 +++++ 3 files changed, 5 insertions(+), 41 deletions(-) delete mode 100644 container/alpine_wip.Dockerfile delete mode 100644 container/headless.Dockerfile create mode 100755 launch_headless.sh diff --git a/container/alpine_wip.Dockerfile b/container/alpine_wip.Dockerfile deleted file mode 100644 index 5aee0f49..00000000 --- a/container/alpine_wip.Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM alpine -RUN apk apk update -RUN apk add build-base -RUN apk add gtk+2.0-dev -RUN wget https://github.com/wxWidgets/wxWidgets/releases/download/v3.1.5/wxWidgets-3.1.5.tar.bz2 -RUN tar xvf wxWidgets-3.1.5.tar.bz2 -WORKDIR wxWidgets-3.1.5 -RUN ./configure --disable-shared --enable-unicode -RUN make install \ No newline at end of file diff --git a/container/headless.Dockerfile b/container/headless.Dockerfile deleted file mode 100644 index b63a9bef..00000000 --- a/container/headless.Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -# buidls only headless version without webapp -FROM ubuntu AS compiler -COPY ./container/install_wxwidgets.sh ./ -RUN ./install_wxwidgets.sh -RUN rm install_wxwidgets.sh - -# hack to create a headless x server, does not work when set in dockerfile? -RUN apt-get install xvfb -y -ENV DISPLAY=:1.0 - -#compile molovol - -#FROM compiler AS builder -RUN apt update; apt install pip -y -RUN apt purge --auto-remove cmake -y; pip install cmake --upgrade -WORKDIR / -COPY src/ src/ -COPY include/ include/ -COPY CMakeLists.txt / -COPY inputfile/ inputfile/ -RUN mkdir cmake -WORKDIR cmake -RUN cmake .. -DCMAKE_BUILD_TYPE=RELEASE -RUN make -#launch.sh is expecting that molovol is residing in bin -RUN mkdir /build/ && mv MoloVol /bin/ && mv inputfile /bin/ - -WORKDIR / -COPY launch_headless.sh launch.sh -RUN chmod +x launch.sh -ENTRYPOINT ["./launch.sh"] -CMD ["-r", "1.2", "-g", "0.2", "-fs", "/inputfile/isobutane.xyz", "-q", "-o", "time,vol"] \ No newline at end of file diff --git a/launch_headless.sh b/launch_headless.sh new file mode 100755 index 00000000..21194a97 --- /dev/null +++ b/launch_headless.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# This script launches the headless version of the application by creating fake display so that wxwidgets is happy. +export DISPLAY=:1.0 +Xvfb :1 -screen 0 1024x768x16 &> xvfb.log & +molovol "$@" From 499d583e80bfa557f4fa68e87c9eb2952206b0bd Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Fri, 10 Jan 2025 14:34:49 +0100 Subject: [PATCH 08/61] fix path --- webserver/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webserver/app.py b/webserver/app.py index 9ab748c3..f8affd1b 100644 --- a/webserver/app.py +++ b/webserver/app.py @@ -15,7 +15,7 @@ app = Flask(__name__) UPLOAD_FOLDER = './userupload/' -EXECUTABLE_CMD = "./molovol" +EXECUTABLE_CMD = "/build/molovol" app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER # check if upload folder exists and create if missing From c1b549dc8764b8fb47fe63713fe10b45efa57735 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sun, 12 Jan 2025 16:49:29 +0100 Subject: [PATCH 09/61] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0ec69dc7..cf7de41f 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ _site/ .idea/ *.pyc +webserver/molovol_wasm.* \ No newline at end of file From 061c750b286e7b5945f55e12b9442152e1f9032e Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Fri, 10 Jan 2025 14:34:49 +0100 Subject: [PATCH 10/61] fix path From a21bc4c5f2ad32c869191601d8abbafb95b0689b Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 13:36:37 +0100 Subject: [PATCH 11/61] initial wasm config --- CMakeLists.txt | 9 +- cmake/wasm-cmake.cmake | 58 +++++++ src/wasm-bindings.wasm_bindings.cpp | 143 +++++++++++++++ webserver/molovol-wasm.js | 259 ++++++++++++++++++++++++++++ 4 files changed, 466 insertions(+), 3 deletions(-) create mode 100644 cmake/wasm-cmake.cmake create mode 100644 src/wasm-bindings.wasm_bindings.cpp create mode 100644 webserver/molovol-wasm.js diff --git a/CMakeLists.txt b/CMakeLists.txt index 515446d7..568b052d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,8 +37,6 @@ if(MOLOVOL_BUILD_GUI) include(${wxWidgets_USE_FILE}) endif() -#find_package(OpenMP) - # Add include path, so that header files can be found include_directories(include) @@ -73,7 +71,11 @@ if(NOT MOLOVOL_ABS_RESOURCE_PATH) endif() # Target MoloVol -add_executable(${EXE_NAME} ${SOURCES} ${OSX_RESOURCE_FILES}) +if(EMSCRIPTEN) + include(${CMAKE_CURRENT_SOURCE_DIR}/wasm.cmake) +else() + add_executable(${EXE_NAME} ${SOURCES} ${OSX_RESOURCE_FILES}) +endif() # XCode, app bundle and libtiff include(MacSpecific) @@ -86,6 +88,7 @@ if(MOLOVOL_ABS_RESOURCE_PATH) target_compile_definitions(${EXE_NAME} PUBLIC -DABS_PATH) endif() +#find_package(OpenMP) # Keeping this around just so I don't forget the syntax # if(OpenMP_CXX_FOUND) # target_link_libraries(target PUBLIC OpenMP::OpenMP_CXX) diff --git a/cmake/wasm-cmake.cmake b/cmake/wasm-cmake.cmake new file mode 100644 index 00000000..a0a301a4 --- /dev/null +++ b/cmake/wasm-cmake.cmake @@ -0,0 +1,58 @@ +# wasm.cmake + +# Ensure we're using Emscripten +if(NOT EMSCRIPTEN) + message(FATAL_ERROR "wasm.cmake should only be included when compiling with Emscripten") +endif() + +# Set C++ standard for WASM build +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Emscripten compiler flags +set(WASM_COMPILER_FLAGS + -s WASM=1 + -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap'] + -s ALLOW_MEMORY_GROWTH=1 + -s EXPORTED_FUNCTIONS=['_malloc','_free'] + -fexceptions +) + +# Link flags specific to WASM +set(WASM_LINK_FLAGS + -s ENVIRONMENT=web + -s MODULARIZE=1 + -s EXPORT_NAME='createMoloVolModule' + -s NO_EXIT_RUNTIME=1 + -s ASSERTIONS=1 + --bind + -s EXPORT_ES6=1 + -s USE_ES6_IMPORT_META=0 +) + +# Apply compiler flags +string(REPLACE ";" " " WASM_COMPILER_FLAGS_STR "${WASM_COMPILER_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_COMPILER_FLAGS_STR}") + +# Create WebAssembly target +add_executable(molovol_wasm + ${CMAKE_CURRENT_SOURCE_DIR}/src/wasm_bindings.cpp + ${MOLOVOL_SOURCES} # This should be defined in main CMakeLists.txt +) + +# Apply link flags +string(REPLACE ";" " " WASM_LINK_FLAGS_STR "${WASM_LINK_FLAGS}") +set_target_properties(molovol_wasm PROPERTIES + LINK_FLAGS "${WASM_LINK_FLAGS_STR}" +) + +# Copy output files to web directory +add_custom_command(TARGET molovol_wasm POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_BINARY_DIR}/molovol_wasm.js + ${CMAKE_CURRENT_SOURCE_DIR}/web/js/molovol_wasm.js + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_BINARY_DIR}/molovol_wasm.wasm + ${CMAKE_CURRENT_SOURCE_DIR}/web/js/molovol_wasm.wasm +) diff --git a/src/wasm-bindings.wasm_bindings.cpp b/src/wasm-bindings.wasm_bindings.cpp new file mode 100644 index 00000000..cb1fe3bb --- /dev/null +++ b/src/wasm-bindings.wasm_bindings.cpp @@ -0,0 +1,143 @@ +# include +# include +# include "molovol.h" +# include +# include +# include + +using namespace emscripten; + +// Structure to hold calculation options +struct CalculationOptions { + bool hetatm; + bool unitcell; + bool surface; + bool exportReport; + bool exportTotal; + bool exportCavities; +}; + +// Structure to hold calculation results +struct CalculationResult { + double resolution; + int depth; + double radius_small; + double radius_large; + struct { + double total; + double accessible; + double contact; + } volume; + struct { + double total; + double accessible; + double contact; + } surface; + std::vector cavities; +}; + +// Main calculation function exposed to JavaScript +CalculationResult calculate_volumes(val structureData, + double radius, + double grid, + val radius2Option, + const CalculationOptions& options) { + // Convert TypedArray to vector + std::vector fileData; + const size_t length = structureData["length"].as(); + fileData.resize(length); + val memory = val::global("Uint8Array").new_(val::global("WebAssembly")["Memory"]["buffer"]); + memory.call("set", structureData); + + // Create temporary file to work with existing MoloVol code + std::string tempFilename = "/tmp/structure.xyz"; // In-memory filesystem + FILE* fp = fopen(tempFilename.c_str(), "wb"); + fwrite(fileData.data(), 1, fileData.size(), fp); + fclose(fp); + + // Initialize MoloVol with parameters + MoloVol calculator; + calculator.setProbeRadius(radius); + calculator.setGridSpacing(grid); + + if (!radius2Option.isUndefined()) { + calculator.setLargeProbeRadius(radius2Option.as()); + } + + // Set options + calculator.setIncludeHETATM(options.hetatm); + calculator.setAnalyzeUnitCell(options.unitcell); + calculator.setCalculateSurface(options.surface); + + // Run calculation + try { + calculator.loadStructure(tempFilename); + calculator.calculate(); + + // Prepare results + CalculationResult result; + result.resolution = calculator.getResolution(); + result.depth = calculator.getDepth(); + result.radius_small = radius; + result.radius_large = radius2Option.isUndefined() ? 0.0 : radius2Option.as(); + + // Get volumes + auto volumes = calculator.getVolumes(); + result.volume.total = volumes.total; + result.volume.accessible = volumes.accessible; + result.volume.contact = volumes.contact; + + // Get surface areas if calculated + if (options.surface) { + auto surfaces = calculator.getSurfaces(); + result.surface.total = surfaces.total; + result.surface.accessible = surfaces.accessible; + result.surface.contact = surfaces.contact; + } + + // Get cavity information + auto cavities = calculator.getCavities(); + for (const auto& cavity : cavities) { + result.cavities.push_back({ + cavity.getId(), + cavity.getVolume(), + options.surface ? cavity.getSurfaceArea() : 0.0 + }); + } + + return result; + } + catch (const std::exception& e) { + // Convert C++ exceptions to JavaScript errors + val::global("Error").new_(std::string("Calculation failed: ") + e.what()).throw_(); + return CalculationResult{}; // Never reached, but satisfies compiler + } +} + +// Bind structures and functions to JavaScript +EMSCRIPTEN_BINDINGS(molovol_module) { + value_object("CalculationOptions") + .field("hetatm", &CalculationOptions::hetatm) + .field("unitcell", &CalculationOptions::unitcell) + .field("surface", &CalculationOptions::surface) + .field("exportReport", &CalculationOptions::exportReport) + .field("exportTotal", &CalculationOptions::exportTotal) + .field("exportCavities", &CalculationOptions::exportCavities) + ; + + value_object("CalculationResult") + .field("resolution", &CalculationResult::resolution) + .field("depth", &CalculationResult::depth) + .field("radius_small", &CalculationResult::radius_small) + .field("radius_large", &CalculationResult::radius_large) + .field("volume", &CalculationResult::volume) + .field("surface", &CalculationResult::surface) + .field("cavities", &CalculationResult::cavities) + ; + + function("calculate_volumes", &calculate_volumes); +} diff --git a/webserver/molovol-wasm.js b/webserver/molovol-wasm.js new file mode 100644 index 00000000..d0eb9091 --- /dev/null +++ b/webserver/molovol-wasm.js @@ -0,0 +1,259 @@ +// molovol.js +import init, { calculate_volumes } from './molovol_wasm.js'; + +// Constants for file size and cleanup +const MAX_STORAGE_SIZE = 4 * 1024 * 1024 * 1024; // 4 GB +const TARGET_STORAGE_SIZE = 3 * 1024 * 1024 * 1024; // 3 GB +const GRACE_PERIOD_MS = 600000; // 10 minutes in milliseconds + +// Initialize WebAssembly module +let wasmModule; +init().then(module => { + wasmModule = module; +}).catch(err => { + console.error("Failed to initialize WASM module:", err); +}); + +// File type validation +const ALLOWED_EXTENSIONS = new Set(['xyz', 'pdb', 'cif']); +function validateExtension(filename) { + const ext = filename.split('.').pop().toLowerCase(); + return ALLOWED_EXTENSIONS.has(ext); +} + +// Error handling +const ERR_PREFIX = "E."; +const ErrorMessages = { + NOFILE: `${ERR_PREFIX}No file was uploaded and previous file could not be used`, + NORADIUS: `${ERR_PREFIX}Small probe radius must be specified`, + NOGRID: `${ERR_PREFIX}Spatial resolution must be specified`, + SMALLGRID: `${ERR_PREFIX}If you would like to use a spatial resolution of less than 0.1, please use the desktop application`, + FORMAT: `${ERR_PREFIX}File format of uploaded file is not supported` +}; + +// File storage management using IndexedDB +class StorageManager { + constructor() { + this.dbName = 'MoloVolStorage'; + this.storeName = 'files'; + this.db = null; + } + + async init() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, 1); + + request.onerror = () => reject(request.error); + request.onsuccess = () => { + this.db = request.result; + resolve(); + }; + + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains(this.storeName)) { + db.createObjectStore(this.storeName, { keyPath: 'id' }); + } + }; + }); + } + + async storeFile(file, metadata = {}) { + const id = crypto.randomUUID(); + const timestamp = Date.now(); + + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([this.storeName], 'readwrite'); + const store = transaction.objectStore(this.storeName); + + const reader = new FileReader(); + reader.onload = () => { + const fileData = { + id, + name: file.name, + type: file.type, + size: file.size, + data: reader.result, + timestamp, + metadata + }; + + store.add(fileData).onsuccess = () => resolve(id); + }; + reader.onerror = () => reject(reader.error); + reader.readAsArrayBuffer(file); + }); + } + + async cleanup() { + const currentSize = await this.getTotalSize(); + if (currentSize <= TARGET_STORAGE_SIZE) return; + + const transaction = this.db.transaction([this.storeName], 'readwrite'); + const store = transaction.objectStore(this.storeName); + const files = await this.getAllFiles(); + + // Sort by timestamp + files.sort((a, b) => a.timestamp - b.timestamp); + + let deletedSize = 0; + const cutoffTime = Date.now() - GRACE_PERIOD_MS; + + for (const file of files) { + if (currentSize - deletedSize <= TARGET_STORAGE_SIZE && + file.timestamp > cutoffTime) { + break; + } + + await store.delete(file.id); + deletedSize += file.size; + } + } + + async getTotalSize() { + const files = await this.getAllFiles(); + return files.reduce((total, file) => total + file.size, 0); + } + + async getAllFiles() { + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([this.storeName], 'readonly'); + const store = transaction.objectStore(this.storeName); + const request = store.getAll(); + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } +} + +// Main MoloVol handler class +export class MoloVolHandler { + constructor() { + this.storage = new StorageManager(); + this.initPromise = this.storage.init(); + } + + async processStructure(file, options) { + await this.initPromise; + + // Validate file + if (!validateExtension(file.name)) { + throw new Error(ErrorMessages.FORMAT); + } + + // Validate required options + if (!options.radius) { + throw new Error(ErrorMessages.NORADIUS); + } + if (!options.grid) { + throw new Error(ErrorMessages.NOGRID); + } + if (parseFloat(options.grid) < 0.1) { + throw new Error(ErrorMessages.SMALLGRID); + } + + // Store file + await this.storage.cleanup(); + const fileId = await this.storage.storeFile(file); + + // Prepare calculation parameters + const params = { + fileId, + radius: parseFloat(options.radius), + grid: parseFloat(options.grid), + radius2: options.radius2 ? parseFloat(options.radius2) : null, + hetatm: options.hetatm || false, + unitcell: options.unitcell || false, + surface: options.surface || false, + exportReport: options.exportReport || false, + exportTotal: options.exportTotal || false, + exportCavities: options.exportCavities || false + }; + + // Execute calculation + try { + const result = await this.executeCalculation(params); + return this.formatResults(result, file.name); + } catch (error) { + throw new Error(`${ERR_PREFIX}${error.message}`); + } + } + + async executeCalculation(params) { + // Get file data from storage + const fileData = await this.getFileData(params.fileId); + + // Convert parameters to WASM-compatible format + const wasmParams = { + structure: new Uint8Array(fileData), + radius: params.radius, + grid: params.grid, + radius2: params.radius2, + options: { + hetatm: params.hetatm, + unitcell: params.unitcell, + surface: params.surface, + exportReport: params.exportReport, + exportTotal: params.exportTotal, + exportCavities: params.exportCavities + } + }; + + // Execute WASM calculation + return calculate_volumes(wasmParams); + } + + async getFileData(fileId) { + const transaction = this.storage.db.transaction([this.storage.storeName], 'readonly'); + const store = transaction.objectStore(this.storage.storeName); + + return new Promise((resolve, reject) => { + const request = store.get(fileId); + request.onsuccess = () => resolve(request.result.data); + request.onerror = () => reject(request.error); + }); + } + + formatResults(result, filename) { + let output = `Results for structure file: ${filename}\n`; + + // Add calculation parameters + output += `Resolution: ${result.resolution} Å\n`; + output += `Depth: ${result.depth}\n`; + output += `Small probe radius: ${result.radius_small} Å\n`; + if (result.radius_large) { + output += `Large probe radius: ${result.radius_large} Å\n`; + } + + // Add volumes and surface areas + output += `\n\n`; + output += `Total volume: ${result.volume.total.toFixed(1)} ų\n`; + if (result.surface) { + output += `Surface area: ${result.surface.total.toFixed(1)} Ų\n`; + } + + // Add cavity information if present + if (result.cavities && result.cavities.length > 0) { + output += `\nCavity ID Volume `; + if (result.surface) { + output += `Surface `; + } + output += `\n (ų) `; + if (result.surface) { + output += `(Ų) `; + } + output += `\n`; + + result.cavities.forEach(cavity => { + output += `${cavity.id.padEnd(10)} ${cavity.volume.toFixed(1).padEnd(12)}`; + if (result.surface) { + output += `${cavity.surface.toFixed(1).padEnd(12)}`; + } + output += `\n`; + }); + } + + return output; + } +} From bd558898662db7058891480efc1babb298ffe134 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 13:39:06 +0100 Subject: [PATCH 12/61] update readme about new compilation capability --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 76bcc334..a543b5ec 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,11 @@ software for your own purposes or propose changes to the developers and take par Dependencies needed for compilation: - Any C++ compiler -- [wxWidgets 3.1.5](https://www.wxwidgets.org) +- cmake +- Optional: [wxWidgets 3.1.5](https://www.wxwidgets.org) + +You can build without the desktop native GUI by setting +`cmake -DMOLOVOL_BUILD_GUI=0` ### MoloVol Web From 947965c210cb5621f2217ff33e9273dbf44be756 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 13:40:33 +0100 Subject: [PATCH 13/61] fix path --- CMakeLists.txt | 2 +- cmake/{wasm-cmake.cmake => wasm.cmake} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename cmake/{wasm-cmake.cmake => wasm.cmake} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 568b052d..0d42c10a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,7 +72,7 @@ endif() # Target MoloVol if(EMSCRIPTEN) - include(${CMAKE_CURRENT_SOURCE_DIR}/wasm.cmake) + include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/wasm.cmake) else() add_executable(${EXE_NAME} ${SOURCES} ${OSX_RESOURCE_FILES}) endif() diff --git a/cmake/wasm-cmake.cmake b/cmake/wasm.cmake similarity index 100% rename from cmake/wasm-cmake.cmake rename to cmake/wasm.cmake From a5767ca45897f0b0634079dbe296f35c6ce18058 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 13:44:49 +0100 Subject: [PATCH 14/61] fix more build issues --- CMakeLists.txt | 10 ++++++---- README.md | 5 ++++- ...sm-bindings.wasm_bindings.cpp => wasm_bindings.cpp} | 0 3 files changed, 10 insertions(+), 5 deletions(-) rename src/{wasm-bindings.wasm_bindings.cpp => wasm_bindings.cpp} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d42c10a..ce11f21f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,10 +100,12 @@ if (MOLOVOL_BUILD_TESTING AND BUILD_TESTING) endif() # Installation instructions for debian package -if (UNIX AND NOT APPLE) - include(DebInstall) -elseif (APPLE) - include(MacInstall) +if (NOT EMSCRIPTEN) # Only do native installations + if (UNIX AND NOT APPLE) + include(DebInstall) + elseif (APPLE) + include(MacInstall) + endif() endif() include(Packing) diff --git a/README.md b/README.md index a543b5ec..4e082d9a 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,9 @@ Dependencies needed for compilation: You can build without the desktop native GUI by setting `cmake -DMOLOVOL_BUILD_GUI=0` +You can build for wasm with +`emcmake cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DMOLOVOL_BUILD_GUI=0 ..` + ### MoloVol Web Instead of using the desktop front-end, you can also use a web interface. MoloVol server provides a REST-API with a web @@ -62,7 +65,7 @@ front-end wrapping the MoloVol CLI interface. To launch, first change the FLASK_ the command `export FLASK_APP=./webserver/app.py` from the project's root directory. Then execute `flask run`. For hosting on a web server check out the next section. -### Containerized application +### Containerized cli application Instead of compiling or running the binaries you can also use a containerized version (for instance using docker or podman) to access the CLI or web interface. diff --git a/src/wasm-bindings.wasm_bindings.cpp b/src/wasm_bindings.cpp similarity index 100% rename from src/wasm-bindings.wasm_bindings.cpp rename to src/wasm_bindings.cpp From 96f0f880dc9ca9ef84d955bb235971125df1def9 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 14:14:40 +0100 Subject: [PATCH 15/61] add wasm controller --- cmake/Sources.cmake | 33 +++--- cmake/wasm.cmake | 1 + src/wasm_bindings.cpp | 244 ++++++++++++++++++++-------------------- src/wasm_controller.cpp | 104 +++++++++++++++++ 4 files changed, 245 insertions(+), 137 deletions(-) create mode 100644 src/wasm_controller.cpp diff --git a/cmake/Sources.cmake b/cmake/Sources.cmake index b17d795d..e9fb1c35 100644 --- a/cmake/Sources.cmake +++ b/cmake/Sources.cmake @@ -1,8 +1,7 @@ -# Base sources (non-GUI) -set(BASE_SOURCES +# Set up core sources needed by all builds +set(CORE_SOURCES src/atom.cpp src/atomtree.cpp - src/base_cmdline.cpp src/cavity.cpp src/crystallographer.cpp src/griddata.cpp @@ -15,7 +14,6 @@ set(BASE_SOURCES src/special_chars.cpp src/vector.cpp src/voxel.cpp - src/controller.cpp ) # GUI-specific sources @@ -26,13 +24,22 @@ set(GUI_SOURCES src/base_init.cpp ) -set(CLI_SOURCES - src/base_cmdline.cpp -) - -if(MOLOVOL_BUILD_GUI) - set(SOURCES ${BASE_SOURCES} ${GUI_SOURCES}) +# Set up sources based on build type +if(EMSCRIPTEN) + set(MOLOVOL_SOURCES + ${CORE_SOURCES} + src/wasm_controller.cpp + ) +elseif(MOLOVOL_BUILD_GUI) + set(SOURCES + ${CORE_SOURCES} + ${GUI_SOURCES} + src/controller.cpp + ) else() - set(SOURCES ${BASE_SOURCES} ${CLI_SOURCES}) -endif() - + set(SOURCES + ${CORE_SOURCES} + src/base_cmdline.cpp + src/controller.cpp + ) +endif() \ No newline at end of file diff --git a/cmake/wasm.cmake b/cmake/wasm.cmake index a0a301a4..5c9b8cab 100644 --- a/cmake/wasm.cmake +++ b/cmake/wasm.cmake @@ -38,6 +38,7 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_COMPILER_FLAGS_STR}") # Create WebAssembly target add_executable(molovol_wasm ${CMAKE_CURRENT_SOURCE_DIR}/src/wasm_bindings.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/wasm_controller.cpp ${MOLOVOL_SOURCES} # This should be defined in main CMakeLists.txt ) diff --git a/src/wasm_bindings.cpp b/src/wasm_bindings.cpp index cb1fe3bb..87ff560a 100644 --- a/src/wasm_bindings.cpp +++ b/src/wasm_bindings.cpp @@ -1,143 +1,139 @@ -# include -# include -# include "molovol.h" -# include -# include -# include +#include +#include +#include +#include +#include +#include +#include "controller.h" +#include "flags.h" +#include "misc.h" using namespace emscripten; -// Structure to hold calculation options -struct CalculationOptions { - bool hetatm; - bool unitcell; - bool surface; - bool exportReport; - bool exportTotal; - bool exportCavities; +// Input options structure for JavaScript +struct MoloVolOptions { + double probe_radius_s; // Required: small probe radius + double grid_resolution; // Required: grid resolution + double probe_radius_l; // Optional: large probe radius (for two-probe mode) + int tree_depth; // Optional: octree depth (default: 4) + bool include_hetatm; // Optional: include HETATM from PDB + bool unit_cell; // Optional: analyze unit cell + bool surface_area; // Optional: calculate surfaces + bool export_report; // Optional: export report + bool export_total_map; // Optional: export cavity maps + bool export_cavity_maps; // Optional: export cavity maps + unsigned output_flags; // Output control flags }; -// Structure to hold calculation results -struct CalculationResult { - double resolution; - int depth; - double radius_small; - double radius_large; - struct { - double total; - double accessible; - double contact; - } volume; - struct { - double total; - double accessible; - double contact; - } surface; - std::vector cavities; -}; +// Initialize Controller for WASM environment +void init_controller() { + Ctrl::getInstance()->disableGUI(); + Ctrl::getInstance()->hush(true); +} -// Main calculation function exposed to JavaScript -CalculationResult calculate_volumes(val structureData, - double radius, - double grid, - val radius2Option, - const CalculationOptions& options) { - // Convert TypedArray to vector - std::vector fileData; - const size_t length = structureData["length"].as(); - fileData.resize(length); - val memory = val::global("Uint8Array").new_(val::global("WebAssembly")["Memory"]["buffer"]); - memory.call("set", structureData); +// Get version information +std::string get_version() { + init_controller(); + return Ctrl::getVersion(); +} + +// Main calculation function +val calculate_volumes(std::string structure_data, const MoloVolOptions& options) { + // Initialize controller + init_controller(); - // Create temporary file to work with existing MoloVol code - std::string tempFilename = "/tmp/structure.xyz"; // In-memory filesystem - FILE* fp = fopen(tempFilename.c_str(), "wb"); - fwrite(fileData.data(), 1, fileData.size(), fp); + // Create temporary file for structure data + std::string temp_filename = "/tmp/structure.tmp"; + FILE* fp = fopen(temp_filename.c_str(), "wb"); + if (!fp) { + val::global("Error").new_(std::string("Failed to create temporary file")).throw_(); + return val::null(); + } + fwrite(structure_data.c_str(), 1, structure_data.length(), fp); fclose(fp); - // Initialize MoloVol with parameters - MoloVol calculator; - calculator.setProbeRadius(radius); - calculator.setGridSpacing(grid); - - if (!radius2Option.isUndefined()) { - calculator.setLargeProbeRadius(radius2Option.as()); + // Run calculation + bool success = Ctrl::getInstance()->runCalculation( + options.probe_radius_s, + options.probe_radius_l, + options.grid_resolution, + temp_filename, + Ctrl::getDefaultElemPath(), + "/tmp", // Temporary directory for any exports + options.tree_depth, + options.include_hetatm, + options.unit_cell, + options.surface_area, + options.probe_radius_l > 0, // probe mode + options.export_report, + options.export_total_map, + options.export_cavity_maps, + options.output_flags + ); + + if (!success) { + val::global("Error").new_(std::string("Calculation failed")).throw_(); + return val::null(); + } + + // Check if calculation was completed + if (!Ctrl::getInstance()->isCalculationDone()) { + val::global("Error").new_(std::string("Calculation was aborted")).throw_(); + return val::null(); } - // Set options - calculator.setIncludeHETATM(options.hetatm); - calculator.setAnalyzeUnitCell(options.unitcell); - calculator.setCalculateSurface(options.surface); + // Return success + val result = val::object(); + result.set("success", true); + result.set("version", Ctrl::getVersion()); - // Run calculation - try { - calculator.loadStructure(tempFilename); - calculator.calculate(); - - // Prepare results - CalculationResult result; - result.resolution = calculator.getResolution(); - result.depth = calculator.getDepth(); - result.radius_small = radius; - result.radius_large = radius2Option.isUndefined() ? 0.0 : radius2Option.as(); - - // Get volumes - auto volumes = calculator.getVolumes(); - result.volume.total = volumes.total; - result.volume.accessible = volumes.accessible; - result.volume.contact = volumes.contact; - - // Get surface areas if calculated - if (options.surface) { - auto surfaces = calculator.getSurfaces(); - result.surface.total = surfaces.total; - result.surface.accessible = surfaces.accessible; - result.surface.contact = surfaces.contact; - } - - // Get cavity information - auto cavities = calculator.getCavities(); - for (const auto& cavity : cavities) { - result.cavities.push_back({ - cavity.getId(), - cavity.getVolume(), - options.surface ? cavity.getSurfaceArea() : 0.0 - }); - } - - return result; + // Note: Additional result data would need to be exposed through the Controller class + // For now, we return success status and version + + return result; +} + +// Convert output format string to flags +unsigned parse_output_format(const std::string& format) { + if (format.empty() || format == "all") { + return mvOUT_ALL; } - catch (const std::exception& e) { - // Convert C++ exceptions to JavaScript errors - val::global("Error").new_(std::string("Calculation failed: ") + e.what()).throw_(); - return CalculationResult{}; // Never reached, but satisfies compiler + + unsigned flags = mvOUT_NONE; + std::istringstream ss(format); + std::string token; + + while (std::getline(ss, token, ',')) { + if (token == "inputfile") flags |= mvOUT_STRUCTURE; + else if (token == "resolution") flags |= mvOUT_RESOLUTION; + else if (token == "depth") flags |= mvOUT_DEPTH; + else if (token == "radius_small") flags |= mvOUT_RADIUS_S; + else if (token == "radius_large") flags |= mvOUT_RADIUS_L; + else if (token == "vol") flags |= mvOUT_VOL; + else if (token == "surf") flags |= mvOUT_SURF; + else if (token == "cavities") flags |= mvOUT_CAVITIES; } + + return flags; } -// Bind structures and functions to JavaScript +// Bind everything to JavaScript EMSCRIPTEN_BINDINGS(molovol_module) { - value_object("CalculationOptions") - .field("hetatm", &CalculationOptions::hetatm) - .field("unitcell", &CalculationOptions::unitcell) - .field("surface", &CalculationOptions::surface) - .field("exportReport", &CalculationOptions::exportReport) - .field("exportTotal", &CalculationOptions::exportTotal) - .field("exportCavities", &CalculationOptions::exportCavities) - ; - - value_object("CalculationResult") - .field("resolution", &CalculationResult::resolution) - .field("depth", &CalculationResult::depth) - .field("radius_small", &CalculationResult::radius_small) - .field("radius_large", &CalculationResult::radius_large) - .field("volume", &CalculationResult::volume) - .field("surface", &CalculationResult::surface) - .field("cavities", &CalculationResult::cavities) + value_object("MoloVolOptions") + .field("probe_radius_s", &MoloVolOptions::probe_radius_s) + .field("grid_resolution", &MoloVolOptions::grid_resolution) + .field("probe_radius_l", &MoloVolOptions::probe_radius_l) + .field("tree_depth", &MoloVolOptions::tree_depth) + .field("include_hetatm", &MoloVolOptions::include_hetatm) + .field("unit_cell", &MoloVolOptions::unit_cell) + .field("surface_area", &MoloVolOptions::surface_area) + .field("export_report", &MoloVolOptions::export_report) + .field("export_total_map", &MoloVolOptions::export_total_map) + .field("export_cavity_maps", &MoloVolOptions::export_cavity_maps) + .field("output_flags", &MoloVolOptions::output_flags) ; - + + function("get_version", &get_version); function("calculate_volumes", &calculate_volumes); -} + function("parse_output_format", &parse_output_format); +} \ No newline at end of file diff --git a/src/wasm_controller.cpp b/src/wasm_controller.cpp new file mode 100644 index 00000000..e69361b6 --- /dev/null +++ b/src/wasm_controller.cpp @@ -0,0 +1,104 @@ +#include "controller.h" +#include "model.h" +#include "misc.h" +#include + +// Initialize static members +Ctrl* Ctrl::s_instance = nullptr; + +// Get singleton instance +Ctrl* Ctrl::getInstance() { + if (s_instance == nullptr) { + s_instance = new Ctrl(); + } + return s_instance; +} + +// Version info +std::string Ctrl::getVersion() { + return s_version; +} + +// Default elements file path +std::string Ctrl::getDefaultElemPath() { + return getResourcesDir() + "/" + s_elem_file; +} + +// GUI control functions +void Ctrl::disableGUI() { + _to_gui = false; +} + +void Ctrl::hush(const bool quiet) { + _quiet = quiet; +} + +// Main calculation function for WASM +bool Ctrl::runCalculation( + const double probe_radius_s, + const double probe_radius_l, + const double grid_resolution, + const std::string& structure_file_path, + const std::string& elements_file_path, + const std::string& output_dir_path, + const int tree_depth, + const bool opt_include_hetatm, + const bool opt_unit_cell, + const bool opt_surface_area, + const bool opt_probe_mode, + const bool exp_report, + const bool exp_total_map, + const bool exp_cavity_maps, + const unsigned display_flag) { + + if (_current_calculation == nullptr) { + _current_calculation = new Model(); + } + + // Initialize calculation + _calculation_finished = false; + + try { + if (!_current_calculation->readAtomsFromFile(structure_file_path, opt_include_hetatm)) { + return false; + } + } catch (const std::exception& e) { + return false; + } + + std::vector included_elements = _current_calculation->listElementsInStructure(); + + if (!_current_calculation->importElemFile(elements_file_path)) { + return false; + } + + if (!_current_calculation->setParameters( + structure_file_path, + output_dir_path, + opt_include_hetatm, + opt_unit_cell, + opt_surface_area, + opt_probe_mode, + probe_radius_s, + probe_radius_l, + grid_resolution, + tree_depth, + exp_report, + exp_total_map, + exp_cavity_maps, + _current_calculation->getRadiusMap(), + included_elements)) { + return false; + } + + // Run calculation + CalcReportBundle data = _current_calculation->generateData(); + _calculation_finished = data.success; + + return data.success; +} + +// Calculation status +bool Ctrl::isCalculationDone() { + return _calculation_finished; +} \ No newline at end of file From 9c7b7c8998e213ace3d6cdcdd57a13528261703a Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 17:20:45 +0100 Subject: [PATCH 16/61] remove extra controller --- cmake/Sources.cmake | 2 +- cmake/wasm.cmake | 1 - src/wasm_controller.cpp | 104 ---------------------------------------- 3 files changed, 1 insertion(+), 106 deletions(-) delete mode 100644 src/wasm_controller.cpp diff --git a/cmake/Sources.cmake b/cmake/Sources.cmake index e9fb1c35..45acb22c 100644 --- a/cmake/Sources.cmake +++ b/cmake/Sources.cmake @@ -3,6 +3,7 @@ set(CORE_SOURCES src/atom.cpp src/atomtree.cpp src/cavity.cpp + src/controller.cpp src/crystallographer.cpp src/griddata.cpp src/importmanager.cpp @@ -28,7 +29,6 @@ set(GUI_SOURCES if(EMSCRIPTEN) set(MOLOVOL_SOURCES ${CORE_SOURCES} - src/wasm_controller.cpp ) elseif(MOLOVOL_BUILD_GUI) set(SOURCES diff --git a/cmake/wasm.cmake b/cmake/wasm.cmake index 5c9b8cab..a0a301a4 100644 --- a/cmake/wasm.cmake +++ b/cmake/wasm.cmake @@ -38,7 +38,6 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_COMPILER_FLAGS_STR}") # Create WebAssembly target add_executable(molovol_wasm ${CMAKE_CURRENT_SOURCE_DIR}/src/wasm_bindings.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/wasm_controller.cpp ${MOLOVOL_SOURCES} # This should be defined in main CMakeLists.txt ) diff --git a/src/wasm_controller.cpp b/src/wasm_controller.cpp deleted file mode 100644 index e69361b6..00000000 --- a/src/wasm_controller.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include "controller.h" -#include "model.h" -#include "misc.h" -#include - -// Initialize static members -Ctrl* Ctrl::s_instance = nullptr; - -// Get singleton instance -Ctrl* Ctrl::getInstance() { - if (s_instance == nullptr) { - s_instance = new Ctrl(); - } - return s_instance; -} - -// Version info -std::string Ctrl::getVersion() { - return s_version; -} - -// Default elements file path -std::string Ctrl::getDefaultElemPath() { - return getResourcesDir() + "/" + s_elem_file; -} - -// GUI control functions -void Ctrl::disableGUI() { - _to_gui = false; -} - -void Ctrl::hush(const bool quiet) { - _quiet = quiet; -} - -// Main calculation function for WASM -bool Ctrl::runCalculation( - const double probe_radius_s, - const double probe_radius_l, - const double grid_resolution, - const std::string& structure_file_path, - const std::string& elements_file_path, - const std::string& output_dir_path, - const int tree_depth, - const bool opt_include_hetatm, - const bool opt_unit_cell, - const bool opt_surface_area, - const bool opt_probe_mode, - const bool exp_report, - const bool exp_total_map, - const bool exp_cavity_maps, - const unsigned display_flag) { - - if (_current_calculation == nullptr) { - _current_calculation = new Model(); - } - - // Initialize calculation - _calculation_finished = false; - - try { - if (!_current_calculation->readAtomsFromFile(structure_file_path, opt_include_hetatm)) { - return false; - } - } catch (const std::exception& e) { - return false; - } - - std::vector included_elements = _current_calculation->listElementsInStructure(); - - if (!_current_calculation->importElemFile(elements_file_path)) { - return false; - } - - if (!_current_calculation->setParameters( - structure_file_path, - output_dir_path, - opt_include_hetatm, - opt_unit_cell, - opt_surface_area, - opt_probe_mode, - probe_radius_s, - probe_radius_l, - grid_resolution, - tree_depth, - exp_report, - exp_total_map, - exp_cavity_maps, - _current_calculation->getRadiusMap(), - included_elements)) { - return false; - } - - // Run calculation - CalcReportBundle data = _current_calculation->generateData(); - _calculation_finished = data.success; - - return data.success; -} - -// Calculation status -bool Ctrl::isCalculationDone() { - return _calculation_finished; -} \ No newline at end of file From aa5539afdfd18b9163cc3f46836c878114438be6 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 17:20:53 +0100 Subject: [PATCH 17/61] documentation --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4e082d9a..6a6f2dde 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,13 @@ Dependencies needed for compilation: - cmake - Optional: [wxWidgets 3.1.5](https://www.wxwidgets.org) -You can build without the desktop native GUI by setting +You can build without the desktop native GUI by setting it up with `cmake -DMOLOVOL_BUILD_GUI=0` -You can build for wasm with +You can build for wasm with set up `emcmake cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DMOLOVOL_BUILD_GUI=0 ..` +then compile with +`emmake make` ### MoloVol Web From ca2edf4224ef9094e678d981c0624695a55d8bd2 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 17:28:49 +0100 Subject: [PATCH 18/61] ignore wasm out --- .gitignore | 2 +- webserver/molovol-wasm.js | 259 -------------------------------------- 2 files changed, 1 insertion(+), 260 deletions(-) delete mode 100644 webserver/molovol-wasm.js diff --git a/.gitignore b/.gitignore index cf7de41f..e286da8b 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,4 @@ _site/ .idea/ *.pyc -webserver/molovol_wasm.* \ No newline at end of file +webserver/molovol_wasm.* diff --git a/webserver/molovol-wasm.js b/webserver/molovol-wasm.js deleted file mode 100644 index d0eb9091..00000000 --- a/webserver/molovol-wasm.js +++ /dev/null @@ -1,259 +0,0 @@ -// molovol.js -import init, { calculate_volumes } from './molovol_wasm.js'; - -// Constants for file size and cleanup -const MAX_STORAGE_SIZE = 4 * 1024 * 1024 * 1024; // 4 GB -const TARGET_STORAGE_SIZE = 3 * 1024 * 1024 * 1024; // 3 GB -const GRACE_PERIOD_MS = 600000; // 10 minutes in milliseconds - -// Initialize WebAssembly module -let wasmModule; -init().then(module => { - wasmModule = module; -}).catch(err => { - console.error("Failed to initialize WASM module:", err); -}); - -// File type validation -const ALLOWED_EXTENSIONS = new Set(['xyz', 'pdb', 'cif']); -function validateExtension(filename) { - const ext = filename.split('.').pop().toLowerCase(); - return ALLOWED_EXTENSIONS.has(ext); -} - -// Error handling -const ERR_PREFIX = "E."; -const ErrorMessages = { - NOFILE: `${ERR_PREFIX}No file was uploaded and previous file could not be used`, - NORADIUS: `${ERR_PREFIX}Small probe radius must be specified`, - NOGRID: `${ERR_PREFIX}Spatial resolution must be specified`, - SMALLGRID: `${ERR_PREFIX}If you would like to use a spatial resolution of less than 0.1, please use the desktop application`, - FORMAT: `${ERR_PREFIX}File format of uploaded file is not supported` -}; - -// File storage management using IndexedDB -class StorageManager { - constructor() { - this.dbName = 'MoloVolStorage'; - this.storeName = 'files'; - this.db = null; - } - - async init() { - return new Promise((resolve, reject) => { - const request = indexedDB.open(this.dbName, 1); - - request.onerror = () => reject(request.error); - request.onsuccess = () => { - this.db = request.result; - resolve(); - }; - - request.onupgradeneeded = (event) => { - const db = event.target.result; - if (!db.objectStoreNames.contains(this.storeName)) { - db.createObjectStore(this.storeName, { keyPath: 'id' }); - } - }; - }); - } - - async storeFile(file, metadata = {}) { - const id = crypto.randomUUID(); - const timestamp = Date.now(); - - return new Promise((resolve, reject) => { - const transaction = this.db.transaction([this.storeName], 'readwrite'); - const store = transaction.objectStore(this.storeName); - - const reader = new FileReader(); - reader.onload = () => { - const fileData = { - id, - name: file.name, - type: file.type, - size: file.size, - data: reader.result, - timestamp, - metadata - }; - - store.add(fileData).onsuccess = () => resolve(id); - }; - reader.onerror = () => reject(reader.error); - reader.readAsArrayBuffer(file); - }); - } - - async cleanup() { - const currentSize = await this.getTotalSize(); - if (currentSize <= TARGET_STORAGE_SIZE) return; - - const transaction = this.db.transaction([this.storeName], 'readwrite'); - const store = transaction.objectStore(this.storeName); - const files = await this.getAllFiles(); - - // Sort by timestamp - files.sort((a, b) => a.timestamp - b.timestamp); - - let deletedSize = 0; - const cutoffTime = Date.now() - GRACE_PERIOD_MS; - - for (const file of files) { - if (currentSize - deletedSize <= TARGET_STORAGE_SIZE && - file.timestamp > cutoffTime) { - break; - } - - await store.delete(file.id); - deletedSize += file.size; - } - } - - async getTotalSize() { - const files = await this.getAllFiles(); - return files.reduce((total, file) => total + file.size, 0); - } - - async getAllFiles() { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction([this.storeName], 'readonly'); - const store = transaction.objectStore(this.storeName); - const request = store.getAll(); - - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }); - } -} - -// Main MoloVol handler class -export class MoloVolHandler { - constructor() { - this.storage = new StorageManager(); - this.initPromise = this.storage.init(); - } - - async processStructure(file, options) { - await this.initPromise; - - // Validate file - if (!validateExtension(file.name)) { - throw new Error(ErrorMessages.FORMAT); - } - - // Validate required options - if (!options.radius) { - throw new Error(ErrorMessages.NORADIUS); - } - if (!options.grid) { - throw new Error(ErrorMessages.NOGRID); - } - if (parseFloat(options.grid) < 0.1) { - throw new Error(ErrorMessages.SMALLGRID); - } - - // Store file - await this.storage.cleanup(); - const fileId = await this.storage.storeFile(file); - - // Prepare calculation parameters - const params = { - fileId, - radius: parseFloat(options.radius), - grid: parseFloat(options.grid), - radius2: options.radius2 ? parseFloat(options.radius2) : null, - hetatm: options.hetatm || false, - unitcell: options.unitcell || false, - surface: options.surface || false, - exportReport: options.exportReport || false, - exportTotal: options.exportTotal || false, - exportCavities: options.exportCavities || false - }; - - // Execute calculation - try { - const result = await this.executeCalculation(params); - return this.formatResults(result, file.name); - } catch (error) { - throw new Error(`${ERR_PREFIX}${error.message}`); - } - } - - async executeCalculation(params) { - // Get file data from storage - const fileData = await this.getFileData(params.fileId); - - // Convert parameters to WASM-compatible format - const wasmParams = { - structure: new Uint8Array(fileData), - radius: params.radius, - grid: params.grid, - radius2: params.radius2, - options: { - hetatm: params.hetatm, - unitcell: params.unitcell, - surface: params.surface, - exportReport: params.exportReport, - exportTotal: params.exportTotal, - exportCavities: params.exportCavities - } - }; - - // Execute WASM calculation - return calculate_volumes(wasmParams); - } - - async getFileData(fileId) { - const transaction = this.storage.db.transaction([this.storage.storeName], 'readonly'); - const store = transaction.objectStore(this.storage.storeName); - - return new Promise((resolve, reject) => { - const request = store.get(fileId); - request.onsuccess = () => resolve(request.result.data); - request.onerror = () => reject(request.error); - }); - } - - formatResults(result, filename) { - let output = `Results for structure file: ${filename}\n`; - - // Add calculation parameters - output += `Resolution: ${result.resolution} Å\n`; - output += `Depth: ${result.depth}\n`; - output += `Small probe radius: ${result.radius_small} Å\n`; - if (result.radius_large) { - output += `Large probe radius: ${result.radius_large} Å\n`; - } - - // Add volumes and surface areas - output += `\n\n`; - output += `Total volume: ${result.volume.total.toFixed(1)} ų\n`; - if (result.surface) { - output += `Surface area: ${result.surface.total.toFixed(1)} Ų\n`; - } - - // Add cavity information if present - if (result.cavities && result.cavities.length > 0) { - output += `\nCavity ID Volume `; - if (result.surface) { - output += `Surface `; - } - output += `\n (ų) `; - if (result.surface) { - output += `(Ų) `; - } - output += `\n`; - - result.cavities.forEach(cavity => { - output += `${cavity.id.padEnd(10)} ${cavity.volume.toFixed(1).padEnd(12)}`; - if (result.surface) { - output += `${cavity.surface.toFixed(1).padEnd(12)}`; - } - output += `\n`; - }); - } - - return output; - } -} From 823d8ca0ab2469b616779c506aebef23e0733fa0 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 17:29:00 +0100 Subject: [PATCH 19/61] change paths --- CMakeLists.txt | 2 +- cmake/wasm.cmake | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ce11f21f..f6b1ac8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ include(Options) # MOLOVOL_BUILD_TESTING # Set name and version -project(MoloVol VERSION 1.1.1) +project(MoloVol VERSION 1.2) # Strip binary for release build set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -s") diff --git a/cmake/wasm.cmake b/cmake/wasm.cmake index a0a301a4..bc4d7c8a 100644 --- a/cmake/wasm.cmake +++ b/cmake/wasm.cmake @@ -6,8 +6,8 @@ if(NOT EMSCRIPTEN) endif() # Set C++ standard for WASM build -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) +#set(CMAKE_CXX_STANDARD 20) #inherit +#set(CMAKE_CXX_STANDARD_REQUIRED ON) #inherit set(CMAKE_CXX_EXTENSIONS OFF) # Emscripten compiler flags @@ -51,8 +51,8 @@ set_target_properties(molovol_wasm PROPERTIES add_custom_command(TARGET molovol_wasm POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/molovol_wasm.js - ${CMAKE_CURRENT_SOURCE_DIR}/web/js/molovol_wasm.js + ${CMAKE_CURRENT_SOURCE_DIR}/webserver/molovol_wasm.js COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/molovol_wasm.wasm - ${CMAKE_CURRENT_SOURCE_DIR}/web/js/molovol_wasm.wasm + ${CMAKE_CURRENT_SOURCE_DIR}/webserver/molovol_wasm.wasm ) From 8f889c3fc8738df49f831fc2a22d3c21976b7a04 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 17:59:47 +0100 Subject: [PATCH 20/61] fix binding names were not matching the original names --- cmake/Sources.cmake | 3 +- include/base_cmdline.h | 66 ++++++++ src/base_cmdline.cpp | 337 +++++++++++++++----------------------- src/wasm_bindings.cpp | 149 +++++++++-------- webserver/test-setup.html | 152 +++++++++++++++++ 5 files changed, 428 insertions(+), 279 deletions(-) create mode 100644 include/base_cmdline.h create mode 100644 webserver/test-setup.html diff --git a/cmake/Sources.cmake b/cmake/Sources.cmake index 45acb22c..637b41ae 100644 --- a/cmake/Sources.cmake +++ b/cmake/Sources.cmake @@ -29,17 +29,16 @@ set(GUI_SOURCES if(EMSCRIPTEN) set(MOLOVOL_SOURCES ${CORE_SOURCES} + src/base_cmdline.cpp ) elseif(MOLOVOL_BUILD_GUI) set(SOURCES ${CORE_SOURCES} ${GUI_SOURCES} - src/controller.cpp ) else() set(SOURCES ${CORE_SOURCES} src/base_cmdline.cpp - src/controller.cpp ) endif() \ No newline at end of file diff --git a/include/base_cmdline.h b/include/base_cmdline.h new file mode 100644 index 00000000..b0f6e40f --- /dev/null +++ b/include/base_cmdline.h @@ -0,0 +1,66 @@ +#ifndef MOLOVOL_BASE_CMDLINE_H +#define MOLOVOL_BASE_CMDLINE_H + +#include +#include +#include +#include + +struct CommandLineOption { + std::string shortName; + std::string longName; + std::string description; + bool isSwitch; // true for flags, false for options with values + bool isRequired; +}; + +class CommandLineParser { +public: + bool parse(int argc, char* argv[]); // Original method for native environment + bool parse(const std::vector& args); // New method for WASM + bool found(const std::string& name) const; + std::optional getValue(const std::string& name) const; + void displayHelp() const; + const std::vector& getOptions() const { return options; } + +private: + const std::vector options = { + {"h", "help", "Display help for command line interface", true, false}, + {"r", "radius", "Probe radius", false, true}, + {"g", "grid", "Spatial resolution of the underlying grid", false, true}, + {"fs", "file-structure", "Path to the structure file", false, true}, + {"fe", "file-elements", "Path to the elements file", false, false}, + {"do", "dir-output", "Path to the output directory", false, false}, + {"r2", "radius2", "Large probe radius (for two-probe mode)", false, false}, + {"d", "depth", "Octree depth", false, false}, + {"ht", "hetatm", "Include HETATM from pdb file", true, false}, + {"uc", "unitcell", "Evaluate unit cell", true, false}, + {"sf", "surface", "Calculate surfaces", true, false}, + {"xr", "export-report", "Export report (requires:-do)", true, false}, + {"xt", "export-total", "Export total surface map (requires:-do)", true, false}, + {"xc", "export-cavities", "Export surface maps for all cavities (requires:-do)", true, false}, + {"o", "output", "Control what parts of the output to display (default:all)", false, false}, + {"q", "quiet", "Silence progress reporting", true, false}, + {"un", "unicode", "Allow unicode output", true, false}, + {"v", "version", "Display the app version", true, false} + }; + + std::map parsedOptions; + std::map parsedFlags; + + std::optional findOption(const std::string& name) const; + bool validateRequiredOptions() const; +}; + +// Validation function declarations +bool validateProbes(double r1, double r2, bool pm); +bool validateExport(const std::string& out_dir, const std::vector& exp_options); +bool validatePdb(const std::string& file, bool hetatm, bool unitcell); + +// Display options constants +extern const std::map DISPLAY_OPTIONS; + +// Display options evaluation function +unsigned evalDisplayOptions(const std::string& output); + +#endif // MOLOVOL_BASE_CMDLINE_H \ No newline at end of file diff --git a/src/base_cmdline.cpp b/src/base_cmdline.cpp index 6847d991..1fab5181 100644 --- a/src/base_cmdline.cpp +++ b/src/base_cmdline.cpp @@ -1,231 +1,156 @@ -#include -#include -#include -#include -#include -#include +#include "base_cmdline.h" #include "controller.h" #include "misc.h" #include "special_chars.h" +#include +#include -struct CommandLineOption { - std::string shortName; - std::string longName; - std::string description; - bool isSwitch; // true for flags, false for options with values - bool isRequired; -}; - -class CommandLineParser { -private: - const std::vector options = { - {"h", "help", "Display help for command line interface", true, false}, - {"r", "radius", "Probe radius", false, true}, - {"g", "grid", "Spatial resolution of the underlying grid", false, true}, - {"fs", "file-structure", "Path to the structure file", false, true}, - {"fe", "file-elements", "Path to the elements file", false, false}, - {"do", "dir-output", "Path to the output directory", false, false}, - {"r2", "radius2", "Large probe radius (for two-probe mode)", false, false}, - {"d", "depth", "Octree depth", false, false}, - {"ht", "hetatm", "Include HETATM from pdb file", true, false}, - {"uc", "unitcell", "Evaluate unit cell", true, false}, - {"sf", "surface", "Calculate surfaces", true, false}, - {"xr", "export-report", "Export report (requires:-do)", true, false}, - {"xt", "export-total", "Export total surface map (requires:-do)", true, false}, - {"xc", "export-cavities", "Export surface maps for all cavities (requires:-do)", true, false}, - {"o", "output", "Control what parts of the output to display (default:all)", false, false}, - {"q", "quiet", "Silence progress reporting", true, false}, - {"un", "unicode", "Allow unicode output", true, false}, - {"v", "version", "Display the app version", true, false} - }; - - std::map parsedOptions; - std::map parsedFlags; - -public: - bool parse(int argc, char* argv[]) { - for (int i = 1; i < argc; i++) { - std::string arg = argv[i]; - if (arg[0] != '-') continue; - - std::string optName = arg.substr(arg[1] == '-' ? 2 : 1); - auto option = findOption(optName); - if (!option) { - std::cerr << "Unknown option: " << arg << std::endl; - return false; - } - - if (option->isSwitch) { - parsedFlags[option->longName] = true; - } else if (i + 1 < argc) { - parsedOptions[option->longName] = argv[++i]; - } else { - std::cerr << "Missing value for option: " << arg << std::endl; - return false; - } - } - - // Don't validate required options for help or version flags - if (parsedFlags.count("help") > 0 || parsedFlags.count("version") > 0) { - return true; - } - - return validateRequiredOptions(); - } - - bool found(const std::string& name) const { - return parsedFlags.count(name) > 0 || parsedOptions.count(name) > 0; - } - - std::optional getValue(const std::string& name) const { - auto it = parsedOptions.find(name); - return it != parsedOptions.end() ? std::optional(it->second) : std::nullopt; - } - - void displayHelp() const { - std::cout << "Usage:\n"; - for (const auto& opt : options) { - std::cout << " -" << opt.shortName << ", --" << opt.longName - << (opt.isRequired ? " (required)" : "") << "\n " - << opt.description << "\n"; - } - } - -private: - std::optional findOption(const std::string& name) const { - for (const auto& opt : options) { - if (opt.shortName == name || opt.longName == name) { - return opt; - } - } - return std::nullopt; - } - - bool validateRequiredOptions() const { - std::vector missing; - for (const auto& opt : options) { - if (opt.isRequired && !found(opt.longName)) { - missing.push_back(opt.longName); - } - } - - if (!missing.empty()) { - Ctrl::getInstance()->displayErrorMessage(910 + missing.size(), missing); - return false; - } - return true; - } -}; - -// Validation functions combined into single-line expressions -bool validateProbes(double r1, double r2, bool pm) { - return !(pm && r2 < r1 && (Ctrl::getInstance()->displayErrorMessage(104), true)); +bool CommandLineParser::parse(const std::vector& args) { + try { + // Check for empty arguments + if (args.empty()) { + Ctrl::getInstance()->displayErrorMessage(901); // Assuming 901 is appropriate error code + return false; + } + + // Process arguments directly without conversion to char* + for (size_t i = 1; i < args.size(); i++) { + const std::string& arg = args[i]; + if (arg.empty()) { + Ctrl::getInstance()->displayErrorMessage(901); // Empty argument + return false; + } + + if (arg[0] != '-') continue; + + std::string optName = arg.substr(arg[1] == '-' ? 2 : 1); + if (optName.empty()) { + Ctrl::getInstance()->displayErrorMessage(901); // Invalid option format + return false; + } + + auto option = findOption(optName); + if (!option) { + std::cerr << "Unknown option: " << arg << std::endl; + return false; + } + + if (option->isSwitch) { + parsedFlags[option->longName] = true; + } else if (i + 1 < args.size()) { + const std::string& value = args[++i]; + if (value.empty() || value[0] == '-') { + std::cerr << "Invalid value for option: " << arg << std::endl; + return false; + } + parsedOptions[option->longName] = value; + } else { + std::cerr << "Missing value for option: " << arg << std::endl; + return false; + } + } + + // Don't validate required options for help or version flags + if (parsedFlags.count("help") > 0 || parsedFlags.count("version") > 0) { + return true; + } + + return validateRequiredOptions(); + } catch (const std::exception& e) { + std::cerr << "Error parsing arguments: " << e.what() << std::endl; + Ctrl::getInstance()->displayErrorMessage(901); // General parsing error + return false; + } } -bool validateExport(const std::string& out_dir, const std::vector& exp_options) { - return !(isIncluded(true, exp_options) && out_dir.empty() && (Ctrl::getInstance()->displayErrorMessage(302), true)); +bool CommandLineParser::found(const std::string& name) const { + return parsedFlags.count(name) > 0 || parsedOptions.count(name) > 0; } -bool validatePdb(const std::string& file, bool hetatm, bool unitcell) { - return !((fileExtension(file) != "pdb" && fileExtension(file) != "cif") && (hetatm || unitcell) && - (Ctrl::getInstance()->displayErrorMessage(115), true)); +std::optional CommandLineParser::getValue(const std::string& name) const { + auto it = parsedOptions.find(name); + return it != parsedOptions.end() ? std::optional(it->second) : std::nullopt; } -// Display options map -static const std::map DISPLAY_OPTIONS = { - {"none", mvOUT_NONE}, {"inputfile", mvOUT_STRUCTURE}, {"resolution", mvOUT_RESOLUTION}, - {"depth", mvOUT_DEPTH}, {"radius_small", mvOUT_RADIUS_S}, {"radius_large", mvOUT_RADIUS_L}, - {"input", mvOUT_INP}, {"hetatm", mvOUT_OPT_HETATM}, {"unitcell", mvOUT_OPT_UNITCELL}, - {"probemode", mvOUT_OPT_PROBEMODE}, {"surface", mvOUT_OPT_SURFACE}, {"options", mvOUT_OPT}, - {"formula", mvOUT_FORMULA}, {"time", mvOUT_TIME}, {"vol_vdw", mvOUT_VOL_VDW}, - {"vol_inaccessible", mvOUT_VOL_INACCESSIBLE}, {"vol_core_s", mvOUT_VOL_CORE_S}, - {"vol_shell_s", mvOUT_VOL_SHELL_S}, {"vol_core_l", mvOUT_VOL_CORE_L}, - {"vol_shell_l", mvOUT_VOL_SHELL_L}, {"vol_mol", mvOUT_VOL_MOL}, {"vol", mvOUT_VOL}, - {"surf_vdw", mvOUT_SURF_VDW}, {"surf_mol", mvOUT_SURF_MOL}, - {"surf_excluded_s", mvOUT_SURF_EXCLUDED_S}, {"surf_accessible_s", mvOUT_SURF_ACCESSIBLE_S}, - {"surf", mvOUT_SURF}, {"cavities", mvOUT_CAVITIES}, {"all", mvOUT_ALL} -}; - -unsigned evalDisplayOptions(const std::string& output) { - std::stringstream ss(output); - std::string option; - unsigned display_flag = 0; - bool unknown_flag = false; - - while (std::getline(ss, option, ',')) { - auto it = DISPLAY_OPTIONS.find(option); - if (it != DISPLAY_OPTIONS.end()) { - display_flag |= it->second; - } else { - unknown_flag = true; - } - } - - if (unknown_flag) { - Ctrl::getInstance()->displayErrorMessage(902); - } - return display_flag; +void CommandLineParser::displayHelp() const { + std::cout << "Usage:\n"; + for (const auto& opt : options) { + std::cout << " -" << opt.shortName << ", --" << opt.longName + << (opt.isRequired ? " (required)" : "") << "\n " + << opt.description << "\n"; + } } -bool evalCmdLine(int argc, char* argv[]) { - CommandLineParser parser; - if (!parser.parse(argc, argv)) return false; - - if (parser.found("help")) { - parser.displayHelp(); - return true; +std::optional CommandLineParser::findOption(const std::string& name) const { + for (const auto& opt : options) { + if (opt.shortName == name || opt.longName == name) { + return opt; + } } + return std::nullopt; +} - parser.found("unicode") ? Symbol::allow_unicode() : Symbol::limit2ascii(); - - if (parser.found("version")) { - Ctrl::getInstance()->version(); - return true; +bool CommandLineParser::validateRequiredOptions() const { + std::vector missing; + for (const auto& opt : options) { + if (opt.isRequired && !found(opt.longName)) { + missing.push_back(opt.longName); + } } - Ctrl::getInstance()->hush(parser.found("quiet")); - - // Parse required options - auto probe_radius_s = std::stod(parser.getValue("radius").value()); - auto grid_resolution = std::stod(parser.getValue("grid").value()); - auto structure_file_path = parser.getValue("file-structure").value(); - - // Optional parameters with defaults - auto elements_file_path = parser.getValue("file-elements").value_or(Ctrl::getDefaultElemPath()); - auto output_dir_path = parser.getValue("dir-output").value_or(""); - auto output = parser.getValue("output").value_or("all"); - auto probe_radius_l = parser.getValue("radius2") ? std::stod(parser.getValue("radius2").value()) : 0.0; - auto tree_depth = parser.getValue("depth") ? std::stol(parser.getValue("depth").value()) : 4; - - bool opt_include_hetatm = parser.found("hetatm"); - bool opt_unit_cell = parser.found("unitcell"); - bool opt_surface_area = parser.found("surface"); - bool opt_probe_mode = parser.found("radius") && parser.found("radius2"); - bool exp_report = parser.found("export-report"); - bool exp_total_map = parser.found("export-total"); - bool exp_cavity_maps = parser.found("export-cavities"); - - if (!validateProbes(probe_radius_s, probe_radius_l, opt_probe_mode) || - !validateExport(output_dir_path, {exp_report, exp_total_map, exp_cavity_maps}) || - !validatePdb(structure_file_path, opt_include_hetatm, opt_unit_cell)) { + if (!missing.empty()) { + Ctrl::getInstance()->displayErrorMessage(910 + missing.size(), missing); return false; } + return true; +} - unsigned display_flag = evalDisplayOptions(output); +// Validation functions +bool validateProbes(double r1, double r2, bool pm) { + return !(pm && r2 < r1 && (Ctrl::getInstance()->displayErrorMessage(104), true)); +} - return Ctrl::getInstance()->runCalculation( - probe_radius_s, probe_radius_l, grid_resolution, structure_file_path, - elements_file_path, output_dir_path, tree_depth, opt_include_hetatm, - opt_unit_cell, opt_surface_area, opt_probe_mode, exp_report, - exp_total_map, exp_cavity_maps, display_flag); +bool validateExport(const std::string& out_dir, const std::vector& exp_options) { + return !(isIncluded(true, exp_options) && out_dir.empty() && (Ctrl::getInstance()->displayErrorMessage(302), true)); } +bool validatePdb(const std::string& file, bool hetatm, bool unitcell) { + return !((fileExtension(file) != "pdb" && fileExtension(file) != "cif") && (hetatm || unitcell) && + (Ctrl::getInstance()->displayErrorMessage(115), true)); +} -int main(int argc, char* argv[]) { - Ctrl::getInstance()->disableGUI(); - if (argc < 2) { - std::cout << "Use --help for usage information\n"; - return 1; +// Define display options map +const std::map DISPLAY_OPTIONS = { + {"none", mvOUT_NONE}, {"inputfile", mvOUT_STRUCTURE}, {"resolution", mvOUT_RESOLUTION}, + {"depth", mvOUT_DEPTH}, {"radius_small", mvOUT_RADIUS_S}, {"radius_large", mvOUT_RADIUS_L}, + {"input", mvOUT_INP}, {"hetatm", mvOUT_OPT_HETATM}, {"unitcell", mvOUT_OPT_UNITCELL}, + {"probemode", mvOUT_OPT_PROBEMODE}, {"surface", mvOUT_OPT_SURFACE}, {"options", mvOUT_OPT}, + {"formula", mvOUT_FORMULA}, {"time", mvOUT_TIME}, {"vol_vdw", mvOUT_VOL_VDW}, + {"vol_inaccessible", mvOUT_VOL_INACCESSIBLE}, {"vol_core_s", mvOUT_VOL_CORE_S}, + {"vol_shell_s", mvOUT_VOL_SHELL_S}, {"vol_core_l", mvOUT_VOL_CORE_L}, + {"vol_shell_l", mvOUT_VOL_SHELL_L}, {"vol_mol", mvOUT_VOL_MOL}, {"vol", mvOUT_VOL}, + {"surf_vdw", mvOUT_SURF_VDW}, {"surf_mol", mvOUT_SURF_MOL}, + {"surf_excluded_s", mvOUT_SURF_EXCLUDED_S}, {"surf_accessible_s", mvOUT_SURF_ACCESSIBLE_S}, + {"surf", mvOUT_SURF}, {"cavities", mvOUT_CAVITIES}, {"all", mvOUT_ALL} +}; + +unsigned evalDisplayOptions(const std::string& output) { + std::stringstream ss(output); + std::string option; + unsigned display_flag = 0; + bool unknown_flag = false; + + while (std::getline(ss, option, ',')) { + auto it = DISPLAY_OPTIONS.find(option); + if (it != DISPLAY_OPTIONS.end()) { + display_flag |= it->second; + } else { + unknown_flag = true; + } + } + + if (unknown_flag) { + Ctrl::getInstance()->displayErrorMessage(902); } - return evalCmdLine(argc, argv) ? 0 : 1; + return display_flag; } \ No newline at end of file diff --git a/src/wasm_bindings.cpp b/src/wasm_bindings.cpp index 87ff560a..e322529c 100644 --- a/src/wasm_bindings.cpp +++ b/src/wasm_bindings.cpp @@ -7,24 +7,10 @@ #include "controller.h" #include "flags.h" #include "misc.h" +#include "base_cmdline.h" using namespace emscripten; -// Input options structure for JavaScript -struct MoloVolOptions { - double probe_radius_s; // Required: small probe radius - double grid_resolution; // Required: grid resolution - double probe_radius_l; // Optional: large probe radius (for two-probe mode) - int tree_depth; // Optional: octree depth (default: 4) - bool include_hetatm; // Optional: include HETATM from PDB - bool unit_cell; // Optional: analyze unit cell - bool surface_area; // Optional: calculate surfaces - bool export_report; // Optional: export report - bool export_total_map; // Optional: export cavity maps - bool export_cavity_maps; // Optional: export cavity maps - unsigned output_flags; // Output control flags -}; - // Initialize Controller for WASM environment void init_controller() { Ctrl::getInstance()->disableGUI(); @@ -37,11 +23,34 @@ std::string get_version() { return Ctrl::getVersion(); } -// Main calculation function -val calculate_volumes(std::string structure_data, const MoloVolOptions& options) { +// Main calculation function using CommandLineParser +val calculate_volumes(const std::vector& args) { // Initialize controller init_controller(); + // Parse command line arguments using CommandLineParser + CommandLineParser parser; + // Debug print arguments + std::string debug_args; + for (const auto& arg : args) { + debug_args += arg + " "; + } + printf("Parsing arguments: %s\n", debug_args.c_str()); + + if (!parser.parse(args)) { + val::global("Error").new_(std::string("Failed to parse arguments: " + debug_args)).throw_(); + return val::null(); + } + + // Check for required options + if (!parser.found("radius") || !parser.found("grid") || !parser.found("file-structure")) { + val::global("Error").new_(std::string("Missing required arguments")).throw_(); + return val::null(); + } + + // Get structure data from the file-structure argument + auto structure_file = parser.getValue("file-structure").value(); + // Create temporary file for structure data std::string temp_filename = "/tmp/structure.tmp"; FILE* fp = fopen(temp_filename.c_str(), "wb"); @@ -49,26 +58,45 @@ val calculate_volumes(std::string structure_data, const MoloVolOptions& options) val::global("Error").new_(std::string("Failed to create temporary file")).throw_(); return val::null(); } - fwrite(structure_data.c_str(), 1, structure_data.length(), fp); + fwrite(structure_file.c_str(), 1, structure_file.length(), fp); fclose(fp); + // Extract values from parser + double probe_radius_s = std::stod(parser.getValue("radius").value()); + double probe_radius_l = parser.found("radius2") ? std::stod(parser.getValue("radius2").value()) : 0.0; + double grid_resolution = std::stod(parser.getValue("grid").value()); + int tree_depth = parser.found("depth") ? std::stoi(parser.getValue("depth").value()) : 4; + + // Get boolean flags + bool include_hetatm = parser.found("hetatm"); + bool unit_cell = parser.found("unitcell"); + bool surface_area = parser.found("surface"); + bool export_report = parser.found("export-report"); + bool export_total_map = parser.found("export-total"); + bool export_cavity_maps = parser.found("export-cavities"); + + // Get output flags + unsigned output_flags = parser.found("output") ? + evalDisplayOptions(parser.getValue("output").value()) : + mvOUT_ALL; + // Run calculation bool success = Ctrl::getInstance()->runCalculation( - options.probe_radius_s, - options.probe_radius_l, - options.grid_resolution, + probe_radius_s, + probe_radius_l, + grid_resolution, temp_filename, Ctrl::getDefaultElemPath(), "/tmp", // Temporary directory for any exports - options.tree_depth, - options.include_hetatm, - options.unit_cell, - options.surface_area, - options.probe_radius_l > 0, // probe mode - options.export_report, - options.export_total_map, - options.export_cavity_maps, - options.output_flags + tree_depth, + include_hetatm, + unit_cell, + surface_area, + probe_radius_l > 0, // probe mode + export_report, + export_total_map, + export_cavity_maps, + output_flags ); if (!success) { @@ -87,53 +115,32 @@ val calculate_volumes(std::string structure_data, const MoloVolOptions& options) result.set("success", true); result.set("version", Ctrl::getVersion()); - // Note: Additional result data would need to be exposed through the Controller class - // For now, we return success status and version - return result; } -// Convert output format string to flags -unsigned parse_output_format(const std::string& format) { - if (format.empty() || format == "all") { - return mvOUT_ALL; - } - - unsigned flags = mvOUT_NONE; - std::istringstream ss(format); - std::string token; - - while (std::getline(ss, token, ',')) { - if (token == "inputfile") flags |= mvOUT_STRUCTURE; - else if (token == "resolution") flags |= mvOUT_RESOLUTION; - else if (token == "depth") flags |= mvOUT_DEPTH; - else if (token == "radius_small") flags |= mvOUT_RADIUS_S; - else if (token == "radius_large") flags |= mvOUT_RADIUS_L; - else if (token == "vol") flags |= mvOUT_VOL; - else if (token == "surf") flags |= mvOUT_SURF; - else if (token == "cavities") flags |= mvOUT_CAVITIES; - } - - return flags; -} - // Bind everything to JavaScript EMSCRIPTEN_BINDINGS(molovol_module) { - value_object("MoloVolOptions") - .field("probe_radius_s", &MoloVolOptions::probe_radius_s) - .field("grid_resolution", &MoloVolOptions::grid_resolution) - .field("probe_radius_l", &MoloVolOptions::probe_radius_l) - .field("tree_depth", &MoloVolOptions::tree_depth) - .field("include_hetatm", &MoloVolOptions::include_hetatm) - .field("unit_cell", &MoloVolOptions::unit_cell) - .field("surface_area", &MoloVolOptions::surface_area) - .field("export_report", &MoloVolOptions::export_report) - .field("export_total_map", &MoloVolOptions::export_total_map) - .field("export_cavity_maps", &MoloVolOptions::export_cavity_maps) - .field("output_flags", &MoloVolOptions::output_flags) - ; + register_vector("VectorString"); + value_object("CommandLineOption") + .field("shortName", &CommandLineOption::shortName) + .field("longName", &CommandLineOption::longName) + .field("description", &CommandLineOption::description) + .field("isSwitch", &CommandLineOption::isSwitch) + .field("isRequired", &CommandLineOption::isRequired) + ; + + register_vector("VectorCommandLineOption"); + + class_("CommandLineParser") + .constructor<>() + .function("parse", select_overload&)>(&CommandLineParser::parse)) + .function("found", &CommandLineParser::found) + .function("getValue", &CommandLineParser::getValue) + .function("displayHelp", &CommandLineParser::displayHelp) + .function("getOptions", &CommandLineParser::getOptions) + ; + function("get_version", &get_version); function("calculate_volumes", &calculate_volumes); - function("parse_output_format", &parse_output_format); } \ No newline at end of file diff --git a/webserver/test-setup.html b/webserver/test-setup.html new file mode 100644 index 00000000..aae0c266 --- /dev/null +++ b/webserver/test-setup.html @@ -0,0 +1,152 @@ + + + + + MoloVol WASM Test + + + +

MoloVol WASM Test

+ +
+
+
+ +
+ +
+ +
+ + +
+ + +
+ + +
+
+ +
+ +
+ + +
+ + +
+
+ + +
+ +
+ + + + \ No newline at end of file From 69ed1bcd38f4b2e7658320b2453f1fd328a32c20 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 18:10:56 +0100 Subject: [PATCH 21/61] fix exe name --- CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index f6b1ac8c..b7254e2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,9 @@ set(CMAKE_OSX_DEPLOYMENT_TARGET 10.11 CACHE STRING "Minimum macOS deployment ver include(ExecutableName) # EXE_NAME +if(EMSCRIPTEN) + set(EXE_NAME molovol_wasm) +endif() # Specify C++ standard set(CMAKE_CXX_STANDARD 20) From c90ba0ade4de85affcb6a8b04d4390efc2bc0850 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 18:23:36 +0100 Subject: [PATCH 22/61] skip cli --- src/wasm_bindings.cpp | 256 ++++++++++++++++++++++++-------------- webserver/test-setup.html | 127 ++++++++++--------- 2 files changed, 230 insertions(+), 153 deletions(-) diff --git a/src/wasm_bindings.cpp b/src/wasm_bindings.cpp index e322529c..d7aff614 100644 --- a/src/wasm_bindings.cpp +++ b/src/wasm_bindings.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "controller.h" #include "flags.h" #include "misc.h" @@ -11,10 +12,30 @@ using namespace emscripten; +// Structure to hold calculation parameters +struct CalculationParams { + // Required parameters + double probe_radius_small; + double grid_resolution; + std::string structure_content; + + // Optional parameters + double probe_radius_large = 0.0; + int tree_depth = 4; + + // Boolean flags + bool include_hetatm = false; + bool unit_cell = false; + bool surface_area = false; + bool export_report = false; + bool export_total_map = false; + bool export_cavity_maps = false; +}; + // Initialize Controller for WASM environment void init_controller() { Ctrl::getInstance()->disableGUI(); - Ctrl::getInstance()->hush(true); + Ctrl::getInstance()->hush(false); // Enable output for debugging } // Get version information @@ -23,93 +44,152 @@ std::string get_version() { return Ctrl::getVersion(); } -// Main calculation function using CommandLineParser -val calculate_volumes(const std::vector& args) { - // Initialize controller - init_controller(); +// Helper function to write file content and verify it +bool writeAndVerifyFile(const std::string& filename, const std::string& content) { + printf("Writing file: %s with content length: %zu\n", filename.c_str(), content.length()); - // Parse command line arguments using CommandLineParser - CommandLineParser parser; - // Debug print arguments - std::string debug_args; - for (const auto& arg : args) { - debug_args += arg + " "; + std::ofstream file(filename); + if (!file) { + printf("Failed to open file for writing: %s\n", filename.c_str()); + return false; } - printf("Parsing arguments: %s\n", debug_args.c_str()); + + file << content; + file.close(); + + // Verify file was written + std::ifstream verify(filename); + if (!verify) { + printf("Failed to verify file existence: %s\n", filename.c_str()); + return false; + } + + return true; +} - if (!parser.parse(args)) { - val::global("Error").new_(std::string("Failed to parse arguments: " + debug_args)).throw_(); +// Main calculation function using direct parameter passing +val calculate_volumes_direct(const CalculationParams& params) { + printf("\n=== Starting calculation with direct parameter binding ===\n"); + + // Initialize controller + init_controller(); + + // Validate parameters + if (params.probe_radius_small <= 0) { + val::global("Error").new_(std::string("Invalid probe radius")).throw_(); return val::null(); } - - // Check for required options - if (!parser.found("radius") || !parser.found("grid") || !parser.found("file-structure")) { - val::global("Error").new_(std::string("Missing required arguments")).throw_(); + + if (params.grid_resolution <= 0 || params.grid_resolution < 0.1) { + val::global("Error").new_(std::string("Invalid grid resolution")).throw_(); return val::null(); } - - // Get structure data from the file-structure argument - auto structure_file = parser.getValue("file-structure").value(); - - // Create temporary file for structure data - std::string temp_filename = "/tmp/structure.tmp"; - FILE* fp = fopen(temp_filename.c_str(), "wb"); - if (!fp) { - val::global("Error").new_(std::string("Failed to create temporary file")).throw_(); + + if (params.structure_content.empty()) { + val::global("Error").new_(std::string("No structure content provided")).throw_(); return val::null(); } - fwrite(structure_file.c_str(), 1, structure_file.length(), fp); - fclose(fp); + + // Write structure file with extension based on content + std::string temp_filename; + if (params.structure_content.find("HETATM") != std::string::npos || + params.structure_content.find("ATOM") != std::string::npos) { + temp_filename = "/tmp/structure.pdb"; + printf("Detected PDB format\n"); + } else if (params.structure_content.find("data_") != std::string::npos) { + temp_filename = "/tmp/structure.cif"; + printf("Detected CIF format\n"); + } else { + // Assume XYZ format + temp_filename = "/tmp/structure.xyz"; + printf("Assuming XYZ format\n"); + } - // Extract values from parser - double probe_radius_s = std::stod(parser.getValue("radius").value()); - double probe_radius_l = parser.found("radius2") ? std::stod(parser.getValue("radius2").value()) : 0.0; - double grid_resolution = std::stod(parser.getValue("grid").value()); - int tree_depth = parser.found("depth") ? std::stoi(parser.getValue("depth").value()) : 4; - - // Get boolean flags - bool include_hetatm = parser.found("hetatm"); - bool unit_cell = parser.found("unitcell"); - bool surface_area = parser.found("surface"); - bool export_report = parser.found("export-report"); - bool export_total_map = parser.found("export-total"); - bool export_cavity_maps = parser.found("export-cavities"); + printf("Structure content preview (first 200 chars):\n%s\n", + params.structure_content.substr(0, 200).c_str()); - // Get output flags - unsigned output_flags = parser.found("output") ? - evalDisplayOptions(parser.getValue("output").value()) : - mvOUT_ALL; + if (!writeAndVerifyFile(temp_filename, params.structure_content)) { + val::global("Error").new_(std::string("Failed to write structure file")).throw_(); + return val::null(); + } + printf("Successfully wrote structure file to: %s\n", temp_filename.c_str()); + + // Write elements file + std::string elements_content = R"(# Atom Symb Name VdW-Radius +1 H Hydrogen 1.200 +6 C Carbon 1.700 +7 N Nitrogen 1.550 +8 O Oxygen 1.520 +9 F Fluorine 1.470 +15 P Phosphorus 1.800 +16 S Sulfur 1.800 +17 Cl Chlorine 1.750)"; + + std::string elements_filename = "/tmp/elements.txt"; + if (!writeAndVerifyFile(elements_filename, elements_content)) { + printf("Failed to write elements file\n"); + val::global("Error").new_(std::string("Failed to write elements file")).throw_(); + return val::null(); + } + + // Set output flags for all relevant information + unsigned output_flags = mvOUT_RESOLUTION | mvOUT_DEPTH | mvOUT_RADIUS_S | mvOUT_RADIUS_L | + mvOUT_OPT | mvOUT_VOL | mvOUT_SURF | mvOUT_CAVITIES; + + printf("\nRunning calculation with parameters:\n"); + printf("- Small probe radius: %f\n", params.probe_radius_small); + printf("- Large probe radius: %f\n", params.probe_radius_large); + printf("- Grid resolution: %f\n", params.grid_resolution); + printf("- Tree depth: %d\n", params.tree_depth); + printf("- Include HETATM: %s\n", params.include_hetatm ? "true" : "false"); + printf("- Unit cell: %s\n", params.unit_cell ? "true" : "false"); + printf("- Surface area: %s\n", params.surface_area ? "true" : "false"); + // Run calculation - bool success = Ctrl::getInstance()->runCalculation( - probe_radius_s, - probe_radius_l, - grid_resolution, - temp_filename, - Ctrl::getDefaultElemPath(), - "/tmp", // Temporary directory for any exports - tree_depth, - include_hetatm, - unit_cell, - surface_area, - probe_radius_l > 0, // probe mode - export_report, - export_total_map, - export_cavity_maps, - output_flags - ); - - if (!success) { - val::global("Error").new_(std::string("Calculation failed")).throw_(); + bool success = false; + try { + success = Ctrl::getInstance()->runCalculation( + params.probe_radius_small, + params.probe_radius_large, + params.grid_resolution, + temp_filename, + elements_filename, + "/tmp", // Temporary directory for any exports + params.tree_depth, + params.include_hetatm, + params.unit_cell, + params.surface_area, + params.probe_radius_large > 0, // probe mode + params.export_report, + params.export_total_map, + params.export_cavity_maps, + output_flags + ); + + printf("Calculation %s\n", success ? "succeeded" : "failed"); + + } catch (const std::exception& e) { + printf("Calculation failed with exception: %s\n", e.what()); + val::global("Error").new_(std::string("Calculation failed with exception: ") + e.what()).throw_(); + return val::null(); + } catch (...) { + printf("Calculation failed with unknown exception\n"); + val::global("Error").new_(std::string("Calculation failed with unknown exception")).throw_(); return val::null(); } - - // Check if calculation was completed - if (!Ctrl::getInstance()->isCalculationDone()) { - val::global("Error").new_(std::string("Calculation was aborted")).throw_(); + + if (!success) { + printf("Calculation failed - checking completion status\n"); + if (!Ctrl::getInstance()->isCalculationDone()) { + printf("Calculation was not completed\n"); + } + val::global("Error").new_(std::string("Calculation failed - Check input file format and parameters")).throw_(); return val::null(); } - + + printf("Calculation completed successfully\n"); + // Return success val result = val::object(); result.set("success", true); @@ -120,27 +200,19 @@ val calculate_volumes(const std::vector& args) { // Bind everything to JavaScript EMSCRIPTEN_BINDINGS(molovol_module) { - register_vector("VectorString"); - - value_object("CommandLineOption") - .field("shortName", &CommandLineOption::shortName) - .field("longName", &CommandLineOption::longName) - .field("description", &CommandLineOption::description) - .field("isSwitch", &CommandLineOption::isSwitch) - .field("isRequired", &CommandLineOption::isRequired) - ; - - register_vector("VectorCommandLineOption"); - - class_("CommandLineParser") - .constructor<>() - .function("parse", select_overload&)>(&CommandLineParser::parse)) - .function("found", &CommandLineParser::found) - .function("getValue", &CommandLineParser::getValue) - .function("displayHelp", &CommandLineParser::displayHelp) - .function("getOptions", &CommandLineParser::getOptions) - ; + value_object("CalculationParams") + .field("probe_radius_small", &CalculationParams::probe_radius_small) + .field("grid_resolution", &CalculationParams::grid_resolution) + .field("structure_content", &CalculationParams::structure_content) + .field("probe_radius_large", &CalculationParams::probe_radius_large) + .field("tree_depth", &CalculationParams::tree_depth) + .field("include_hetatm", &CalculationParams::include_hetatm) + .field("unit_cell", &CalculationParams::unit_cell) + .field("surface_area", &CalculationParams::surface_area) + .field("export_report", &CalculationParams::export_report) + .field("export_total_map", &CalculationParams::export_total_map) + .field("export_cavity_maps", &CalculationParams::export_cavity_maps); function("get_version", &get_version); - function("calculate_volumes", &calculate_volumes); + function("calculate_volumes", &calculate_volumes_direct); } \ No newline at end of file diff --git a/webserver/test-setup.html b/webserver/test-setup.html index aae0c266..a4b72b8c 100644 --- a/webserver/test-setup.html +++ b/webserver/test-setup.html @@ -34,28 +34,28 @@

MoloVol WASM Test

- -
+ +
- -
+ +
- -
+ +
- -
+ +
- -
+ +
- -
+ +
- -
+ +
@@ -77,40 +77,6 @@

MoloVol WASM Test

console.error('Failed to load WASM module:', err); }); - // Convert form data to command line arguments - function formDataToArgs(formData, fileContent) { - const args = []; - - // Add file content as base64 string - args.push('--file-structure'); - args.push(fileContent); - - // Add required parameters - args.push('--radius'); - args.push(formData.get('radius')); - - args.push('--grid'); - args.push(formData.get('grid')); - - // Add optional parameters - if (formData.get('radius2')) { - args.push('--radius2'); - args.push(formData.get('radius2')); - } - - if (formData.get('depth')) { - args.push('--depth'); - args.push(formData.get('depth')); - } - - // Add boolean flags - if (formData.get('hetatm')) args.push('--hetatm'); - if (formData.get('unitcell')) args.push('--unitcell'); - if (formData.get('surface')) args.push('--surface'); - - return args; - } - document.getElementById('molovolForm').addEventListener('submit', async (e) => { e.preventDefault(); @@ -121,27 +87,66 @@

MoloVol WASM Test

const outputDiv = document.getElementById('output'); try { + // Validate the file const structureFile = document.getElementById('structure').files[0]; - const fileContent = await structureFile.text(); // Get file content as text - - // Convert form data to command line arguments - const formData = new FormData(e.target); - const args = formDataToArgs(formData, fileContent); + const validExtensions = ['.xyz', '.cif', '.pdb']; + const fileExtension = '.' + structureFile.name.split('.').pop().toLowerCase(); + if (!validExtensions.includes(fileExtension)) { + throw new Error(`Invalid file type. Please use one of: ${validExtensions.join(', ')}`); + } - // Create a vector of strings for the WASM call - const argsVector = new wasmModule.VectorString(); - args.forEach(arg => argsVector.push_back(arg)); + // Get file content + const fileContent = await structureFile.text(); + console.log('File details:', { + name: structureFile.name, + type: structureFile.type, + size: structureFile.size, + contentLength: fileContent.length + }); + console.log('File content preview:', fileContent.substring(0, 200)); - // Call the WASM function - const result = wasmModule.calculate_volumes(argsVector); + // Basic content validation based on file type + if (fileExtension === '.pdb' && !fileContent.includes('HETATM') && !fileContent.includes('ATOM')) { + throw new Error('Invalid PDB file: no ATOM or HETATM records found'); + } else if (fileExtension === '.cif' && !fileContent.includes('data_')) { + throw new Error('Invalid CIF file: no data_ block found'); + } - // Clean up the vector - argsVector.delete(); + if (!fileContent || fileContent.length === 0) { + throw new Error('File appears to be empty'); + } + + // Create calculation parameters object + const params = { + probe_radius_small: parseFloat(document.getElementById('probe_radius_small').value), + grid_resolution: parseFloat(document.getElementById('grid_resolution').value), + structure_content: fileContent, + probe_radius_large: parseFloat(document.getElementById('probe_radius_large').value || "0"), + tree_depth: parseInt(document.getElementById('tree_depth').value), + include_hetatm: document.getElementById('include_hetatm').checked, + unit_cell: document.getElementById('unit_cell').checked, + surface_area: document.getElementById('surface_area').checked, + export_report: false, + export_total_map: false, + export_cavity_maps: false + }; + + // Validate required parameters + if (params.probe_radius_small <= 0) { + throw new Error('Small probe radius must be positive'); + } + if (params.grid_resolution < 0.1) { + throw new Error('Grid spacing must be at least 0.1'); + } + + console.log('Calling calculate_volumes with params:', params); + const result = wasmModule.calculate_volumes(params); + console.log('Calculation result:', result); outputDiv.textContent = result.success ? `Calculation completed successfully!\nVersion: ${result.version}` : 'Calculation failed'; - + } catch (err) { outputDiv.textContent = `Error: ${err.message}`; console.error(err); From bc8ff16b2218c656ac23bcb34626ca8d0e9a4755 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 18:33:08 +0100 Subject: [PATCH 23/61] try loading the resource files --- cmake/wasm.cmake | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cmake/wasm.cmake b/cmake/wasm.cmake index bc4d7c8a..d7de10d7 100644 --- a/cmake/wasm.cmake +++ b/cmake/wasm.cmake @@ -6,8 +6,6 @@ if(NOT EMSCRIPTEN) endif() # Set C++ standard for WASM build -#set(CMAKE_CXX_STANDARD 20) #inherit -#set(CMAKE_CXX_STANDARD_REQUIRED ON) #inherit set(CMAKE_CXX_EXTENSIONS OFF) # Emscripten compiler flags @@ -29,6 +27,10 @@ set(WASM_LINK_FLAGS --bind -s EXPORT_ES6=1 -s USE_ES6_IMPORT_META=0 + + # Preload resource files + --preload-file ${CMAKE_CURRENT_SOURCE_DIR}/inputfile/elements.txt@/inputfile/elements.txt + --preload-file ${CMAKE_CURRENT_SOURCE_DIR}/inputfile/space_groups.txt@/inputfile/space_groups.txt ) # Apply compiler flags @@ -55,4 +57,7 @@ add_custom_command(TARGET molovol_wasm POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/molovol_wasm.wasm ${CMAKE_CURRENT_SOURCE_DIR}/webserver/molovol_wasm.wasm -) + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_BINARY_DIR}/molovol_wasm.data + ${CMAKE_CURRENT_SOURCE_DIR}/webserver/molovol_wasm.data +) \ No newline at end of file From 5767f574d37a4d3d841bd04daa3d2beaf4f7925e Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 18:52:20 +0100 Subject: [PATCH 24/61] checks to help debug file loading issues --- cmake/wasm.cmake | 10 ++++++++++ src/model_filereading.cpp | 25 ++++++++++++++++++++++++- src/wasm_bindings.cpp | 8 ++++---- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/cmake/wasm.cmake b/cmake/wasm.cmake index d7de10d7..376399b1 100644 --- a/cmake/wasm.cmake +++ b/cmake/wasm.cmake @@ -17,6 +17,16 @@ set(WASM_COMPILER_FLAGS -fexceptions ) +function(require_file FILE_PATH) + if(NOT EXISTS "${FILE_PATH}") + message(FATAL_ERROR "Required file not found: ${FILE_PATH}") + endif() +endfunction() + +# Usage +require_file("${CMAKE_CURRENT_SOURCE_DIR}/inputfile/elements.txt") +require_file("${CMAKE_CURRENT_SOURCE_DIR}/inputfile/space_groups.txt") + # Link flags specific to WASM set(WASM_LINK_FLAGS -s ENVIRONMENT=web diff --git a/src/model_filereading.cpp b/src/model_filereading.cpp index 81c2401c..ff715da9 100644 --- a/src/model_filereading.cpp +++ b/src/model_filereading.cpp @@ -13,7 +13,9 @@ #include #include #include - +#ifdef __EMSCRIPTEN__ +#include +#endif /////////////////// // IMPORT STRUCT // /////////////////// @@ -33,6 +35,27 @@ ElementsFileBundle extractDataFromElemFile(const std::string& elem_path); // generates three maps for assigning a radius, weight and atomic number respectively, to an element symbol // sets the maps to members of the model class bool Model::importElemFile(const std::string& elem_path){ + #ifdef __EMSCRIPTEN__ + EM_ASM({ + console.log('Attempted elements file path: ' + UTF8ToString($0)); + console.log('Current working directory: ' + FS.cwd()); + console.log('Root directory contents:'); + try { + var contents = FS.analyzePath('/'); + console.log(contents); + } catch (e) { + console.error('Error analyzing root directory:', e); + } + + console.log('Trying to list /inputfile directory:'); + try { + var dirContents = FS.readdir('/inputfile'); + console.log(dirContents); + } catch (e) { + console.error('Error reading /inputfile directory:', e); + } + }, elem_path.c_str()); + #endif ElementsFileBundle data = extractDataFromElemFile(elem_path); setRadiusMap(data.rad_map); _elem_weight = data.weight_map; diff --git a/src/wasm_bindings.cpp b/src/wasm_bindings.cpp index d7aff614..2a33de8a 100644 --- a/src/wasm_bindings.cpp +++ b/src/wasm_bindings.cpp @@ -94,14 +94,14 @@ val calculate_volumes_direct(const CalculationParams& params) { std::string temp_filename; if (params.structure_content.find("HETATM") != std::string::npos || params.structure_content.find("ATOM") != std::string::npos) { - temp_filename = "/tmp/structure.pdb"; + temp_filename = "/structure.pdb"; printf("Detected PDB format\n"); } else if (params.structure_content.find("data_") != std::string::npos) { - temp_filename = "/tmp/structure.cif"; + temp_filename = "/structure.cif"; printf("Detected CIF format\n"); } else { // Assume XYZ format - temp_filename = "/tmp/structure.xyz"; + temp_filename = "/structure.xyz"; printf("Assuming XYZ format\n"); } @@ -126,7 +126,7 @@ val calculate_volumes_direct(const CalculationParams& params) { 16 S Sulfur 1.800 17 Cl Chlorine 1.750)"; - std::string elements_filename = "/tmp/elements.txt"; + std::string elements_filename = "/elements.txt"; if (!writeAndVerifyFile(elements_filename, elements_content)) { printf("Failed to write elements file\n"); val::global("Error").new_(std::string("Failed to write elements file")).throw_(); From a8cb18fb41bc002f4224bfa487c60b6c47a2b685 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 22:20:49 +0100 Subject: [PATCH 25/61] fix writing only uploaded file --- src/wasm_bindings.cpp | 51 +++++++------------------------------------ 1 file changed, 8 insertions(+), 43 deletions(-) diff --git a/src/wasm_bindings.cpp b/src/wasm_bindings.cpp index 2a33de8a..647967bb 100644 --- a/src/wasm_bindings.cpp +++ b/src/wasm_bindings.cpp @@ -90,48 +90,7 @@ val calculate_volumes_direct(const CalculationParams& params) { return val::null(); } - // Write structure file with extension based on content - std::string temp_filename; - if (params.structure_content.find("HETATM") != std::string::npos || - params.structure_content.find("ATOM") != std::string::npos) { - temp_filename = "/structure.pdb"; - printf("Detected PDB format\n"); - } else if (params.structure_content.find("data_") != std::string::npos) { - temp_filename = "/structure.cif"; - printf("Detected CIF format\n"); - } else { - // Assume XYZ format - temp_filename = "/structure.xyz"; - printf("Assuming XYZ format\n"); - } - - printf("Structure content preview (first 200 chars):\n%s\n", - params.structure_content.substr(0, 200).c_str()); - - if (!writeAndVerifyFile(temp_filename, params.structure_content)) { - val::global("Error").new_(std::string("Failed to write structure file")).throw_(); - return val::null(); - } - printf("Successfully wrote structure file to: %s\n", temp_filename.c_str()); - - // Write elements file - std::string elements_content = R"(# Atom Symb Name VdW-Radius -1 H Hydrogen 1.200 -6 C Carbon 1.700 -7 N Nitrogen 1.550 -8 O Oxygen 1.520 -9 F Fluorine 1.470 -15 P Phosphorus 1.800 -16 S Sulfur 1.800 -17 Cl Chlorine 1.750)"; - - std::string elements_filename = "/elements.txt"; - if (!writeAndVerifyFile(elements_filename, elements_content)) { - printf("Failed to write elements file\n"); - val::global("Error").new_(std::string("Failed to write elements file")).throw_(); - return val::null(); - } // Set output flags for all relevant information unsigned output_flags = mvOUT_RESOLUTION | mvOUT_DEPTH | mvOUT_RADIUS_S | mvOUT_RADIUS_L | @@ -146,6 +105,12 @@ val calculate_volumes_direct(const CalculationParams& params) { printf("- Unit cell: %s\n", params.unit_cell ? "true" : "false"); printf("- Surface area: %s\n", params.surface_area ? "true" : "false"); + // Write structure content to temporary file + std::string input_filepath = "/tmp/structure_input.pdb"; // or appropriate extension + if (!writeAndVerifyFile(input_filepath, params.structure_content)) { + val::global("Error").new_(std::string("Failed to write structure file")).throw_(); + return val::null(); + } // Run calculation bool success = false; try { @@ -153,8 +118,8 @@ val calculate_volumes_direct(const CalculationParams& params) { params.probe_radius_small, params.probe_radius_large, params.grid_resolution, - temp_filename, - elements_filename, + input_filepath,//strucutre input file + "inputfile/elements.txt",//elements "/tmp", // Temporary directory for any exports params.tree_depth, params.include_hetatm, From cb592cd91376d7836f7751992b380ff905777519 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 22:40:15 +0100 Subject: [PATCH 26/61] add file extension to wasm --- cmake/wasm.cmake | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmake/wasm.cmake b/cmake/wasm.cmake index 376399b1..43420657 100644 --- a/cmake/wasm.cmake +++ b/cmake/wasm.cmake @@ -11,10 +11,12 @@ set(CMAKE_CXX_EXTENSIONS OFF) # Emscripten compiler flags set(WASM_COMPILER_FLAGS -s WASM=1 - -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap'] + -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','FS','allocate'] # Added 'allocate' -s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_FUNCTIONS=['_malloc','_free'] -fexceptions + -s FORCE_FILESYSTEM=1 + -s DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['$allocate','$ALLOC_NORMAL'] # Added this line ) function(require_file FILE_PATH) @@ -37,6 +39,8 @@ set(WASM_LINK_FLAGS --bind -s EXPORT_ES6=1 -s USE_ES6_IMPORT_META=0 + -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','FS','allocate'] # Added 'allocate' here too + -s DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['$allocate','$ALLOC_NORMAL'] # And here # Preload resource files --preload-file ${CMAKE_CURRENT_SOURCE_DIR}/inputfile/elements.txt@/inputfile/elements.txt From fe3d4eedaefc4fd94afad41bef64b733ecb2fad5 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sat, 11 Jan 2025 22:48:57 +0100 Subject: [PATCH 27/61] more debugging --- src/model_filereading.cpp | 151 ++++++++++++++++++++------------------ webserver/test-setup.html | 51 ++++++++++--- 2 files changed, 119 insertions(+), 83 deletions(-) diff --git a/src/model_filereading.cpp b/src/model_filereading.cpp index ff715da9..1146776b 100644 --- a/src/model_filereading.cpp +++ b/src/model_filereading.cpp @@ -35,33 +35,11 @@ ElementsFileBundle extractDataFromElemFile(const std::string& elem_path); // generates three maps for assigning a radius, weight and atomic number respectively, to an element symbol // sets the maps to members of the model class bool Model::importElemFile(const std::string& elem_path){ - #ifdef __EMSCRIPTEN__ - EM_ASM({ - console.log('Attempted elements file path: ' + UTF8ToString($0)); - console.log('Current working directory: ' + FS.cwd()); - console.log('Root directory contents:'); - try { - var contents = FS.analyzePath('/'); - console.log(contents); - } catch (e) { - console.error('Error analyzing root directory:', e); - } - - console.log('Trying to list /inputfile directory:'); - try { - var dirContents = FS.readdir('/inputfile'); - console.log(dirContents); - } catch (e) { - console.error('Error reading /inputfile directory:', e); - } - }, elem_path.c_str()); - #endif ElementsFileBundle data = extractDataFromElemFile(elem_path); setRadiusMap(data.rad_map); _elem_weight = data.weight_map; _elem_Z = data.atomic_num_map; - - return (data.rad_map.size()); + return data.rad_map.size(); } // used for importing only the radius map from the radius file @@ -70,57 +48,83 @@ std::unordered_map Model::extractRadiusMap(const std::strin return extractDataFromElemFile(elem_path).rad_map; } -ElementsFileBundle extractDataFromElemFile(const std::string& elem_path){ - ElementsFileBundle data; +ElementsFileBundle extractDataFromElemFile(const std::string& elem_path) { + ElementsFileBundle data; - auto hasCorrectFormat = [](std::vector substrings){ - if (substrings.size() != 4){return false;} - if (substrings[0].find_first_not_of("0123456789") != std::string::npos){return false;} - for (char i : {2,3}){ - if (substrings[i].find_first_not_of("0123456789E.+e") != std::string::npos){return false;} + #ifdef __EMSCRIPTEN__ + // Use Emscripten's FS to read the file into a string + std::string fileContent; + fileContent = EM_ASM_INT({ + var path = UTF8ToString($0); + console.log("Trying to list directory: " + path); + var dirContents = FS.readdir("/inputfile"); + console.log("Directory contents:", dirContents); + + console.log("Checking file existence in WASM filesystem " + path); + try { + var content = FS.readFile(path, { encoding: "utf8" }); + console.log("File content loaded, length:", content.length); + console.log("First 100 chars:", content.substring(0, 100)); + return content; + } catch (e) { + console.error("Error reading file with FS:", e); + return ""; + } + }, elem_path.c_str()); + + if (fileContent.empty()) { + std::cerr << "Failed to read elements file or file is empty" << std::endl; + return data; } - return true; - }; - std::string line; - std::ifstream inp_file(elem_path); - bool invalid_symbol_detected = false; - bool invalid_radius_value = false; - bool invalid_weight_value = false; - while(getline(inp_file,line)){ - std::vector substrings = ImportMngr::splitLine(line); - // substrings[0]: Atomic Number - // substrings[1]: Element Symbol - // substrings[2]: Radius - // substrings[3]: Weight - if(hasCorrectFormat(substrings)){ - substrings[1] = ImportMngr::strToValidSymbol(substrings[1]); - // skip entry if element symbol invalid - if (substrings[1].empty()){ - invalid_symbol_detected = true; - } - else { - try{data.rad_map[substrings[1]] = std::stod(substrings[2]);} - catch (const std::invalid_argument& e){ - data.rad_map[substrings[1]] = 0; - invalid_radius_value = true; - } - try{data.weight_map[substrings[1]] = std::stod(substrings[3]);} - catch (const std::invalid_argument& e){ - data.weight_map[substrings[1]] = 0; - invalid_weight_value = true; + // Parse the input using stringstream + std::istringstream inp_stream(fileContent); + + #else + // Use std::ifstream for native builds + std::ifstream inp_stream(elem_path); + if (!inp_stream.is_open()) { + std::cerr << "Failed to open file: " << elem_path << std::endl; + return data; + } + #endif + + // Parse the input stream + std::string line; + auto hasCorrectFormat = [](std::vector substrings) { + if (substrings.size() != 4) { return false; } + if (substrings[0].find_first_not_of("0123456789") != std::string::npos) { return false; } + for (char i : {2, 3}) { + if (substrings[i].find_first_not_of("0123456789E.+e") != std::string::npos) { return false; } } - try{data.atomic_num_map[substrings[1]] = std::stoi(substrings[0]);} - catch (const std::invalid_argument& e){ - data.atomic_num_map[substrings[1]] = 0; + return true; + }; + + // Debug output at the start of parsing + std::cerr << "Starting to parse file content" << std::endl; + + while (getline(inp_stream, line)) { + std::vector substrings = ImportMngr::splitLine(line); + if (hasCorrectFormat(substrings)) { + substrings[1] = ImportMngr::strToValidSymbol(substrings[1]); + if (!substrings[1].empty()) { + try { + data.rad_map[substrings[1]] = std::stod(substrings[2]); + std::cerr << "Added radius for " << substrings[1] << ": " << data.rad_map[substrings[1]] << std::endl; + } + catch (const std::invalid_argument&) { data.rad_map[substrings[1]] = 0; } + try { data.weight_map[substrings[1]] = std::stod(substrings[3]); } + catch (const std::invalid_argument&) { data.weight_map[substrings[1]] = 0; } + try { data.atomic_num_map[substrings[1]] = std::stoi(substrings[0]); } + catch (const std::invalid_argument&) { data.atomic_num_map[substrings[1]] = 0; } + } } - } } - } - if (invalid_symbol_detected) {Ctrl::getInstance()->displayErrorMessage(106);} - if (invalid_radius_value) {Ctrl::getInstance()->displayErrorMessage(107);} - if (invalid_weight_value) {Ctrl::getInstance()->displayErrorMessage(108);} - return data; + + // Debug output at the end of parsing + std::cerr << "Finished parsing. Found " << data.rad_map.size() << " elements" << std::endl; + + return data; } ////////////////////// @@ -129,14 +133,17 @@ ElementsFileBundle extractDataFromElemFile(const std::string& elem_path){ bool Model::readAtomsFromFile(const std::string& filepath, bool include_hetatm){ clearAtomData(); - + + // Debug output for filename and extension + std::string extension = fileExtension(filepath); std::vector atom_list; + // XYZ file import - if (fileExtension(filepath) == "xyz"){ + if (extension == "xyz"){ atom_list = ImportMngr::readFileXYZ(filepath); } // PDB file import - else if (fileExtension(filepath) == "pdb"){ + else if (extension == "pdb"){ const std::pair,UnitCell> import_data = ImportMngr::readFilePDB(filepath, include_hetatm); atom_list = import_data.first; @@ -146,7 +153,7 @@ bool Model::readAtomsFromFile(const std::string& filepath, bool include_hetatm){ } } // CIF file import - else if (fileExtension(filepath) == "cif"){ + else if (extension == "cif"){ try{ const std::pair,UnitCell> import_data = ImportMngr::readFileCIF(filepath); atom_list = import_data.first; diff --git a/webserver/test-setup.html b/webserver/test-setup.html index a4b72b8c..ba976186 100644 --- a/webserver/test-setup.html +++ b/webserver/test-setup.html @@ -65,17 +65,43 @@

MoloVol WASM Test

+ +
+ © 2023-2024 Molovol Web is brought to you by + Benedikt S. Vogler and Jasmin B. Maglic based on + Molovol Desktop by J. B. Maglic and Roy Lavendomme. +
+ © 2020-2024 The MoloVol logo and wordmark were designed by Jasmin B. Maglic +
+ + \ No newline at end of file From 68f2effbc7c6fc3bfea5a482f3ffa52f8c3e0747 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sun, 12 Jan 2025 14:55:37 +0100 Subject: [PATCH 30/61] reorganize cmake --- cmake/Sources.cmake | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/cmake/Sources.cmake b/cmake/Sources.cmake index 637b41ae..ed6e962e 100644 --- a/cmake/Sources.cmake +++ b/cmake/Sources.cmake @@ -17,21 +17,15 @@ set(CORE_SOURCES src/voxel.cpp ) -# GUI-specific sources -set(GUI_SOURCES - src/base_guicontrol.cpp - src/base_constr.cpp - src/base_event.cpp - src/base_init.cpp -) - # Set up sources based on build type -if(EMSCRIPTEN) - set(MOLOVOL_SOURCES - ${CORE_SOURCES} - src/base_cmdline.cpp - ) -elseif(MOLOVOL_BUILD_GUI) +if(MOLOVOL_BUILD_GUI) + # GUI-specific sources + set(GUI_SOURCES + src/base_guicontrol.cpp + src/base_constr.cpp + src/base_event.cpp + src/base_init.cpp + ) set(SOURCES ${CORE_SOURCES} ${GUI_SOURCES} From c300b3330f4625e072f25566a099c72a7c7574d6 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sun, 12 Jan 2025 15:00:22 +0100 Subject: [PATCH 31/61] update version number --- include/controller.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/controller.h b/include/controller.h index 41dca8a0..3cf1eae9 100644 --- a/include/controller.h +++ b/include/controller.h @@ -77,7 +77,7 @@ class Ctrl { void displayCavityList(CalcReportBundle&, const unsigned=mvOUT_ALL); std::string getErrorMessage(const int); - inline static const std::string s_version = "1.1.1"; + inline static const std::string s_version = "1.2"; inline static const std::string s_elem_file = "elements.txt"; }; From bf8dd956a140eb9e979df5c74b814329f98f82bf Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sun, 12 Jan 2025 15:27:05 +0100 Subject: [PATCH 32/61] fix compilation again --- cmake/Sources.cmake | 15 ++-- cmake/wasm.cmake | 12 ++-- include/base_cmdline.h | 17 ++++- src/base_cmdline.cpp | 154 +++++++++++++++++++++++++++++++---------- 4 files changed, 148 insertions(+), 50 deletions(-) diff --git a/cmake/Sources.cmake b/cmake/Sources.cmake index ed6e962e..89008f72 100644 --- a/cmake/Sources.cmake +++ b/cmake/Sources.cmake @@ -19,18 +19,19 @@ set(CORE_SOURCES # Set up sources based on build type if(MOLOVOL_BUILD_GUI) - # GUI-specific sources - set(GUI_SOURCES - src/base_guicontrol.cpp - src/base_constr.cpp - src/base_event.cpp - src/base_init.cpp - ) + # GUI-specific sources + set(GUI_SOURCES + src/base_guicontrol.cpp + src/base_constr.cpp + src/base_event.cpp + src/base_init.cpp + ) set(SOURCES ${CORE_SOURCES} ${GUI_SOURCES} ) else() + # CLI build set(SOURCES ${CORE_SOURCES} src/base_cmdline.cpp diff --git a/cmake/wasm.cmake b/cmake/wasm.cmake index f430358e..f5932b6a 100644 --- a/cmake/wasm.cmake +++ b/cmake/wasm.cmake @@ -11,7 +11,7 @@ set(CMAKE_CXX_EXTENSIONS OFF) # Emscripten compiler flags set(WASM_COMPILER_FLAGS -s WASM=1 - -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','FS','allocate'] # Added 'allocate' + -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','FS','allocate'] -s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_FUNCTIONS=['_malloc','_free'] -fexceptions @@ -24,7 +24,7 @@ function(require_file FILE_PATH) endif() endfunction() -# Usage +# Verify required files exist require_file("${CMAKE_CURRENT_SOURCE_DIR}/inputfile/elements.txt") require_file("${CMAKE_CURRENT_SOURCE_DIR}/inputfile/space_groups.txt") @@ -38,7 +38,7 @@ set(WASM_LINK_FLAGS --bind -s EXPORT_ES6=1 -s USE_ES6_IMPORT_META=0 - -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','FS'] # Added 'allocate' here too + -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','FS'] # Preload resource files --preload-file ${CMAKE_CURRENT_SOURCE_DIR}/inputfile/elements.txt@/inputfile/elements.txt @@ -49,10 +49,10 @@ set(WASM_LINK_FLAGS string(REPLACE ";" " " WASM_COMPILER_FLAGS_STR "${WASM_COMPILER_FLAGS}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WASM_COMPILER_FLAGS_STR}") -# Create WebAssembly target +# Create WebAssembly target with all sources including bindings add_executable(molovol_wasm - ${CMAKE_CURRENT_SOURCE_DIR}/src/wasm_bindings.cpp - ${MOLOVOL_SOURCES} # This should be defined in main CMakeLists.txt + ${SOURCES} + src/wasm_bindings.cpp ) # Apply link flags diff --git a/include/base_cmdline.h b/include/base_cmdline.h index b0f6e40f..b0b7b65b 100644 --- a/include/base_cmdline.h +++ b/include/base_cmdline.h @@ -5,6 +5,7 @@ #include #include #include +#include "flags.h" struct CommandLineOption { std::string shortName; @@ -57,8 +58,20 @@ bool validateProbes(double r1, double r2, bool pm); bool validateExport(const std::string& out_dir, const std::vector& exp_options); bool validatePdb(const std::string& file, bool hetatm, bool unitcell); -// Display options constants -extern const std::map DISPLAY_OPTIONS; +// Display options constants - declare as extern +inline const std::map DISPLAY_OPTIONS = { + {"none", mvOUT_NONE}, {"inputfile", mvOUT_STRUCTURE}, {"resolution", mvOUT_RESOLUTION}, + {"depth", mvOUT_DEPTH}, {"radius_small", mvOUT_RADIUS_S}, {"radius_large", mvOUT_RADIUS_L}, + {"input", mvOUT_INP}, {"hetatm", mvOUT_OPT_HETATM}, {"unitcell", mvOUT_OPT_UNITCELL}, + {"probemode", mvOUT_OPT_PROBEMODE}, {"surface", mvOUT_OPT_SURFACE}, {"options", mvOUT_OPT}, + {"formula", mvOUT_FORMULA}, {"time", mvOUT_TIME}, {"vol_vdw", mvOUT_VOL_VDW}, + {"vol_inaccessible", mvOUT_VOL_INACCESSIBLE}, {"vol_core_s", mvOUT_VOL_CORE_S}, + {"vol_shell_s", mvOUT_VOL_SHELL_S}, {"vol_core_l", mvOUT_VOL_CORE_L}, + {"vol_shell_l", mvOUT_VOL_SHELL_L}, {"vol_mol", mvOUT_VOL_MOL}, {"vol", mvOUT_VOL}, + {"surf_vdw", mvOUT_SURF_VDW}, {"surf_mol", mvOUT_SURF_MOL}, + {"surf_excluded_s", mvOUT_SURF_EXCLUDED_S}, {"surf_accessible_s", mvOUT_SURF_ACCESSIBLE_S}, + {"surf", mvOUT_SURF}, {"cavities", mvOUT_CAVITIES}, {"all", mvOUT_ALL} +}; // Display options evaluation function unsigned evalDisplayOptions(const std::string& output); diff --git a/src/base_cmdline.cpp b/src/base_cmdline.cpp index 1fab5181..5e9246f7 100644 --- a/src/base_cmdline.cpp +++ b/src/base_cmdline.cpp @@ -9,7 +9,7 @@ bool CommandLineParser::parse(const std::vector& args) { try { // Check for empty arguments if (args.empty()) { - Ctrl::getInstance()->displayErrorMessage(901); // Assuming 901 is appropriate error code + Ctrl::getInstance()->displayErrorMessage(901); return false; } @@ -17,7 +17,7 @@ bool CommandLineParser::parse(const std::vector& args) { for (size_t i = 1; i < args.size(); i++) { const std::string& arg = args[i]; if (arg.empty()) { - Ctrl::getInstance()->displayErrorMessage(901); // Empty argument + Ctrl::getInstance()->displayErrorMessage(901); return false; } @@ -25,7 +25,7 @@ bool CommandLineParser::parse(const std::vector& args) { std::string optName = arg.substr(arg[1] == '-' ? 2 : 1); if (optName.empty()) { - Ctrl::getInstance()->displayErrorMessage(901); // Invalid option format + Ctrl::getInstance()->displayErrorMessage(901); return false; } @@ -58,18 +58,29 @@ bool CommandLineParser::parse(const std::vector& args) { return validateRequiredOptions(); } catch (const std::exception& e) { std::cerr << "Error parsing arguments: " << e.what() << std::endl; - Ctrl::getInstance()->displayErrorMessage(901); // General parsing error + Ctrl::getInstance()->displayErrorMessage(901); return false; } } +bool CommandLineParser::parse(int argc, char* argv[]) { + std::vector args; + for (int i = 0; i < argc; ++i) { + args.push_back(argv[i]); + } + return parse(args); +} + bool CommandLineParser::found(const std::string& name) const { return parsedFlags.count(name) > 0 || parsedOptions.count(name) > 0; } std::optional CommandLineParser::getValue(const std::string& name) const { auto it = parsedOptions.find(name); - return it != parsedOptions.end() ? std::optional(it->second) : std::nullopt; + if (it != parsedOptions.end()) { + return it->second; + } + return std::nullopt; } void CommandLineParser::displayHelp() const { @@ -105,35 +116,6 @@ bool CommandLineParser::validateRequiredOptions() const { return true; } -// Validation functions -bool validateProbes(double r1, double r2, bool pm) { - return !(pm && r2 < r1 && (Ctrl::getInstance()->displayErrorMessage(104), true)); -} - -bool validateExport(const std::string& out_dir, const std::vector& exp_options) { - return !(isIncluded(true, exp_options) && out_dir.empty() && (Ctrl::getInstance()->displayErrorMessage(302), true)); -} - -bool validatePdb(const std::string& file, bool hetatm, bool unitcell) { - return !((fileExtension(file) != "pdb" && fileExtension(file) != "cif") && (hetatm || unitcell) && - (Ctrl::getInstance()->displayErrorMessage(115), true)); -} - -// Define display options map -const std::map DISPLAY_OPTIONS = { - {"none", mvOUT_NONE}, {"inputfile", mvOUT_STRUCTURE}, {"resolution", mvOUT_RESOLUTION}, - {"depth", mvOUT_DEPTH}, {"radius_small", mvOUT_RADIUS_S}, {"radius_large", mvOUT_RADIUS_L}, - {"input", mvOUT_INP}, {"hetatm", mvOUT_OPT_HETATM}, {"unitcell", mvOUT_OPT_UNITCELL}, - {"probemode", mvOUT_OPT_PROBEMODE}, {"surface", mvOUT_OPT_SURFACE}, {"options", mvOUT_OPT}, - {"formula", mvOUT_FORMULA}, {"time", mvOUT_TIME}, {"vol_vdw", mvOUT_VOL_VDW}, - {"vol_inaccessible", mvOUT_VOL_INACCESSIBLE}, {"vol_core_s", mvOUT_VOL_CORE_S}, - {"vol_shell_s", mvOUT_VOL_SHELL_S}, {"vol_core_l", mvOUT_VOL_CORE_L}, - {"vol_shell_l", mvOUT_VOL_SHELL_L}, {"vol_mol", mvOUT_VOL_MOL}, {"vol", mvOUT_VOL}, - {"surf_vdw", mvOUT_SURF_VDW}, {"surf_mol", mvOUT_SURF_MOL}, - {"surf_excluded_s", mvOUT_SURF_EXCLUDED_S}, {"surf_accessible_s", mvOUT_SURF_ACCESSIBLE_S}, - {"surf", mvOUT_SURF}, {"cavities", mvOUT_CAVITIES}, {"all", mvOUT_ALL} -}; - unsigned evalDisplayOptions(const std::string& output) { std::stringstream ss(output); std::string option; @@ -153,4 +135,106 @@ unsigned evalDisplayOptions(const std::string& output) { Ctrl::getInstance()->displayErrorMessage(902); } return display_flag; -} \ No newline at end of file +} + +#ifndef EMSCRIPTEN +int main(int argc, char* argv[]) { + auto ctrl = Ctrl::getInstance(); + ctrl->disableGUI(); // Ensure we're in CLI mode + + CommandLineParser parser; + if (!parser.parse(argc, argv)) { + return 1; + } + + if (parser.found("help")) { + parser.displayHelp(); + return 0; + } + + if (parser.found("version")) { + ctrl->version(); + return 0; + } + + // Get required parameters with validation + auto probe_radius = parser.getValue("radius"); + if (!probe_radius) { + ctrl->displayErrorMessage(109); + return 1; + } + double probe_radius_s = std::stod(*probe_radius); + + auto grid = parser.getValue("grid"); + if (!grid) { + ctrl->displayErrorMessage(109); + return 1; + } + double grid_resolution = std::stod(*grid); + + auto structure_file = parser.getValue("file-structure"); + if (!structure_file) { + ctrl->displayErrorMessage(102); + return 1; + } + + // Optional parameters + std::string elements_file_path = parser.getValue("file-elements").value_or(Ctrl::getDefaultElemPath()); + std::string output_dir_path = parser.getValue("dir-output").value_or(""); + + double probe_radius_l = 0.0; + if (auto radius2 = parser.getValue("radius2")) { + probe_radius_l = std::stod(*radius2); + if (probe_radius_l < probe_radius_s) { + ctrl->displayErrorMessage(104); + return 1; + } + } + + int tree_depth = 3; // Default value + if (auto depth = parser.getValue("depth")) { + tree_depth = std::stoi(*depth); + } + + // Boolean flags + bool opt_include_hetatm = parser.found("hetatm"); + bool opt_unit_cell = parser.found("unitcell"); + bool opt_surface_area = parser.found("surface"); + bool opt_probe_mode = parser.getValue("radius2").has_value(); + bool exp_report = parser.found("export-report"); + bool exp_total_map = parser.found("export-total"); + bool exp_cavity_maps = parser.found("export-cavities"); + + // Set display options + unsigned display_flag = mvOUT_ALL; + if (auto output = parser.getValue("output")) { + display_flag = evalDisplayOptions(*output); + } + + // Handle quiet mode + if (parser.found("quiet")) { + ctrl->hush(true); + } + + // Run the calculation + bool success = ctrl->runCalculation( + probe_radius_s, + probe_radius_l, + grid_resolution, + *structure_file, + elements_file_path, + output_dir_path, + tree_depth, + opt_include_hetatm, + opt_unit_cell, + opt_surface_area, + opt_probe_mode, + exp_report, + exp_total_map, + exp_cavity_maps, + display_flag + ); + + return success ? 0 : 1; +} +#endif \ No newline at end of file From ba518bd991989a62c09f68842f94f7c8bed075c9 Mon Sep 17 00:00:00 2001 From: "Benedikt S. Vogler" Date: Sun, 12 Jan 2025 15:35:13 +0100 Subject: [PATCH 33/61] output it to the view --- webserver/test-setup.html | 109 ++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 64 deletions(-) diff --git a/webserver/test-setup.html b/webserver/test-setup.html index ba976186..ad0822e5 100644 --- a/webserver/test-setup.html +++ b/webserver/test-setup.html @@ -65,43 +65,43 @@

MoloVol WASM Test

- - \ No newline at end of file diff --git a/webserver/wasmform.html b/webserver/wasmform.html index 94c6c7ae..693d2c2e 100644 --- a/webserver/wasmform.html +++ b/webserver/wasmform.html @@ -22,8 +22,6 @@

- MoloVol Web allows you to conveniently use MoloVol - directly through your browser. An equivalent desktop version that offers slightly more options is available for download here along with the user manual. @@ -32,7 +30,7 @@

-
+
@@ -93,31 +86,58 @@

Output

-