@gmod/gbz-base 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/query.js +3 -1
- package/dist/cli.js +26 -7
- package/dist/cli.js.map +1 -0
- package/dist/db.d.ts +5 -5
- package/dist/db.js +35 -14
- package/dist/db.js.map +1 -0
- package/dist/filehandle.js +1 -0
- package/dist/filehandle.js.map +1 -0
- package/dist/gbwt/bytecode.d.ts +1 -1
- package/dist/gbwt/bytecode.js +7 -3
- package/dist/gbwt/bytecode.js.map +1 -0
- package/dist/gbwt/node.js +7 -2
- package/dist/gbwt/node.js.map +1 -0
- package/dist/gbwt/record.js +8 -2
- package/dist/gbwt/record.js.map +1 -0
- package/dist/gbwt/sequence.js +8 -1
- package/dist/gbwt/sequence.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -0
- package/dist/lcs.js +32 -10
- package/dist/lcs.js.map +1 -0
- package/dist/query.d.ts +1 -1
- package/dist/query.js +1 -0
- package/dist/query.js.map +1 -0
- package/dist/sqlite/btree.d.ts +1 -1
- package/dist/sqlite/btree.js +17 -5
- package/dist/sqlite/btree.js.map +1 -0
- package/dist/sqlite/database.d.ts +1 -1
- package/dist/sqlite/database.js +12 -2
- package/dist/sqlite/database.js.map +1 -0
- package/dist/sqlite/pager.d.ts +3 -3
- package/dist/sqlite/pager.js +6 -4
- package/dist/sqlite/pager.js.map +1 -0
- package/dist/sqlite/record.js +3 -1
- package/dist/sqlite/record.js.map +1 -0
- package/dist/subgraph.d.ts +2 -2
- package/dist/subgraph.js +116 -29
- package/dist/subgraph.js.map +1 -0
- package/package.json +36 -11
- package/src/cli.ts +203 -0
- package/src/db.ts +322 -0
- package/src/filehandle.ts +4 -0
- package/src/gbwt/bytecode.ts +87 -0
- package/src/gbwt/node.ts +64 -0
- package/src/gbwt/record.ts +154 -0
- package/src/gbwt/sequence.ts +50 -0
- package/src/index.ts +31 -0
- package/src/lcs.ts +282 -0
- package/src/query.ts +79 -0
- package/src/sqlite/btree.ts +255 -0
- package/src/sqlite/database.ts +117 -0
- package/src/sqlite/pager.ts +78 -0
- package/src/sqlite/record.ts +100 -0
- package/src/subgraph.ts +1066 -0
package/src/gbwt/node.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const ENDMARKER = 0
|
|
2
|
+
|
|
3
|
+
export type Orientation = 'forward' | 'reverse'
|
|
4
|
+
|
|
5
|
+
export function encodeNode(id: number, orientation: Orientation) {
|
|
6
|
+
return 2 * id + (orientation === 'reverse' ? 1 : 0)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function nodeId(handle: number) {
|
|
10
|
+
return Math.floor(handle / 2)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function nodeOrientation(handle: number): Orientation {
|
|
14
|
+
return handle % 2 === 0 ? 'forward' : 'reverse'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isReverse(handle: number) {
|
|
18
|
+
return handle % 2 === 1
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function flipNode(handle: number) {
|
|
22
|
+
return handle % 2 === 0 ? handle + 1 : handle - 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type NodeSide = 'left' | 'right'
|
|
26
|
+
|
|
27
|
+
export function flipSide(side: NodeSide): NodeSide {
|
|
28
|
+
return side === 'left' ? 'right' : 'left'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function entrySide(orientation: Orientation): NodeSide {
|
|
32
|
+
return orientation === 'forward' ? 'left' : 'right'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function exitSide(orientation: Orientation): NodeSide {
|
|
36
|
+
return orientation === 'forward' ? 'right' : 'left'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function entryOrientation(side: NodeSide): Orientation {
|
|
40
|
+
return side === 'left' ? 'forward' : 'reverse'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function exitOrientation(side: NodeSide): Orientation {
|
|
44
|
+
return side === 'right' ? 'forward' : 'reverse'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function edgeIsCanonical(from: number, to: number) {
|
|
48
|
+
const fromId = nodeId(from)
|
|
49
|
+
const toId = nodeId(to)
|
|
50
|
+
return isReverse(from)
|
|
51
|
+
? toId > fromId || (toId === fromId && !isReverse(to))
|
|
52
|
+
: toId >= fromId
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function pathIsCanonical(path: number[]) {
|
|
56
|
+
const first = path[0]
|
|
57
|
+
const last = path[path.length - 1]
|
|
58
|
+
if (first === undefined || last === undefined) {
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
return isReverse(first) === isReverse(last)
|
|
62
|
+
? !isReverse(first)
|
|
63
|
+
: edgeIsCanonical(first, last)
|
|
64
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { ByteCodeReader, RunReader } from './bytecode.ts'
|
|
2
|
+
import { ENDMARKER, flipNode, nodeId } from './node.ts'
|
|
3
|
+
|
|
4
|
+
export interface Pos {
|
|
5
|
+
node: number
|
|
6
|
+
offset: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function decompressEdges(bytes: Uint8Array): Pos[] {
|
|
10
|
+
const reader = new ByteCodeReader(bytes)
|
|
11
|
+
const sigma = reader.int()
|
|
12
|
+
if (sigma === undefined || sigma === 0) {
|
|
13
|
+
return []
|
|
14
|
+
}
|
|
15
|
+
const edges: Pos[] = []
|
|
16
|
+
let prev = 0
|
|
17
|
+
for (let i = 0; i < sigma; i++) {
|
|
18
|
+
const delta = reader.int()
|
|
19
|
+
const offset = reader.int()
|
|
20
|
+
if (delta === undefined || offset === undefined) {
|
|
21
|
+
throw new Error('GBWT edge list ends early')
|
|
22
|
+
}
|
|
23
|
+
prev += delta
|
|
24
|
+
edges.push({ node: prev, offset })
|
|
25
|
+
}
|
|
26
|
+
return edges
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class GbwtRecord {
|
|
30
|
+
readonly edges: Pos[]
|
|
31
|
+
readonly bwt: Uint8Array
|
|
32
|
+
|
|
33
|
+
constructor(edges: Pos[], bwt: Uint8Array) {
|
|
34
|
+
this.edges = edges
|
|
35
|
+
this.bwt = bwt
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
runs() {
|
|
39
|
+
return new RunReader(this.bwt, this.edges.length)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private edge(rank: number) {
|
|
43
|
+
const edge = this.edges[rank]
|
|
44
|
+
if (edge === undefined) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`GBWT run refers to edge rank ${rank} of ${this.edges.length}`,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
return edge
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
lf(i: number): Pos | undefined {
|
|
53
|
+
const offsets = this.edges.map(e => e.offset)
|
|
54
|
+
let offset = 0
|
|
55
|
+
for (const run of this.runs()) {
|
|
56
|
+
const edge = this.edge(run.value)
|
|
57
|
+
const soFar = offsets[run.value]!
|
|
58
|
+
if (offset + run.len > i) {
|
|
59
|
+
return edge.node === ENDMARKER
|
|
60
|
+
? undefined
|
|
61
|
+
: { node: edge.node, offset: soFar + (i - offset) }
|
|
62
|
+
}
|
|
63
|
+
offsets[run.value] = soFar + run.len
|
|
64
|
+
offset += run.len
|
|
65
|
+
}
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private edgeTo(node: number) {
|
|
70
|
+
let low = 0
|
|
71
|
+
let high = this.edges.length
|
|
72
|
+
while (low < high) {
|
|
73
|
+
const mid = (low + high) >> 1
|
|
74
|
+
const edge = this.edges[mid]!
|
|
75
|
+
if (edge.node === node) {
|
|
76
|
+
return mid
|
|
77
|
+
}
|
|
78
|
+
if (edge.node < node) {
|
|
79
|
+
low = mid + 1
|
|
80
|
+
} else {
|
|
81
|
+
high = mid
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
offsetTo(pos: Pos): number | undefined {
|
|
88
|
+
if (pos.node === ENDMARKER) {
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
const rank = this.edgeTo(pos.node)
|
|
92
|
+
if (rank === undefined) {
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
let succRank = this.edges[rank]!.offset
|
|
96
|
+
if (succRank > pos.offset) {
|
|
97
|
+
return undefined
|
|
98
|
+
}
|
|
99
|
+
let offset = 0
|
|
100
|
+
for (const run of this.runs()) {
|
|
101
|
+
offset += run.len
|
|
102
|
+
if (run.value !== rank) {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
succRank += run.len
|
|
106
|
+
if (succRank > pos.offset) {
|
|
107
|
+
return offset - (succRank - pos.offset)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
predecessorAt(i: number): number | undefined {
|
|
114
|
+
const counts = this.edges.map(e => ({
|
|
115
|
+
node: e.node === ENDMARKER ? ENDMARKER : flipNode(e.node),
|
|
116
|
+
count: 0,
|
|
117
|
+
}))
|
|
118
|
+
for (const run of this.runs()) {
|
|
119
|
+
const entry = counts[run.value]
|
|
120
|
+
if (entry) {
|
|
121
|
+
entry.count += run.len
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (let rank = 1; rank < counts.length; rank++) {
|
|
125
|
+
const prev = counts[rank - 1] as { node: number; count: number }
|
|
126
|
+
const curr = counts[rank] as { node: number; count: number }
|
|
127
|
+
if (nodeId(prev.node) === nodeId(curr.node)) {
|
|
128
|
+
counts[rank - 1] = curr
|
|
129
|
+
counts[rank] = prev
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
let offset = 0
|
|
133
|
+
for (const entry of counts) {
|
|
134
|
+
offset += entry.count
|
|
135
|
+
if (offset > i) {
|
|
136
|
+
return entry.node === ENDMARKER ? undefined : entry.node
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return undefined
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
decompress(): Pos[] {
|
|
143
|
+
const offsets = this.edges.map(e => e.offset)
|
|
144
|
+
const result: Pos[] = []
|
|
145
|
+
for (const run of this.runs()) {
|
|
146
|
+
const edge = this.edge(run.value)
|
|
147
|
+
for (let k = 0; k < run.len; k++) {
|
|
148
|
+
result.push({ node: edge.node, offset: offsets[run.value]! })
|
|
149
|
+
offsets[run.value] = offsets[run.value]! + 1
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return result
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const DECODE = ['', 'A', 'C', 'G', 'T', 'N']
|
|
2
|
+
|
|
3
|
+
export function encodedSequenceLength(encoded: Uint8Array) {
|
|
4
|
+
const last = encoded[encoded.length - 1]
|
|
5
|
+
if (last === undefined) {
|
|
6
|
+
return 0
|
|
7
|
+
}
|
|
8
|
+
let value = last
|
|
9
|
+
let inLast = 0
|
|
10
|
+
for (let i = 0; i < 3; i++) {
|
|
11
|
+
if (value % 6 === 0) {
|
|
12
|
+
break
|
|
13
|
+
}
|
|
14
|
+
value = Math.floor(value / 6)
|
|
15
|
+
inLast += 1
|
|
16
|
+
}
|
|
17
|
+
return 3 * (encoded.length - 1) + inLast
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function decodeSequence(encoded: Uint8Array) {
|
|
21
|
+
let result = ''
|
|
22
|
+
for (const byte of encoded) {
|
|
23
|
+
let value = byte
|
|
24
|
+
for (let i = 0; i < 3; i++) {
|
|
25
|
+
const base = DECODE[value % 6]!
|
|
26
|
+
if (base === '') {
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
29
|
+
value = Math.floor(value / 6)
|
|
30
|
+
result += base
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const COMPLEMENT: Record<string, string> = {
|
|
37
|
+
A: 'T',
|
|
38
|
+
C: 'G',
|
|
39
|
+
G: 'C',
|
|
40
|
+
T: 'A',
|
|
41
|
+
N: 'N',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function reverseComplement(sequence: string) {
|
|
45
|
+
let result = ''
|
|
46
|
+
for (let i = sequence.length - 1; i >= 0; i--) {
|
|
47
|
+
result += COMPLEMENT[sequence[i]!] ?? 'N'
|
|
48
|
+
}
|
|
49
|
+
return result
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export {
|
|
2
|
+
GBZBase,
|
|
3
|
+
GENERIC_SAMPLE,
|
|
4
|
+
GbzRecord,
|
|
5
|
+
SCHEMA_VERSION,
|
|
6
|
+
SchemaVersionError,
|
|
7
|
+
formatPathName,
|
|
8
|
+
} from './db.ts'
|
|
9
|
+
export type { GbzPath, HaplotypeSample, PathName } from './db.ts'
|
|
10
|
+
export type { ByteSource } from './filehandle.ts'
|
|
11
|
+
export type { Pos } from './gbwt/record.ts'
|
|
12
|
+
export { Subgraph } from './subgraph.ts'
|
|
13
|
+
export type {
|
|
14
|
+
HaplotypeAlignment,
|
|
15
|
+
HaplotypeOutput,
|
|
16
|
+
PathIdentity,
|
|
17
|
+
PathPosition,
|
|
18
|
+
ReferencePath,
|
|
19
|
+
SubgraphJson,
|
|
20
|
+
SubgraphPath,
|
|
21
|
+
ToJsonOptions,
|
|
22
|
+
} from './subgraph.ts'
|
|
23
|
+
export {
|
|
24
|
+
subgraphAroundNodes,
|
|
25
|
+
subgraphAtOffset,
|
|
26
|
+
subgraphInInterval,
|
|
27
|
+
} from './query.ts'
|
|
28
|
+
export type { PathQuery, QueryOptions } from './query.ts'
|
|
29
|
+
export { SqliteDatabase } from './sqlite/database.ts'
|
|
30
|
+
export { weightedLcs } from './lcs.ts'
|
|
31
|
+
export * as nodes from './gbwt/node.ts'
|
package/src/lcs.ts
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
interface Point {
|
|
2
|
+
weight: number
|
|
3
|
+
a: number
|
|
4
|
+
b: number
|
|
5
|
+
matches: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function prefixSums(sequence: number[], weight: (x: number) => number) {
|
|
9
|
+
const sums = [0]
|
|
10
|
+
for (let i = 0; i < sequence.length; i++) {
|
|
11
|
+
sums.push(sums[i]! + weight(sequence[i]!))
|
|
12
|
+
}
|
|
13
|
+
return sums
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class MinHeap {
|
|
17
|
+
private items: number[] = []
|
|
18
|
+
|
|
19
|
+
push(value: number) {
|
|
20
|
+
const items = this.items
|
|
21
|
+
items.push(value)
|
|
22
|
+
let i = items.length - 1
|
|
23
|
+
while (i > 0) {
|
|
24
|
+
const parent = (i - 1) >> 1
|
|
25
|
+
if (items[parent]! <= value) {
|
|
26
|
+
break
|
|
27
|
+
}
|
|
28
|
+
items[i] = items[parent]!
|
|
29
|
+
i = parent
|
|
30
|
+
}
|
|
31
|
+
items[i] = value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
peek() {
|
|
35
|
+
return this.items[0]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pop() {
|
|
39
|
+
const items = this.items
|
|
40
|
+
const top = items[0]
|
|
41
|
+
const last = items.pop()
|
|
42
|
+
if (items.length > 0 && last !== undefined) {
|
|
43
|
+
let i = 0
|
|
44
|
+
for (;;) {
|
|
45
|
+
const left = 2 * i + 1
|
|
46
|
+
const right = left + 1
|
|
47
|
+
let smallest = i
|
|
48
|
+
let value = last
|
|
49
|
+
if (left < items.length && items[left]! < value) {
|
|
50
|
+
smallest = left
|
|
51
|
+
value = items[left]!
|
|
52
|
+
}
|
|
53
|
+
if (right < items.length && items[right]! < value) {
|
|
54
|
+
smallest = right
|
|
55
|
+
}
|
|
56
|
+
if (smallest === i) {
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
items[i] = items[smallest]!
|
|
60
|
+
i = smallest
|
|
61
|
+
}
|
|
62
|
+
items[i] = last
|
|
63
|
+
}
|
|
64
|
+
return top
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class Matrix {
|
|
69
|
+
readonly aSums: number[]
|
|
70
|
+
readonly bSums: number[]
|
|
71
|
+
readonly a: number[]
|
|
72
|
+
readonly b: number[]
|
|
73
|
+
readonly points = new Map<number, Map<number, Point>>()
|
|
74
|
+
private pendingEdits = new MinHeap()
|
|
75
|
+
|
|
76
|
+
constructor(a: number[], b: number[], weight: (x: number) => number) {
|
|
77
|
+
this.a = a
|
|
78
|
+
this.b = b
|
|
79
|
+
this.aSums = prefixSums(a, weight)
|
|
80
|
+
this.bSums = prefixSums(b, weight)
|
|
81
|
+
this.set(0, 0, { weight: 0, a: 0, b: 0, matches: 0 })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private set(edits: number, diagonal: number, point: Point) {
|
|
85
|
+
let row = this.points.get(edits)
|
|
86
|
+
if (!row) {
|
|
87
|
+
row = new Map()
|
|
88
|
+
this.points.set(edits, row)
|
|
89
|
+
this.pendingEdits.push(edits)
|
|
90
|
+
}
|
|
91
|
+
row.set(diagonal, point)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get(edits: number, diagonal: number) {
|
|
95
|
+
return this.points.get(edits)?.get(diagonal)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private tryInsert(edits: number, diagonal: number, point: Point) {
|
|
99
|
+
const existing = this.get(edits, diagonal)
|
|
100
|
+
if (!existing || point.weight > existing.weight) {
|
|
101
|
+
this.set(edits, diagonal, point)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
aWeight(offset: number) {
|
|
106
|
+
return this.aSums[offset + 1]! - this.aSums[offset]!
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
bWeight(offset: number) {
|
|
110
|
+
return this.bSums[offset + 1]! - this.bSums[offset]!
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
extend(edits: number): Point | undefined {
|
|
114
|
+
const row = this.points.get(edits)
|
|
115
|
+
if (!row) {
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
const diagonals = [...row.keys()].sort((x, y) => x - y)
|
|
119
|
+
for (const diagonal of diagonals) {
|
|
120
|
+
const found = row.get(diagonal)
|
|
121
|
+
if (!found) {
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
const point = { ...found }
|
|
125
|
+
while (
|
|
126
|
+
point.a < this.a.length &&
|
|
127
|
+
point.b < this.b.length &&
|
|
128
|
+
this.a[point.a] === this.b[point.b]
|
|
129
|
+
) {
|
|
130
|
+
point.weight += 2 * this.aWeight(point.a)
|
|
131
|
+
point.a += 1
|
|
132
|
+
point.b += 1
|
|
133
|
+
point.matches += 1
|
|
134
|
+
}
|
|
135
|
+
if (point.matches > 0) {
|
|
136
|
+
row.set(diagonal, point)
|
|
137
|
+
}
|
|
138
|
+
if (point.a === this.a.length && point.b === this.b.length) {
|
|
139
|
+
return point
|
|
140
|
+
}
|
|
141
|
+
if (point.a < this.a.length) {
|
|
142
|
+
const w = this.aWeight(point.a)
|
|
143
|
+
this.tryInsert(edits + w, diagonal + w, {
|
|
144
|
+
weight: point.weight,
|
|
145
|
+
a: point.a + 1,
|
|
146
|
+
b: point.b,
|
|
147
|
+
matches: 0,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
if (point.b < this.b.length) {
|
|
151
|
+
const w = this.bWeight(point.b)
|
|
152
|
+
this.tryInsert(edits + w, diagonal - w, {
|
|
153
|
+
weight: point.weight,
|
|
154
|
+
a: point.a,
|
|
155
|
+
b: point.b + 1,
|
|
156
|
+
matches: 0,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return undefined
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
nextEdits(edits: number) {
|
|
164
|
+
while (
|
|
165
|
+
this.pendingEdits.peek() !== undefined &&
|
|
166
|
+
this.pendingEdits.peek()! <= edits
|
|
167
|
+
) {
|
|
168
|
+
this.pendingEdits.pop()
|
|
169
|
+
}
|
|
170
|
+
return this.pendingEdits.peek()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
predecessor(
|
|
174
|
+
a: number,
|
|
175
|
+
b: number,
|
|
176
|
+
edits: number,
|
|
177
|
+
): [Point, number] | undefined {
|
|
178
|
+
const diagonal = this.aSums[a]! - this.bSums[b]!
|
|
179
|
+
const prev =
|
|
180
|
+
a > 0 && this.aWeight(a - 1) <= edits
|
|
181
|
+
? this.get(edits - this.aWeight(a - 1), diagonal - this.aWeight(a - 1))
|
|
182
|
+
: undefined
|
|
183
|
+
const next =
|
|
184
|
+
b > 0 && this.bWeight(b - 1) <= edits
|
|
185
|
+
? this.get(edits - this.bWeight(b - 1), diagonal + this.bWeight(b - 1))
|
|
186
|
+
: undefined
|
|
187
|
+
if (prev && next) {
|
|
188
|
+
return prev.weight > next.weight
|
|
189
|
+
? [prev, edits - this.aWeight(a - 1)]
|
|
190
|
+
: [next, edits - this.bWeight(b - 1)]
|
|
191
|
+
}
|
|
192
|
+
if (prev) {
|
|
193
|
+
return [prev, edits - this.aWeight(a - 1)]
|
|
194
|
+
}
|
|
195
|
+
if (next) {
|
|
196
|
+
return [next, edits - this.bWeight(b - 1)]
|
|
197
|
+
}
|
|
198
|
+
return undefined
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function weightedLcs(
|
|
203
|
+
a: number[],
|
|
204
|
+
b: number[],
|
|
205
|
+
weight: (x: number) => number,
|
|
206
|
+
): [pairs: [number, number][], weight: number] {
|
|
207
|
+
let prefix = 0
|
|
208
|
+
while (prefix < a.length && prefix < b.length && a[prefix] === b[prefix]) {
|
|
209
|
+
prefix += 1
|
|
210
|
+
}
|
|
211
|
+
let suffix = 0
|
|
212
|
+
while (
|
|
213
|
+
suffix < a.length - prefix &&
|
|
214
|
+
suffix < b.length - prefix &&
|
|
215
|
+
a[a.length - 1 - suffix] === b[b.length - 1 - suffix]
|
|
216
|
+
) {
|
|
217
|
+
suffix += 1
|
|
218
|
+
}
|
|
219
|
+
const pairs: [number, number][] = []
|
|
220
|
+
let total = 0
|
|
221
|
+
for (let i = 0; i < prefix; i++) {
|
|
222
|
+
pairs.push([i, i])
|
|
223
|
+
total += weight(a[i]!)
|
|
224
|
+
}
|
|
225
|
+
const [middle, middleWeight] = weightedLcsCore(
|
|
226
|
+
a.slice(prefix, a.length - suffix),
|
|
227
|
+
b.slice(prefix, b.length - suffix),
|
|
228
|
+
weight,
|
|
229
|
+
)
|
|
230
|
+
for (const [i, j] of middle) {
|
|
231
|
+
pairs.push([i + prefix, j + prefix])
|
|
232
|
+
}
|
|
233
|
+
total += middleWeight
|
|
234
|
+
for (let i = suffix; i > 0; i--) {
|
|
235
|
+
pairs.push([a.length - i, b.length - i])
|
|
236
|
+
total += weight(a[a.length - i]!)
|
|
237
|
+
}
|
|
238
|
+
return [pairs, total]
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function weightedLcsCore(
|
|
242
|
+
a: number[],
|
|
243
|
+
b: number[],
|
|
244
|
+
weight: (x: number) => number,
|
|
245
|
+
): [pairs: [number, number][], weight: number] {
|
|
246
|
+
if (a.length === 0 || b.length === 0) {
|
|
247
|
+
return [[], 0]
|
|
248
|
+
}
|
|
249
|
+
const matrix = new Matrix(a, b, weight)
|
|
250
|
+
let edits = 0
|
|
251
|
+
let point: Point = { weight: 0, a: 0, b: 0, matches: 0 }
|
|
252
|
+
for (;;) {
|
|
253
|
+
const end = matrix.extend(edits)
|
|
254
|
+
if (end) {
|
|
255
|
+
point = end
|
|
256
|
+
break
|
|
257
|
+
}
|
|
258
|
+
const next = matrix.nextEdits(edits)
|
|
259
|
+
if (next === undefined) {
|
|
260
|
+
break
|
|
261
|
+
}
|
|
262
|
+
edits = next
|
|
263
|
+
}
|
|
264
|
+
const result: [number, number][] = []
|
|
265
|
+
const finalWeight = point.weight / 2
|
|
266
|
+
point = { ...point }
|
|
267
|
+
for (;;) {
|
|
268
|
+
for (let i = 0; i < point.matches; i++) {
|
|
269
|
+
point.a -= 1
|
|
270
|
+
point.b -= 1
|
|
271
|
+
result.push([point.a, point.b])
|
|
272
|
+
}
|
|
273
|
+
const pred = matrix.predecessor(point.a, point.b, edits)
|
|
274
|
+
if (!pred) {
|
|
275
|
+
break
|
|
276
|
+
}
|
|
277
|
+
point = { ...pred[0] }
|
|
278
|
+
edits = pred[1]
|
|
279
|
+
}
|
|
280
|
+
result.reverse()
|
|
281
|
+
return [result, finalWeight]
|
|
282
|
+
}
|
package/src/query.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { GENERIC_SAMPLE } from './db.ts'
|
|
2
|
+
import { Subgraph } from './subgraph.ts'
|
|
3
|
+
|
|
4
|
+
import type { GBZBase, PathName } from './db.ts'
|
|
5
|
+
import type { HaplotypeOutput } from './subgraph.ts'
|
|
6
|
+
|
|
7
|
+
export interface QueryOptions {
|
|
8
|
+
context?: number
|
|
9
|
+
haplotypes?: HaplotypeOutput
|
|
10
|
+
limit?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PathQuery {
|
|
14
|
+
sample?: string
|
|
15
|
+
contig: string
|
|
16
|
+
haplotype?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function pathName(query: PathQuery, fragment: number): PathName {
|
|
20
|
+
return {
|
|
21
|
+
sample: query.sample ?? GENERIC_SAMPLE,
|
|
22
|
+
contig: query.contig,
|
|
23
|
+
haplotype: query.haplotype ?? 0,
|
|
24
|
+
fragment,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function subgraphAtOffset(
|
|
29
|
+
db: GBZBase,
|
|
30
|
+
query: PathQuery,
|
|
31
|
+
offset: number,
|
|
32
|
+
opts: QueryOptions = {},
|
|
33
|
+
) {
|
|
34
|
+
const subgraph = new Subgraph(db)
|
|
35
|
+
subgraph.limit = opts.limit
|
|
36
|
+
const reference = await subgraph.pathPosition(pathName(query, offset))
|
|
37
|
+
await subgraph.aroundPosition(
|
|
38
|
+
reference.position.handle,
|
|
39
|
+
reference.position.nodeOffset,
|
|
40
|
+
opts.context ?? 100,
|
|
41
|
+
)
|
|
42
|
+
subgraph.extractPaths(reference, opts.haplotypes ?? 'all')
|
|
43
|
+
return subgraph
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function subgraphInInterval(
|
|
47
|
+
db: GBZBase,
|
|
48
|
+
query: PathQuery,
|
|
49
|
+
start: number,
|
|
50
|
+
end: number,
|
|
51
|
+
opts: QueryOptions = {},
|
|
52
|
+
) {
|
|
53
|
+
const subgraph = new Subgraph(db)
|
|
54
|
+
subgraph.limit = opts.limit
|
|
55
|
+
const reference = await subgraph.pathPosition(pathName(query, start))
|
|
56
|
+
await subgraph.aroundInterval(
|
|
57
|
+
reference.position,
|
|
58
|
+
end - start,
|
|
59
|
+
opts.context ?? 100,
|
|
60
|
+
)
|
|
61
|
+
subgraph.extractPaths(reference, opts.haplotypes ?? 'all')
|
|
62
|
+
return subgraph
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function subgraphAroundNodes(
|
|
66
|
+
db: GBZBase,
|
|
67
|
+
nodes: number[],
|
|
68
|
+
opts: QueryOptions = {},
|
|
69
|
+
) {
|
|
70
|
+
const haplotypes = opts.haplotypes ?? 'all'
|
|
71
|
+
if (haplotypes === 'reference-only') {
|
|
72
|
+
throw new Error('Cannot output a reference path in a node-based query')
|
|
73
|
+
}
|
|
74
|
+
const subgraph = new Subgraph(db)
|
|
75
|
+
subgraph.limit = opts.limit
|
|
76
|
+
await subgraph.aroundNodes(nodes, opts.context ?? 100)
|
|
77
|
+
subgraph.extractPaths(undefined, haplotypes)
|
|
78
|
+
return subgraph
|
|
79
|
+
}
|