diff --git a/.travis.yml b/.travis.yml index 0ccea9935f..1a0184b19a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,8 @@ matrix: - GMX_THREAD_MPI=ON - gmxapi_DIR=$HOME/gromacs - GROMACS_DIR=$HOME/gromacs + - GROMACS_TAG=devel +# Todo: GROMACS_TAG=devel needs to be fixed before merging to a release branch. # It is clearly a bad idea to have the development branch of a project requiring the development branch # of another project in the primary build recipe. We either need to build and test development and master @@ -40,11 +42,11 @@ before_install: - apt list --installed - test -n $CC && unset CC - test -n $CXX && unset CXX - - wget https://github.com/kassonlab/gromacs-gmxapi/archive/master.zip - - unzip master.zip + - wget https://github.com/kassonlab/gromacs-gmxapi/archive/$GROMACS_TAG.zip + - unzip $GROMACS_TAG.zip install: - - pushd gromacs-gmxapi-master + - pushd gromacs-gmxapi-$GROMACS_TAG - mkdir build - pushd build - cmake -DCMAKE_INSTALL_PREFIX=$HOME/gromacs -DGMX_FFT_LIBRARY=fftpack -DGMX_GPU=OFF -DGMX_OPENMP=OFF -DGMX_SIMD=None -DGMX_USE_RDTSCP=OFF -DGMX_MPI=$GMX_MPI -DGMX_THREAD_MPI=$GMX_THREAD_MPI .. diff --git a/CMakeLists.txt b/CMakeLists.txt index 1bd06ce860..abf6764329 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,8 +5,42 @@ cmake_minimum_required(VERSION 3.4.3) #list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) # Sets the PROJECT_VERSION variable, as well... -project(gmxpy VERSION 0.0.5) +project(gmxpy VERSION 0.0.6) + +# If the user is not in a virtual environment and is not a privileged user and has not specified an install locatoin +# for the Python module (GMXAPI_INSTALL_PATH), this option causes the automatic install location to query the user +# site-packages directory instead of using the default site-packages directory for the interpreter. +option(GMXAPI_USER_INSTALL "Override the default site-packages directory with the user-specific Python packages directory.\ +(Do not use with virtual environments.)") +# Since a user may have multiple virtual environments with different Python interpreters, it is generally confusing to +# have a package for a virtual environment installed in the user's default user site-packages directory. + +unset(PYTHONINTERP_FOUND) +unset(PYTHONLIBS_FOUND) +find_package(PythonInterp) +if (PYTHONINTERP_FOUND) + message(STATUS "Found Python interpreter: ${PYTHON_EXECUTABLE}") + find_package(PythonLibs) + if (PYTHONLIBS_FOUND) + if (GMXAPI_USER_INSTALL) + execute_process(COMMAND ${PYTHON_EXECUTABLE} -m site --user OUTPUT_VARIABLE GMXAPI_DEFAULT_SITE_PACKAGES OUTPUT_STRIP_TRAILING_WHITESPACE) + message(STATUS "Python user site-packages directory is ${GMXAPI_DEFAULT_SITE_PACKAGES}") + else() + execute_process(COMMAND ${PYTHON_EXECUTABLE} -c "import sys; import os; print(os.path.abspath(os.path.join(sys.prefix, 'lib', 'python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}', 'site-packages')))" OUTPUT_VARIABLE GMXAPI_DEFAULT_SITE_PACKAGES OUTPUT_STRIP_TRAILING_WHITESPACE) + message(STATUS "Python site-packages directory is ${GMXAPI_DEFAULT_SITE_PACKAGES}") + endif() + else() + message(FATAL "Found Python interpreter ${PYTHON_EXECUTABLE} but this Python installation does not have developer tools." + "Set PYTHON_EXECUTABLE to the Python interpreter that was installed with a working Python.h header file.") + endif() +else() + message(FATAL "Could not find Python interpreter. Set CMake flag -DPYTHON_EXECUTABLE=/path/to/python to hint.") +endif() + + +set(GMXAPI_INSTALL_PATH ${GMXAPI_DEFAULT_SITE_PACKAGES}/gmx CACHE PATH "Path to Python module install location (site-packages).") + add_subdirectory(pybind11) -add_subdirectory(src/gmx/core) +add_subdirectory(src/gmx) diff --git a/setup.py b/setup.py index 7801d001cd..0c567fd23d 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ from setuptools.command.test import test as TestCommand #import gmx.version -__version__ = '0.0.5' +__version__ = '0.0.6' extra_link_args=[] @@ -210,7 +210,7 @@ def build_extension(self, ext): if build_gromacs: # TODO! We need to distinguish dev branch builds from master branch builds or always build with the # master branch of the dependency. For one thing, as is, this line needs to be toggled for every release. - gromacs_url = "https://github.com/kassonlab/gromacs-gmxapi/archive/dev_5.zip" + gromacs_url = "https://github.com/kassonlab/gromacs-gmxapi/archive/devel.zip" gmxapi_DIR = os.path.join(extdir, 'data/gromacs') if build_for_readthedocs: extra_cmake_args = ['-DCMAKE_INSTALL_PREFIX=' + gmxapi_DIR, @@ -248,7 +248,7 @@ def build_extension(self, ext): os.makedirs(self.build_temp) # cmake_args += ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + extdir] - cmake_args += ['-DCMAKE_INSTALL_PREFIX=' + extdir] + cmake_args += ['-DGMXAPI_INSTALL_PATH=' + extdir] # if platform.system() == "Windows": # cmake_args += ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}'.format(cfg.upper(), extdir)] try: diff --git a/src/gmx/CMakeLists.txt b/src/gmx/CMakeLists.txt new file mode 100644 index 0000000000..a349b6087a --- /dev/null +++ b/src/gmx/CMakeLists.txt @@ -0,0 +1,30 @@ +file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/gmx/data) + +configure_file(__init__.py ${CMAKE_BINARY_DIR}/gmx/ COPYONLY) +configure_file(context.py ${CMAKE_BINARY_DIR}/gmx/ COPYONLY) +configure_file(data.py ${CMAKE_BINARY_DIR}/gmx/ COPYONLY) +configure_file(exceptions.py ${CMAKE_BINARY_DIR}/gmx/ COPYONLY) +configure_file(fileio.py ${CMAKE_BINARY_DIR}/gmx/ COPYONLY) +configure_file(status.py ${CMAKE_BINARY_DIR}/gmx/ COPYONLY) +configure_file(system.py ${CMAKE_BINARY_DIR}/gmx/ COPYONLY) +configure_file(util.py ${CMAKE_BINARY_DIR}/gmx/ COPYONLY) +configure_file(workflow.py ${CMAKE_BINARY_DIR}/gmx/ COPYONLY) + +configure_file(data/topol.tpr ${CMAKE_BINARY_DIR}/gmx/data/ COPYONLY) + +configure_file(version.in ${CMAKE_BINARY_DIR}/gmx/version.py @ONLY) + +file(COPY test DESTINATION ${CMAKE_BINARY_DIR}/gmx/) + +# Todo: we need a target dependent on these files to force fresh configure / copy and to do python -m compileall +# for the install target. + +install(DIRECTORY ${CMAKE_BINARY_DIR}/gmx/ + DESTINATION ${GMXAPI_INSTALL_PATH} + FILES_MATCHING PATTERN "*.py" PATTERN "*.ini" + ) + +install(DIRECTORY ${CMAKE_BINARY_DIR}/gmx/data + DESTINATION ${GMXAPI_INSTALL_PATH}) + +add_subdirectory(core) diff --git a/src/gmx/context.py b/src/gmx/context.py index 23e5926a74..0c1a5d045b 100644 --- a/src/gmx/context.py +++ b/src/gmx/context.py @@ -9,6 +9,8 @@ __all__ = ['Context', 'DefaultContext'] + +import contextlib import os import warnings import networkx as nx @@ -24,6 +26,21 @@ logger = logging.getLogger(__name__) logger.info('Importing gmx.context') +# ref http://code.activestate.com/recipes/576620-changedirectory-context-manager/#c3 +# Does this really not already exist in os.path or something? +@contextlib.contextmanager +def working_directory(path): + """A context manager which changes the working directory to the given + path, and then changes it back to its previous value on exit. + + """ + prev_cwd = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(prev_cwd) + class Context(object): """ Proxy to API Context provides Python context manager. @@ -588,6 +605,7 @@ def __enter__(self): instantiate objects to perform the work. In the first implementation, we kind of muddle things into a single pass. """ + self.__cwd = os.getcwd() import numpy try: from mpi4py import MPI @@ -675,12 +693,6 @@ def update(send, recv, tag=None): if workdir_list is None: workdir_list = [os.path.join('.', str(i)) for i in range(self.size)] self.__workdir_list = list([os.path.abspath(dir) for dir in workdir_list]) - for dir in self.__workdir_list: - if os.path.exists(dir): - if not os.path.isdir(dir): - raise exceptions.FileError('{} is not a valid working directory.'.format(dir)) - else: - os.mkdir(dir) # Check the session "width" against the available parallelism if (self.size > comm_size): @@ -704,6 +716,13 @@ def update(send, recv, tag=None): logger.info("Launching work on rank {}.".format(self.rank)) # Launch the work for this rank self.workdir = self.__workdir_list[self.rank] + if os.path.exists(self.workdir): + if not os.path.isdir(self.workdir): + raise exceptions.FileError('{} is not a valid working directory.'.format(self.workdir)) + else: + os.mkdir(self.workdir) + + # This session will live in a subdirectory of the working directory os.chdir(self.workdir) logger.info('rank {} changed directory to {}'.format(self.rank, self.workdir)) sorted_nodes = nx.topological_sort(graph) @@ -762,6 +781,7 @@ def __exit__(self, exception_type, exception_value, traceback): # \todo Make sure session has ended on all ranks before continuing and handle final errors. self._session = None + os.chdir(self.__cwd) return False def get_context(work=None): diff --git a/src/gmx/core/CMakeLists.txt b/src/gmx/core/CMakeLists.txt index 49ab1956f0..4e50b9b450 100644 --- a/src/gmx/core/CMakeLists.txt +++ b/src/gmx/core/CMakeLists.txt @@ -7,7 +7,7 @@ # cmake was invoked with `-DCMAKE_PREFIX_PATH=...` pointing to the GROMACS # installation directory. We can also check now for a GROMACS_DIR environment # variable and provide it with the HINTS option. -find_package(gmxapi 0.0.5 REQUIRED +find_package(gmxapi 0.0.6 REQUIRED HINTS "$ENV{GROMACS_DIR}" ) @@ -39,6 +39,8 @@ set_target_properties(pygmx_core PROPERTIES OUTPUT_NAME core) # pygmx_core # PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${PYGMX_DIRECTORY}") +set_target_properties(pygmx_core PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/gmx) + target_include_directories(pygmx_core PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} @@ -71,7 +73,6 @@ endif() set_target_properties(pygmx_core PROPERTIES INSTALL_RPATH_USE_LINK_PATH TRUE) install(TARGETS pygmx_core - LIBRARY DESTINATION - ${CMAKE_INSTALL_PREFIX} - ARCHIVE DESTINATION ${CMAKE_INSTALL_PREFIX} - RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}) + LIBRARY DESTINATION ${GMXAPI_INSTALL_PATH} + ARCHIVE DESTINATION ${GMXAPI_INSTALL_PATH} + RUNTIME DESTINATION ${GMXAPI_INSTALL_PATH}) diff --git a/src/gmx/core/export_context.cpp b/src/gmx/core/export_context.cpp index 8f0aa3c502..edbd388b34 100644 --- a/src/gmx/core/export_context.cpp +++ b/src/gmx/core/export_context.cpp @@ -2,10 +2,13 @@ // Created by Eric Irrgang on 3/18/18. // +#include +#include #include "core.h" #include "gmxapi/context.h" +#include "gromacs/utility/init.h" namespace gmxpy { @@ -97,15 +100,21 @@ void setMDArgs(std::vector* mdargs, py::dict params) void export_context(py::module &m) { using ::gmxapi::Context; - // Export execution context class - py::class_> context(m, "Context"); - context.def(py::init(), "Create a default execution context."); - context.def("setMDArgs", &Context::setMDArgs, "Set MD runtime parameters."); using MDArgs = std::vector; py::class_> mdargs(m, "MDArgs"); mdargs.def(py::init(), "Create an empty MDArgs object."); mdargs.def("set", &setMDArgs, "Assign parameters in MDArgs from Python dict."); + + // Export execution context class + py::class_> context(m, "Context"); + context.def(py::init(), "Create a default execution context."); + context.def("setMDArgs", &Context::setMDArgs, "Set MD runtime parameters."); + + // During the registration of the gmx.core.Context Python type, perform appropriate environment initialization + // and deinitialize at module destruction. + gmx::init(nullptr, nullptr); + m.add_object("_current_context", py::capsule([](){ gmx::finalize(); std::cout << "Shutting down GROMACS" << std::endl; })); } } // end namespace gmxpy::detail diff --git a/src/gmx/core/export_system.cpp b/src/gmx/core/export_system.cpp index fa443a4201..e1e05e464e 100644 --- a/src/gmx/core/export_system.cpp +++ b/src/gmx/core/export_system.cpp @@ -32,7 +32,9 @@ void export_system(py::module &m) py::class_ > system(m, "MDSystem"); system.def(py::init(), "A blank system object is possible, but not useful. Use a helper function."); system.def("launch", - [](System* system){ return system->launch(); }, + [](System* system){ + return system->launch(); + }, "Launch the configured workflow in the default context."); system.def("launch", [](System* system, std::shared_ptr context) diff --git a/src/gmx/data.py b/src/gmx/data.py index 502f19105a..e4752863c3 100644 --- a/src/gmx/data.py +++ b/src/gmx/data.py @@ -18,6 +18,6 @@ raise exceptions.OptionalFeatureNotAvailableWarning("Need pkg_resources from setuptools package to access gmx package data.") if os.path.exists(_tpr_filename) and os.path.isfile(_tpr_filename): - tpr_filename = _tpr_filename + tpr_filename = os.path.abspath(_tpr_filename) else: raise exceptions.OptionalFeatureNotAvailableError('Package data file data/topol.tpr not accessible at {}'.format(_tpr_filename)) diff --git a/src/gmx/test/test_mpiarraycontext.py b/src/gmx/test/test_mpiarraycontext.py index dd8e2acab9..51bb439a44 100644 --- a/src/gmx/test/test_mpiarraycontext.py +++ b/src/gmx/test/test_mpiarraycontext.py @@ -15,12 +15,6 @@ # add the handlers to the logger logging.getLogger().addHandler(ch) -import gmx -import gmx.core -import os -# Get a test tpr filename -from gmx.data import tpr_filename - try: from mpi4py import MPI withmpi_only = pytest.mark.skipif(not MPI.Is_initialized() or MPI.COMM_WORLD.Get_size() < 2, @@ -28,6 +22,12 @@ except ImportError: withmpi_only = pytest.mark.skip(reason="Test requires at least 2 MPI ranks, but mpi4py is not available.") +import gmx +import gmx.core +import os +# Get a test tpr filename +from gmx.data import tpr_filename + class ConsumerElement(gmx.workflow.WorkElement): """Simple workflow element to test the shared data resource.""" def __init__(self, name): @@ -138,18 +138,19 @@ def launch_test_consumer(rank=None): @pytest.mark.usefixtures("cleandir") class ArrayContextTestCase(unittest.TestCase): def test_basic(self): - md = gmx.workflow.from_tpr(tpr_filename) - context = gmx.context.ParallelArrayContext(md) - with context as session: - session.run() + # Todo: let Context run work that will fit, even if it is narrower. + # md = gmx.workflow.from_tpr(tpr_filename) + # context = gmx.context.ParallelArrayContext(md) + # with context as session: + # session.run() md = gmx.workflow.from_tpr([tpr_filename, tpr_filename]) context = gmx.context.ParallelArrayContext(md) with context as session: session.run() # This is a sloppy way to see if the current rank had work to do. - if hasattr(context, "workdir"): - rank = context.rank + rank = context.rank + if rank == 0: output_path = os.path.join(context.workdir, 'traj.trr') assert(os.path.exists(output_path)) print("Worker {} produced {}".format(rank, output_path)) @@ -178,11 +179,18 @@ def test_shared_data(self): context.work = workspec # Confirm that oversized width is caught - import mpi4py - size = mpi4py.MPI.COMM_WORLD.Get_size() + from mpi4py import MPI + size = MPI.COMM_WORLD.Get_size() + rank = MPI.COMM_WORLD.Get_rank() + logging.debug("Attempting to launch work with width 3 on rank {}".format(rank)) if size < width: with pytest.raises(gmx.exceptions.UsageError): - context.__enter__() + with context: + pass + # We need to make sure that all ranks in the communicator enter and exit the context. We can probably handle this better. + else: + with context: + pass # Create a workspec that we expect to be runnable. consumer.workspec = None @@ -196,6 +204,7 @@ def test_shared_data(self): context.add_operation(consumer.namespace, consumer.operation, translate_test_consumer) context.work = workspec + logging.debug("Attempting to run session with width {} on rank {}".format(size, rank)) with context as session: session.run() assert session.graph.nodes[consumer.name]['check'] == True diff --git a/src/gmx/test/test_pymd.py b/src/gmx/test/test_pymd.py index b116b74614..468fc578c6 100644 --- a/src/gmx/test/test_pymd.py +++ b/src/gmx/test/test_pymd.py @@ -62,19 +62,6 @@ def test_APIObjectsFromTpr(self): apisystem = gmx.core.from_tpr(tpr_filename) assert isinstance(apisystem, gmx.core.MDSystem) assert hasattr(apisystem, 'launch') - session = apisystem.launch() - assert hasattr(session, 'run') - session.run() - # Test rerunability - # system = gmx.System() - # runner = gmx.runner.SimpleRunner() - # runner._runner = apirunner - # system.runner = runner - # assert isinstance(system, gmx.System) - # assert isinstance(system.runner, gmx.runner.Runner) - # assert isinstance(system.runner._runner, gmx.core.SimpleRunner) - # with gmx.context.DefaultContext(system.runner) as session: - # session.run() def test_SystemFromTpr(self): system = gmx.System._from_file(tpr_filename) system.run() diff --git a/src/gmx/version.in b/src/gmx/version.in new file mode 100644 index 0000000000..251b6b9f7a --- /dev/null +++ b/src/gmx/version.in @@ -0,0 +1,11 @@ +# Version file generated by CMake +__version__ = "@PROJECT_VERSION@" +major = @PROJECT_VERSION_MAJOR@ +minor = @PROJECT_VERSION_MINOR@ +patch = @PROJECT_VERSION_PATCH@ + +def api_is_at_least(a, b, c): + return (major >= a) and (minor >= b) and (patch >= c) + +# Todo: Release status needs to be automatically maintained. +release = False