Skip to content

Commit bd7c558

Browse files
authored
add first insd_curation package (#103)
* add first insd_curation package * code cleanup * fix possible external data consumption fail
1 parent 5b4c4df commit bd7c558

1 file changed

Lines changed: 169 additions & 0 deletions

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
let [<Literal>]PACKAGE_METADATA = """(*
2+
---
3+
Name: insdc_curation
4+
Description: Validates certain INSDC record curation metadata.
5+
Summary: |
6+
Validates certain INSDC record curation metadata.
7+
- fastqc files
8+
- experiment accession annotation is present and correct
9+
- descends from a correctly named sample
10+
MajorVersion: 0
11+
MinorVersion: 0
12+
PatchVersion: 1
13+
Publish: true
14+
Authors:
15+
- FullName: Kevin Schneider
16+
Affiliation: DataPLANT
17+
Tags:
18+
- Name: INSDC
19+
- Name: Metadata curation
20+
ReleaseNotes: |
21+
- fastqc process graph validation
22+
---
23+
*)"""
24+
25+
#r "nuget: ARCtrl"
26+
#r "nuget: ARCtrl.QueryModel, 3.0.0-alpha.4"
27+
#r "nuget: ARCExpect.Core, 7.0.0-alpha"
28+
#r "nuget: FsHttp"
29+
#r "nuget: FSharpAux"
30+
#r "nuget: FSharpAux.IO"
31+
32+
open ARCtrl
33+
open ARCtrl.QueryModel
34+
open ARCExpect
35+
open FsHttp
36+
open FSharpAux.IO.SchemaReader
37+
open Expecto
38+
open System.IO
39+
open System
40+
41+
let arcDir = Directory.GetCurrentDirectory()
42+
43+
let arc =
44+
try ARC.load arcDir with
45+
| _ -> ARC(identifier = "unable to load arc from this dir")
46+
47+
let project_accession = arc.Identifier
48+
49+
module Domain =
50+
51+
open FSharpAux.IO.SchemaReader.Attribute
52+
open System.Collections.Generic
53+
54+
/// represents the relations provided by ENA portal API in TSV form
55+
type INSDC_Relations = {
56+
[<FieldAttribute("study_accession")>]
57+
study_accession: string
58+
[<FieldAttribute("sample_accession")>]
59+
sample_accession: string
60+
[<FieldAttribute("experiment_accession")>]
61+
experiment_accession: string
62+
[<FieldAttribute("run_accession")>]
63+
run_accession: string
64+
[<FieldAttribute("fastq_ftp")>]
65+
fastq_ftp: string
66+
}
67+
68+
/// 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.
69+
let splitByFastq (r: INSDC_Relations) =
70+
match r.fastq_ftp.Split(';') |> Array.filter (fun s -> s <> "") with
71+
| [||] -> [ r ] // no fastq → keep row as-is
72+
| files -> [ for f in files -> { r with fastq_ftp = f } ]
73+
74+
/// lookup index for retrieving INSDC_Relations by any of the accession numbers or fastq file name. Last-wins in case of duplicates.
75+
let buildIndex (records: INSDC_Relations seq) =
76+
let d = Dictionary<string, INSDC_Relations>(StringComparer.OrdinalIgnoreCase)
77+
for r in records do
78+
for v in [
79+
r.study_accession
80+
r.sample_accession
81+
r.experiment_accession
82+
r.run_accession
83+
r.fastq_ftp
84+
] do
85+
if not (String.IsNullOrEmpty v) then d[v] <- r // last-wins
86+
d
87+
88+
module OntologyTerms =
89+
let experiment_accession_term =
90+
OntologyAnnotation.fromTermAnnotation(
91+
"NCIT:C175892",
92+
name = "Experiment Accession Number"
93+
)
94+
95+
module ExpectedData =
96+
97+
let reader = new Csv.CsvReader<Domain.INSDC_Relations>(SchemaMode=Csv.Fill)
98+
99+
let relations =
100+
try
101+
http {
102+
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"
103+
}
104+
|> Request.send
105+
|> Response.toText
106+
|> fun response -> reader.ReadFromString(response, '\t', firstLineHasHeader = true)
107+
|> Seq.collect Domain.splitByFastq
108+
|> List.ofSeq
109+
with
110+
| _ -> []
111+
112+
let relations_index = Domain.buildIndex relations
113+
114+
let tryFindRelation (s: string) =
115+
match relations_index.TryGetValue s with
116+
| true, r -> Some r
117+
| false, _ -> None
118+
119+
120+
let create_insdc_relation_validation_cases_for_fastq_file (node: QNode) =
121+
122+
let samples =
123+
arc.ArcTables.SamplesOf(node)
124+
|> Seq.map (fun n -> n.Name)
125+
|> Array.ofSeq
126+
127+
let experiment_accession_actual =
128+
try
129+
Some (arc.PreviousParametersOf(node)).[Domain.OntologyTerms.experiment_accession_term]
130+
with
131+
| _ -> None
132+
133+
let expected_relations = Expect.wantSome (ExpectedData.tryFindRelation node.Name) "Experiment accession annotation is missing in INSDC relations"
134+
135+
testList node.Name [
136+
137+
test $"has correct experiment accession annotation (expected: {expected_relations.experiment_accession})" {
138+
let actual = Expect.wantSome experiment_accession_actual "Experiment accession annotation is missing"
139+
Expect.equal actual.ValueText expected_relations.experiment_accession "Experiment accession annotation does not match node name"
140+
}
141+
142+
test $"descends from correct sample (expected: {expected_relations.sample_accession})" {
143+
Expect.contains samples expected_relations.sample_accession $"Sample accession {expected_relations.sample_accession} not found in ARC"
144+
}
145+
]
146+
147+
let fastqc_cases =
148+
testList "fastqc" (
149+
arc.TryGetAssay("sequencing")
150+
|> Option.map (fun assay ->
151+
assay.LastData
152+
|> Seq.map (fun node -> create_insdc_relation_validation_cases_for_fastq_file node)
153+
|> Seq.toList
154+
)
155+
|> Option.defaultValue []
156+
)
157+
158+
Setup.ValidationPackage(
159+
metadata = Setup.Metadata(PACKAGE_METADATA, AVPRIndex.Frontmatter.FSharpFrontmatter),
160+
CriticalValidationCases = [
161+
test "has sequencing assay" {
162+
Expect.isSome (arc.TryGetAssay("sequencing")) "No sequencing assay found"
163+
}
164+
fastqc_cases
165+
]
166+
)
167+
|> Execute.ValidationPipeline(
168+
basePath = arcDir
169+
)

0 commit comments

Comments
 (0)