【问题标题】:Symlink (auto-generated) directories via Snakemake通过 Snakemake 进行符号链接(自动生成)目录
【发布时间】:2020-07-09 15:53:44
【问题描述】:

我正在尝试为 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>/<voter> 文件时创建 english/<flavour> 目录,但在将其用作创建 <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 测试的。


编辑:

【问题讨论】:

  • 您应该删除makefile 标签,因为这不是make / makefile 问题。需要注意的是,解决“不止一次”错误的方法是引入一个 $(sort ...),它的副作用也是唯一的。
  • @MadScientist:好吧,我没有使用GNU make 标签,因为我认为snakemake 只是make 的另一种变体(可以说,我理解)。关于“不止一次”错误:我知道(我什至写它是可以修复的),我只是为了这个例子而没有打扰。但是谢谢你提醒我$(sort ...) 有这种副作用,这会比我通常保留订单的 Marco 更简单。所以我想社区通过“错误”标签吸引你的注意力是有好处的。感谢您的反馈。
  • 我个人认为makefile 是 POSIX 派生的 makefile 的标签,snakemake 不是...但我不拥有 SO 并且意见不一:)。很高兴我能帮上忙;干杯!
  • 我不知道snakemake,但我的结论是否正确,您的translation 任务并没有真正咨询文件系统,而是从YAML 结构创建符号链接?这个任务是否以某种我看不到的方式以编程方式依赖于“英语”任务?
  • snakemake.readthedocs.io/en/stable/project_info/… 建议在ln 命令上使用-r 标志。这是我在您的示例和他们的示例之间看到的唯一区别。它还指出,snakemake 将符号链接视为文件

标签: python makefile directory symlink snakemake


【解决方案1】:

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

相关代码改动:

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}'"""

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

【讨论】:

  • 是的,我也有类似的想法:基本上,我使用output: "{lang}_translation/{trans}/{voter}" 和通过适当的input: lambda wc: [...] 输入相应的输入并修改rule_symlink 以作用于目录。这会带来额外的问题,即在并行运行规则时可能会创建相同的符号链接,因此您的解决方案向前迈进了一步,但正如您自己指出的那样,它仍然不会导致工作的 Snakefile。因此,Snakemake 似乎不仅对目录不好,而且对符号链接也不好。即使是基本的cp 也有-P 来处理这些事情。 ;-)
【解决方案2】:

我想用更新版本的 Snakemake (5.20.1) 进行测试,我想出了类似于 Manalavan Gajapathy 提出的答案:

### Setup ###

configfile: "config.yaml"

VOTERS = list({voter for flavour in config["flavours"].keys() for voter in config["flavours"][flavour]})

### 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 $(dirname '{input}') '{output}'"


### Rules ###

rule all:
    input: votes, translations

rule english:
    output: "english/{flavour}/{voter}"
    # To avoid considering ".done" as a voter
    wildcard_constraints:
        voter="|".join(VOTERS),
    shell: create_file_cmd

def get_voters(wildcards):
    return [f"english/{wildcards.flavour}/{voter}" for voter in config["flavours"][wildcards.flavour]]

rule flavour:
    input: get_voters
    output: "english/{flavour}/.done"
    shell: create_file_cmd

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

这会运行并创建所需的输出,但在重新运行时会失败并显示ChildIOException(即使没有更多工作要做)。

【讨论】:

  • 感谢您的回答。但是,我并没有真正看到这如何解决误报循环依赖的潜在问题。很高兴有一个替代方法来处理目录输入问题(虽然我不喜欢用隐藏文件弄乱输出,但我想可以很容易地将这些文件隐藏在.snakemake 目录中)但核心问题仍然存在。
  • 我同意 .done 文件是一个丑陋的黑客。这只是为了报告更新的 Snakemake 会发生什么。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-14
  • 2023-03-25
相关资源
最近更新 更多