首页 > 解决方案 > 通过 Snakemake 进行符号链接(自动生成)目录

问题描述

我正在尝试为 Snakemake 工作流程中的别名输出目录创建一个符号链接目录结构。

让我们考虑以下示例:

很久以前,在一个遥远的星系中,有人想找到宇宙中最好的冰淇淋口味,并进行了一项调查。我们的示例工作流程旨在通过目录结构表示投票。该调查是用英语进行的(因为在那个外国星系中他们都说英语),但结果也应该被非英语人士理解。符号链接来救援。

为了让我们人类和 Snakemake 都可以解析输入,我们将它们粘贴到 YAML 文件中:

cat config.yaml
flavours:
  chocolate:
    - vader
    - luke
    - han
  vanilla:
    - yoda
    - leia
  berry:
    - windu
translations:
  french:
    chocolat: chocolate
    vanille: vanilla
    baie: berry
  german:
    schokolade: chocolate
    vanille: vanilla
    beere: berry

为了创建相应的目录树,我从这个简单的 Snakefile 开始:

### Setup ###

configfile: "config.yaml"


### Targets ###

votes = ["english/" + flavour + "/" + voter
         for flavour, voters in config["flavours"].items()
         for voter in voters]

translations = {language + "_translation/" + translation
                for language, translations in config["translations"].items()
                for translation in translations.keys()}


### Commands ###

create_file_cmd = "touch '{output}'"

relative_symlink_cmd = "ln --symbolic --relative '{input}' '{output}'"


### Rules ###

rule all:
    input: votes, translations

rule english:
    output: "english/{flavour}/{voter}"
    shell: create_file_cmd

rule translation:
    input: lambda wc: "english/" + config["translations"][wc.lang][wc.trans]
    output: "{lang}_translation/{trans}"
    shell: relative_symlink_cmd

我相信还有更多的“pythonic”方法可以实现我想要的,但这只是一个简单的例子来说明我的问题。

使用 运行上述工作流程snakemake,我收到以下错误:

Building DAG of jobs...
MissingInputException in line 33 of /tmp/snakemake.test/Snakefile
Missing input files for rule translation:
english/vanilla

因此,虽然 Snakemake 足够聪明地english/<flavour>在尝试创建文件时创建目录,但在将其用作创建符号链接english/<flavour>/<voter>的输入时,它似乎“忘记”了该目录的存在。<language>_translation/<flavour>

作为中间步骤,我将以下补丁应用于 Snakefile:

27c27
<     input: votes, translations
---
>     input: votes#, translations

english现在,工作流按预期运行并创建了目录(snakemake -q仅输出):

Job counts:
        count   jobs
        1       all
        6       english
        7

现在创建了目标目录,我回到了 Snakefile 的初始版本并重新运行它:

Job counts:
        count   jobs
        1       all
        6       translation
        7
ImproperOutputException in line 33 of /tmp/snakemake.test/Snakefile
Outputs of incorrect type (directories when expecting files or vice versa). Output directories must be flagged with directory(). for rule translation:
french_translation/chocolat
Exiting because a job execution failed. Look above for error message

虽然我不确定指向目录的符号链接是否符合目录的条件,但我继续并应用了一个新补丁来遵循建议:

35c35
<     output: "{lang}_translation/{trans}"
---
>     output: directory("{lang}_translation/{trans}")

有了这个,snakemake最后创建了符号链接:

Job counts:
        count   jobs
        1       all
        6       translation
        7

作为确认,这是生成的目录结构:

english
├── berry
│   └── windu
├── chocolate
│   ├── han
│   ├── luke
│   └── vader
└── vanilla
    ├── leia
    └── yoda
french_translation
├── baie -> ../english/berry
├── chocolat -> ../english/chocolate
└── vanille -> ../english/vanilla
german_translation
├── beere -> ../english/berry
├── schokolade -> ../english/chocolate
└── vanille -> ../english/vanilla

9 directories, 6 files

但是,除了无法在不运行snakemake两次(并在其间修改目标)的情况下创建此结构之外,即使简单地重新运行工作流也会导致错误:

Building DAG of jobs...
ChildIOException:
File/directory is a child to another output:
/tmp/snakemake.test/english/berry
/tmp/snakemake.test/english/berry/windu

所以我的问题是:如何在工作的 Snakefile 中实现上述逻辑?

请注意,我不是在寻求更改 YAML 文件和/或 Snakefile 中的数据表示的建议。这只是一个突出(和隔离)我在更复杂场景中遇到的问题的示例。

可悲的是,虽然到目前为止我自己无法解决这个问题,但我设法获得了一个可以工作的 GNU make 版本(即使“YAML 解析”充其量是 hackish):

### Setup ###

configfile := config.yaml


### Targets ###

votes := $(shell awk ' \
  NR == 1 { next } \
  /^[^ ]/ { exit } \
  NF == 1 { sub(":", "", $$1); dir = "english/" $$1 "/"; next } \
  { print dir $$2 } \
  ' '$(configfile)')

translations := $(shell awk ' \
  NR == 1 { next } \
  /^[^ ]/ { trans = 1; next } \
  ! trans { next } \
  { sub(":", "", $$1) } \
  NF == 1 { dir = $$1 "_translation/"; next } \
  { print dir $$1 } \
  ' '$(configfile)')


### Commands ###

create_file_cmd = touch '$@'

create_dir_cmd = mkdir --parent '$@'

relative_symlink_cmd = ln --symbolic --relative '$<' '$@'


### Rules ###

all : $(votes) $(translations)

$(sort $(dir $(votes) $(translations))) : % :
    $(create_dir_cmd)
$(foreach vote, $(votes), $(eval $(vote) : | $(dir $(vote))))
$(votes) : % :
    $(create_file_cmd)

translation_targets := $(shell awk ' \
  NR == 1 { next } \
  /^[^ ]/ { trans = 1; next } \
  ! trans { next } \
  NF != 1 { print "english/" $$2 "/"} \
  ' '$(configfile)')
define translation
$(word $(1), $(translations)) : $(word $(1), $(translation_targets)) | $(dir $(word $(1), $(translations)))
    $$(relative_symlink_cmd)
endef
$(foreach i, $(shell seq 1 $(words $(translations))), $(eval $(call translation, $(i))))

运行make这个工作得很好:

mkdir --parent 'english/chocolate/'
touch 'english/chocolate/vader'
touch 'english/chocolate/luke'
touch 'english/chocolate/han'
mkdir --parent 'english/vanilla/'
touch 'english/vanilla/yoda'
touch 'english/vanilla/leia'
mkdir --parent 'english/berry/'
touch 'english/berry/windu'
mkdir --parent 'french_translation/'
ln --symbolic --relative 'english/chocolate/' 'french_translation/chocolat'
ln --symbolic --relative 'english/vanilla/' 'french_translation/vanille'
ln --symbolic --relative 'english/berry/' 'french_translation/baie'
mkdir --parent 'german_translation/'
ln --symbolic --relative 'english/chocolate/' 'german_translation/schokolade'
ln --symbolic --relative 'english/vanilla/' 'german_translation/vanille'
ln --symbolic --relative 'english/berry/' 'german_translation/beere'

生成的树与上面显示的树相同。

此外,make再次运行也可以:

make: Nothing to be done for 'all'.

所以我真的希望解决方案不是回到老式的 GNU make 以及我多年来内化的所有不可读的黑客,而是有一种方法可以说服 Snakemake 也按照我的要求去做。;-)

以防万一:这是使用 Snakemake 版本 5.7.1 测试的。


编辑:

标签: pythonmakefiledirectorysymlinksnakemake

解决方案


这是解决您的第一个问题的一种方法(即,只运行一次snakemake 以获得所有所需的输出)。我使用 rule 的输出文件作为 ruleenglish的输入translation,并修改后一个 rule 的 shell 命令以反映这一点。以我的经验,使用目录作为输入并不适用于snakemake,如果我没记错的话,directory()标签input会被忽略。

相关代码更改:

relative_symlink_cmd = """ln -s \
        "$(realpath --relative-to="$(dirname '{output}')" "$(dirname {input[0]})")" \
        '{output}'"""

rule translation:
    input: lambda wc: ["english/" + config["translations"][wc.lang][wc.trans] + "/" + voter for voter in config['flavours'][config["translations"][wc.lang][wc.trans]]]
    output: directory("{lang}_translation/{trans}")
    shell: relative_symlink_cmd

您的第二个问题很棘手,因为当您再次运行snakemake 时,它​​会将符号链接解析为其相应的源文件,这会导致ChildIOException错误。这可以通过替换relative_symlink_cmd为自己的目录而不是符号链接来验证,如下所示。在这种情况下,snakemake 按预期工作。

relative_symlink_cmd = """mkdir -p '{output}'"""

我不知道如何解决这个问题。


推荐阅读