Skip to content

Commit 1c1cb7e

Browse files
authored
Merge pull request #3618 from lexming/modload-neuron
revamp NEURON easyblock, incl. adopt `module_load_environment`
2 parents 31c86ea + bab8724 commit 1c1cb7e

File tree

1 file changed

+109
-186
lines changed

1 file changed

+109
-186
lines changed

easybuild/easyblocks/n/neuron.py

Lines changed: 109 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -30,243 +30,166 @@
3030
"""
3131
import os
3232
import re
33+
import tempfile
3334

34-
from easybuild.easyblocks.generic.configuremake import ConfigureMake
3535
from easybuild.easyblocks.generic.cmakemake import CMakeMake
3636
from easybuild.easyblocks.generic.pythonpackage import det_pylibdir
3737
from easybuild.framework.easyconfig import CUSTOM
38+
from easybuild.tools import LooseVersion
3839
from easybuild.tools.build_log import EasyBuildError
3940
from easybuild.tools.config import build_option
41+
from easybuild.tools.filetools import write_file
4042
from easybuild.tools.modules import get_software_root
4143
from easybuild.tools.run import run_shell_cmd
4244
from easybuild.tools.systemtools import get_shared_lib_ext
4345

44-
from easybuild.tools import LooseVersion
45-
4646

4747
class EB_NEURON(CMakeMake):
4848
"""Support for building/installing NEURON."""
4949

50-
def __init__(self, *args, **kwargs):
51-
"""Initialisation of custom class variables for NEURON."""
52-
super(EB_NEURON, self).__init__(*args, **kwargs)
53-
54-
self.hostcpu = 'UNKNOWN'
55-
self.with_python = False
56-
self.pylibdir = 'UNKNOWN'
57-
5850
@staticmethod
5951
def extra_options():
6052
"""Custom easyconfig parameters for NEURON."""
61-
6253
extra_vars = {
6354
'paranrn': [True, "Enable support for distributed simulations.", CUSTOM],
6455
}
6556
return CMakeMake.extra_options(extra_vars)
6657

67-
def configure_step(self):
68-
"""Custom configuration procedure for NEURON."""
69-
if LooseVersion(self.version) < LooseVersion('7.8.1'):
70-
71-
# make sure we're using the correct configure command
72-
# (required because custom easyconfig parameters from CMakeMake are picked up)
73-
self.cfg['configure_cmd'] = "./configure"
74-
75-
# enable support for distributed simulations if desired
76-
if self.cfg['paranrn']:
77-
self.cfg.update('configopts', '--with-paranrn')
78-
79-
# specify path to InterViews if it is available as a dependency
80-
interviews_root = get_software_root('InterViews')
81-
if interviews_root:
82-
self.cfg.update('configopts', "--with-iv=%s" % interviews_root)
83-
else:
84-
self.cfg.update('configopts', "--without-iv")
85-
86-
# optionally enable support for Python as alternative interpreter
87-
python_root = get_software_root('Python')
88-
if python_root:
89-
self.with_python = True
90-
self.cfg.update('configopts', "--with-nrnpython=%s/bin/python" % python_root)
58+
def __init__(self, *args, **kwargs):
59+
"""Initialisation of custom class variables for NEURON."""
60+
super(EB_NEURON, self).__init__(*args, **kwargs)
9161

92-
# determine host CPU type
93-
cmd = "./config.guess"
94-
res = run_shell_cmd(cmd)
62+
self.python_root = None
63+
self.pylibdir = 'UNKNOWN'
9564

96-
self.hostcpu = res.output.split('\n')[0].split('-')[0]
97-
self.log.debug("Determined host CPU type as %s" % self.hostcpu)
65+
def prepare_step(self, *args, **kwargs):
66+
"""Custom prepare step with python detection"""
67+
super(EB_NEURON, self).prepare_step(*args, **kwargs)
9868

99-
# determine Python lib dir
100-
self.pylibdir = det_pylibdir()
69+
self.python_root = get_software_root('Python')
10170

102-
# complete configuration with configure_method of parent
103-
ConfigureMake.configure_step(self)
71+
def configure_step(self):
72+
"""Custom configuration procedure for NEURON."""
73+
# enable support for distributed simulations if desired
74+
if self.cfg['paranrn']:
75+
self.cfg.update('configopts', '-DNRN_ENABLE_MPI=ON')
10476
else:
105-
# enable support for distributed simulations if desired
106-
if self.cfg['paranrn']:
107-
self.cfg.update('configopts', '-DNRN_ENABLE_MPI=ON')
108-
else:
109-
self.cfg.update('configopts', '-DNRN_ENABLE_MPI=OFF')
110-
111-
# specify path to InterViews if it is available as a dependency
112-
interviews_root = get_software_root('InterViews')
113-
if interviews_root:
114-
self.cfg.update('configopts', "-DIV_DIR=%s -DNRN_ENABLE_INTERVIEWS=ON" % interviews_root)
115-
else:
116-
self.cfg.update('configopts', "-DNRN_ENABLE_INTERVIEWS=OFF")
117-
118-
# no longer used it seems
119-
self.hostcpu = ''
120-
121-
# optionally enable support for Python as alternative interpreter
122-
python_root = get_software_root('Python')
123-
if python_root:
124-
self.with_python = True
125-
self.cfg.update('configopts', "-DNRN_ENABLE_PYTHON=ON -DPYTHON_EXECUTABLE=%s/bin/python" % python_root)
126-
self.cfg.update('configopts', "-DNRN_ENABLE_MODULE_INSTALL=ON "
127-
"-DNRN_MODULE_INSTALL_OPTIONS='--prefix=%s'" % self.installdir)
128-
else:
129-
self.cfg.update('configopts', "-DNRN_ENABLE_PYTHON=OFF")
130-
131-
# determine Python lib dir
132-
self.pylibdir = det_pylibdir()
133-
134-
# complete configuration with configure_method of parent
135-
CMakeMake.configure_step(self)
77+
self.cfg.update('configopts', '-DNRN_ENABLE_MPI=OFF')
13678

137-
def install_step(self):
138-
"""Custom install procedure for NEURON."""
139-
140-
super(EB_NEURON, self).install_step()
141-
142-
# with the CMakeMake, the python module is installed automatically
143-
if LooseVersion(self.version) < LooseVersion('7.8.1'):
144-
if self.with_python:
145-
pypath = os.path.join('src', 'nrnpython')
146-
try:
147-
pwd = os.getcwd()
148-
os.chdir(pypath)
149-
except OSError as err:
150-
raise EasyBuildError("Failed to change to %s: %s", pypath, err)
151-
152-
cmd = "python setup.py install --prefix=%s" % self.installdir
153-
run_shell_cmd(cmd)
154-
155-
try:
156-
os.chdir(pwd)
157-
except OSError as err:
158-
raise EasyBuildError("Failed to change back to %s: %s", pwd, err)
159-
160-
def sanity_check_step(self):
161-
"""Custom sanity check for NEURON."""
162-
shlib_ext = get_shared_lib_ext()
163-
binpath = os.path.join(self.hostcpu, 'bin')
164-
libpath = os.path.join(self.hostcpu, 'lib', 'lib%s.' + shlib_ext)
165-
# hoc_ed is not included in the sources of 7.4. However, it is included in the binary distribution.
166-
# Nevertheless, the binary has a date old enough (June 2014, instead of November 2015 like all the
167-
# others) to be considered a mistake in the distribution
168-
binaries = ["neurondemo", "nrngui", "nrniv", "nrnivmodl", "nocmodl", "modlunit", "nrnmech_makefile",
169-
"mkthreadsafe"]
170-
libs = ["nrniv"]
171-
sanity_check_dirs = ['share/nrn']
172-
173-
if LooseVersion(self.version) < LooseVersion('7.4'):
174-
binaries += ["hoc_ed"]
175-
176-
if LooseVersion(self.version) < LooseVersion('7.8.1'):
177-
binaries += ["bbswork.sh", "hel2mos1.sh", "ivoc", "memacs", "mos2nrn", "mos2nrn2.sh", "oc"]
178-
binaries += ["nrn%s" % x for x in ["iv_makefile", "oc", "oc_makefile", "ocmodl"]]
179-
libs += ["ivoc", "ivos", "memacs", "meschach", "neuron_gnu", "nrnmpi", "nrnoc", "nrnpython",
180-
"oc", "ocxt", "scopmath", "sparse13", "sundials"]
181-
sanity_check_dirs += ['include/nrn']
182-
# list of included binaries changed with cmake. See
183-
# https://github.com/neuronsimulator/nrn/issues/899
79+
# specify path to InterViews if it is available as a dependency
80+
interviews_root = get_software_root('InterViews')
81+
if interviews_root:
82+
self.cfg.update('configopts', f"-DIV_DIR={interviews_root} -DNRN_ENABLE_INTERVIEWS=ON")
18483
else:
185-
binaries += ["nrnpyenv.sh", "set_nrnpyenv.sh", "sortspike"]
186-
libs += ["rxdmath"]
187-
sanity_check_dirs += ['include']
188-
if self.with_python:
189-
sanity_check_dirs += [os.path.join("lib", "python"),
190-
os.path.join("lib", "python%(pyshortver)s", "site-packages")]
191-
192-
# this is relevant for installations of Python packages for multiple Python versions (via multi_deps)
193-
# (we can not pass this via custom_paths, since then the %(pyshortver)s template value will not be resolved)
194-
# ensure that we only add to paths specified in the EasyConfig
195-
sanity_check_files = [os.path.join(binpath, x) for x in binaries] + [libpath % x for x in libs]
196-
self.cfg['sanity_check_paths'] = {
197-
'files': sanity_check_files,
198-
'dirs': sanity_check_dirs,
199-
}
84+
self.cfg.update('configopts', "-DNRN_ENABLE_INTERVIEWS=OFF")
85+
86+
# optionally enable support for Python as alternative interpreter
87+
if self.python_root:
88+
python_cfgopts = " ".join([
89+
"-DNRN_ENABLE_PYTHON=ON",
90+
f"-DPYTHON_EXECUTABLE={self.python_root}/bin/python",
91+
"-DNRN_ENABLE_MODULE_INSTALL=ON",
92+
f"-DNRN_MODULE_INSTALL_OPTIONS='--prefix={self.installdir}'",
93+
])
94+
self.cfg.update('configopts', python_cfgopts)
95+
else:
96+
self.cfg.update('configopts', "-DNRN_ENABLE_PYTHON=OFF")
20097

201-
super(EB_NEURON, self).sanity_check_step()
98+
# determine Python lib dir
99+
self.pylibdir = det_pylibdir()
202100

203-
try:
204-
fake_mod_data = self.load_fake_module()
205-
except EasyBuildError as err:
206-
self.log.debug("Loading fake module failed: %s" % err)
207-
208-
# test NEURON demo
209-
inp = '\n'.join([
210-
"demo(3) // load the pyramidal cell model.",
211-
"init() // initialise the model",
212-
"t // should be zero",
213-
"soma.v // will print -65",
214-
"run() // run the simulation",
215-
"t // should be 5, indicating that 5ms were simulated",
216-
"soma.v // will print a value other than -65, indicating that the simulation was executed",
217-
"quit()",
218-
])
219-
res = run_shell_cmd("neurondemo", stdin=inp, fail_on_error=False)
220-
221-
validate_regexp = re.compile(r"^\s+-65\s*\n\s+5\s*\n\s+-68.134337", re.M)
222-
if res.exit_code or not validate_regexp.search(res.output):
223-
raise EasyBuildError("Validation of NEURON demo run failed.")
224-
else:
225-
self.log.info("Validation of NEURON demo OK!")
101+
# complete configuration with configure_method of parent
102+
CMakeMake.configure_step(self)
226103

104+
def test_step(self):
105+
"""Custom tests for NEURON."""
227106
if build_option('mpi_tests'):
228107
nproc = self.cfg['parallel']
229108
try:
230-
cwd = os.getcwd()
231-
os.chdir(os.path.join(self.cfg['start_dir'], 'src', 'parallel'))
232-
233-
cmd = self.toolchain.mpi_cmd_for("nrniv -mpi test0.hoc", nproc)
234-
res = run_shell_cmd(cmd, fail_on_error=False)
235-
236-
os.chdir(cwd)
109+
hoc_file = os.path.join(self.cfg['start_dir'], 'src', 'parallel', 'test0.hoc')
110+
cmd = self.toolchain.mpi_cmd_for(f"bin/nrniv -mpi {hoc_file}", nproc)
111+
res = run_shell_cmd(cmd)
237112
except OSError as err:
238113
raise EasyBuildError("Failed to run parallel hello world: %s", err)
239114

240115
valid = True
241116
for i in range(0, nproc):
242-
validate_regexp = re.compile("I am %d of %d" % (i, nproc))
117+
validate_regexp = re.compile(f"I am {i:d} of {nproc:d}")
243118
if not validate_regexp.search(res.output):
244119
valid = False
245120
break
246121
if res.exit_code or not valid:
247122
raise EasyBuildError("Validation of parallel hello world run failed.")
248-
else:
249-
self.log.info("Parallel hello world OK!")
123+
self.log.info("Parallel hello world OK!")
250124
else:
251125
self.log.info("Skipping MPI testing of NEURON since MPI testing is disabled")
252126

253-
if self.with_python:
254-
cmd = "python -c 'import neuron; neuron.test()'"
255-
run_shell_cmd(cmd)
127+
def sanity_check_step(self):
128+
"""Custom sanity check for NEURON."""
129+
shlib_ext = get_shared_lib_ext()
130+
131+
binaries = ["mkthreadsafe", "modlunit", "neurondemo", "nocmodl", "nrngui", "nrniv", "nrnivmodl",
132+
"nrnmech_makefile", "nrnpyenv.sh", "set_nrnpyenv.sh", "sortspike"]
133+
libs = ["nrniv", "rxdmath"]
134+
sanity_check_dirs = ['include', 'share/nrn']
256135

257-
# cleanup
258-
self.clean_up_fake_module(fake_mod_data)
136+
if self.python_root:
137+
sanity_check_dirs += [os.path.join("lib", "python")]
138+
if LooseVersion(self.version) < LooseVersion('8'):
139+
sanity_check_dirs += [os.path.join("lib", "python%(pyshortver)s", "site-packages")]
259140

260-
def make_module_req_guess(self):
261-
"""Custom guesses for environment variables (PATH, ...) for NEURON."""
141+
# this is relevant for installations of Python packages for multiple Python versions (via multi_deps)
142+
# (we can not pass this via custom_paths, since then the %(pyshortver)s template value will not be resolved)
143+
# ensure that we only add to paths specified in the EasyConfig
144+
sanity_check_files = [os.path.join('bin', x) for x in binaries]
145+
sanity_check_files += [f'lib/lib{soname}.{shlib_ext}' for soname in libs]
262146

263-
guesses = super(EB_NEURON, self).make_module_req_guess()
147+
custom_paths = {
148+
'files': sanity_check_files,
149+
'dirs': sanity_check_dirs,
150+
}
264151

265-
guesses.update({
266-
'PATH': [os.path.join(self.hostcpu, 'bin')],
267-
})
152+
# run NEURON demo
153+
demo_dir = tempfile.mkdtemp()
154+
demo_inp_file = os.path.join(demo_dir, 'neurondemo.inp')
155+
demo_inp_cmds = '\n'.join([
156+
"demo(3) // load the pyramidal cell model.",
157+
"init() // initialise the model",
158+
"t // should be zero",
159+
"soma.v // will print -65",
160+
"run() // run the simulation",
161+
"t // should be 5, indicating that 5ms were simulated",
162+
"soma.v // will print a value other than -65, indicating that the simulation was executed",
163+
"quit()",
164+
])
165+
write_file(demo_inp_file, demo_inp_cmds)
166+
167+
demo_sanity_cmd = f"neurondemo < {demo_inp_file} 2>&1"
168+
demo_regexp_version = f'NEURON -- VERSION {self.version}'
169+
demo_regexp_soma = '68.134337'
170+
171+
custom_commands = [
172+
f"{demo_sanity_cmd} | grep -c '{demo_regexp_version}'",
173+
f"{demo_sanity_cmd} | grep -c '{demo_regexp_soma}'",
174+
]
175+
176+
if self.python_root:
177+
custom_commands.append("python -c 'import neuron; neuron.test()'")
178+
179+
super(EB_NEURON, self).sanity_check_step(custom_paths=custom_paths, custom_commands=custom_commands)
180+
181+
def make_module_step(self, *args, **kwargs):
182+
"""
183+
Custom paths of NEURON module load environment
184+
"""
185+
if self.python_root:
186+
# location of neuron python package
187+
if LooseVersion(self.version) < LooseVersion('8'):
188+
self.module_load_environment.PYTHONPATH = [os.path.join("lib", "python*", "site-packages")]
189+
else:
190+
self.module_load_environment.PYTHONPATH = [os.path.join('lib', 'python')]
268191

269-
return guesses
192+
return super(EB_NEURON, self).make_module_step(*args, **kwargs)
270193

271194
def make_module_extra(self):
272195
"""Define extra module entries required."""
@@ -279,8 +202,8 @@ def make_module_extra(self):
279202
val = os.getenv(var)
280203
if val:
281204
txt += self.module_generator.set_environment(var, val)
282-
self.log.debug("%s set to %s, adding it to module" % (var, val))
205+
self.log.debug(f"{var} set to {val}, adding it to module")
283206
else:
284-
self.log.debug("%s not set: %s" % (var, os.environ.get(var, None)))
207+
self.log.debug(f"{var} not set: {os.environ.get(var, None)}")
285208

286209
return txt

0 commit comments

Comments
 (0)