Skip to content
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

Workflow crashing with file not found due to strange nipype.pipeline.engine.utils.modify_path behavior #2944

Closed
stilley2 opened this issue Jun 13, 2019 · 24 comments · Fixed by #2971
Milestone

Comments

@stilley2
Copy link
Contributor

Summary

When getting the node results, the modify_path function will mistakenly identify some outputs as files, causing the run to fail.

Actual behavior

Workflow crashes with a file not found error due to output being mistakenly identified as a file. This occurs when the output is a string (or bytes? untested), and a file with the same name exists in the current working directory.

Expected behavior

modify_path does not identify the object as a file.

How to replicate the behavior

See below

Script/Workflow details

The following script will fail if there is a file named "22" in the directory from which it is called.

from nipype.pipeline import engine as pe
from nipype.interfaces.base import BaseInterfaceInputSpec, TraitedSpec, BaseInterface, traits


class Concat2InputSpec(BaseInterfaceInputSpec):
    x = traits.String()


class Concat2OutputSpec(TraitedSpec):
    y = traits.String()


class Concat2(BaseInterface):
    input_spec = Concat2InputSpec
    output_spec = Concat2OutputSpec

    def _run_interface(self, runtime):
        self._outval = self.inputs.x + '2'
        return runtime

    def _list_outputs(self):
        outputs = self._outputs().get()
        outputs['y'] = self._outval
        return outputs


if __name__ == '__main__':
    node = pe.Node(Concat2(x=2), 'double')
    node.run()

Please put URL to code or code here (if not too long).

Platform details:

{'commit_hash': '7fbc7cbac',
 'commit_source': 'repository',
 'networkx_version': '2.3',
 'nibabel_version': '2.4.1',
 'nipype_version': '1.2.1-dev+g7fbc7cbac',
 'numpy_version': '1.16.4',
 'pkg_path': '/home/stilley2/dev/nipype_debug/nipype/nipype',
 'scipy_version': '1.3.0',
 'sys_executable': '/home/stilley2/dev/nipype_debug/pyenv/bin/python',
 'sys_platform': 'linux',
 'sys_version': '3.7.3 (default, May 11 2019, 00:38:04) \n'
                '[GCC 9.1.1 20190503 (Red Hat 9.1.1-1)]',
 'traits_version': '5.1.1'}

Execution environment

  • My python environment outside container
@effigies
Copy link
Member

For added context, running the provided script produces the following error:

190618-19:12:45,52 nipype.workflow INFO:
	 [Node] Setting-up "double" in "/private/var/folders/yb/10x6vxlx40s1dhv5vc163m680000gp/T/tmpwhu6niza/double".
190618-19:12:45,57 nipype.workflow INFO:
	 [Node] Running "double" ("__main__.Concat2")
Traceback (most recent call last):
  File "test.py", line 29, in <module>
    node.run()
  File "/anaconda3/lib/python3.6/site-packages/nipype/pipeline/engine/nodes.py", line 487, in run
    self, report_type='postexec', is_mapnode=isinstance(self, MapNode))
  File "/anaconda3/lib/python3.6/site-packages/nipype/pipeline/engine/utils.py", line 149, in write_report
    result = node.result  # Locally cache result
  File "/anaconda3/lib/python3.6/site-packages/nipype/pipeline/engine/nodes.py", line 197, in result
    return _load_resultfile(self.output_dir(), self.name)[0]
  File "/anaconda3/lib/python3.6/site-packages/nipype/pipeline/engine/utils.py", line 359, in load_resultfile
    basedir=path).items()):
  File "/anaconda3/lib/python3.6/site-packages/nipype/pipeline/engine/utils.py", line 478, in modify_paths
    val, relative=relative, basedir=basedir)
  File "/anaconda3/lib/python3.6/site-packages/nipype/pipeline/engine/utils.py", line 498, in modify_paths
    raise IOError('File %s not found' % out)
OSError: File /private/var/folders/yb/10x6vxlx40s1dhv5vc163m680000gp/T/tmpwhu6niza/double/22 not found

Further, this was introduced in #2325, due to the saving/loading of the resultfile. I suspect the fact that we're losing trait information is the problem. Still trying to work through whether the correct response is the fix proposed in #2945.

@effigies
Copy link
Member

cc @oesteban you might see the way to go here better, since #2325 was your refactor.

@oesteban
Copy link
Contributor

I'm not sure this is a problem of nipype - although I acknowledge there could be potential file name collisions conducive to this problem and we may want to keep track of existing files in the results file.

I'll wait on the original fMRIPrep issue to understand the problem.

@stilley2
Copy link
Contributor Author

I'm pretty sure this is a nipype issue and not an fmriprep issue. The above example does not rely on fmriprep, and definitely results in undesirable behavior. I agree with @effigies that the main issue is loss of type information, and trying to recover that information, so #2945 is probably not an ideal solution.

@oesteban
Copy link
Contributor

oesteban commented Jun 19, 2019

I can only imagine two situations where this is an issue:

  • If an interface has both an string output and a file output, and both values coincide (which is a pretty particular condition, and I'm not sure there is an interface actually doing this).
  • If a previous run of a workflow is manipulated adding such a file (which is artificial and this is why I'm not convinced this is a nipype issue).

For that reason, I think we should first replicate and identify the error condition in fMRIPrep and then decide whether it is worth tracking file traits.

WDYT @satra?

@stilley2
Copy link
Contributor Author

The example script I posted doesn't satisfy either criteria, but still causes an issue. To cause this issue all that is required is:

  1. an interface has a string output
  2. A file with a name equal to that string exists in the directory from which the python script was called (i.e., not the workflow's working directory).

@oesteban
Copy link
Contributor

Gotcha, I hadn't realized of number 2. Well, I guess then #2945 should address this issue.

@effigies
Copy link
Member

One question I have is why we're modifying paths in the resultfile.

@oesteban
Copy link
Contributor

It is a buggy filter to convert paths to relative (before storing) and then back to absolute (after loading).

@effigies
Copy link
Member

Sure, but why?

@oesteban
Copy link
Contributor

I would imagine (and that was there before my refactor) that it is to make the result file work even if you move the work directory somewhere else. For a single run (no reuse), I agree it doesn't make much sense. @satra may have a better understanding of this.

@satra
Copy link
Member

satra commented Jun 23, 2019

@oesteban and @effigies - this should be controlled through a config option:

https://github.com/nipy/nipype/blob/master/nipype/utils/config.py#L65

but it sounds like the option is not being implemented properly. the default is false, which means it should not use relative paths unless forced.

@effigies
Copy link
Member

I think that's being handled inside modify_paths:

if relative:
if config.getboolean('execution', 'use_relative_paths'):
out = relpath(object, start=basedir)
else:
out = object
else:
out = os.path.abspath(os.path.join(basedir, object))

The problem is that we're identifying paths as strings that exist on the filesystem relative to some directory (either CWD or node-WD), which can sometimes be true for String traits, as well. So avoiding relative paths won't save us when the string output is a valid path; forcing to absolute will still modify the string.

@satra
Copy link
Member

satra commented Jun 26, 2019

@effigies - in other situations we check the traits metadata to determine if this is a string vs a file. (hash_files=False). a good improvement would be to separate out a Path object that is a string vs a Path object that is a file.

@effigies
Copy link
Member

in other situations we check the traits metadata to determine if this is a string vs a file. (hash_files=False).

Yes, but this is operating on a dict, without trait information. I'm working on a strategy to pass in traits as type hints.

a good improvement would be to separate out a Path object that is a string vs a Path object that is a file.

Could you clarify? Are you suggesting to use pathlib.Path as the object that is pickled?

@satra
Copy link
Member

satra commented Jul 1, 2019

@effigies - i simply meant altering the traits_extension File class to either return a string or a pathlib object depending on the traits metadata.

@effigies
Copy link
Member

@effigies - i simply meant altering the traits_extension File class to either return a string or a pathlib object depending on the traits metadata.

I've looked at the traits stuff, and have decided I don't really know how to start. Just noting this, in case somebody else has time to play with traits.

@oesteban
Copy link
Contributor

Having a look right this minute

@oesteban
Copy link
Contributor

@satra the new trait would return pathlike if exists is True or this new metadata is True, correct?

@satra
Copy link
Member

satra commented Jul 16, 2019

@oesteban - the metadata already exists, so the validate function in the File trait would return a Path object if hash_files is not False

@satra
Copy link
Member

satra commented Jul 16, 2019

i would start with updating the File/Directory trait in traits_extensiion instead of creating a new trait.

@oesteban
Copy link
Contributor

Sorry, I was very unclear. Please check the draft PR above - I was actually modifying File/Directory.

@oesteban
Copy link
Contributor

@satra, the metadata name is nohash, correct?

@satra
Copy link
Member

satra commented Jul 16, 2019

the metadata name is hash_files, nohash is when a particular traits item should not be included in the hash computation. while hash_files either hashes the string (when False) or the content/timestamp (when True or missing)

oesteban added a commit to oesteban/nipype that referenced this issue Jul 18, 2019
Building a solution to nipy#2944, starting from a refactor of
``aggregate_outputs`` to be robuster and perform the referrencing when
requested via the new arguiment ``rebase_cwd``.
oesteban added a commit to oesteban/nipype that referenced this issue Jul 19, 2019
Two new methods ``resolve_path_traits`` and ``rebase_path_traits`` are
being included.

They take trait instances from a spec (selected via
``spec.trait('traitname')``, the value and a base path.

These two functions will be usefull to progress towards nipy#2944.
oesteban added a commit to oesteban/nipype that referenced this issue Jul 19, 2019
oesteban added a commit to oesteban/nipype that referenced this issue Jul 31, 2019
Two new methods ``resolve_path_traits`` and ``rebase_path_traits`` are
being included.

They take trait instances from a spec (selected via
``spec.trait('traitname')``, the value and a base path.

These two functions will be usefull to progress towards nipy#2944.
oesteban added a commit to oesteban/nipype that referenced this issue Jul 31, 2019
Two new methods ``resolve_path_traits`` and ``rebase_path_traits`` are
being included.

They take trait instances from a spec (selected via
``spec.trait('traitname')``, the value and a base path.

These two functions will be usefull to progress towards nipy#2944.
oesteban added a commit to oesteban/nipype that referenced this issue Jul 31, 2019
Two new methods ``resolve_path_traits`` and ``rebase_path_traits`` are
being included.

They take trait instances from a spec (selected via
``spec.trait('traitname')``, the value and a base path.

These two functions will be usefull to progress towards nipy#2944.
oesteban added a commit to oesteban/nipype that referenced this issue Jul 31, 2019
oesteban added a commit to oesteban/nipype that referenced this issue Aug 1, 2019
oesteban added a commit to oesteban/nipype that referenced this issue Aug 1, 2019
Two new methods ``resolve_path_traits`` and ``rebase_path_traits`` are
being included.

They take trait instances from a spec (selected via
``spec.trait('traitname')``, the value and a base path.

These two functions will be usefull to progress towards nipy#2944.
oesteban added a commit to oesteban/nipype that referenced this issue Aug 1, 2019
oesteban added a commit to oesteban/nipype that referenced this issue Aug 1, 2019
Once we figure out the problem of ``OutputMultiObject``, we could go
ahead and set fix nipy#2944, fix nipreps/fmriprep#1674, close nipy#2945.
oesteban added a commit to oesteban/nipype that referenced this issue Aug 1, 2019
It seems that nipy#2944 has uncovered a rats-nest hidden in the engine. In
resolving that issue, I found out that a great deal of boilerplate was
set in place when loading/saving results to deal with
``OutputMulti{Object,Path}`` traits. The reason being that these traits
flatten single-element-list values.

This PR fixes the pickling behavior of traited specs containing these
types of traits.

Additionally, this PR also avoids the ``modify_paths`` function that was
causing problems originally in nipy#2944. Therefore, this PR effectively
make results files static, meaning: caching if the ``base_dir`` of the
workflow is changed will not work anymore.

I plan to re-insert this feature (results file mobility) with nipy#2971.
This PR is just to split that one in more digestible bits.

All the boilerplate mentioned above has been cleaned up.
oesteban added a commit to oesteban/nipype that referenced this issue Aug 1, 2019
oesteban added a commit to oesteban/nipype that referenced this issue Aug 1, 2019
Once we figure out the problem of ``OutputMultiObject``, we could go
ahead and set fix nipy#2944, fix nipreps/fmriprep#1674, close nipy#2945.
oesteban added a commit to oesteban/nipype that referenced this issue Aug 1, 2019
oesteban added a commit to oesteban/nipype that referenced this issue Aug 2, 2019
oesteban added a commit to oesteban/nipype that referenced this issue Aug 2, 2019
Once we figure out the problem of ``OutputMultiObject``, we could go
ahead and set fix nipy#2944, fix nipreps/fmriprep#1674, close nipy#2945.
oesteban added a commit to oesteban/nipype that referenced this issue Aug 2, 2019
oesteban added a commit to oesteban/nipype that referenced this issue Aug 6, 2019
oesteban added a commit to oesteban/nipype that referenced this issue Aug 6, 2019
Once we figure out the problem of ``OutputMultiObject``, we could go
ahead and set fix nipy#2944, fix nipreps/fmriprep#1674, close nipy#2945.
oesteban added a commit to oesteban/nipype that referenced this issue Aug 6, 2019
@effigies effigies added this to the 1.2.1 milestone Aug 16, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment