Validation Package ceplas-02-biometadata

Language: F#

ceplas; study; assay; quality-arc

v1.0.0 released on 08/20/2026

by Dominik Brilhaus; Heinrich Lukas Weil

5 Downloads

Validates the ARC's "biological" metadata.


Install with arc-validate

 arc-validate package install ceplas-02-biometadata --version 1.0.0

Include in a PLANTDataHUB CQC pipeline

validation_packages:
  - name: ceplas-02-biometadata
    version: 1.0.0

Description

## Critical quality criteria

- ARC contains at least one study or assay or workflow or run

- ARC contains any annotation column (Characteristic, Parameter, Factor)

- Every study contains at least one annotation table

- Every study annotation table contains basic information

- Every study annotation table column contains values

- Every study annotation table row contains values

- Every assay contains at least one annotation table

- Every assay annotation table contains basic information

- Every assay annotation table column contains values

- Every assay annotation table row contains values

- Every run contains at least one annotation table

- Every run annotation table contains basic information

- Every run annotation table column contains values

- Every run annotation table row contains values

## Non-critical quality criteria

- Every study contains a title

- Every study contains a description

- Every study contains contacts

- Every assay contains a title

- Every assay contains a description

- Every assay contains performers

- Every assay contains a measurement type

- Every assay contains a technology type

- Every assay contains a technology platform

- Every workflow contains a title

- Every workflow contains a description

- Every workflow contains contacts

- Every run contains a title

- Every run contains a description

- Every run contains performers

- Every run contains a measurement type

- Every run contains a technology type

- Every run contains a technology platform

- Every annotation table contains some annotation column


Release notes

Release leveled validation packages.


Browse code for v1.0.0 (fsharp)
let [<Literal>]PACKAGE_METADATA = """(*
---
Name: ceplas-02-biometadata
Summary: Validates the ARC's "biological" metadata.
Description: |
        ## Critical quality criteria
        - ARC contains at least one study or assay or workflow or run
        - ARC contains any annotation column (Characteristic, Parameter, Factor)
        - Every study contains at least one annotation table
        - Every study annotation table contains basic information            
        - Every study annotation table column contains values
        - Every study annotation table row contains values
        - Every assay contains at least one annotation table
        - Every assay annotation table contains basic information            
        - Every assay annotation table column contains values
        - Every assay annotation table row contains values
        - Every run contains at least one annotation table
        - Every run annotation table contains basic information            
        - Every run annotation table column contains values
        - Every run annotation table row contains values

        ## Non-critical quality criteria
        - Every study contains a title
        - Every study contains a description
        - Every study contains contacts
        - Every assay contains a title
        - Every assay contains a description
        - Every assay contains performers
        - Every assay contains a measurement type
        - Every assay contains a technology type
        - Every assay contains a technology platform
        - Every workflow contains a title
        - Every workflow contains a description
        - Every workflow contains contacts
        - Every run contains a title
        - Every run contains a description
        - Every run contains performers
        - Every run contains a measurement type
        - Every run contains a technology type
        - Every run contains a technology platform
        - Every annotation table contains some annotation column

MajorVersion: 1
MinorVersion: 0
PatchVersion: 0
Publish: true
Authors:
  - FullName: Dominik Brilhaus
    Email: brilhaus@hhu.de
    Affiliation: CEPLAS
    AffiliationLink: https://ceplas.eu
  - FullName: Heinrich Lukas Weil
    Email: weil@nfdi4plants.org
    Affiliation: RPTU Kaiserslautern
    AffiliationLink: http://rptu.de/startseite
Tags:
  - Name: ceplas
  - Name: study
  - Name: assay
  - Name: quality-arc
ReleaseNotes: |
    Release leveled validation packages.
---
*)"""

#r "nuget: ARCExpect.Core, 7.0.0-alpha"
#r "nuget: ARCtrl.QueryModel, 3.0.0-alpha.4"
#r "nuget: Fable.SimpleHttp"

open ARCtrl
open ARCtrl.QueryModel
open Expecto
open ARCExpect
open System.IO



let hasAnnotationColumns (a: ARC)=
    a.ArcTables
    |> Seq.exists (fun t ->
        t.Columns
        |> Seq.exists (fun c ->
            c.Header.isCharacteristic || 
            c.Header.isParameter|| 
            c.Header.isFactor
        ))

let characteristicCount (t : ArcTable)=
    t.Columns
    |> Seq.filter (fun c -> c.Header.isCharacteristic)
    |> Seq.length

let parameterCount (t : ArcTable)=
    t.Columns
    |> Seq.filter (fun c -> c.Header.isParameter)
    |> Seq.length
    
let factorCount (t : ArcTable)=
    t.Columns
    |> Seq.filter (fun c -> c.Header.isFactor)
    |> Seq.length

let isEmptyAnnoTable (t : ArcTable) =
    t.ISAValues
    |> Seq.forall (fun kv -> not kv.Value.HasValue)

let emptyAnnoCols (t : ArcTable) =
    t.ISAValues
    |> Seq.groupBy (fun kv -> kv.Value.NameText)
    |> Seq.choose (fun (header, values) ->
        if values |> Seq.forall (fun kv -> not kv.Value.HasValue) then
            Some header
        else
            None
    )


let isEmptyCell (cell: CompositeCell) =
    cell.isFreeText && cell.AsFreeText = ""
    ||
    cell.isTerm && cell.AsTerm.NameText = ""
    ||
    cell.isUnitized && fst cell.AsUnitized = ""
    ||
    cell.isData && cell.AsData.NameText = ""

let emptyAnnoRows (t: ArcTable) =
    [0 .. t.RowCount - 1]
    |> Seq.choose (fun rowIndex ->
        let row = t.GetRow rowIndex

        if row |> Seq.forall isEmptyCell then
            Some rowIndex
        else
            None
    )



// Input:

let arcDir = Directory.GetCurrentDirectory()

////////////////////////

let arc =
    try ARC.load arcDir with
    | _ -> ARC(identifier = "placeholder")

arc.MakeDataFilesAbsolute()
arc.DataContextMapping()

// Validations

let criticalCases =     
    testList "criticalCases" [

    ////////////////////////////////////
    ////// ARC Study + Assay
    ////////////////////////////////////    
    
    // TestCase Critical: ARC contains at least one study or assay or workflow or run

    testCase "ARC contains at least one study or assay or workflow or run" <| fun _ ->

        Expect.isGreaterThan (arc.StudyCount + arc.AssayCount + arc.WorkflowCount + arc.RunCount) 0
            "ARC does not contain any study or assay or workflow or run"

    // TestCase Critical: ARC contains any annotation column (Characteristic, Parameter, Factor)

    testCase "ARC contains any annotation column (Characteristic, Parameter, Factor)" <| fun _ ->
        Expect.isTrue (hasAnnotationColumns arc)
            "ARC contains no annotation column (Characteristic, Parameter, Factor)"

    for s in arc.Studies do
        
        // TestCase Critical: Every study contains at least one annotation table
        testCase $"Study '{s.Identifier}' contains annotation table" <| fun _ ->
            
            Expect.isGreaterThan s.TableCount 0 
                $"Study '{s.Identifier}' contains no annotation table"
        
        for t in s.Tables do

            let tableAssociationText = $"Table '{t.Name}' of study '{s.Identifier}'"

            // TestCase Critical: Every study annotation table contains basic information            
                
            testCase $"{tableAssociationText} contains basic information" <| fun _ ->
                Expect.isFalse (isEmptyAnnoTable t)
                    $"{tableAssociationText} is empty"
                Expect.isGreaterThanOrEqual t.ColumnCount 2
                    $"{tableAssociationText} contains less than 2 columns"
                Expect.isGreaterThan t.RowCount 0
                    $"{tableAssociationText} contains no rows"

            if not (isEmptyAnnoTable t) then
                
                // TestCase Critical: Every study annotation table column contains values
                let emptyCols = emptyAnnoCols t
                let headers = String.concat ", " emptyCols

                testCase $"{tableAssociationText}: All columns contain values" <| fun _ ->
                    Expect.isTrue (emptyCols |> Seq.isEmpty)
                        $"{tableAssociationText} contains empty column(s): {headers}"            
             
                // TestCase Critical: Every study annotation table row contains values
                let emptyRows = emptyAnnoRows t
                let rowIds = emptyRows |> Seq.map string |> String.concat ", "

                testCase $"{tableAssociationText}: All rows contain values" <| fun _ ->
                    Expect.isTrue (emptyRows |> Seq.isEmpty)
                        $"{tableAssociationText} contains empty row(s): {rowIds}"

    for a in arc.Assays do
        
        // TestCase Critical: Every assay contains at least one annotation table
        testCase $"Assay '{a.Identifier}' contains annotation table" <| fun _ ->
            
            Expect.isGreaterThan a.TableCount 0
                $"Assay '{a.Identifier}' contains no annotation table"

        for t in a.Tables do

            let tableAssociationText = $"Table '{t.Name}' of assay '{a.Identifier}'"

            // TestCase Critical: Every assay annotation table contains basic information            
                
            testCase $"{tableAssociationText} contains basic information" <| fun _ ->
                Expect.isFalse (isEmptyAnnoTable t)
                    $"{tableAssociationText} is empty"
                Expect.isGreaterThanOrEqual t.ColumnCount 2
                    $"{tableAssociationText} contains less than 2 columns"
                Expect.isGreaterThan t.RowCount 0
                    $"{tableAssociationText} contains no rows"

            if not (isEmptyAnnoTable t) then
                
                // TestCase Critical: Every assay annotation table column contains values
                let emptyCols = emptyAnnoCols t
                let headers = String.concat ", " emptyCols

                testCase $"{tableAssociationText}: All columns contain values" <| fun _ ->
                    Expect.isTrue (emptyCols |> Seq.isEmpty)
                        $"{tableAssociationText} contains empty column(s): {headers}"            
             
                // TestCase Critical: Every assay annotation table row contains values
                let emptyRows = emptyAnnoRows t
                let rowIds = emptyRows |> Seq.map string |> String.concat ", "

                testCase $"{tableAssociationText}: All rows contain values" <| fun _ ->
                    Expect.isTrue (emptyRows |> Seq.isEmpty)
                        $"{tableAssociationText} contains empty row(s): {rowIds}"
                    
    for r in arc.Runs do
        
        // TestCase Critical: Every run contains at least one annotation table
        testCase $"Run {r.Identifier} contains annotation table" <| fun _ ->
            Expect.isGreaterThan r.TableCount 0
                $"Run {r.Identifier} contains no annotation table"
        
        for t in r.Tables do
                
            let tableAssociationText = $"Table '{t.Name}' of run '{r.Identifier}'"

            // TestCase Critical: Every run annotation table contains basic information            
                
            testCase $"{tableAssociationText} contains basic information" <| fun _ ->
                Expect.isFalse (isEmptyAnnoTable t)
                    $"{tableAssociationText} is empty"
                Expect.isGreaterThanOrEqual t.ColumnCount 2
                    $"{tableAssociationText} contains less than 2 columns"
                Expect.isGreaterThan t.RowCount 0
                    $"{tableAssociationText} contains no rows"

            if not (isEmptyAnnoTable t) then
                
                // TestCase Critical: Every run annotation table column contains values
                let emptyCols = emptyAnnoCols t
                let headers = String.concat ", " emptyCols

                testCase $"{tableAssociationText}: All columns contain values" <| fun _ ->
                    Expect.isTrue (emptyCols |> Seq.isEmpty)
                        $"{tableAssociationText} contains empty column(s): {headers}"            
             
                // TestCase Critical: Every run annotation table row contains values
                let emptyRows = emptyAnnoRows t
                let rowIds = emptyRows |> Seq.map string |> String.concat ", "

                testCase $"{tableAssociationText}: All rows contain values" <| fun _ ->
                    Expect.isTrue (emptyRows |> Seq.isEmpty)
                        $"{tableAssociationText} contains empty row(s): {rowIds}"

    ]
    

let nonCriticalCases =
    testList "nonCriticalCases" [

    /////////////////////////////////////////////////////////////////
    ////// ARC Study top level metadata
    /////////////////////////////////////////////////////////////////
        
    for s in arc.Studies do
        
        // TestCase Non-critical: Every study contains a title
        testCase $"Study '{s.Identifier}' contains title" <| fun _ ->
            // Study title exists
            Expect.isSome s.Title
                $"Study '{s.Identifier}' contains no title"
            // Study title is longer than 3 characters
            Expect.isGreaterThan s.Title.Value.Length 4
                $"Study '{s.Identifier}' contains no meaningful title (i.e. longer than 3 characters):'{s.Title.Value}'"
        
        // TestCase Non-critical: Every study contains a description
        testCase $"Study '{s.Identifier}' contains description" <| fun _ ->
            // Study description exists
            Expect.isSome s.Description
                $"Study '{s.Identifier}' contains no description"
            // Study description is longer than 30 characters
            Expect.isGreaterThan s.Description.Value.Length 30
                $"Study '{s.Identifier}' contains no meaningful description (i.e. longer than 30 characters):'{s.Description.Value}'"

        // TestCase Non-critical: Every study contains contacts
        testCase $"Study '{s.Identifier}' contains contacts" <| fun _ ->
            Expect.isGreaterThan s.Contacts.Count 0
                $"Study '{s.Identifier}' contains no contacts"
    
    /////////////////////////////////////////////////////////////////
    ////// ARC Assay top level metadata
    /////////////////////////////////////////////////////////////////

    for a in arc.Assays do

        // TestCase Non-critical: Every assay contains a title
        testCase $"Assay '{a.Identifier}' contains title" <| fun _ ->
            // Assay title exists
            Expect.isSome a.Title
                $"Assay '{a.Identifier}' contains no title"
            // Assay title is longer than 4 characters
            Expect.isGreaterThan a.Title.Value.Length 4
                $"Assay '{a.Identifier}' contains no meaningful title (i.e. longer than 3 characters):'{a.Title.Value}'"
        
        // TestCase Non-critical: Every assay contains a description
        testCase $"Assay '{a.Identifier}' contains description" <| fun _ ->
            // Assay description exists
            Expect.isSome a.Description
                $"Assay '{a.Identifier}' contains no description"
            // Assay description is longer than 30 characters
            Expect.isGreaterThan a.Description.Value.Length  30
                $"Assay '{a.Identifier}' contains no meaningful description (i.e. longer than 30 characters):'{a.Description.Value}'"

        // TestCase Non-critical: Every assay contains performers
        testCase $"Study '{a.Identifier}' contains contacts" <| fun _ ->
            Expect.isGreaterThan a.Performers.Count 0
                $"Study '{a.Identifier}' contains no performers"

        // TestCase Non-critical: Every assay contains a measurement type
        testCase $"Assay '{a.Identifier}' contains top-level metadata measurement type" <| fun _ ->
            Expect.isSome a.MeasurementType
                $"Assay '{a.Identifier}' contains no top-level metadata measurement type"
        
        // TestCase Non-critical: Every assay contains a technology type
        testCase $"Assay '{a.Identifier}' contains top-level metadata technology type" <| fun _ ->
            Expect.isSome a.TechnologyType
                $"Assay '{a.Identifier}' contains no top-level metadata technology type"
        
        // TestCase Non-critical: Every assay contains a technology platform
        testCase $"Assay '{a.Identifier}' contains top-level metadata technology platform" <| fun _ ->
            Expect.isSome a.TechnologyPlatform
                $"Assay '{a.Identifier}' contains no top-level metadata technology platform"

    /////////////////////////////////////////////////////////////////
    ////// ARC Workflow top level metadata
    /////////////////////////////////////////////////////////////////

    for w in arc.Workflows do

        // TestCase Non-critical: Every workflow contains a title
        testCase $"Workflow '{w.Identifier}' contains title" <| fun _ ->
            // Workflow title exists
            Expect.isSome w.Title
                $"Workflow '{w.Identifier}' contains no title"
            // Workflow title is longer than 4 characters
            Expect.isGreaterThan w.Title.Value.Length 3
                $"Workflow '{w.Identifier}' contains no meaningful title (i.e. longer than 3 characters):'{w.Title.Value}'"
        
        // TestCase Non-critical: Every workflow contains a description
        testCase $"Workflow '{w.Identifier}' contains description" <| fun _ ->
            // Workflow description exists
            Expect.isSome w.Description
                $"Workflow '{w.Identifier}' contains no description"
            // Workflow description is longer than 30 characters
            Expect.isGreaterThan w.Description.Value.Length 30
                $"Workflow '{w.Identifier}' contains no meaningful description (i.e. longer than 30 characters):'{w.Description.Value}'"
        
        // TestCase Non-critical: Every workflow contains contacts
        testCase $"Workflow '{w.Identifier}' contains contacts" <| fun _ ->
            Expect.isGreaterThan w.Contacts.Count 0
                $"Workflow '{w.Identifier}' contains no contacts"

    /////////////////////////////////////////////////////////////////
    ////// ARC Run top level metadata
    /////////////////////////////////////////////////////////////////

    for r in arc.Runs do

        // TestCase Non-critical: Every run contains a title
        testCase $"Run '{r.Identifier}' contains title" <| fun _ ->
            // Run title exists
            Expect.isSome r.Title
                $"Run '{r.Identifier}' contains no title"
            // Run title is longer than 4 characters
            Expect.isGreaterThan r.Title.Value.Length 3
                $"Run '{r.Identifier}' contains no meaningful title (i.e. longer than 3 characters):'{r.Title.Value}'"
        
        // TestCase Non-critical: Every run contains a description
        testCase $"Run '{r.Identifier}' contains description" <| fun _ ->
            // Run description exists
            Expect.isSome r.Description
                $"Run '{r.Identifier}' contains no description"
            // Run description is longer than 30 characters
            Expect.isGreaterThan r.Description.Value.Length 30
                $"Run '{r.Identifier}' contains no meaningful description (i.e. longer than 30 characters):'{r.Description.Value}'"

        // TestCase Non-critical: Every run contains performers
        testCase $"Run '{r.Identifier}' contains contacts" <| fun _ ->
            Expect.isGreaterThan r.Performers.Count 0
                $"Run '{r.Identifier}' contains no performers"

        // TestCase Non-critical: Every run contains a measurement type
        testCase $"Run '{r.Identifier}' contains top-level metadata measurement type" <| fun _ ->
            Expect.isSome r.MeasurementType
                $"Run '{r.Identifier}' contains no top-level metadata measurement type"
        
        // TestCase Non-critical: Every run contains a technology type
        testCase $"Run '{r.Identifier}' contains top-level metadata technology type" <| fun _ ->
            Expect.isSome r.TechnologyType
                $"Run '{r.Identifier}' contains no top-level metadata technology type"
        
        // TestCase Non-critical: Every run contains a technology platform
        testCase $"Run '{r.Identifier}' contains top-level metadata technology platform" <| fun _ ->
            Expect.isSome r.TechnologyPlatform
                $"Run '{r.Identifier}' contains no top-level metadata technology platform"


        // TestCase Non-critical: Every annotation table contains some annotation column
        
        if hasAnnotationColumns arc then

            for t in arc.ArcTables do
                testCase $"Table {t.Name} contains annotation column" <| fun _ ->
                
                    let annoColCount = characteristicCount t + parameterCount t + factorCount t
                
                    Expect.isGreaterThan annoColCount 0
                        $"Table {t.Name} contains no annotation column"

    ]

// Execution:
Setup.ValidationPackage(
    metadata = Setup.Metadata(
        PACKAGE_METADATA,
        AVPRIndex.Frontmatter.FrontmatterLanguage.FSharpFrontmatter
        ),
    CriticalValidationCases = [criticalCases],
    NonCriticalValidationCases = [nonCriticalCases]
)
|> Execute.ValidationPipeline(
    basePath = arcDir
)

Available versions
Version Released on
1.0.0 08/20/2026