With the minimap2/align (and other read mapping mo...
# modules
j
With the minimap2/align (and other read mapping modules), the output BAM files all receive the meta object of the input reads channel. However, often I want the opposite, and would like to get the meta object from the reference in the output channel (as I am usually working with mapping a single set of reads to multiple assemblies). Seems like there could be a couple methods for doing this - either a) fix it in my pipeline myself - merge the reads + reference into a channel,
multiMap
, and then enter them into the module, or b) add an explicit input channel to flag to the module which meta to emit (reads, reference, possibly their combination?). Does anyone have strong feelings about b), one way or the other? Do we prefer busier module code and tidier pipeline code, or busier pipeline code with streamlined modules? edit; for reference, here's a subworkflow where in one instance I need the 'reference' meta and in another I need the 'reads' meta: https://github.com/sanger-tol/genomeassembly/blob/dev/subworkflows/local/purging/main.nf#L43
Something like:
Copy code
output:
tuple val(meta_out), path("*.bam")                       , optional: true, emit: bam

script:
...
if (!(output_meta in ['reads', 'reference', 'combine'])){
    log.error("Error: Invalid value for output_meta")
}
if(output_meta == 'reads')     { meta_out = meta         }
if(output_meta == 'reference') { meta_out = meta2        }
if(output_meta == 'combine'  ) { meta_out = meta + meta2 }
...
j
I would say in this case since it's not very common to want the reference meta as output, you could modify the module in your pipeline and use
nf-core modules patch
💯 1
otherwise I would prefer your option a
option b looks like overengineering the module
j
I do currently have the module patched as above - but I was curious if there was more general interest in this functionality as I tend to find patches don't play well with module updates
j
yes it's true that the patch doesn't always resolve conflicts with very big changes. But I would guess changing only one value in the output channel shouldn't cause many problems?
j
Ah, but sometimes I also want it the normal way... 😅
j
ah ok! this complicates thing 😄
then I think your option a is the best
but let's see if more people is interested in your functionality
👍 1
j
I have thoughts when dog walk finished!
Basically multiMaps everywhere 👌
We do that a lot in taxprofiler: multiMap before the module to merge the reads and reference meta
And then you have everything in 'position one'
I wouldn't mess with the modules
I've found that it was rare that I would want to drop the meta from one or the other of reads/references afterwards
j
The problem with the multiMap approach is that it requires (currently?) that you know what meta keys are needed to join the channels, so you can pull them out accordingly for the join - if you want to do this in a shared subworkflow, where you can't necessarily make assumptions about meta contents, that becomes more difficult
j
Ah I see...
Maybe you can check the Bwa Aln subworkflow...
I think @Thiseas C. Lamnidis may hace (partially?) addressed this?
But may make assumptions that the only info you need to pull out is `meta.id`or something... But I don't remember exactly
a
If you need it in a shared subworkflow then I would just make the subworkflow have whatever assumptions you want (ie id should be the unique key, or id + reference etc) then whatever workflow calls the subworkflow can handle preprocessing the meta into the right format before and after the subworkflow
👍 1
j
Musing on this a bit more - personally, I do think this should be the responsibility of the module to output the desired data, rather than finagling the code around the module to “trick” it into putting out the desired data. I think mapping a single set of reads against multiple sets of contigs is not that unusual a pattern - off the of my head, this might be a pattern that you’d want to use in mag, magmap, genomeassembler? For example, assemble the reads into assemblies with different tools, add a
meta.assembler
key, and then map the reads to all the assemblies - you’d want to keep that assembler key in the output. That said, the code I suggested above is indeed wordy! Another way to solve the problem might be to add a new
ext
directive, available in all modules where there is more than one meta object, that is set by default as:
Copy code
def out_meta = task.ext.out_meta ?: meta
That takes one line of code in the module, is generalisable to all module cases, and gives pipeline developers full control over the required outputs of a module. And its customisable on a per-case basis if you import the module with different names.
a
This would absolutely not be a good idea in my opinion.
ext
variables are already not particularly intuitive and the fact they are set in config makes the transparency to users and other devs very poor. Plus, you would really need to have the option for all meta in a process (and there can be many). I think you would be better off trying to coalesce support around injecting a reference meta value in any modules directly, although that would also require gaining support for some meta key within nf-core that may now already be widely used.
As for it the module outputting the desired data, that is why
patch
exists. Given that this seems to be a local non-nf-core pipeline it would be the most logical option to add some code which is necessary for your own pipeline but not for the majority.
j
I take your point about discoverability, and if there really is no interest in this functionality I'll just go ahead with a mixture of either patching local modules, or forking the nf-core module to our shared module repo where we need the functionality across pipelines/subworkflows. But I do think this would be nice to support in nf-core directly.
Plus, you would really need to have the option for all meta in a process (and there can be many).
I think this is solved by the above, as you can compute any arbitrary combination of metas with it, e.g.:
Copy code
withName: processWithFourMetas {
    ext.out_meta = { meta + meta2 + meta3 }
}
a
That assumes there is only ever 1 meta in your output channels though? (I think this is mostly, maybe even always currently the case, but it’s not guaranteed)
j
I do think this should be the responsibility of the module to output the desired data, rather than finagling the code around the module to “trick” it into putting out the desired data.
The problem is unfortunately the diversity of what data people want to emit etc. The reason why we've been very VERY careful about messing with meta is that you can be sent down a lot of pipeline wrangling to change your pipeline's meta fields just to match each module's/subworkflows assumptions as to what each meta is called.
For example, assemble the reads into assemblies with different tools, add a
meta.assembler
key, and then map the reads to all the assemblies - you’d want to keep that assembler key in the output.
That said, @Maxime Garcia has wanted to come up with a controlled vocab for parameters and meta terms for a long time... we could annoy him by being first and develop a controlled vocab for metadata terms within #C070369GP7T for example 😉
a
The more I think about it though, the more I wonder whether this is a fundamental issue with
nf-core
modules design (which would likely be a huge fight/non-starter to overturn). It’s the same issue I take with the
fasta
and
fai
channels being separate (which is actually something I’m surprised doesn’t break your usage) which is that, unless the channels are single-value the fasta/fai would get incorrectly mixed
✅ 1
In fact, how do you even manage this at this point?
j
It’s the same issue I take with the fasta and fai channels being separate (which is actually something I’m surprised doesn’t break your usage) which is that, unless the channels are single-value the fasta/fai would get incorrectly mixed
Yeah this will very likely change soon ™️ anyway... Ben is not happy about it either but we are waiting for some more fundamental changes to Nextflow before making this shift
a
If you pass multiple different reference files, there is no way to ensure each one is used with each of your input samples is there?
j
If you pass multiple different reference files, there is no way to ensure each one is used with each of your input samples is there?
That's why I suggested multiMap
So we are thinkikng along the same lines I think
m
I'd love to go to 1 input with meta for data and 1 input with meta for references
j
But how do you exsport that?
You don't re-export your reference
m
I'd you need to re-export then it's input data
a
But I don’t think multiMap solves that does it? You could still end up with e.g.
Copy code
ch_in = (sample1, sample1, sample2, sample2)
ch_ref = (ref1, ref2, ref1, ref2)
But get them actually processed as:
Copy code
sample1, ref1
sample1, ref1
sample2, ref2
sample2, ref2
Like, I don’t think that current nf-core module structure for e.g.
minimap2
allows you to safely align all of your samples against all of the references?
You can either have multiple samples and 1 reference, or 1 sample and multiple references. But I don’t think you can have multiple of both
j
T oget that to work
j
If you pass multiple different reference files, there is no way to ensure each one is used with each of your input samples is there?
In my case, the input sample is a value channel, so it gets reused. But yes, otherwise you would need to multiMap. But that's true in both orientations - a bunch of samples against a single reference, a bunch of references against a single sample, or a bunch against a bunch
m
I'd have the reference id in the meta map of my input, and use cross/combine
a
Are we suggesting that
multiMap
guarantees channel order?
j
It does (or should do)
a
Because if not, then this simply does not safely work for multi vs multi.
j
As it makes 'sub channels' of the same item in a channel
a
It does (or should do)
Ok, I always just assumed that no channel order could ever be guaranteed but happy to be wrong here
j
IT's how I've been using it everywhere after being advised (I think?) by Ben
j
It guarantees channel order insofar as it splits a single channel into multiple channels, no? You have to combine the data you need into a single entry first before splitting
j
YEs
a
Maybe it’s just output channel order that is completely random then. I assumed it was any channel. (Becasue multimap channels are still channels)
j
I don't know if joins in general preserve order - but I would expect otherwise multiMap works on a FIFO principle
a
I wasn’t sure about how
multiMap
was implemented and I could imagine a scenario where parallel processing of the input channel would have allowed some weirdness.
Copy code
ch_in = Channel.of([1,2], [10,1], [5, 20])
ch_in.view()

ch_in.multiMap{
    a, b ->
    A:
        [a]
    B:
        sleep(b * 1000)
        [b]
}.set{
multi
}

multi.B.view{"B->${it}"}
multi.A.view{"A->${it}"}
This results in the pairs being printed in the same order they entered though so I think it probably is order-safe. I’m a bit leery of this still though, given how much we drill in to people to not rely on channel order (one of the major reasons for
meta
in the first place)
So, given that (currently) to make a pipeline work for multi vs multi with nf-core modules like this you would need a
multiMap
anyway, I am even less inclined to think that the modules should handle injecting values into the meta.
j
Hmm, OK - point taken re:
multiMap
! I would still like a way to get all the input metadata out of a module (feels important for data provenance if nothing else) but see that there's not a straightforward way of doing this/there are workarounds, so I'll tackle it on my own end for now (I would ideally like to be join-agnostic!).
j
As I've now got lost within the thread: you didn't want to add all the info after the pre-multiMap join into the first
meta
entry (for the primary input channel) was simply because of joining afterwards?
Because I honestly I'm not sure if you can avoid every being join-agnostic really... once you're combining metas as input, your output files are conceptually intrinsically 'imprinted' with all of those bits of meta information (so to say): the output files have assigned to them now all attributes of both input/reference (or db or whatever).
(if that makes sense... I'm tired and going loopy reading a draft article for the last hour 😅)
j
I wanted to turn the subworkflow into a shared subworkflow (for our repo, not nf-core), so I ideally wouldn't want to make /any/ assumptions about the contents of the meta object. But joining requires knowing something about the meta contents
👍 1
j
yeah I just don't think it's really possible unless you somehow have an additional input channel which describes what the meta keys are for each of the input files?
That way you can add/extract those meta fields as necessary around the joins?
j
Maybe that's the best solution
j
Oh wait that's your option (b) 😅
Sorry! (told you I was lost)
j
My option b) was to modify the module itself, not the subworkflow
j
Ah... but why not also at the subworkflow level?
j
Indeed!
j
That keeps the modules very simple/clean
Subworkflow is pipeline level code where you can be more messy
j
My preference would be to shove the jank into the module so that the pipeline code is very readable and clear 🫢 But I might be alone in that
j
Ha, my instinct is the opposite
💯 1
j
The less weird channel manipulations, the better
j
In the sense that modules can be mostly standardised with common structures as they are the 'atomic' thing, whereas HOW you combine the modules is much more 'custom'
Each person wants to use a module in a different way, so it's up to the pipeline dev to work out how to link these
(this is personal opinion: not official canon 🙂 )
a
The less weird channel manipulations, the better
I used to feel this way but I am now much more used to using and reading channel manipulations and they feel cleaner now. You can also write code that is just channel manipulations and then bung it in a subworkflow and test it independently if you really want
👍 2
j
This is true - but I think modules can be flexible enough to support different use cases, especially if there's a consistent interface. I really wouldn't want to add various sets of weird epicycles to a module either
j
Normally I have: pipeline workflow (clean) -> higher level subworkflow to link subworkflows (clean) -> lower-level subworkflows to link modules (messy AF) -> modules (clean)
The higher level subworkflows should general describe the general workflow and order of things, but if you need to go 'deeper' at this point you have to expect with the 'real' plumbing
👍 1
a
Agree with everything from James there
j
I'm right!
My daughter normally says I'm WRONG with everything!
a
All my channel magic is typically in local subworkflows
this 2
j
And it's also why shared subworkflows has been tricker/less popular - it's worked mostly with genomics because many of those steps ARE very routine for large areas e.g. human cancer genomics - you are only ever dealing with single reference genome basically
But everyone else who edeals with 'real' (messy) biology we've seen much less uptake
a
everyone else who deals with 'real' (messy) biology
I feel slighted, human cancer genomics can be messy too! 🥹
😆 1
j
Come back to me when you have to handle 100,000s of uncharacterised microbes who do whatever the fuck they want and not just within their own freaking species!
I may have some level of trauma
j
@Daniel Lundin pointed out in the meta-omics meeting today that he has a similar use case for the diamond/blastp module: https://github.com/nf-core/modules/pull/7403
m
@Jim Downie you know you are efficient
t
late to the party, but I agree with the
multiMap
approach. Here’s how we go about doing this in eager: We have a custom util function (
addNewMetaFromAttributes
) that pulls parts of the metas of a channel into a new meta, that can then be used for a
join
->
multiMap
chain. The multimap generally also removes the created “join meta”.
Copy code
/*
This function can be applied to the contents of a channel row-by-row with a .map operator.
It assumes that the first element of the channel row is a map of metadata. It will then create a new metadata map
that consists of the source_attributes of the original metadata map, but named after the corresponding target_attributes.
The new metadata map is then prepended to the channel row, becoming the new first element.
If the remove flag is set to true, then the original metadata map is removed from the channel row.

Example:
ch_my_channel=Channel.of( [ [id:'id', sample_id:'sample_id', single_end:true, reference:"hs37d5" ], "bam", "bai" ] )

ch_my_channel.map{ row -> addNewMetaFromAttributes(row, "id", "new_attribute", false) }
// This will create a new channel with the following rows:
[ [ new_attribute: 'id' ], [id:'id', sample_id:'sample_id', single_end:true, reference:"hs37d5" ], "bam", "bai" ]

ch_my_channel.map{ row -> addNewMetaFromAttributes(row, ["id", "single_end"], ["new_attribute", "endedness"] , true) }
// This will create a new channel with the following rows:
[ [ new_attribute: 'id' , endedness: true ], "bam", "bai" ]

*/
def addNewMetaFromAttributes( ArrayList row, Object source_attributes, Object target_attributes, boolean remove = false) {
    def meta = row[0]
    def meta2 = [:]

    // Read in target and source attributes and create a mapping between them
    // Option A: both attributes are Strings
    if ((source_attributes instanceof String) && (target_attributes instanceof String)) {
            meta2[target_attributes] = meta[source_attributes]

    } else if ((source_attributes instanceof List) && (target_attributes instanceof List)) {
        if (source_attributes.size() == target_attributes.size()) {
            for (int i = 0; i < source_attributes.size(); i++) {
                // Option B: Both are lists of same size
                meta2[target_attributes[i]] = meta[source_attributes[i]]
            }
        } else {
            // Option C: Both lists, but uneven. Error.
            throw new IllegalArgumentException("Error: The target_attributes and source_attributes lists do not have the same size.")
        }
    } else {
        // Option D: Not both the same type or acceptable types. Error.
        throw new IllegalArgumentException("Error: target_attributes and source_attributes must be of same type (both Strings or both Lists).")

    }

    def new_row = [ meta2 ] + row

    // If replace is true, then remove the old meta
    if (remove && new_row.size() > 1) {
        new_row.remove(1)
    }

    return new_row
}
Feel free to use/adapt this to your use case. 🙂