-
Notifications
You must be signed in to change notification settings - Fork 532
Adding niftyreg #1913
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding niftyreg #1913
Conversation
as well as one workflow for groupwise registration
Raising Exception if Niftyreg not found
Hi, The travis-ci tests failed because we are checking in the base.py for the niftyreg interface that niftyreg is installed followed by checking that the version is bigger than a minimun version. We are raising an exception if niftyreg not found, the version is smaller than 1.5.10 (min) or version smaller different than a version required. In the test, the docstring test failed because the system running Travis CI build don't have niftyreg installed so when we create the node in the example, it's failing. Should I just skip testing the docstring? How should we implement this option to put a version control? Kind Regards, |
For travis CI - we try to keep these tests short, so yes, please decorate the test functions with the For circle - we try for these to be very comprehensive, so we would add niftyreg in our Docker image (the file in Once niftyreg is installed, the tests should not be skipped since it should be available. Let me know if you need more help with this |
Yes, all our tests have the decorator for checking if the executable is installed. The problem came from the docstring in the interfaces because we added some test to check the version in the base interface for NiftyReg. I added '#doctest: +SKIP' for the docstring. Cheers, |
Sorry, I probably confused you by So, the The doctests in of nipype interfaces should work always. Particularly, you should remove the skip directive on the The only one place where you would like to add |
Yeah, we have all the @skipif decorator for the regression tests and it makes sense but I might be confused on the # doctest: +SKIP one. I will try to sort it out. Thank you @oesteban for the help. Cheers, |
@oesteban , how do you advice to do with the cmdline() in the docstring in the specific case of the output are generated with the fullpath. For example: Expected: Should I avoid doing the cmdline in the docstring? Kind Regards, |
Codecov Report
@@ Coverage Diff @@
## master #1913 +/- ##
==========================================
- Coverage 72.55% 72.13% -0.42%
==========================================
Files 1064 1080 +16
Lines 54170 55049 +879
Branches 7810 7904 +94
==========================================
+ Hits 39303 39712 +409
- Misses 13649 14092 +443
- Partials 1218 1245 +27
Continue to review full report at Codecov.
|
Hi @byvernault, look at this example: https://github.com/nipy/nipype/blob/master/nipype/interfaces/fsl/epi.py#L74 So:
Particularly, the The |
return None | ||
|
||
@property | ||
def cmdline(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the idea behind this? I have the impression that this should be just an option of the input spec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reg_average executable as an alternate options to use a command file to describe the command line that the user want to execute. The reason behind this argument is that some cases the command is too long for the terminal to execute it.
Marc designed this argument and we decided to use it by default without the user notifying. Does it make sense?
It's the same thing that using the options but we are not limited by the length of the command line.
I will work on adding the doctests ELLIPSIS.
Thanks,
Cheers,
Ben
Hmm. I have a : ValueError: line 15 of the doctest for ... has an invalid option: '+ALLOW_UNICODE' When using the ALLOW_UNICODE and running the nosetests. Am I doing something wrong? Kind Regards, Ben |
Hi Ben, sorry I missed this. It seems you are using it right now. Here, and in the other PRs there are little things to fix. If you go to https://circleci.com/gh/nipy/nipype/2098?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link and see the detailed description of the crashes, you'll see the following:
|
Hi @oesteban, Thanks for the help. I think we are getting closer to get this branch ready. I don't see any test failing for the interfaces. Do you have an idea what makes travis-ci to fail? It says it's the third job for python 2.7. Kind Regards, |
Hi @byvernault, you hit an open issue we have in travis (#1788). As for CircleCI, I think you caught it in a hiccups, I have restarted that job. Yes, this is starting to look very good to merge :) |
Great. I though I had something that needed to change again :). Thanks again for all the help. Kind Regards, |
Hi @oesteban , Do you have an idea of timeline when you are confident about adding our three packages to nipype? Kind Regards, |
Sorry @byvernault, I'm on vacation right now. I'll try to allocate a few hours to review your PRs next week. Maybe @satra or @mgxd can take a look on this, so when I look back at it we have some more eyes spotting those things to improve. Again, my apologies. |
Thanks for looking into this. We might need to edit one thing on the niftyfit package. I will try to do so today. Cheers, Kind Regards |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'll post this review, while i look over the workflows. but a lot of the boilerplate operations in the interfaces can be reduced.
nipype/interfaces/niftyreg/base.py
Outdated
return True | ||
|
||
|
||
class PositiveInt(traits.BaseInt): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
while this works, you could have used: a = traits.Range(low=0)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@satra, thx. I just changed all of them to use Range traits instead of rewritting something that does the same.
Cheers,
nipype/interfaces/niftyreg/base.py
Outdated
final_bn = ''.join((final_bn, suffix)) | ||
return os.path.abspath(os.path.join(out_dir, final_bn + final_ext)) | ||
|
||
def _gen_filename(self, name): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this function, the previous one, and the next one can be removed and the traits extended with name_source
and name_template
. i'll provide an example when i come across the relevant interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @satra , it's used in the interfaces for RegTools for example to avoid repeating some of the code. Should I remove them and put them in each interfaces?
In those interfaces, we name the output depending on the input image and the type specified. Can we use two values with name_source and name_template?
Cheers,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it depends on what you would like to do. there are two things that may help:
-
keep_extension (traits metadata): allows carrying the extension of name_source through to output.
-
_overload_extension (class function): here is an example of use in the afni base class. https://github.com/nipy/nipype/blob/master/nipype/interfaces/afni/base.py#L218
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@satra, on this part of the interfaces, I left it as it is since for some interfaces, we can not use the name_source since it doesn't allow naming with several variables if I understood the doc well.
For example, Reg_Measure, set the outfile with the basename flo_file where we add the measure_type as suffix and then the extension.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah i see. while this can indeed be done in overload_extension
, we will likely have a cleaner mechanism in the next release to support multiple variables.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great to know.
Do you want us to use the overload_extension in all those cases or leave it like that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i would prefer overload extension over gen_filename at this point. when we implement the multiple variables option, it will simply allow us to improve the namesource and template.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, working on it.
nipype/interfaces/niftyreg/reg.py
Outdated
verbosity_off_flag = traits.Bool(argstr='-voff', | ||
desc='Turn off verbose output') | ||
# Set the number of omp thread to use | ||
omp_core_val = traits.Int(desc='Number of openmp thread to use', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how does this work if OMP_NUM_THREADS
is set? will this override it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. If the omp_core_val is set, it is used, otherwise the environment variable. If none are set, the maximum number of thread is used, as defined by omp_get_num_procs() in C.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks @mmodat - in nipype there are a few interfaces that use OMP (AFNI, ITK for example). in addition to setting a parameter such as this, you may want to override OMP_NUM_THREADS as well, such as here: https://github.com/nipy/nipype/blob/master/nipype/interfaces/afni/base.py#L190
hopefully over the next iteration, we will achieve a coherent numthreads setting independent of whether this means OMP, MKL, python threads, cores on a cluster, etc.,.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @satra for the suggestion. However, if you agree, we rather leave it has it is here at it gives us more flexibility within our workflows. We included code similar to the one in AFNI but at the level of the workflow rather than at the level of the executables.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mmodat - you can leave that input as is, but to interact with other pieces of nipype, you should set the interfaces self.num_threads to the same value as the input always. also the default value of that input should be 1. i.e. every interface should by default run with one thread and it's up to the user/workflow designer to alter it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@satra Sounds good, we will alter it then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do you think about the changes done?
def _run_interface(self, runtime):
# Update num threads estimate from OMP_NUM_THREADS env var
# Default to 1 if not set
if not isdefined(self.inputs.environ['OMP_NUM_THREADS']):
self.inputs.environ['OMP_NUM_THREADS'] = self.num_threads
return super(NiftyRegCommand, self)._run_interface(runtime)
We set the OMP_NUM_THREADS to the number of threads if not set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@byvernault - they look good. (i suspect that you haven't hit the other files yet.
and this _run_interface
looks good. you may want to consider moving omp_core_val
to a base class inputspec and creating this _run_interface
function in the base class. seems like all your interfaces support it.
i would also add the following at the beginning of run interface.
if isdefined(self.inputs.omp_core_val):
self.num_threads = self.inputs.omp_core_val
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about adding this as well but actually in the code of all the executables, it reads this value if set. It means that even if OMP_NUM_THREADS is set, it won't be used by the executables since the options is set by the user.
I was considering about doing the inputspec for all the interfaces with the omp_core_val but I haven't done it yet.
nipype/interfaces/niftyreg/reg.py
Outdated
argstr='-omp %i') | ||
|
||
# Affine output transformation matrix file | ||
aff_file = File(genfile=True, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aff_file = File(name_source=['flo_file'], name_template='%s_aff.txt',
desc='The output affine matrix file',
argstr='-aff %s')
similarly for res_file below.
nipype/interfaces/niftyreg/reg.py
Outdated
input_spec = RegAladinInputSpec | ||
output_spec = RegAladinOutputSpec | ||
|
||
def _gen_filename(self, name): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as a result of name source and template this function can be removed.
nipype/interfaces/niftyreg/reg.py
Outdated
return None | ||
|
||
def _list_outputs(self): | ||
outputs = self.output_spec().get() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this function can be reduced to:
def _list_outputs(self):
outputs = super(RegAladin, self)._list_outputs()
# Make a list of the linear transformation file and the input image
aff = os.path.abspath(outputs['aff_file'])
flo = os.path.abspath(self.inputs.flo_file)
outputs['avg_output'] = '%s %s' % (aff, flo)
return outputs
it seems that the avg_output
here is combining things to make it easy for some other interface. this breaks the dataflow paradigm as the next interface will not be able to use such a string to determine the hash of the input properly. it would be better to do such a merge separately in a workflow rather than as an output.
Thx @satra, I will work on doing the changes you requested. Kind Regards, |
output_spec = RegJacobianOutputSpec | ||
|
||
def _gen_filename(self, name): | ||
if name == 'out_file': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fix here as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How should I proceed to name the file with name_source/name_template in the case of:
the file will be named: '{0}_{1}.ext'.format(self.inputs.trans_file, self.inputs.type)?
I think I did all the changes. Let me know if you see something else that you want me to edit. Thank you for your help. Do you have a rough idea when you will be able to merge the requests to nipype? Kind Regards. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have suggested some changes, but this contribution is great already. Thanks a lot!
nipype/interfaces/niftyreg/base.py
Outdated
|
||
def get_custom_path(command): | ||
try: | ||
specific_dir = os.environ['NIFTYREGDIR'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about:
def get_custom_path(command):
return os.path.join(os.getenv('NIFTYREGDIR', ''), command)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I should have changed this before to what you have. Thx. I will edit it.
nipype/interfaces/niftyreg/base.py
Outdated
|
||
|
||
def no_niftyreg(cmd='reg_f3d'): | ||
if True in [os.path.isfile(os.path.join(path, cmd)) and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True in [...]
should be replaced by any
:
import shutil
def no_niftyreg(cmd='reg_f3d'):
try:
return shutil.which(cmd) is None
except AttributeError: # Python < 3.3
return not any([os.path.isfile(os.path.join(path, cmd)) and os.access(os.path.join(path, cmd), os.X_OK) for path in os.environ["PATH"].split(os.pathsep)]
@satra, I think we should provide an all-python compatible which
function in filemanip
. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, it makes sense.
class NiftyRegCommandInputSpec(CommandLineInputSpec): | ||
"""Input Spec for niftyreg interfaces.""" | ||
# Set the number of omp thread to use | ||
omp_core_val = traits.Int(desc='Number of openmp thread to use', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we have a default value here? What is the default in niftyreg?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We didn't set a default value because if this is not set by the user, we are using the OMP_NUM_THREADS value.
def version(self): | ||
return self.get_version() | ||
|
||
def exists(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def exists(self):
return not self.get_version() is None
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry to butt in, but not x is None
should be x is not None
. Just a style issue, but a lot of style checkers will yell at you.
Speaking of which, have you run all of this through a style checker, such as flake8
? Nipype has somewhat intermittent style compliance, but for new contributions it's good to start off on the right foot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, no problem.
I am using flake8 but it does not complain with this one. I will edit it.
Cheers,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry about that
def _run_interface(self, runtime): | ||
# Update num threads estimate from OMP_NUM_THREADS env var | ||
# Default to 1 if not set | ||
if not isdefined(self.inputs.environ['OMP_NUM_THREADS']): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can remove this if you made:
omp_core_val = traits.Int(1, usedefault=True, desc='Number of openmp thread to use',
argstr='-omp %i')
Up there in the input spec definition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For omp_core_val, if it's fine, we would like to leave it like that without having a default value and set it to one by the env variable.
On our hand, we have other scripts where when we run the workflow, we can specify the number of cores for qsub and we set the OMP_NUM_THREADS. If we set by default the value of omp_core_val, then it won't read the value set by OMP_NUM_THREADS.
If really needed, we can set it to the value of OMP_NUM_THREADS if set or 1.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the resource multiproc plugin uses the numthreads variable to determine whether it will schedule a process on a multicore machine. by setting this variable when omp_core_val or OMP_NUM_THREADS is set, you make the process more efficient by aligning requests with allocation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@byvernault - if you can take care of this, and we can make sure all the tests pass this should be ready for merge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. How do you want me to proceed?
Do you want me to set this in the _format_arg ?
def _format_arg(self, name, spec, value):
if name == 'omp_core_val':
self.numthreads = value
return super(NiftyRegCommand, self)._format_arg(name, spec, value)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that would be a simple place to do that.
-res im2_res.nii.gz -rmask mask.nii' | ||
|
||
""" | ||
_cmd = get_custom_path('reg_f3d') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@satra: this is interesting, maybe we should provide a more reliable way of finding binaries. There is the which
util we could add to filemanip
, but also, this is very interesting - to allow users to build absolute paths from environment variables. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@oesteban - i thought that this was an interesting approach and one that could be extended to support containers as well. let's merge this PR in before this current release (tomorrow) and then think about the few additional pieces (namesource and this custom path updates) for the next release.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@satra I just recalled we have nipype.interfaces.base._exists_in_path
. We could place this one in filemanip
(where it should probably live), and make use of shutil.which
for python > 3.3.
WDYT?
all other tests pass and the mapnode crash is preventing on py27 test. |
Dear nipype team,
We are ready to add niftyreg to nipype. I copied all the interfaces we generated for this package as well as the tests. I also copied the workflow (groupwise.py). I did some changes to be compatible python 3 but not sure if everything should be fine for the workflow. If you see anything strange, let me know.
I tested it with python 2 and python 3 for the interfaces. It's working on my computer.
Let me know if you see anything not compatible.
Kind Regards,
Ben