Hi <!here>, I just added the last small changes to...
# wg-meta-pipelines
n
Hi <!here>, I just added the last small changes to the document explaining the new guidelines. Could some of you proofread this? hackmd.io/YmVPL55sTomtFIpNKlRKBA. (Thank you all for the input and help on the document πŸŽ‰)
πŸ‘€ 3
πŸ‘ 2
m
There are some small bugs like
meta.id
still being there. Going through and fixing
n
Ah i thought I replaced them all, my bad πŸ˜…
j
Looks good to me. Very excited for all these changes!
πŸ˜† 2
n
Thanks Jim 🀣
m
What to do with 5 + 6? Ideally we want proper naming, but what to do with this?
n
The common consensus was that this would be a problem to solve for another time πŸ˜…
Let's see how the naming will grow organically and we can still intervene if necessary
b
should work. in fact I think you could (1) load the samplesheet records without creating a meta map and (2) set
sample += [meta: sample]
(set the meta record to the sample record itself) since meta is typed as
Record
in the process, nextflow won't try to stage any files in it. it's a bit silly but it works 🀷
n
That's good to know! Thanks for confirming πŸ˜›
m
What collection type should multiple paths be?
List<Path>
,
Set<Path>
, or
Bag<Path>
etc?
b
any of those are valid. just depends on whether you care about ordering. Set vs Bag are usually interchangeable for files
m
Thanks. So then we say
List<Path>
or
Bag<Path>
for convenience/reference
n
Does the order of files impact the resumability of the process?
m
I would assume yes
4. The output record MUST follow this structure:
- The
id
field containing the input identifier from the input record
- The optional
meta
Record
from the input record and all other fields should be all possible outputs of the process
This last bullet point doesn't read well. I'm not quite sure what it means
j
β€œThe optional meta record can be used to supply sample-level metadata beyond the identifier β€˜id’. It must always be emitted in the output record.”
πŸ‘πŸ½ 1
n
Feel free to change the wording if you know a better way to explain it πŸ˜… (english is not my first language πŸ˜›)
m
It's mine, but I still suck at it
🀣 3
100 rainbow 1
n
That made me feel a bit better 🀣
b
Does the order of files impact the resumability of the process?
if you use a List then yes, for Bag and Set no
so basically you can decide whether you want ordering to matter
n
Okay thanks!
m
If we define an input field as
Set
but the input record has a
List
, is there an implicit type conversion ? And is caching done on the list or the set ? (thinking about the resumability)
b
β€’ you can convert List/Bag to Set with
toSet()
β€’ you can convert Set/Bag to List with
toSorted()
there is no implicit conversion. the type checker will flag it
πŸ‘ 1
πŸŽ‰ 1
a
Are we forcing just 1 input record? I know that's good practice but it feels very strict and the occasional record + value may be required.
m
Yes, I believe so
They'll have to be explicit in the inputs by joining channels
How are
env
and
stdout
types covered?
n
Yes this will also make the module way more flexible at almost zero cost. It will need a bit of a change in people's mindset on writing pipelines as everything will become sample-based (but that's a good thing IMO)
πŸ‘ 1
a
Are we allowed to use named records in the inputs?
Copy code
input:
MySuperSpecialRecord // instead of record()
as long as it's passed with the module, I guess? πŸ€”
n
We decided against it because of naming collisions with records
It's also more clear if the definition is stated in the input block
j
My feeling regarding collection types is that we should stick with Lists for the most part, as I would expect most tools have order-dependent outputs that users might want to control
a
We decided against it because of naming collisions with records
How so? Because SampleRecord may clash? Note unless you import it, a record is local to the file so I don't know how likely this is
b
How are
env
and
stdout
types covered?
typed process has
env()
and
stdout()
in the output block
πŸ‘πŸ½ 1
My feeling regarding collection types is that we should stick with Lists for the most part
that seems to be the more common case from what I've seen. I don't think I've needed a
Set<Path>
input yet a counterexample is multiqc, which takes a
Bag<Path>
since it collects all the upstream outputs with
collect
Adam is right, you shouldn't have any name conflicts as long as the record type stays in the module But you might want to just enforce one style for now, for consistency, and consider opening it up in the future if a good use case arises
n
Ah I must have missremembered that records could only occur once in a pipeline. What advantages would having named records have for you @Adam Talbot?
a
Looks much cleaner, less record boiler plate in the process. Named inputs, so it's easier to know what you're referring to (although if you force
input.size == 1
that's less of a concern).
Copy code
// private record type
record MySampleRecord {
    id: String
    // stuff
}

process USES_MYSAMPLERECORD {
    input:
        samples: MySampleRecord

    script:
    """
    echo 'Hello ${samples.id}!'
    """
}
I'm gonna say something bold...do we need
id
as a required field? If I want to refer to a sample as
sampleId + treatment + replicate
, I can do that now with a meta record πŸ‘€ or too much too soon πŸ˜…
n
the common consensus seemed to be that an anonymous record in the input block was cleaner and easier to read than a record declaration somewhere else in the process. another downside is the input variable names are longer (
samples.id
in your case,
id
if you used anonymous records)
'm gonna say something bold...do we need
id
as a required field? If I want to refer to a sample as
sampleId + treatment + replicate
Couldn't you just set the id field to this combination?
a
I hate embedding meta data in IDs
πŸ’― 1
j
I have a question actually, because this so far only touches on typed processes but not subworkflows. Can you type subworkflows anonymously with records, or do we need to make named types?
b
Can you type subworkflows anonymously with records, or do we need to make named types?
subworkflows should use record types
j
Not read the chat yet but while reading through: > (this includes the id field or any other required meta fields like single_end if needed) > I would use this chance to drop all meta fields except
id
. As in `id`is the only standard/hardcoded/required one. Everything else is up to pipeline dev. Everything to control the module should come via as an 'input'.
πŸ‘ 1
party this 2
j
subworkflows should use record types
Yes, but do they need to take a named type in order to enforce contents, or can we pass an anonymous function like we do with process inputs?
n
I would use this chance to drop all meta fields
that's exactly what we are doing, even
id
is technically no longer required in meta and it's become fully optional
I hate embedding meta data in IDs
How else would you do this in modules without breaking the flexibility?
j
I would use this chance to drop all meta fields
that's exactly what we are doing, even id is technically no longer required in meta and it's become fully optional Right. But the first part of my message was a quote from the text. Can I remove 'any other required meta fields like single_end' then?
meow yes 1
n
yes! they should be required input values as they are required for the process to run
πŸ‘ 1
Meta is fully reserved now for optional fields (and for pipeline devs to pass along meta data that has no effect on the process itself)
party this 1
j
This bit then is also not valid right? Essentially meta is just for passing through the module (it's only used for pipeline logic, essentially)? Everything for module logic should come via top level inputs, Correct?
n
Good point, that must have been a leftover from a previous version. i'll update it
❀️ 1
Is this better?
Copy code
The `meta` record. This record is used to pass meta data from the input of the module to the output of the module. Content of this record MUST never be used in the process code (but it can however be used in `ext` configuration options by the pipeline developer)
πŸ‘ 1
a
> How else would you do this in modules without breaking the flexibility?
Copy code
record SampleInfo {
    id: String
    treatment: Treatment
    replicate: Int?
}

process DO_STUFF {
    input:
    record(
        meta: SampleInfo,
        bam: File,
        bai: File
    )

    script:
    def prefix = ext.prefix ?: meta.findAll { it?.trim() }.join('_')
    // etc
}
Sorry, I may be going too far here πŸ˜† probably best to keep it consistent with ye olde nf-core
n
Yeah... πŸ˜…
i'd prefer to keep that in pipeline code since it's dependent on the specific pipeline
party this 1
πŸ‘ 1
b
Yes, but do they need to take a named type in order to enforce contents, or can we pass an anonymous function like we do with process inputs?
you can pass an anonymous record. the type checker will verify that it satisfies the record type
>
Copy code
The `meta` record. This record is used to pass meta data from the input of the module to the output of the module. Content of this record MUST never be used in the process code (but it can however be used in `ext` configuration options by the pipeline developer)
I would only allow
ext
to be used by a pipeline user at runtime. The developer should never need it because they can do it with workflow logic instead (see #626). It's also easier to compose a pipeline if you don't have all the
ext
/
publishDir
config off to the side
n
Right, that's also a new guideline that slipped my mind. i'll rephrase
b
that also seems to be the only remaining purpose of the meta map, to allow for runtime override via
ext
n
Yes! And also to help pipeline developers with more complex joins
a
It's also for passing metadata through a pipeline, without having to do a separate -> process -> join loop at every step
πŸ‘ 2
j
It's also for passing metadata through a pipeline, without having to do a separate -> process -> join loop at every step
Which could also be avoided if something like this was implemented in Nextflow, which would remove the need for
meta
entirely https://nfcore.slack.com/archives/C07ACNLELR2/p1792161665296959
j
I missed this mega-thread in my catchup yesterday! FWIW @Adam Talbot my initial prototype had all the named record type setup (I still find that nicer), but we stripped it in favour of anonymous inline definitions so as not to scare the horses more than we needed.
booo 1
Thank you Nicolas for the heroic wrangling!
party this 1
πŸŽ‰ 1
n
Thank you! But it wasn't a one-man show. Thanks to all of you for participating and sharing opinions on this subject. Our approach wouldn't have been nearly as good otherwise πŸ˜›
I'll try to get a draft of the blog post done some time by next week
a
am I allowed to use named records inside the input record at least?
Copy code
input:
record(
    id: String,
    thingy: Thingy, // special thingy type
    bam: Path,
    bai: Path?
)
n
In what case would you use that? The new guidelines state that all required values should in the top-level of the record
sadpanda 1
a
One of the advantages of records is the ability to model your data properly. Flattening it every time removes a big part of that advantage. Here's a more obvious example:
Copy code
input:
record(
    id: String,
    thingy: List[Thingy], // list of thingy
    bam: Path,
    bai: Path?
)
Where thingy might have multiple useful things. If we flatten, we get:
Copy code
input:
record(
    id: String,
    part_1_of_thingy: List[String],
    part_2_of_thingy: List[Int],
    part_3_of_thingy: List[Path],    // Hope these guys are ordered correctly!
    bam: Path,
    bai: Path?
)
n
Yes, but how would we ever agree on such a structure in nf-core? πŸ˜…
a
a realistic example might be:
Copy code
record Vcf {
    id: String,
    vcf: Path,
    tbi: Path?
}

...
process COMBINE_VCFS {

    input:
    record(
        id: String,
        vcfs: List[Vcf]
    )
}
how would we ever agree on such a structure in nf-core?
1. Long arguments on Slack 2. Overly verbose and unproductive pull reviews
🀣 2
j
I agree that's a nice approach, and it works well if you have tight integration between the pipeline and the modules. But nf-core modules need to be quite generic, which decouples them from the pipeline context. So such data structures can only really be the domain of a pipeline. And it adds questions about how and where Vcf is defined, does it need to be imported if it's shared among modules, or are there as many Vcf implementations as there are modules that use it?
party this 3
a
where Vcf is defined
In the same file, above the process
does it need to be imported
No
if it's shared among modules
No, they're just compatibility checks. They're closer to interfaces not types.
are there as many Vcf implementations as there are modules that use it
yep. that's fine. doesn't impact anything really.
j
doesn't impact anything really
That's fine until for one reason or another a particular module uses different field names, or extra field names, etc., right? And then what seemed clean is a bit more complicated and less predictable - you need to read the manual every time
a
File 1:
Copy code
record Vcf {
    id: String,
    vcf: Path,
    tbi: Path?
}

...
process COMBINE_VCFS {

    input:
    record(
        id: String,
        vcfs: List[Vcf]
    )
}
File 2:
Copy code
record CompletelyDifferentVcfType {
    id: String,
    vcf: Path,
    tbi: Path?
}

...
process OTHER_VCF_PROCESS {

    input:
    record(
        id: String,
        vcf: CompletelyDifferentVcfType
    )
}
workflow:
Copy code
include { COMBINE_VCFS      } from '<http://combine_vcfs.nf|combine_vcfs.nf>'
include { OTHER_VCF_PROCESS } from '<http://other_vcf_process.nf|other_vcf_process.nf>' // No type imports!

workflow {
   COMBINE_VCFS( ch_vcf.collect() ) // I know this doesn't work but I'm being quick!
   OTHER_VCF_PROCESS( ch_vcf )
}
until for one reason or another a particular module uses different field names, or extra field names, etc., right?
Different is still a problem with flat record types? you're gonna have to
.map.map.map
every other line. Extra don't matter, they get ignored.
j
I mean extra required fields
Different is obviously still a problem, but at least in a flat record there's only one place you have to do a lookup to see what's required
Also, does the above actually work? It's not clear from what Ben said here if the files in
Vcf
would be staged? https://nfcore.slack.com/archives/C07ACNLELR2/p1792409303837149?thread_ts=1792408042.634609&amp;cid=C07ACNLELR2
meow yes 1
m
If one can settle on the record names, it means we can also settle @James Fellows Yates variable naming issue too
a
Also, does the above actually work? It's not clear from what Ben said here if the files in
Vcf
would be staged?
yes. Ben is referring to a bare record where nothing is declared as a
Path
πŸ‘ 1
b
when it comes to file staging: β€’ nested
Record
is ignored because nextflow can't see the types in it β€’ nested record type like
Vcf
will have its path fields staged (
Path
and
Path
collections) β€’ a collection of record types like
List<Vcf>
is not staged automatically. you need an explicit stageAs:
Copy code
input:
record(
    id: String,
    vcfs: List<Vcf>
)

stage:
stageAs vcfs.collect { v -> v.vcf }, '*'
stageAs vcfs.collect { v -> v.tbi }.findAll { tbi -> tbi != null }, '*'
πŸ‘ 2
🀨 1
I never implemented this case because I was too scared. But now with claude I fear nothing, so I can look into it for 27.04
πŸ˜† 2
🦸 1
claude deal with it 1
nextflow can do this. it might even be eligible for 26.10 as a bug fix. I'm just trying to figure out if it's the right move or too much magic
a
b
that PR was merged, so you will be able to do paths / path collections / records / record collections and nest these to your heart's desire. nextflow will stage every declared Path it finds from these