Snakemake:避免在执行 shell 命令之前删除输出文件

Snakemake: Avoid removing output files before executing the shell command

是否有可能避免在执行 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 脚本)。我预计类似以下规则的东西应该可以解决这个问题,但是由于 output.jsons 在 运行 之前被删除 python 脚本,所有 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 规则更新,因此比较不会'不对。

我认为 Snakemake 目前没有解决您问题的方法。我认为您必须从 create_transformation_jsons.py 中提取 input/output 逻辑,并为 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

这有点复杂,但我认为它对您来说是无缝的。

解决方案涉及调用 snakemake 两次,但您可以将其包装在 shell 脚本中。在第一次调用中,您在 --dryrun 中使用 snakemake 来确定将更新哪些 json,在第二次调用中,此信息用于制作 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。这对你有用吗?我还没有测试代码的所有部分,所以对它持保留态度。