Skip to content
118 changes: 88 additions & 30 deletions easybuild/easyblocks/g/gromacs.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,15 @@ def extra_options():
'mpisuffix': ['_mpi', "Suffix to append to MPI-enabled executables (only for GROMACS < 4.6)", CUSTOM],
'mpiexec': ['mpirun', "MPI executable to use when running tests", CUSTOM],
'mpiexec_numproc_flag': ['-np', "Flag to introduce the number of MPI tasks when running tests", CUSTOM],
'mpi_only': [False, "Only build for MPI and skip nompi.", CUSTOM],
'mpi_numprocs': [0, "Number of MPI tasks to use when running tests", CUSTOM],
'python_pkg': [None, "Build gmxapi Python package. None (default) is auto-detect." +
"True or False forces behaviour.", CUSTOM],
'ignore_plumed_version_check': [False, "Ignore the version compatibility check for PLUMED", CUSTOM],
'plumed': [None, "Try to apply PLUMED patches. None (default) is auto-detect. " +
"True or False forces behaviour.", CUSTOM],
'plumed': [None, "Try to enable PLUMED support. None (default) is auto-detect. " +
"'native' enables native PLUMED support for GROMACS 2025 and newer." +
"'patch' (or True) applies PLUMED patches." +
"False disables PLUMED support.", CUSTOM],
})
return extra_vars

Expand Down Expand Up @@ -117,13 +122,13 @@ def get_gromacs_arch(self):
# http://manual.gromacs.org/documentation/2018/install-guide/index.html#simd-support
if 'MIC-AVX512' in optarch and LooseVersion(self.version) >= LooseVersion('2016'):
res = 'AVX_512_KNL'
elif 'AVX512' in optarch and LooseVersion(self.version) >= LooseVersion('2016'):
elif ('AVX512' in optarch or 'X86-64-V4' in optarch) and LooseVersion(self.version) >= LooseVersion('2016'):
res = 'AVX_512'
elif 'AVX2' in optarch and LooseVersion(self.version) >= LooseVersion('5.0'):
elif ('AVX2' in optarch or 'X86-64-V3' in optarch) and LooseVersion(self.version) >= LooseVersion('5.0'):
res = 'AVX2_256'
elif 'AVX' in optarch:
res = 'AVX_256'
elif 'SSE3' in optarch or 'SSE2' in optarch or 'MARCH=NOCONA' in optarch:
elif 'SSE3' in optarch or 'SSE2' in optarch or 'MARCH=NOCONA' in optarch or 'X86-64-V2' in optarch:
# Gromacs doesn't have any GMX_SIMD=SSE3 but only SSE2 and SSE4.1 [1].
# According to [2] the performance difference between SSE2 and SSE4.1 is minor on x86
# and SSE4.1 is not supported by AMD Magny-Cours[1].
Expand Down Expand Up @@ -215,43 +220,81 @@ def configure_step(self):
self.cfg.update('configopts', "-DGMX_GPU=OFF")

# PLUMED detection
# enable PLUMED support if PLUMED is listed as a dependency
# and PLUMED support is either explicitly enabled (plumed = True) or unspecified ('plumed' not defined)
# enable PLUMED support if PLUMED is listed as a dependency.
# plumed = 'native' will enable GROMACS' native PLUMED support ('-DGMX_USE_PLUMED=ON')
# for GROMACS 2025 and newer. plumed = 'patch' specifically requests PLUMED patches.
# In auto-detect ('plumed = None' or not defined) will prefer native support for 2026
# and newer. Older versions of GROMACS patches are applied to enable PLUMED support.
# plumed = True behaves like plumed = 'patch' for backwards compatibility.
plumed_root = get_software_root('PLUMED')
plumed_patches = False
if self.cfg['plumed'] and not plumed_root:
msg = "PLUMED support has been requested but PLUMED is not listed as a dependency."
raise EasyBuildError(msg)
elif plumed_root and self.cfg['plumed'] is False:
self.log.info('PLUMED was found, but compilation without PLUMED has been requested.')
plumed_root = None
elif plumed_root and self.cfg['plumed'] == 'patch':
self.log.info('PLUMED was found, and PLUMED patching has been requested.')
plumed_patches = True
elif plumed_root and self.cfg['plumed'] == 'native':
msg = 'PLUMED was found, and native PLUMED support has been requested.'
if gromacs_version >= '2025':
msg += ' Will use native PLUMED support.'
plumed_patches = False
self.log.info(msg)
else:
msg += " Native PLUMED support is only available with GROMACS 2025 and newer."
raise EasyBuildError(msg)
elif plumed_root and self.cfg['plumed'] is True:
msg = 'PLUMED was found, and PLUMED support has been requested.'
msg += ' Will apply PLUMED patches.'
plumed_patches = True
self.log.info(msg)
elif plumed_root and self.cfg['plumed'] is None:
msg = 'PLUMED was found.'
if gromacs_version >= '2026':
# Even though native support is available since GROMACS 2025, we'll only use
# it as default with 2026 and newer to avoid a sudden change in behaviour.
msg += ' Will use native PLUMED support.'
plumed_patches = False
else:
msg += ' Will apply PLUMED patches.'
plumed_patches = True
self.log.info(msg)

if plumed_root:
self.log.info('PLUMED support has been enabled.')

# Need to check if PLUMED has an engine for this version
engine = 'gromacs-%s' % self.version
if gromacs_version >= '2025' and plumed_patches is False:
self.log.info('Native PLUMED support has been enabled.')
self.cfg.update('configopts', '-DGMX_USE_PLUMED=ON')

res = run_shell_cmd("plumed-patch -l")
if not re.search(engine, res.output):
plumed_ver = get_software_version('PLUMED')
msg = "There is no support in PLUMED version %s for GROMACS %s: %s" % (plumed_ver, self.version,
res.output)
if self.cfg['ignore_plumed_version_check']:
self.log.warning(msg)
else:
raise EasyBuildError(msg)
else:
# Need to check if PLUMED has an engine for this version
engine = 'gromacs-%s' % self.version

res = run_shell_cmd("plumed-patch -l")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A long command line option would facilitate understanding for those less familiar with PLUMED: --plumed-patch --list-engines.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.
Those lines originate from upstream. I just idented them while adding an additional if-clause.

I'll suggest that change for my upstream PR.

if not re.search(engine, res.output):
plumed_ver = get_software_version('PLUMED')
msg = "There is no support in PLUMED version %s for GROMACS %s: %s" % (plumed_ver, self.version,
res.output)
if self.cfg['ignore_plumed_version_check']:
self.log.warning(msg)
else:
raise EasyBuildError(msg)

# PLUMED patching must be done at different stages depending on
# version of GROMACS. Just prepare first part of cmd here
plumed_cmd = "plumed-patch -p -e %s" % engine
# PLUMED patching must be done at different stages depending on
# version of GROMACS. Just prepare first part of cmd here
plumed_cmd = "plumed-patch -p -e %s" % engine

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Long options: plumed-patch --patch --engine %s


# Ensure that the GROMACS log files report how the code was patched
# during the build, so that any problems are easier to diagnose.
# The GMX_VERSION_STRING_OF_FORK feature is available since 2020.
if (gromacs_version >= '2020' and
'-DGMX_VERSION_STRING_OF_FORK=' not in self.cfg['configopts']):
gromacs_version_string_suffix = 'EasyBuild-%s' % EASYBUILD_VERSION
if plumed_root:
if plumed_patches:
gromacs_version_string_suffix += '-PLUMED-%s' % get_software_version('PLUMED')
self.cfg.update('configopts', '-DGMX_VERSION_STRING_OF_FORK=%s' % gromacs_version_string_suffix)

Expand Down Expand Up @@ -291,7 +334,8 @@ def configure_step(self):
ConfigureMake.configure_step(self)

# Now patch GROMACS for PLUMED between configure and build
if plumed_root:
if plumed_root and plumed_patches:
self.log.info('Applying PLUMED patches to GROMACS.')
run_shell_cmd(plumed_cmd)

else:
Expand Down Expand Up @@ -330,7 +374,14 @@ def configure_step(self):
if gromacs_version >= '2020':
# build Python bindings if Python is loaded as a dependency
python_root = get_software_root('Python')
if python_root:
if self.cfg['python_pkg'] is True and not python_root:
msg = "Building Python gmxapi has been requested but Python is not listed as a dependency."
raise EasyBuildError(msg)
elif python_root and self.cfg['python_pkg'] is False:
msg = "Python was found, but compilation without Python gmxapi has been requested."
self.log.info(msg)
self.cfg.update('configopts', "-DGMX_PYTHON_PACKAGE=OFF")
elif python_root:
self.cfg.update('configopts', "-DGMX_PYTHON_PACKAGE=ON")
bin_python = os.path.join(python_root, 'bin', 'python')
# For find_package(PythonInterp)
Expand All @@ -340,7 +391,7 @@ def configure_step(self):
self.cfg.update('configopts', "-DPython3_FIND_VIRTUALENV=STANDARD")

# Now patch GROMACS for PLUMED before cmake
if plumed_root:
if plumed_root and plumed_patches:
if gromacs_version >= '5.1':
# Use shared or static patch depending on
# setting of self.cfg['build_shared_libs']
Expand Down Expand Up @@ -655,8 +706,12 @@ def sanity_check_step(self):
else:
mpisuff = '_mpi'

mpi_bins.extend([binary + mpisuff for binary in mpi_bins])
mpi_libnames.extend([libname + mpisuff for libname in mpi_libnames])
if self.cfg['mpi_only']:
mpi_bins = [binary + mpisuff for binary in mpi_bins]
mpi_libnames = [libname + mpisuff for libname in mpi_libnames]
else:
mpi_bins.extend([binary + mpisuff for binary in mpi_bins])
mpi_libnames.extend([libname + mpisuff for libname in mpi_libnames])

suffixes = ['']

Expand Down Expand Up @@ -769,9 +824,12 @@ def run_all_steps(self, *args, **kwargs):
if precisions == []:
raise EasyBuildError("No precision selected. At least one of single/double_precision must be unset or True")

mpitypes = ['nompi']
if self.toolchain.options.get('usempi', None):
mpitypes.append('mpi')
if self.cfg['mpi_only']:
mpitypes = ['mpi']
else:
mpitypes = ['nompi']
if self.toolchain.options.get('usempi', None):
mpitypes.append('mpi')

# We need to count the number of variations to build.
versions_built = []
Expand Down