Skip to content

Commit 2ea4929

Browse files
authored
Merge pull request #12 from BioFSharp/developer
Import tested code from ProteomIQon
2 parents a873206 + 9bb2403 commit 2ea4929

11 files changed

Lines changed: 532 additions & 11 deletions

‎RELEASE_NOTES.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
#### 0.2.1 (Released 2026-08-30)
2+
3+
* SparsePeakArray: new module upstreamed from ProteomIQon (sparse binned peak vectors over PeakArray with dot product)
4+
* SearchDB: add Sequence+GlobalMod ModSequence lookup upstreamed from ProteomIQon (prepareSelectModsequenceBySequenceAndGMod, getThreadSafePeptideLookUpFromFileBySequenceAndGMod)
5+
* ProteinInference: adopt the newer ProteomIQon behavior - PSMInput reads the ModelScore column (was PercolatorScore), assignTranscriptsToGenes takes a tryParseProteinID function instead of a regex string, createProteinModelInfoFromEntry reads the GFF3 ID attribute (was Name) and tolerates unknown strand characters as Forward (fixes a runtime match failure), isGene/isRNA generalized over the GFF line type parameter
6+
* FDRControl: add the PEP value machinery upstreamed from ProteomIQon (getLogisticRegressionFunction, createTargetDecoyHis, calculatePEPValues, logitTransformPepValues, initCalculateLin taking a trace callback instead of an NLog logger)
7+
18
#### 0.2.0 (Released 2026-08-29)
29

310
* Upgrade to BioFSharp 2.0.0 (bundles the former BioFSharp.IO package) and migrate the codebase over the breaking API changes

‎src/BioFSharp.Mz/BioFSharp.Mz.fsproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<Compile Include="AssemblyInfo.fs" />
3434
<Compile Include="Peak.fs" />
3535
<Compile Include="PeakArray.fs" />
36+
<Compile Include="SparsePeakArray.fs" />
3637
<Compile Include="PeakList.fs" />
3738
<Compile Include="Fragmentation.fs" />
3839
<Compile Include="Caching.fs" />

‎src/BioFSharp.Mz/FDRControl.fs‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace BioFSharp.Mz
22

3+
open System
34
open FSharp.Stats
45
open FSharpAux
56
open FSharp.Stats.Fitting
@@ -247,3 +248,114 @@ module FDRControl =
247248
// takes a score from the dataset and assigns it a q value
248249
let interpolation = Interpolation.LinearSpline.predict linearSplineCoeff
249250
interpolation
251+
252+
/// for given data, creates a logistic regression model and returns a mapping function for this model
253+
let getLogisticRegressionFunction (x:vector) (y:vector) epsilon =
254+
let alpha =
255+
match FSharp.Stats.Fitting.LogisticRegression.Univariable.estimateAlpha epsilon x y with
256+
| Some a -> a
257+
| None -> failwith "Could not find an alpha for logistic regression of fdr data"
258+
let weight = FSharp.Stats.Fitting.LogisticRegression.Univariable.coefficient epsilon alpha x y
259+
FSharp.Stats.Fitting.LogisticRegression.Univariable.predict weight
260+
261+
/// Creates a Histogram based on a given score of a target/decoy dataset. Each bin contains the information of the total count, the decoy count and the median score.
262+
/// (Bin, Count, DecoyCount, Median Score)
263+
let createTargetDecoyHis bandwidth (isDecoy: 'a -> bool) (decoyScoreF: 'a -> float) (targetScoreF: 'a -> float) (data: 'a[]) =
264+
let halfBw = bandwidth / 2.0
265+
let scoreDecoyInfo =
266+
data
267+
|> Array.map (fun x ->
268+
if isDecoy x then
269+
{|Score = decoyScoreF x; Decoy = true|}
270+
else
271+
{|Score = targetScoreF x; Decoy = false|}
272+
)
273+
scoreDecoyInfo
274+
|> Array.groupBy (fun x ->
275+
floor (x.Score / bandwidth))
276+
|> Array.map (fun (k,values) ->
277+
let count = (Array.length(values))
278+
let decoyCount = (values |> Array.filter (fun x -> x.Decoy = true) |> Array.length)
279+
let medianScore = values |> Array.map (fun x -> x.Score) |> Array.median
280+
// first part of the tuple only needed for debugging
281+
if k < 0. then
282+
((k * bandwidth) + halfBw, count, decoyCount, medianScore)
283+
else
284+
((k + 1.) * bandwidth - halfBw, count, decoyCount, medianScore)
285+
)
286+
287+
/// Calculates the PEP value based on the ratio of Decoys to targets at a given score
288+
let calculatePEPValues (totalCountF: 'a -> float) (decoyCountF: 'a -> float) (scoreF: 'a -> float) (dataFreq: 'a[]) =
289+
dataFreq
290+
|> Array.map (fun x ->
291+
scoreF x,(decoyCountF x)/(totalCountF x)
292+
)
293+
|> Array.sortBy fst
294+
|> Array.toList
295+
296+
/// Logit transforms pep values (log10)
297+
let logitTransformPepValues score pepVal =
298+
Array.zip score pepVal
299+
// 0 and 1 are + and - infinity
300+
|> Array.filter (fun (y,x) -> x <> 0. && x <> 1.)
301+
|> Array.map (fun (score,pep) ->
302+
score,
303+
log10 (pep/(1.-pep))
304+
)
305+
|> Array.unzip
306+
307+
/// Calculates monotonized PEP values for a target/decoy dataset based on the decoy/target ratio. Entries are binned with a given bandwidth as intital estiamtor based on the scores.
308+
/// Returns a function which maps from score to PEP value based on a fit of a linear function using linear regression. The linear regression is performed on the logit transformed
309+
/// pep values. The fit focuses on the pep values centered aound the middle of the score distribution
310+
let initCalculateLin (trace: string -> unit) bandwidth (isDecoy: 'a -> bool) (decoyScoreF: 'a -> float) (targetScoreF: 'a -> float) (data: 'a[]) =
311+
let lowerScore, upperScore =
312+
let decoy =
313+
data
314+
|> Array.filter isDecoy
315+
|> Array.map decoyScoreF
316+
|> Array.filter (fun x -> x < 0.)
317+
|> Array.median
318+
let target =
319+
data
320+
|> Array.filter (isDecoy >> not)
321+
|> Array.map targetScoreF
322+
|> Array.filter (fun x -> x > 0.)
323+
|> Array.median
324+
decoy, target
325+
trace (sprintf "Lower Score: %f; Upper Score: %f" lowerScore upperScore)
326+
let filteredData =
327+
data
328+
|> Array.filter (fun entry ->
329+
if isDecoy entry then
330+
let score = decoyScoreF entry
331+
score >= lowerScore && score <= upperScore
332+
else
333+
let score = targetScoreF entry
334+
score >= lowerScore && score <= upperScore
335+
)
336+
trace (sprintf "Initial Bandwidth: %f" bandwidth)
337+
let fittingFunction, score, pep =
338+
let xPointRange =
339+
let min = Math.Min((Array.minBy targetScoreF filteredData) |> targetScoreF, (Array.minBy decoyScoreF filteredData) |> decoyScoreF)
340+
let max = Math.Max((Array.maxBy targetScoreF filteredData) |> targetScoreF, (Array.maxBy decoyScoreF filteredData) |> decoyScoreF)
341+
max-min
342+
let upperBW = Math.Min(10., xPointRange/10.)
343+
[|bandwidth .. 0.1 .. upperBW|]
344+
|> Array.choose (fun bw ->
345+
let targetDecoyHis = createTargetDecoyHis bw (isDecoy: 'a -> bool) (decoyScoreF: 'a -> float) (targetScoreF: 'a -> float) (filteredData: 'a[])
346+
let score',pep' =
347+
calculatePEPValues (fun (_,count,_,_) -> float count) (fun (_,_,decoyCount,_) -> float decoyCount) (fun (_,_,_,medianScore) -> medianScore) targetDecoyHis
348+
|> Array.ofList
349+
|> Array.unzip
350+
let logitScore, logitPEPVal = logitTransformPepValues score' pep'
351+
let coeff = Fitting.LinearRegression.OLS.Linear.Univariable.fit (vector logitScore) (vector logitPEPVal)
352+
let fittingFunction' = (Fitting.LinearRegression.OLS.Linear.Univariable.predict coeff) >> (fun x -> 10.**(x)/(1.+10.**(x)))
353+
let sos = FSharp.Stats.Fitting.GoodnessOfFit.calculateSumOfSquares fittingFunction' score' pep'
354+
if coeff.[1] < 0. then
355+
Some (sos.Error/sos.Count, fittingFunction', score', pep', bw)
356+
else
357+
None
358+
)
359+
|> Array.minBy (fun (error,_,_,_,_) -> error)
360+
|> fun (error, fit,s,p,bw) -> trace (sprintf "Chosen Bandwidth: %f" bw); fit,s,p
361+
fittingFunction

‎src/BioFSharp.Mz/ProteinInference.fs‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ module ProteinInference =
7373
PepSequenceID : int
7474
[<FieldAttribute("StringSequence")>]
7575
Seq :string
76-
[<FieldAttribute("PercolatorScore")>]
76+
[<FieldAttribute("ModelScore")>]
7777
Score : float
7878
}
7979

@@ -165,13 +165,13 @@ module ProteinInference =
165165
String.filter (fun c -> System.Char.IsLower c |> not && c <> '[' && c <> ']') pepSeq
166166

167167
/// Checks if GFF line describes gene
168-
let isGene (item: GFFLine<seq<char>>) =
168+
let isGene (item: GFFLine<'a>) =
169169
match item with
170170
| GFFEntryLine x -> x.Feature = "gene"
171171
| _ -> false
172172

173173
/// Checks if GFF line describes rna
174-
let isRNA (item: GFFLine<seq<char>>) =
174+
let isRNA (item: GFFLine<'a>) =
175175
match item with
176176
| GFFEntryLine x -> if x.Feature = "mRNA" then Some x else None
177177
| _ -> None
@@ -183,7 +183,7 @@ module ProteinInference =
183183
let attributes = entry.Attributes
184184
/// Same as in FastA file
185185
let spliceVariantID =
186-
match Map.tryFind "Name" attributes with
186+
match Map.tryFind "ID" attributes with
187187
| Some res ->
188188
res.Head
189189
| None ->
@@ -194,12 +194,13 @@ module ProteinInference =
194194
match entry.Strand with
195195
|'+' -> StrandDirection.Forward
196196
|'-' -> StrandDirection.Reverse
197+
| _ -> StrandDirection.Forward
197198

198199
createProteinModelInfo spliceVariantID chromosomeID direction locus i Seq.empty Seq.empty
199200

200201
/// By reading GFF creates the protein models (relationships of proteins to each other) which basically means grouping the rnas over the gene loci
201202
/// TODO: Don't group over order but rather group over id
202-
let assignTranscriptsToGenes regexPattern (gffLines: seq<GFFLine<seq<char>>>) =
203+
let assignTranscriptsToGenes tryParseProteinID (gffLines: seq<GFFLine<'a>>) =
203204
gffLines
204205
// transcripts are grouped by the gene they originate from
205206
|> Seq.groupWhen isGene
@@ -212,13 +213,14 @@ module ProteinInference =
212213
|> Seq.mapi (fun i element ->
213214
// every transcript of gene gets its own number i and other info is collected from element and used for info of protein
214215
let modelInfo = createProteinModelInfoFromEntry i locus element
215-
let r = System.Text.RegularExpressions.Regex.Match(modelInfo.Id,regexPattern)
216+
let r = tryParseProteinID modelInfo.Id
216217
// the gff3 id has to be matched with the sequence in the fasta file. therefore the regexpattern is used
217-
if r.Success then
218-
r.Value,
218+
match r with
219+
| Some v ->
220+
v,
219221
modelInfo
220-
else
221-
failwithf "could not match gff3 entry id %s with regexpattern %s. Either gff3 file is corrupt or regexpattern is not correct" modelInfo.Id regexPattern
222+
| None ->
223+
failwithf "could not match gff3 entry id %s with regex pattern. Either gff3 file is corrupt or regexpattern is not correct" modelInfo.Id
222224
)
223225

224226
| _ -> Seq.empty

‎src/BioFSharp.Mz/SearchDB.fs‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,20 @@ module SearchDB =
870870
| true -> Some (reader.GetDouble(0))
871871
| false -> Option.None)
872872

873+
/// Prepares statement to select a ModSequence entry by Sequence and GlobalMod
874+
let prepareSelectModsequenceBySequenceAndGMod (cn:SQLiteConnection) =
875+
let querystring = "SELECT * FROM ModSequence WHERE Sequence=@sequence AND GlobalMod=@globalMod"
876+
let cmd = new SQLiteCommand(querystring, cn)
877+
cmd.Parameters.Add("@sequence", Data.DbType.String) |> ignore
878+
cmd.Parameters.Add("@globalMod", Data.DbType.Int32) |> ignore
879+
fun (sequence:string) (globalMod:int) ->
880+
cmd.Parameters.["@sequence"].Value <- sequence
881+
cmd.Parameters.["@globalMod"].Value <- globalMod
882+
use reader = cmd.ExecuteReader()
883+
match reader.Read() with
884+
| true -> (reader.GetInt32(0), reader.GetInt32(1),reader.GetDouble(2), reader.GetInt64(3), reader.GetString(4), reader.GetInt32(5))
885+
| false -> -1,-1,nan,-1L,"",-1
886+
873887
/// Prepares statement to select a Protein Accession entry by ID
874888
let prepareSelectProteinAccessionByID (cn:SQLiteConnection) (tr) =
875889
let querystring = "SELECT Accession FROM Protein WHERE ID=@id "
@@ -1671,7 +1685,15 @@ module SearchDB =
16711685
selectModsequenceByMassRange lowerMass' upperMass'
16721686
|> List.map (createLookUpResultBy parseAAString)
16731687
)
1674-
1688+
1689+
/// Returns a LookUpResult
1690+
let getThreadSafePeptideLookUpFromFileBySequenceAndGMod (cn:SQLiteConnection) sdbParams =
1691+
let parseAAString = initOfModAminoAcidString sdbParams.IsotopicMod (sdbParams.FixedMods@sdbParams.VariableMods)
1692+
let selectModsequenceByID = Db.SQLiteQuery.prepareSelectModsequenceBySequenceAndGMod cn
1693+
(fun sequence globalMod ->
1694+
selectModsequenceByID sequence globalMod
1695+
|> (createLookUpResultBy parseAAString))
1696+
16751697
let copyDBIntoMemory (cn:SQLiteConnection) =
16761698
//cn.Open()
16771699
let inMemoryDB = new SQLiteConnection("Data Source=:memory:;cache=shared;Version=3")
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
namespace BioFSharp.Mz
2+
3+
module SparsePeakArray =
4+
5+
///
6+
type SparsePeakArray = {
7+
Data : System.Collections.Generic.IDictionary<int,float>
8+
MzToBinIdx : float -> int
9+
BinIdxToMz : int -> float
10+
}
11+
12+
///
13+
let dot (x:SparsePeakArray) (y:SparsePeakArray) =
14+
x.Data
15+
|> Seq.fold (fun (acc:float) xi ->
16+
let present,yi = y.Data.TryGetValue xi.Key
17+
if present then acc + (yi * xi.Value) else acc
18+
) 0.
19+
20+
///
21+
let initMzToBinIdx width offset x = int ((x / width) + offset)
22+
23+
let initBinIdxToMz width offset x =
24+
((float x) - offset) * width
25+
26+
27+
///
28+
let peaksToNearestBinVector binWidth offset (minMassBoarder:float) (maxMassBoarder:float) (pkarr:PeakArray<_>) =
29+
let mzToBinIdx = initMzToBinIdx binWidth offset
30+
let binIdxToMz = initBinIdxToMz binWidth offset
31+
let keyValues =
32+
pkarr
33+
|> Array.choose (fun p ->
34+
if p.Mz < maxMassBoarder && p.Mz > minMassBoarder then
35+
let index = (mzToBinIdx p.Mz)
36+
Some (index, p.Intensity)
37+
else
38+
None
39+
)
40+
|> Array.groupBy fst
41+
|> Array.map (fun (idx,data) -> idx, data |> Array.sumBy snd)
42+
{
43+
Data = keyValues |> dict
44+
MzToBinIdx = mzToBinIdx
45+
BinIdxToMz = binIdxToMz
46+
}

‎tests/BioFSharp.Mz.Tests/BioFSharp.Mz.Tests.fsproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
<ItemGroup>
1010
<Compile Include="PeakTests.fs" />
11+
<Compile Include="SparsePeakArrayTests.fs" />
1112
<Compile Include="CachingTests.fs" />
1213
<Compile Include="StatsExtensionTests.fs" />
1314
<Compile Include="SignalDetectionTests.fs" />

0 commit comments

Comments
 (0)