Skip to content

[MAINT] Cleaning / simplify Node #2325

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

Merged
merged 38 commits into from
Jan 8, 2018
Merged

Conversation

oesteban
Copy link
Contributor

@oesteban oesteban commented Dec 1, 2017

This PR uses some code I had under the hood before 0.14.0 that simplifies Node. These are the changes:

  • Move nipype.pipeline.engine.utils.make_output_dir to nipype.utils.filemanip.makedirs and change the function signature to be consistent with the os.makedirs of Python >=3.3. The behavior has been barely modified (if exists_ok=False falls back to the traditional os.makedirs; runs former nipype code otherwise).
  • Cache output directory with first call to output_dir().
  • Simplify hash_exists - should implement the same in a hopefully more reliable way (less branches)
  • Simplify Node.run - again, reducing code branches.
  • Simplify Node._run_interface, moving the chdirs to Node.run. Seems to fix [BUG] FileNotFoundError: [Errno 2] No such file or directory nipreps/fmriprep#868, however I haven't checked deep enough.
  • Pep8 fixes, tidy up imports, etc.

@oesteban oesteban changed the title [MAINT] Cleaning / simplify Node [MAINT/WIP] Cleaning / simplify Node Dec 2, 2017
result, _, _ = self._load_resultfile(cwd)
return result
# Cache first
if not self._result:
Copy link
Member

Choose a reason for hiding this comment

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

we don't want to do this i think. this will increase memory consumption. we want results to be loaded on the fly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I'll see how to improve the process

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, no self._result anymore.

@@ -189,12 +188,11 @@ def interface(self):

@property
def result(self):
if self._result:
return self._result
Copy link
Member

Choose a reason for hiding this comment

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

i think this was a legacy from before. we should check when self._result is actually not None.

for hf in hashfiles:
os.remove(hf)

if updatehash and len(hashfiles) == 1:
Copy link
Member

Choose a reason for hiding this comment

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

i don't think the second clause is necessary or the os.remove statement.

the intent of updatehash==True was to always update the hash and return true. not just when there was only one hashfile. it was intended to be used cautiously.

return path


def emptydirs(path):
Copy link
Member

Choose a reason for hiding this comment

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

docstring

@oesteban oesteban requested review from djarecka and mgxd December 5, 2017 07:42
@oesteban oesteban changed the title [MAINT/WIP] Cleaning / simplify Node [MAINT] Cleaning / simplify Node Dec 5, 2017
@oesteban
Copy link
Contributor Author

oesteban commented Dec 5, 2017

@djarecka, @mgxd another maintenance PR for you to review, sorry about that. I don't have more of these for now... if it is of any help. The idea of these two refactorings is making things ready for the code rodeo and to scrub some rust off.

Here I'm changing some of the functionality of Node, so this PR requires a deeper scrutiny from you.

@oesteban
Copy link
Contributor Author

oesteban commented Dec 7, 2017

if op.exists(outdir):
# Find previous hashfiles
hashfiles = glob(op.join(outdir, '_0x*.json'))
if len(hashfiles) > 1: # Remove hashfiles if more than one found
Copy link
Member

Choose a reason for hiding this comment

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

or (hashfiles and hashfiles[0] != hashfile)

this would ensure that two hashfiles are never left in the directory.

also we should check for any unfinished hashfiles in the directory and raise an exception. i.e. tell the user that you can update a hashfile in an unfinished hashfile situation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Regarding the second idea: are you sure we want to error? We don't want to just dismiss any viable hashfile and clear up the folder (if an unfinished hashfile was found)?

I'm pushing a commit for this, with error. Let me know if we just want to clear up the folder and return no hashfiles.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

BTW @satra, what do you think about passing the responsibilities of hashing/caching on to the interface?

The run() of the interface would have a force_run argument (so it can be done dynamically) and a updatehash to also support that option.

It'd be great to have the interfaces cache themselves.

@satra
Copy link
Member

satra commented Dec 7, 2017

@oesteban - passing hashing to interfaces is a bigger discussion of merging node and interface into a common baseclass. let's leave that out of this and talk about separately. @djarecka has finally gotten back to the engine revision. let's review the base class question after that's completed.

the dichotomy for caching has often been around whether we should be caching or users do it. i think making memcache be the default way interfaces run would be a good thing, but that's a drastic change, so we leave that for 2.0.

@oesteban
Copy link
Contributor Author

oesteban commented Dec 7, 2017

Oh, sure thing. I wasn't proposing that for this PR :)

Have you thought about rising an error or just go on when an unfinished hashfile is found in the output directory (when it shouldn't)?

except OSError:
# Changing back to cwd is probably not necessary
# but this makes sure there's somewhere to change to.
cwd = os.path.split(outdir)[0]
Copy link
Collaborator

Choose a reason for hiding this comment

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

you can unify and use op everywhere

@@ -304,143 +354,109 @@ def run(self, updatehash=False):
updatehash: boolean
Update the hash stored in the output directory
Copy link
Collaborator

Choose a reason for hiding this comment

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

I know you didn't change this, but I'm really missing longer explanation of updatehash or suggestions when users should use it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If @satra agrees, we can remove it. I think it is an ancient feature.

The idea being that, when you run with updatehash=True, even if the hash of some input changed, the interface will not be run again and reuse the old output as output.

For instance, you forgot to mark with nohash=True some of your inputs (say the number of threads), but you don't want to rerun interfaces that are already cached. Then you would change the number of threads and run with updatehash=True. When nipype finds some of your interfaces with this problem cached, they are not run again and the hash of this input gets updated.

I'm not sure the example is a use case but I understand the feature that way.

Copy link
Member

Choose a reason for hiding this comment

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

it's true that this is rarely used currently, but when people were using interactive workflows and moving around the working directory this became a very useful thing. examples are:

  1. you run a workflow and move the working directory from one filesystem to another. all the hashes will change
  2. you go into the working directory to debug something and messed things up. you can quickly rerun an update hash that updates your cache

Copy link
Collaborator

@djarecka djarecka Jan 4, 2018

Choose a reason for hiding this comment

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

Thanks for explanations! if we decide to leave it, can we extend the docstring?

if not force_run and str2bool(self.config['execution']['stop_on_first_rerun']):
raise Exception('Cannot rerun when "stop_on_first_rerun" is set to True')

# Hashfile while running, remove if exists already
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is probably more abut the comment placing: I understand that you don't want to remove unfinished hashfile here..?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll check.

@@ -1156,8 +1037,9 @@ def _make_nodes(self, cwd=None):
base_dir=op.join(cwd, 'mapflow'),
name=nodename)
node.plugin_args = self.plugin_args
node._interface.inputs.trait_set(
node.interface.inputs.trait_set(
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this change necessary? just want to understand

Copy link
Contributor Author

Choose a reason for hiding this comment

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

in general, it is preferable to access public members rather than private members (marked generally with underscore) from outside the object.

Copy link
Collaborator

Choose a reason for hiding this comment

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

ok, yes, it is for node, not for self...

# Values in common keys would differ quite often,
# so we need to join the messages together
for k in new_keys.intersection(old_keys):
same = False
Copy link
Collaborator

Choose a reason for hiding this comment

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

not needed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

you sure? how would you modify this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

it's too late in Europe to be sure about anything, but regardless what happens in this loop, same should be defined/changed either in try part or except part, so looked to me that it doesn't have to be defined before try/except

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added to #2320. Even though this PR is changing a lot already, I'd leave this catch for a new one.

Copy link
Member

Choose a reason for hiding this comment

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

@djarecka I agree that you can remove this one line. same is defined before being accessed in all branches here.

(This comment in response to #2387.)

@djarecka
Copy link
Collaborator

djarecka commented Jan 4, 2018

@oesteban - thanks for this PR! I've tried to review the changes again today and just had some small questions/comments.
After this is merged I'll check which points from #2320 are still relevant.

@@ -904,7 +1142,7 @@ def _standardize_iterables(node):
fields = set(node.inputs.copyable_trait_names())
# Flag indicating whether the iterables are in the alternate
# synchronize form and are not converted to a standard format.
synchronize = False
# synchronize = False # OE: commented out since it is not used
Copy link
Collaborator

Choose a reason for hiding this comment

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

is these comments are valid for node.synchronize instead of synchronize. would be good to clean it so it's clear

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't want to remove it since the comment above seemed relevant. But I agree that once we clarify the intent of this synchronize, we should clean up these comments.

Copy link
Collaborator

Choose a reason for hiding this comment

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

that was my guess that the these comments are relevant also for node.synchronize but wasn't sure if entirely.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added to #2320

Copy link
Member

Choose a reason for hiding this comment

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

This line was added in 2a6bb8f, and was unused then. @djarecka I think it's safe to remove this line in #2387.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@effigies my concerns were more about the comments explaining synchronize. Does it still apply to node.synchronize after all changes? I don't fully understand the comments and never used it, so if you can confirm (or suggest changes) that would be great.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, sorry. It does read to me that there was intent to switch synchronize behavior with this flag, and that the two lines above only apply to that case. I think I would remove them, because the comment below seems to accurately describe the operation.

Copy link
Member

@mgxd mgxd left a comment

Choose a reason for hiding this comment

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

thanks for the clean up - at first glance functionality should remain the same - LGTM

return op.abspath(op.join(outputdir, self.name))

self._output_dir = op.abspath(op.join(outputdir, self.name))
return self._output_dir

def set_input(self, parameter, val):
""" Set interface input value"""
Copy link
Member

Choose a reason for hiding this comment

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

excess space

if result and result.outputs:
val = getattr(result.outputs, parameter)
return val
return getattr(self.result.outputs, parameter, None)

def help(self):
""" Print interface help"""
Copy link
Member

Choose a reason for hiding this comment

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

Space


def _copyfiles_to_wd(self, outdir, execute, linksonly=False):
def _copyfiles_to_wd(self, execute=True, linksonly=False):
""" copy files over and change the inputs"""
Copy link
Member

Choose a reason for hiding this comment

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

space

**deepcopy(self._interface.inputs.get()))
node.interface.resource_monitor = self._interface.resource_monitor
Copy link
Member

Choose a reason for hiding this comment

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

self.interface.resource_monior

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would opt for self._interface. I've made sure no self.interface remain.

from copy import deepcopy
from glob import glob
from distutils.version import LooseVersion
Copy link
Member

Choose a reason for hiding this comment

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

from ... import LooseVersion



def write_report(node, report_type=None, is_mapnode=False):
"""
Copy link
Member

Choose a reason for hiding this comment

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

Can just make this one line if not expanding

if level > len(colorset) - 2:
level = 3 # Loop back to blue
level = 3 # Loop back to blue
Copy link
Member

Choose a reason for hiding this comment

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

extra space?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, apparently there is some PEP somewhere advising to have 2 spaces before the inline comments

outdir = op.join(outdir, '_tempinput')
makedirs(outdir, exist_ok=True)

for info in self.interface._get_filecopy_info():
Copy link
Member

Choose a reason for hiding this comment

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

self._interface and self.interface should be equal - but as an added layer of security wouldn't it be better to keep the usage consistent?

@oesteban oesteban added this to the 0.14.1 milestone Jan 7, 2018
@oesteban
Copy link
Contributor Author

oesteban commented Jan 7, 2018

I'm seeing this from time to time:

File "/usr/local/miniconda/lib/python3.6/multiprocessing/forkserver.py", line 178, in main
_serve_one(s, listener, alive_r, handler)
File "/usr/local/miniconda/lib/python3.6/multiprocessing/forkserver.py", line 212, in _serve_one
code = spawn._main(child_r)
File "/usr/local/miniconda/lib/python3.6/multiprocessing/spawn.py", line 115, in _main
self = reduction.pickle.load(from_parent)
File "/usr/local/miniconda/lib/python3.6/site-packages/niworkflows/nipype/__init__.py", line 13, in <module>
from .utils.config import NipypeConfig
File "/usr/local/miniconda/lib/python3.6/site-packages/niworkflows/nipype/utils/__init__.py", line 4, in <module>
from .config import NUMPY_MMAP
File "/usr/local/miniconda/lib/python3.6/site-packages/niworkflows/nipype/utils/config.py", line 82, in <module>
""" % (homedir, os.getcwd())
FileNotFoundError: [Errno 2] No such file or directory

Will let you know when I find why this is happening.

@djarecka djarecka mentioned this pull request Jan 8, 2018
12 tasks
@oesteban
Copy link
Contributor Author

oesteban commented Jan 8, 2018

Alright, this is looking good. The FileNotFoundError seems to be under control with the new code, I'll keep an eye on these kinds of errors. Unless you want me to go one extra round of checking, this is ready to hit merge.

@mgxd mgxd merged commit 9be816e into nipy:master Jan 8, 2018
@oesteban oesteban deleted the ref/Node-cleanup branch January 9, 2018 06:00
oesteban added a commit to nipreps/niworkflows that referenced this pull request Jan 9, 2018
@mgxd
Copy link
Member

mgxd commented Jan 10, 2018

@oesteban - while using this PR

--- Logging error ---
Traceback (most recent call last):
  File "/opt/conda/envs/neuro/lib/python3.6/logging/__init__.py", line 992, in emit
    msg = self.format(record)
  File "/opt/conda/envs/neuro/lib/python3.6/logging/__init__.py", line 838, in format
    return fmt.format(record)
  File "/opt/conda/envs/neuro/lib/python3.6/logging/__init__.py", line 575, in format
    record.message = record.getMessage()
  File "/opt/conda/envs/neuro/lib/python3.6/logging/__init__.py", line 338, in getMessage
    msg = msg % self.args
TypeError: not enough arguments for format string
Call stack:
  File "/opt/conda/envs/neuro/bin/mindboggle123", line 315, in <module>
    mbFlow.run(plugin=args.plugin)
  File "/opt/conda/envs/neuro/lib/python3.6/site-packages/nipype/pipeline/engine/workflows.py", line 574, in run
    runner.run(execgraph, updatehash=updatehash, config=self.config)
  File "/opt/conda/envs/neuro/lib/python3.6/site-packages/nipype/pipeline/plugins/linear.py", line 43, in run
    node.run(updatehash=updatehash)
  File "/opt/conda/envs/neuro/lib/python3.6/site-packages/nipype/pipeline/engine/nodes.py", line 443, in run
    result = self._run_interface(execute=True)
  File "/opt/conda/envs/neuro/lib/python3.6/site-packages/nipype/pipeline/engine/nodes.py", line 520, in _run_interface
    return self._run_command(execute)
  File "/opt/conda/envs/neuro/lib/python3.6/site-packages/nipype/pipeline/engine/nodes.py", line 594, in _run_command
    self._interface.__class__.__name__)
Message: '[Node] Running "%s" ("%s.%s"), a CommandLine Interface with command:\nantsCorticalThickness.sh -a /datastore/sub-BANDA001/anat/sub-BANDA001_T1w.nii.gz -m /output/work/Mindboggle123/antsCorticalThickness/T_template0_BrainCerebellumProbabilityMask.nii.gz -e /opt/data/OASIS-30_Atropos_template/T_template0.nii.gz -d 3 -f /opt/data/OASIS-30_Atropos_template/T_template0_BrainCerebellumExtractionMask.nii.gz -s nii.gz -o /output/ants_subjects/sub-BANDA001/ants -p nipype_priors/BrainSegmentationPrior%02d.nii.gz -t /opt/data/OASIS-30_Atropos_template/T_template0_BrainCerebellum.nii.gz -j 1'
Arguments: ('antsCorticalThickness', 'nipype.interfaces.ants.segmentation', 'antsCorticalThickness')

@mgxd
Copy link
Member

mgxd commented Jan 10, 2018

perhaps we should move towards .format, since any command containing % will raise this error

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[BUG] FileNotFoundError: [Errno 2] No such file or directory
5 participants