Jim Downie
08/12/2025, 12:49 PMmultiMap , 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#L43Jim Downie
08/12/2025, 12:54 PMoutput:
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úlia Mir
08/12/2025, 12:57 PMnf-core modules patchJúlia Mir
08/12/2025, 12:57 PMJúlia Mir
08/12/2025, 12:58 PMJim Downie
08/12/2025, 1:00 PMJúlia Mir
08/12/2025, 1:02 PMJim Downie
08/12/2025, 1:03 PMJúlia Mir
08/12/2025, 1:03 PMJúlia Mir
08/12/2025, 1:03 PMJúlia Mir
08/12/2025, 1:03 PMJames Fellows Yates
08/12/2025, 2:43 PMJames Fellows Yates
08/12/2025, 3:06 PMJames Fellows Yates
08/12/2025, 3:07 PMJames Fellows Yates
08/12/2025, 3:07 PMJames Fellows Yates
08/12/2025, 3:07 PMJames Fellows Yates
08/12/2025, 3:08 PMJim Downie
08/12/2025, 3:25 PMJames Fellows Yates
08/12/2025, 3:45 PMJames Fellows Yates
08/12/2025, 3:46 PMJames Fellows Yates
08/12/2025, 3:46 PMJames Fellows Yates
08/12/2025, 3:47 PMArthur
08/12/2025, 4:06 PMJim Downie
08/12/2025, 7:51 PMmeta.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:
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.Arthur
08/13/2025, 1:04 AMext 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.Arthur
08/13/2025, 1:11 AMpatch 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.Jim Downie
08/13/2025, 8:02 AMPlus, 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.:
withName: processWithFourMetas {
ext.out_meta = { meta + meta2 + meta3 }
}Arthur
08/13/2025, 8:04 AMJames Fellows Yates
08/13/2025, 8:06 AMI 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 aThat 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 😉key, and then map the reads to all the assemblies - you’d want to keep that assembler key in the output.meta.assembler
Arthur
08/13/2025, 8:07 AMnf-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 mixedArthur
08/13/2025, 8:08 AMJames Fellows Yates
08/13/2025, 8:08 AMIt’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 mixedYeah 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
Arthur
08/13/2025, 8:09 AMJames Fellows Yates
08/13/2025, 8:09 AMIf 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
James Fellows Yates
08/13/2025, 8:09 AMMaxime Garcia
08/13/2025, 8:09 AMJames Fellows Yates
08/13/2025, 8:10 AMJames Fellows Yates
08/13/2025, 8:10 AMMaxime Garcia
08/13/2025, 8:10 AMArthur
08/13/2025, 8:11 AMch_in = (sample1, sample1, sample2, sample2)
ch_ref = (ref1, ref2, ref1, ref2)
But get them actually processed as:
sample1, ref1
sample1, ref1
sample2, ref2
sample2, ref2Arthur
08/13/2025, 8:12 AMminimap2 allows you to safely align all of your samples against all of the references?Arthur
08/13/2025, 8:13 AMJames Fellows Yates
08/13/2025, 8:13 AMJames Fellows Yates
08/13/2025, 8:13 AMJim Downie
08/13/2025, 8:13 AMIf 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
Maxime Garcia
08/13/2025, 8:15 AMArthur
08/13/2025, 8:15 AMmultiMap guarantees channel order?James Fellows Yates
08/13/2025, 8:15 AMArthur
08/13/2025, 8:16 AMJames Fellows Yates
08/13/2025, 8:16 AMArthur
08/13/2025, 8:16 AMIt does (or should do)Ok, I always just assumed that no channel order could ever be guaranteed but happy to be wrong here
James Fellows Yates
08/13/2025, 8:17 AMJim Downie
08/13/2025, 8:18 AMJames Fellows Yates
08/13/2025, 8:18 AMArthur
08/13/2025, 8:18 AMJim Downie
08/13/2025, 8:19 AMArthur
08/13/2025, 8:42 AMmultiMap was implemented and I could imagine a scenario where parallel processing of the input channel would have allowed some weirdness.
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)Arthur
08/13/2025, 8:46 AMmultiMap anyway, I am even less inclined to think that the modules should handle injecting values into the meta.Jim Downie
08/13/2025, 9:46 AMmultiMap! 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!).James Fellows Yates
08/13/2025, 9:49 AMmeta entry (for the primary input channel) was simply because of joining afterwards?James Fellows Yates
08/13/2025, 9:52 AMJames Fellows Yates
08/13/2025, 9:52 AMJim Downie
08/13/2025, 9:52 AMJames Fellows Yates
08/13/2025, 9:53 AMJames Fellows Yates
08/13/2025, 9:54 AMJim Downie
08/13/2025, 9:55 AMJames Fellows Yates
08/13/2025, 9:55 AMJames Fellows Yates
08/13/2025, 9:55 AMJim Downie
08/13/2025, 9:55 AMJames Fellows Yates
08/13/2025, 9:56 AMJim Downie
08/13/2025, 9:56 AMJames Fellows Yates
08/13/2025, 9:56 AMJames Fellows Yates
08/13/2025, 9:56 AMJim Downie
08/13/2025, 9:57 AMJames Fellows Yates
08/13/2025, 9:57 AMJim Downie
08/13/2025, 9:57 AMJames Fellows Yates
08/13/2025, 9:58 AMJames Fellows Yates
08/13/2025, 9:58 AMJames Fellows Yates
08/13/2025, 10:01 AMArthur
08/13/2025, 10:03 AMThe less weird channel manipulations, the betterI 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
Jim Downie
08/13/2025, 10:03 AMJames Fellows Yates
08/13/2025, 10:04 AMJames Fellows Yates
08/13/2025, 10:05 AMArthur
08/13/2025, 10:21 AMJames Fellows Yates
08/13/2025, 10:22 AMJames Fellows Yates
08/13/2025, 10:22 AMArthur
08/13/2025, 10:22 AMJames Fellows Yates
08/13/2025, 10:26 AMJames Fellows Yates
08/13/2025, 10:26 AMArthur
08/13/2025, 10:28 AMeveryone else who deals with 'real' (messy) biologyI feel slighted, human cancer genomics can be messy too! 🥹
James Fellows Yates
08/13/2025, 10:29 AMJames Fellows Yates
08/13/2025, 10:29 AMJim Downie
08/14/2025, 1:45 PMMaxime Garcia
08/14/2025, 2:51 PMThiseas C. Lamnidis
08/15/2025, 8:52 AMmultiMap 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”.
/*
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. 🙂