Snakemake:在执行shell命令

时间:2018-03-08 16:33:04

标签: python snakemake

是否有可能避免在执行shell命令之前删除snakemake规则中定义的输出文件?我在这里找到了对此行为的描述:http://snakemake.readthedocs.io/en/stable/project_info/faq.html#can-the-output-of-a-rule-be-a-symlink

我要做的是为输入列表和输出文件列表(N:M关系)定义规则。如果其中一个输入文件已更改,则应触发此规则。然后,在shell命令中调用的python脚本仅创建那些与现有文件相比不存在或内容已更改的输出(即,在python脚本中实现更改检测)。我期望类似下面的规则应该解决这个问题,但是在运行python脚本之前删除了output.jsons,所有的output.jsons都将使用新的时间戳创建,而不是只有那些已经更改的时间戳。

rule jsons:
"Create transformation files out of landmark correspondences."
input:
    matchfiles = ["matching/%04i-%04i.h5" % (SECTIONS[i], SECTIONS[i+1]) for i in range(len(SECTIONS)-1)]
output:
    jsons = ["transformation/{section}_transformation.json".format(section=s) for s in SECTIONS]
shell:
    "python create_transformation_jsons.py --matchfiles {input.matchfiles} --outfiles {output.jsons}"

如果没有可能避免删除Snakemake中的输出文件,是否还有人知道如何将此工作流映射到snakemake规则而不更新所有输出文件?

更新

我试图通过更改Snakemake源代码来解决这个问题。我删除了jobs.py中的行self.remove_existing_output(),以避免在执行规则之前删除输出文件。此外,我在executors.handle_job_success中调用self.dag.check_and_touch_output()时添加了参数no_touch=True。这非常有效,因为输出文件现在既不会在执行规则之前被删除也不会被触及。但是仍然会针对每个json文件触发遵循json文件作为输入的规则(即使它没有更改),因为Snakemake认识到json文件之前被定义为输出,并且必须已经更改。 所以我认为避免删除输出文件并不能解决我的问题,也许解决方法 - 如果存在 - 是唯一的方法......

更新2

我还尝试通过将上面定义的jsons规则的输出路径更改为transformation/tmp/...并添加以下规则来查找解决方法而不更改Snakemake源代码:

def cmp_jsons(wildcards):
    section = int(wildcards.section)
    # compare json for given section in transformation/ with json in transformation/tmp/
    # return [] if json did not change
    # return path to tmp json filename if json has changed
rule copy:
    input:
        json_tmp = cmp_jsons
    output:
        jsonfile = "transformation/B21_{section,\d+}_affine_transformation.json"
    shell:
        "cp {input.json_tmp} {output.jsonfile}"

但是,由于在工作流程开始之前评估输入函数,因此tmp-jsons尚未存在或尚未由jsons规则更新,因此比较不会正确。

2 个答案:

答案 0 :(得分:0)

我认为Snakemake目前无法解决您的问题。我认为您必须从create_transformation_jsons.py中提取输入/输出逻辑,并为Snakefile中的每个关系编写单独的规则。了解可以生成匿名规则可能对您有所帮助,例如:在for循环中。 How to deal with a variable of output files in a rule

最近Snakemake在执行规则时开始清除日志,我打开了issue on that。该问题的解决方案也可能对您有所帮助。但这都是在不确定的未来,所以不要指望它。

<强>更新

这是另一种方法。您的规则中没有任何通配符,因此我假设您只运行一次规则。我还假设在执行时您可以列出正在更新的部分。我打电话给列表SECTIONS_PRUNED。然后,您可以制定一个仅将这些文件标记为输出文件的规则。

rule jsons:
"Create transformation files out of landmark correspondences."
input:
    matchfiles = ["matching/%04i-%04i.h5" % (SECTIONS[i], SECTIONS[i+1]) for i in range(len(SECTIONS)-1)]
output:
    jsons = ["transformation/{section}_transformation.json".format(section=s) for s in SECTIONS_PRUNED]
params:
    jsons = [f"transformation/{s}_transformation.json" for s in SECTIONS]
run:
    shell("python create_transformation_jsons.py --matchfiles {input.matchfiles} --outfiles {params.jsons}")

我最初认为使用shadow: "minimal"来确保SECTIONS_PRUNED未能声明的任何文件都不会虚假更新。但是,带阴影的情况可能更糟:错过的文件会被更新并留在阴影目录中(并且会被删除而不被注意)。使用shadow,您还需要将json文件复制到shadow目录中,让脚本找出要生成的内容。

所以更好的解决方案可能是不使用阴影。如果SECTIONS_PRUNED未能声明所有更新的文件,则第二次执行snakemake将高位(并修复)此并确保所有下游分析都正确完成。

更新2

另一种更简单的方法是将工作流分成两部分,不要让snakemake知道json规则产生输出文件。

rule jsons:
"Create transformation files out of landmark correspondences."
input:
    matchfiles = ["matching/%04i-%04i.h5" % (SECTIONS[i], SECTIONS[i+1]) for i in range(len(SECTIONS)-1)]
params:
    jsons = [f"transformation/{s}_transformation.json" for s in SECTIONS]
shell:
    "python create_transformation_jsons.py --matchfiles {input.matchfiles} --outfiles {params.jsons}"

分两部分运行snakemake,用相关的规则名称替换 all

$ snakemake jsons
$ snakemake all

答案 1 :(得分:0)

这涉及更多,但我认为它可以无缝地为您服务。

解决方案涉及两次调用snakemake,但您可以将其包装在shell脚本中。在第一次调用中,您在--dryrun中使用snakemake来确定哪些jsons将被更新,在第二次调用中,此信息用于生成DAG。我使用--config在两种模式之间切换。这是Snakefile。

def get_match_files(wildcards):
    """Used by jsons_fake to figure which match files each json file depend on"""
    section = wildcards.section

    ### Do stuff to figure out what matching files this json depend on
    # YOUR CODE GOES HERE
    idx = SECTIONS.index(int(section)) # I have no idea if this is what you need
    matchfiles = ["matching/%04i-%04i.h5" % (SECTIONS[idx], SECTIONS[idx + 1])]

    return matchfiles

def get_json_output_files(fn):
    """Used by jsons. Read which json files will be updated from fn"""
    try:
        json_files = []
        with open(fn, 'r') as fh:
            for line in fh:
                if not line:
                    continue  # skip empty lines
                split_line = line.split(maxsplit=1)
                if split_line[0] == "output:":
                    json_files.append(split_line[1])  # Assumes there is only 1 output file pr line. If more, modify.
    except FileNotFoundError:
        print(f"Warning, could not find {fn}. Updating all json files.")
        json_files = expand("transformation/{section}_transformation.json", section=SECTIONS)

    return json_files


if "configuration_run" in config:
    rule jsons_fake:
        "Fake rule used for figuring out which json files will be created."
        input:
            get_match_files
        output:
            jsons = "transformation/{section}_transformation.json"
        run:
            raise NotImplementedError("This rule is not meant to be executed")

    rule jsons_all:
        input: expand("transformation/{s}_transformation.json", s=SECTIONS]

else:
    rule jsons:
        "Create transformation files out of landmark correspondences."
        input:
            matchfiles = ["matching/%04i-%04i.h5" % (SECTIONS[i], SECTIONS[i+1]) for i in range(len(SECTIONS)-1)]
        output:
            jsons = get_json_output_files('json_dryrun') # This is called at rule creation
        params:
            jsons=expand("transformation/{s}_transformation.json", s=SECTIONS]
        run:
            shell("python create_transformation_jsons.py --matchfiles {input.matchfiles} --outfiles {params.jsons}")

为避免两次调用Snakemake,您可以将其包装在shell脚本中mysnakemake

#!/usr/bin/env bash

snakemake jsons_all --dryrun --config configuration_run=yes | grep -A 2 'jsons_fake:' > json_dryrun
snakemake $@

并像调用snakemake一样调用脚本,例如:mysnakemake all -j 2。这对你有用吗?我还没有对代码的所有部分进行测试,所以请耐心等待。