Snakemake: Execute rule based on value of wildcard

Viewed 44

I have pipeline that takes in input different species. If the value of the {species} wildcard is "mouse" or "human", I need to do some pre-processing common to both species and execute some rules, otherwise execute another set of rules. This is pseudocode of what I'm trying to achieve:

SPECIES = ['mouse', 'human', 'pig']

rule all:
    input:
        expand('{species}.done', species=SPECIES),

if wildcards.species in ['mouse', 'human']:
    rule prepare_data:
        output:
            'some.data'

    rule mouse_human:
        input:
            'some.data',
        output:
            '{species}.tmp',
else:
    rule animal:
        # Note file "some.data" is not needed
        output:
            '{species}.tmp',

rule done:
    input:
        '{species}.tmp',
    output:
        '{species}.done',

That is: If {species} is "mouse" or "human", run rule prepare_data (only once) and then run rule mouse_human twice, once for human once for mouse. If {species} is "pig" or something else run only rule animal.

The pseudocode above won't run because if wildcards.species in ['mouse', 'human']: is not valid syntax. How can I do that?

A possible solution would be this:

rule prepare_data:
    output:
        'some.data',

rule species:
    input:
        'some.data',
    output:
        '{species}.tmp',
    run:
        if wildcards.species in ['mouse', 'human']:
   `        # Do human/mouse stuff using "some.data" and output {species}.txt
        else:
            # Do other stuff and output {species}.tmp, ignore "some.data"

However, rule prepare_data would always run even if the user's input data does not include "mouse" or "human". This is wasteful and I would like to avoid it.

3 Answers

I came up with this solution:

rule prepare_data:
    output:
        'some.data',
    shell:
        r"""
        some expensive computation
        """

rule prepare_dummy_data:
    output:
        touch('dummy.data'),

rule species:
    input:
        dat= lambda wc: 'some.data' if wc.species in ['human', 'mouse'] \
                         else 'dummy.data',
    output:
        '{species}.tmp',
    run:
        if wildcards.species in ['mouse', 'human']:
   `        # Do human/mouse stuff using "some.data" and output {species}.txt
        else:
            # Do other stuff and output {species}.tmp, ignore {input.dat}

This is only a minor variation of the answer by @dariober (also OP), but might be preferable in some contexts (e.g. when the condition tree becomes wide):

SPECIES = ['mouse', 'human', 'pig']
SPECIAL_SPECIES_CONSTRAINT = "|".join(['human', 'mouse'])

rule prepare_data:
    output: 'some.data',

ruleorder: species_special > species

rule species:
    output: '{species}.tmp',

rule species_special:
    """Treat mouse/human as special."""
    input: dat='some.data'
    output: '{species}.tmp',
    wildcard_constraints: species=SPECIAL_SPECIES_CONSTRAINT

Combining SultanOrazbayev's idea of specialized rules with input functions:

SPECIES = ['mouse', 'human', 'pig']

rule all:
    input:
        expand('{species}.done', species=SPECIES),

rule prepare_data:
    output:
        'some.data'

def animal_input(wildcards):
    if wildcards.species in ['mouse', 'human']:
        return 'some.data'
    raise ValueError()  # caught by snakemake, will try next rule

ruleorder:
    mouse_human > animal

rule mouse_human:
    input:
        animal_input
    output:
        '{species}.tmp',

rule animal:
    # Note file "some.data" is not needed
    output:
        '{species}.tmp',

rule done:
    input:
        '{species}.tmp',
    output:
        '{species}.done',

Instead of raising a value error, you could return an empty string for no input and have a single rule. Depending on what the rule body does, you may need to handle the case when input is empty separately.

Related