Символьные (автоматически сгенерированные) каталоги через Snakemake - PullRequest
6 голосов
/ 09 июля 2020

Я пытаюсь создать структуру каталогов символических ссылок для псевдонимов выходных каталогов в рабочем процессе Snakemake.

Давайте рассмотрим следующий пример:

Давно go в галактике далеко-далеко кто-то хотел найти лучший вкус мороженого во Вселенной и провел опрос. Наш пример рабочего процесса направлен на представление голосов в виде структуры каталогов. Опрос проводился на английском sh (потому что так все говорят в той чужой галактике), но результаты должны быть поняты и людьми, не говорящими на английском sh. Ссылки Symboli c приходят на помощь.

Чтобы сделать вводимые данные доступными для нас, людей, а также 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

To создать соответствующее дерево каталогов, я начал с этого простого файла 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

Я уверен, что есть и другие способы 'pythoni c' для достижения того, что я хотел, но это всего лишь быстрый пример для иллюстрации моя проблема.

Запуская вышеуказанный рабочий процесс с 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

Итак, мой вопрос: Как я могу реализовать приведенный выше лог c в рабочем Snakefile?

Обратите внимание, что я не ищу совета по изменению представления данных в файле YAML и / или Snakefile. Это просто пример, чтобы выделить (и изолировать) проблему, с которой я столкнулся в более сложном сценарии.

К сожалению, хотя я пока не мог понять это самостоятельно, мне удалось получить рабочую версию GNU make (даже несмотря на то, что «синтаксический анализ YAML» в лучшем случае - hacki sh):

### 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'.

Так что я действительно надеюсь, что решение не в том, чтобы go вернуться к старомодному GNU make со всеми нечитаемыми хаками, которые я усвоил за эти годы, но есть способ убедить Snakemake сделать то, что я написал. ; -)

На всякий случай: Тестировался с помощью Snakemake версии 5.7.1.

правки:

Ответы [ 2 ]

1 голос
/ 31 июля 2020

Я хотел протестировать более новую версию Snakemake (5.20.1), и я придумал что-то похожее на ответ, предложенный Маналаваном Гаджапати:

### 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 (даже если больше нечего делать).

1 голос
/ 13 июля 2020

Вот способ решить ваш первый вопрос (ie. Запустите snakemake только один раз, чтобы получить все желаемые результаты). Я использую выходные файлы правила english в качестве входных данных для правила translation, и команда оболочки последнего правила изменена, чтобы отразить это. По моему опыту, использование каталогов в качестве входных данных не очень хорошо работает со 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}'"""

Я не знаю, как это обойти.

...