Validation Package insdc_curation

Language: F#

INSDC; Metadata curation

v0.0.1 released on 06/30/2026

by Kevin Schneider

158 Downloads

Validates certain INSDC record curation metadata. - fastqc files - experiment accession annotation is present and correct - descends from a correctly named sample


Install with arc-validate

 arc-validate package install insdc_curation --version 0.0.1

Include in a PLANTDataHUB CQC pipeline

validation_packages:
  - name: insdc_curation
    version: 0.0.1

Description

Validates certain INSDC record curation metadata.


Release notes

- fastqc process graph validation


Browse code for v0.0.1 (fsharp)
let [<Literal>]PACKAGE_METADATA = """(*
---
Name: insdc_curation
Description: Validates certain INSDC record curation metadata.
Summary: |
  Validates certain INSDC record curation metadata.
    - fastqc files
      - experiment accession annotation is present and correct
      - descends from a correctly named sample
MajorVersion: 0
MinorVersion: 0
PatchVersion: 1
Publish: true
Authors:
  - FullName: Kevin Schneider
    Affiliation: DataPLANT
Tags:
  - Name: INSDC
  - Name: Metadata curation
ReleaseNotes: |
  - fastqc process graph validation
---
*)"""

#r "nuget: ARCtrl"
#r "nuget: ARCtrl.QueryModel, 3.0.0-alpha.4"
#r "nuget: ARCExpect.Core, 7.0.0-alpha"
#r "nuget: FsHttp"
#r "nuget: FSharpAux"
#r "nuget: FSharpAux.IO"

open ARCtrl
open ARCtrl.QueryModel
open ARCExpect
open FsHttp
open FSharpAux.IO.SchemaReader
open Expecto
open System.IO
open System

let arcDir = Directory.GetCurrentDirectory()

let arc =
    try ARC.load arcDir with
    | _ -> ARC(identifier = "unable to load arc from this dir")

let project_accession = arc.Identifier

module Domain =

    open FSharpAux.IO.SchemaReader.Attribute
    open System.Collections.Generic

    /// represents the relations provided by ENA portal API in TSV form
    type INSDC_Relations = {
        [<FieldAttribute("study_accession")>]
        study_accession: string
        [<FieldAttribute("sample_accession")>]
        sample_accession: string
        [<FieldAttribute("experiment_accession")>]
        experiment_accession: string
        [<FieldAttribute("run_accession")>]
        run_accession: string
        [<FieldAttribute("fastq_ftp")>]
        fastq_ftp: string
    }

    /// Some records map from sample to multiple fastq files in a single line (e.g., paired end reads). This function splits such records into multiple records, one for each fastq file.
    let splitByFastq (r: INSDC_Relations) =
        match r.fastq_ftp.Split(';') |> Array.filter (fun s -> s <> "") with
        | [||]  -> [ r ]                                        // no fastq → keep row as-is
        | files -> [ for f in files -> { r with fastq_ftp = f } ]

    /// lookup index for retrieving INSDC_Relations by any of the accession numbers or fastq file name. Last-wins in case of duplicates.
    let buildIndex (records: INSDC_Relations seq) =
        let d = Dictionary<string, INSDC_Relations>(StringComparer.OrdinalIgnoreCase)
        for r in records do
            for v in [ 
                r.study_accession
                r.sample_accession
                r.experiment_accession
                r.run_accession
                r.fastq_ftp 
            ] do
                if not (String.IsNullOrEmpty v) then d[v] <- r   // last-wins
        d

    module OntologyTerms = 
        let experiment_accession_term =
            OntologyAnnotation.fromTermAnnotation(
                "NCIT:C175892",
                name = "Experiment Accession Number"
            )

module ExpectedData =

    let reader = new Csv.CsvReader<Domain.INSDC_Relations>(SchemaMode=Csv.Fill)

    let relations = 
        try
            http {
                GET $"https://www.ebi.ac.uk/ena/portal/api/filereport?accession={project_accession}&result=read_run&fields=study_accession,sample_accession,experiment_accession,run_accession,tax_id,scientific_name,fastq_ftp,submitted_ftp,bam_ftp&format=tsv&download=true&limit=0"
            }
            |> Request.send
            |> Response.toText
            |> fun response -> reader.ReadFromString(response, '\t', firstLineHasHeader = true)
            |> Seq.collect Domain.splitByFastq
            |> List.ofSeq
        with
            | _ -> []

    let relations_index = Domain.buildIndex relations

    let tryFindRelation (s: string) =
        match relations_index.TryGetValue s with
        | true, r  -> Some r
        | false, _ -> None


let create_insdc_relation_validation_cases_for_fastq_file (node: QNode) =

    let samples =
        arc.ArcTables.SamplesOf(node)
        |> Seq.map (fun n -> n.Name)
        |> Array.ofSeq

    let experiment_accession_actual =
        try 
            Some (arc.PreviousParametersOf(node)).[Domain.OntologyTerms.experiment_accession_term]
        with
            | _ -> None

    let expected_relations = Expect.wantSome (ExpectedData.tryFindRelation node.Name) "Experiment accession annotation is missing in INSDC relations" 

    testList node.Name [

        test $"has correct experiment accession annotation (expected: {expected_relations.experiment_accession})" {
            let actual = Expect.wantSome experiment_accession_actual "Experiment accession annotation is missing"
            Expect.equal actual.ValueText expected_relations.experiment_accession "Experiment accession annotation does not match node name"
        }

        test $"descends from correct sample (expected: {expected_relations.sample_accession})" {
            Expect.contains samples expected_relations.sample_accession $"Sample accession {expected_relations.sample_accession} not found in ARC"
        }
    ]

let fastqc_cases =
    testList "fastqc" (
        arc.TryGetAssay("sequencing")
        |> Option.map (fun assay ->
            assay.LastData
            |> Seq.map (fun node -> create_insdc_relation_validation_cases_for_fastq_file node)
            |> Seq.toList
        ) 
        |> Option.defaultValue []
    )

Setup.ValidationPackage(
    metadata = Setup.Metadata(PACKAGE_METADATA, AVPRIndex.Frontmatter.FSharpFrontmatter),
    CriticalValidationCases = [
        test "has sequencing assay" {
            Expect.isSome (arc.TryGetAssay("sequencing")) "No sequencing assay found"
        }
        fastqc_cases
    ]
)
|> Execute.ValidationPipeline(
    basePath = arcDir
)

Available versions
Version Released on
0.0.1 06/30/2026