///|
/// Composition metrics for one coordinate window.
pub struct SequenceProfile {
start : Int
end : Int
length : Int
gc_percent : Double
ambiguous : Int
entropy : Double
motif_hits : Int
} derive(Debug, Eq)
///|
/// Compute composition and motif metrics for overlapping windows.
pub fn sequence_profile(
sequence : String,
window_size : Int,
step : Int,
motif : String,
) -> Array[SequenceProfile] {
let result : Array[SequenceProfile] = []
let normalized = normalize_sequence(sequence)
if window_size <= 0 || step <= 0 || window_size > normalized.length() {
return result
}
let mut start = 0
while start + window_size <= normalized.length() {
let end = start + window_size
let piece = normalized.unsafe_substring(start~, end~)
let composition = composition_report(piece)
result.push({
start: start + 1,
end,
length: piece.length(),
gc_percent: composition.gc_percent,
ambiguous: composition.ambiguous,
entropy: composition.entropy,
motif_hits: if motif == "" {
0
} else {
iupac_motif_count(piece, motif)
},
})
start += step
}
result
}
///|
/// Return the window with the highest GC percentage, if one exists.
pub fn highest_gc_profile(
profiles : Array[SequenceProfile],
) -> SequenceProfile? {
if profiles.length() == 0 {
return None
}
Some(
profiles.fold(init=profiles[0], fn(best, item) {
if item.gc_percent > best.gc_percent {
item
} else {
best
}
}),
)
}
///|
/// Return the window with the greatest ambiguity burden.
pub fn most_ambiguous_profile(
profiles : Array[SequenceProfile],
) -> SequenceProfile? {
if profiles.length() == 0 {
return None
}
Some(
profiles.fold(init=profiles[0], fn(best, item) {
if item.ambiguous > best.ambiguous {
item
} else {
best
}
}),
)
}
///|
/// Export profiles as tab-separated rows for plotting or spreadsheet review.
pub fn sequence_profiles_tsv(profiles : Array[SequenceProfile]) -> String {
let out = StringBuilder()
out.write_string(
"start\tend\tlength\tgc_percent\tambiguous\tentropy\tmotif_hits\n",
)
for item in profiles {
out.write_string(
item.start.to_string() +
"\t" +
item.end.to_string() +
"\t" +
item.length.to_string() +
"\t",
)
out.write_string(
item.gc_percent.to_string() +
"\t" +
item.ambiguous.to_string() +
"\t" +
item.entropy.to_string() +
"\t" +
item.motif_hits.to_string() +
"\n",
)
}
out.to_string()
}
///|
pub fn SequenceProfile::to_json_string(self : SequenceProfile) -> String {
"{\"start\":" +
self.start.to_string() +
",\"end\":" +
self.end.to_string() +
",\"length\":" +
self.length.to_string() +
",\"gc_percent\":" +
self.gc_percent.to_string() +
",\"ambiguous\":" +
self.ambiguous.to_string() +
",\"entropy\":" +
self.entropy.to_string() +
",\"motif_hits\":" +
self.motif_hits.to_string() +
"}"
}
///|
/// Calculate a length-weighted mean GC percentage across profiles.
pub fn profile_mean_gc(profiles : Array[SequenceProfile]) -> Double {
let total = profiles.fold(init=0, fn(sum, item) { sum + item.length })
if total == 0 {
0.0
} else {
profiles.fold(init=0.0, fn(sum, item) {
sum + item.gc_percent * item.length.to_double()
}) /
total.to_double()
}
}
///|
/// Count motif occurrences represented by a profile series.
pub fn profile_total_motif_hits(profiles : Array[SequenceProfile]) -> Int {
profiles.fold(init=0, fn(total, item) { total + item.motif_hits })
}
///|
/// Check whether any profile contains ambiguity symbols.
pub fn profile_has_ambiguity(profiles : Array[SequenceProfile]) -> Bool {
profiles.any(fn(item) { item.ambiguous > 0 })
}
///|
/// Return the number of windows in a profile series.
pub fn profile_window_count(profiles : Array[SequenceProfile]) -> Int {
profiles.length()
}