Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ..
Expand Down
38 changes: 36 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=[]

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
30 changes: 30 additions & 0 deletions src/gmx/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
32 changes: 26 additions & 6 deletions src/gmx/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

__all__ = ['Context', 'DefaultContext']


import contextlib
import os
import warnings
import networkx as nx
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
11 changes: 6 additions & 5 deletions src/gmx/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
)

Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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})
17 changes: 13 additions & 4 deletions src/gmx/core/export_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
// Created by Eric Irrgang on 3/18/18.
//

#include <iostream>
#include <zconf.h>
#include "core.h"

#include "gmxapi/context.h"

#include "gromacs/utility/init.h"

namespace gmxpy
{
Expand Down Expand Up @@ -97,15 +100,21 @@ void setMDArgs(std::vector<std::string>* mdargs, py::dict params)
void export_context(py::module &m)
{
using ::gmxapi::Context;
// Export execution context class
py::class_<Context, std::shared_ptr<Context>> 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<std::string>;
py::class_<MDArgs, std::unique_ptr<MDArgs>> 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, std::shared_ptr<Context>> 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
Expand Down
4 changes: 3 additions & 1 deletion src/gmx/core/export_system.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ void export_system(py::module &m)
py::class_<System, std::shared_ptr<System> > 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> context)
Expand Down
2 changes: 1 addition & 1 deletion src/gmx/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
39 changes: 24 additions & 15 deletions src/gmx/test/test_mpiarraycontext.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@
# 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,
reason="Test requires at least 2 MPI ranks, but MPI is not initialized or too small.")
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):
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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
13 changes: 0 additions & 13 deletions src/gmx/test/test_pymd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading