Skip to content

v2.0.0: Refactor for sample-wise parameterisation#171

Open
nschan wants to merge 172 commits intonf-core:devfrom
nschan:refactor-assemblers
Open

v2.0.0: Refactor for sample-wise parameterisation#171
nschan wants to merge 172 commits intonf-core:devfrom
nschan:refactor-assemblers

Conversation

@nschan
Copy link
Collaborator

@nschan nschan commented Jul 9, 2025

Updated Feb 04 2026

As suggested here this is full refactor of genomeassembler to support sample-level parameterisation of everything.

Why?
Often when doing genome assembly, we do not know what works best. With this change, this pipeline can be used to compare different settings for the same set of reads, to compare the assembly outcome. Samples that share the same value in group will be combined during reporting and preprocessing to facilitate comparisons of strategies on the same input(s).

Per-sample parameterisation

Initially, I implemented this poorly, based largely around join()'ing various maps back to other maps. nextflow does not have a joinoperator for maps, so this was a big mess and turned out to be hard to read, annoying to write, and constantly blocking. To summarise: bad idea, cannot recommend. This attempt contributes to the large number of commits in this branch.

While contemplating my failure to implement sample-level parameterisation, I realised that the solution to this problem is "meta-stuffing", also referred to as "meta-smuggling" by @prototaxites, who seems to have arrived at a similar conclusion at around the same time.
In the purest form, this would implement everything in a single meta-map, which is in slot 0 of the channel traveling through the pipeline. I will use meta to refer to [0] of list-channels from here on.
Note: A pure meta-stuffing implementation would have required additional refactoring of some subworkflows, in particular of QC which takes more than one input channel, something I did not want to do.

How this works:
Everything goes into meta: params are turned into k/v pairs for each sample, unless the samplesheet contains a different value for that sample with the same key, resulting in a massive meta-map. values are pulled from meta as required for channel inputs, meta is recreated / updated from channel outputs. This largely enables flow-control at the sample level, when combined with branch, filter and related operators. In some cases, joins cannot be avoided (or would need to be traded for concurrent processes), but I tried to minimise them.
This means that the pipeline is essentially free of if { } statements, since flow control is done via channels. This also means that the pipeline DAG is always rendered in full, irrespective of whether the nodes will actually be visited by a sample. It might be possible to optimise DAG rendering by inspecting all created meta-maps and creating some global variables based on their content, to again conditionally include subworkflows, but this is beyond the scope of this refactor.

I made an effort to provide flexibility in combining assemblers, polishers, or scaffolding tools, as I thought it was reasonable, but this does not offer full-factorial combinations, which become especially tricky if things should happen in order.

Generally, I am very happy with this approach, it offers great flexibility, and is surprisingly nice to write once meta has been constructed. Given that global parameters can still be set, the samplesheet may or may not be kind of wide, for a single sample everything could be done via params and the only column in the samplesheet would be sample. In the most ridiculous case of setting all parameters differently for each sample, the samplesheet could grow to around 50 columns.

Grouping

Grouping is implemented as an extension of "meta-smuggling", essentially smuggling multiple meta maps through a channel (inside meta), replacing the value of meta.id with the group id.
Here is the relevant code for grouping and un-grouping while maintaining meta-maps:

    some_channel
         // Move group information into channel, if it exists
        .filter { it -> it.meta.group }
        .map { it -> [it.meta, it.meta.group, it.meta.ontreads] }
        // Group by group
        .groupTuple(by: 1)
        // Collect all sample-meta into a group meta slot named metas
        // Use unique reads; user responsible to group correctly
        .map {
            it ->
                [
                    [
                        id: it[1], // the group
                        metas: it[0]
                    ],
                    it[2].unique()[0] // Ontreads
                ]
        }

After this input channel has been processed, the samples are
recreated from meta[metas]:

    process.OUT
        // Take samples with metas in slot [0]
        .filter { it -> it[0].metas }
        .flatMap { it ->
            // $it looks like [meta, output_path]
            // recreate meta from metas and update path.
            it[0].metas
                  .collect { meta -> [
                                meta: meta - meta.subMap("ontreads") + [ontreads: it[1]]
                                ]
                            }
        }
        .mix(
            process.OUT
                .filter { it -> !it[0].metas }
        )

More

Since switching to meta-stuffing made things much easier, I have also added an HiC scaffolding subworkflow, and support for dorado polish (experimental, as dorado polish does not work reliably) in the polishing subworkflow

@nf-core-bot
Copy link
Member

nf-core-bot commented Jul 9, 2025

Warning

Newer version of the nf-core template is available.

Your pipeline is using an old version of the nf-core template: 3.5.1.
Please update your pipeline to the latest version.

For more documentation on how to update your pipeline, please see the nf-core documentation and Synchronisation documentation.

@nschan nschan changed the title Refactor for sample-wise parameterisation v2.0.0: Refactor for sample-wise parameterisation Sep 4, 2025
@nschan
Copy link
Collaborator Author

nschan commented Sep 4, 2025

This comment is outdated, and this information is now also in the PR text.

Since the initial start of this refactor, I noticed that when doing multiple assemblies from the same set of reads it is kind of a waste to send those reads through preprocessing multiple times. I also figured that assemblies from the same set of reads are likely to be compared to each other. To reduce redundant work and make comparisons easier, there is now a group parameters, that can be used to put samples using the same reads into a group. Note: putting samples with different inputs into the same group will give wrong results.
I also noticed over the course of refactoring that there is some information that simply needs to be passed to processes (mostly for config purposes), since fetching those values from params kind of defeats the idea of parameterising per-sample. Those get stuffed into meta as needed. Generally, I am storing all information in the channel-map during "transit" between processes. Overall, this results in a lot of not super-concise channel manipulation.
I have now run some more extensive tests, and overall the logic seems to work as intended.

Copy link
Contributor

@nvnieuwk nvnieuwk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi I've done my best but I found it pretty hard to read the code in this pipeline. so I can't approve this at this point... I've left a few comments. Here are some more tips to help with the readability:

  1. Don't use it in closures, try to set a variable name for each item in the channel entry instead (e.g. instead of .map { it -> ...} do .map { meta, file1, file2 -> ...}. This makes it easier for me to understand what is in the channel at that point and will make it easier for future you (and others) to work on the pipeline later.
  2. Use more clear variable names
  3. Try to put some more comments above big code blocks with a short explanation of what this piece of code is for. (Especially on harder to understand pieces of code).

But anyways, I'm still really impressed with what you've done here and this really will be a massive improvement to the pipeline!

nschan and others added 7 commits February 13, 2026 09:38
* Template update for nf-core/tools version 3.2.1

* Template update for nf-core/tools version 3.3.1

* merge template 3.3.1 - fix linting

* update pre-commit

* merge template 3.3.1 - fix linting

* pre-commit config?

* pre-commit config?

* reinstall links

* try larger runner

* smaller run, disable bloom filter for hifiasm test

* updated test snapshot

* updated test snapshot

* update nftignore

* update nftignore

* update nftignore

* update nftignore

* update nftignore

* update nftignore

* update nftignore

* update nftignore

* update nftignore

* Update .github/actions/nf-test/action.yml

Co-authored-by: Matthias Hörtenhuber <mashehu@users.noreply.github.com>

* Update docs/output.md

Co-authored-by: Matthias Hörtenhuber <mashehu@users.noreply.github.com>

* remove .nf-test.log

---------

Co-authored-by: Niklas Schandry <niklas@bio.lmu.de>
Co-authored-by: Matthias Hörtenhuber <mashehu@users.noreply.github.com>
@nschan nschan force-pushed the refactor-assemblers branch 2 times, most recently from 83c7f7a to 9d160c6 Compare February 18, 2026 15:11
@nschan nschan force-pushed the refactor-assemblers branch from 9d160c6 to 2c08e3a Compare February 18, 2026 15:22
@vagkaratzas vagkaratzas self-requested a review February 20, 2026 08:07
Copy link

@vagkaratzas vagkaratzas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The review will be coming in separate comments, ofc :P

  • What is this assets/report folder? Is there not a more nf-core place to put/execute those?
  • Need a workflow dark, and SVGs for both light and dark metro maps

// Read preparation
includeConfig 'modules/ont-prep.config'
includeConfig 'modules/hifi-prep.config'
includeConfig 'modules/trimgalore.config'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see the reason of splitting configs, but I still think I would prefer to have all the module configurations in one file. Will let you decide, and/or the second reviewer :P

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see why you would prefer this, but in practice, having a massive module configuration file turned out to be very hard for me manage and I spent a lot of time looking for specific configurations in there and kept introducing duplications etc. There are around 70 withName selectors across the config files.

@nschan
Copy link
Collaborator Author

nschan commented Feb 20, 2026

The review will be coming in separate comments, ofc :P

ok ;)

  • What is this assets/report folder? Is there not a more nf-core place to put/execute those?

assets/report contains the scripts for generating the report. If there is a better place to put them I am happy to put them somewhere else, but idk where.

  • Need a workflow dark, and SVGs for both light and dark metro maps

Good point, I will add those.

Copy link

@vagkaratzas vagkaratzas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another round. Hopefully another last one soon!


//fastplong_jsons.view { it -> "UNQIE JSONS: $it"}

REPORT( report_files,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing for multiqc? :O

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not using multiqc for reporting

Copy link

@vagkaratzas vagkaratzas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final one. There is no way that everything will be working as intended with all these changes (and strategies..!)
I would suggest creating nf-test for all local subworkflows and modules if they dont already have. But I wouldn't stop you from a release for that.
BUT! I think this is a great opportunity to have a track for the upcoming hackathon for beginners and not only, to write nf-tests for everything. Just a suggestion ;)

def args = task.ext.args ?: ''

"""
dorado aligner \\

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this hard to push to nf-core?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -14,7 +14,8 @@ process GENOMESCOPE {
tuple val(meta), path("*_plot.log.png") , emit: plot_log

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://nf-co.re/modules/genomescope2/
is on nf-core/modules, update and use that one, or maybe just patch it?

@@ -2,37 +2,27 @@ process GFA_2_FA {
tag "${meta.id}"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gfatools_gfa2fa also on nf-core modules

@@ -10,7 +10,8 @@ process COUNT {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://nf-co.re/modules/jellyfish_count/ also (I made that one >.<)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A, great, I wasn't aware that you made an nf-core module, happy to switch!

@@ -10,24 +10,16 @@ process DUMP {

output:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


ch_versions = ch_versions.mix(RUN_RAGTAG.out.versions)
}
channel.empty().set { links_busco }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the preferred nf-core way is

Suggested change
channel.empty().set { links_busco }
ch_links_busco = channel.empty()

instead of .set. But I guess too late for that now, given the number of files in teh PR xD

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is preferred, but not mandatory, I would like to keep it with .set. I know some people prefer channel = but its ugly :/


emit:
scaffolds
ch_main = ch_main_scaffolded

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main is not very self-explanatory as a name

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. I am not sure how to better name this, since the main channel does not necessarily contain all steps for all samples. I think I am using main to mean that this has been transformed correctly and that everything is ready to move back into the main workflow / next subworkflow.

only once, and then the original channel is restored.

Brief description how this works:
// Move group information into channel, if it exists

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too many comments and code there. I guess it should be deleted? The explanation should be enough I guess

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is mainly for reviewers and potential future contributors. If this is useless, I can remove it.

@nschan
Copy link
Collaborator Author

nschan commented Feb 20, 2026

Final one. There is no way that everything will be working as intended with all these changes (and strategies..!) I would suggest creating nf-test for all local subworkflows and modules if they dont already have. But I wouldn't stop you from a release for that. BUT! I think this is a great opportunity to have a track for the upcoming hackathon for beginners and not only, to write nf-tests for everything. Just a suggestion ;)

I would hope that everything works as intended and I did some larger tests before asking for reviews. Still it is likely that someone will try something that I did not think of; full_test tests a bunch of strategies. Having a track for this at the hackathon would be nice, but for me this overlaps with teaching obligations and I will not be able to participate myself. I am not sure how productive putting up a bunch of stuff without being involved would be?

@vagkaratzas
Copy link

Final one. There is no way that everything will be working as intended with all these changes (and strategies..!) I would suggest creating nf-test for all local subworkflows and modules if they dont already have. But I wouldn't stop you from a release for that. BUT! I think this is a great opportunity to have a track for the upcoming hackathon for beginners and not only, to write nf-tests for everything. Just a suggestion ;)

I would hope that everything works as intended and I did some larger tests before asking for reviews. Still it is likely that someone will try something that I did not think of; full_test tests a bunch of strategies. Having a track for this at the hackathon would be nice, but for me this overlaps with teaching obligations and I will not be able to participate myself. I am not sure how productive putting up a bunch of stuff without being involved would be?

I would ask the thematically closest persons / facility / nf-core slack channels for potential track leaders that would want to oversee this if they don't already have a project but feel confident enough with nf-tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants