@jbrowse/modifications-utils 4.1.14
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/LICENSE +201 -0
- package/README.md +192 -0
- package/esm/consts.d.ts +12 -0
- package/esm/consts.js +42 -0
- package/esm/cytosineContext.d.ts +26 -0
- package/esm/cytosineContext.js +62 -0
- package/esm/detectSimplexModifications.d.ts +21 -0
- package/esm/detectSimplexModifications.js +34 -0
- package/esm/forEachModRefPos.d.ts +2 -0
- package/esm/forEachModRefPos.js +15 -0
- package/esm/getMethBins.d.ts +34 -0
- package/esm/getMethBins.js +92 -0
- package/esm/getModPositions.d.ts +19 -0
- package/esm/getModPositions.js +211 -0
- package/esm/getModProbabilities.d.ts +30 -0
- package/esm/getModProbabilities.js +63 -0
- package/esm/getModTypes.d.ts +31 -0
- package/esm/getModTypes.js +48 -0
- package/esm/getTagAlt.d.ts +20 -0
- package/esm/getTagAlt.js +31 -0
- package/esm/index.d.ts +14 -0
- package/esm/index.js +9 -0
- package/esm/types.d.ts +5 -0
- package/esm/types.js +1 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [2020] [Evolutionary Software Foundation]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# @jbrowse/modifications-utils
|
|
2
|
+
|
|
3
|
+
Pure MM/ML base-modification tag parsers (methylation, etc.)
|
|
4
|
+
|
|
5
|
+
<!-- API_DOCS_START -->
|
|
6
|
+
|
|
7
|
+
## API
|
|
8
|
+
|
|
9
|
+
Auto-generated from `#api` JSDoc tags in this package. Do not edit by hand.
|
|
10
|
+
|
|
11
|
+
### getMethBins
|
|
12
|
+
|
|
13
|
+
Bins per-read base modifications and their probabilities onto reference
|
|
14
|
+
positions, returning typed arrays for methylated/unmethylated calls. Only
|
|
15
|
+
cytosines in `context` are considered (default CpG); plants also use CHG/CHH.
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// type signature
|
|
19
|
+
({…}: ParsedModData, context?: CytosineContext) => { methBins: number[]; hydroxyMethBins: number[]; methProbs: number[]; hydroxyMethProbs: number[]; }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/modifications-utils/src/getMethBins.ts)
|
|
23
|
+
|
|
24
|
+
### getModPositions
|
|
25
|
+
|
|
26
|
+
Parse MM tag to extract modification positions on the read sequence.
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
// type signature
|
|
30
|
+
(mm: string, fseq: string, fstrand: number) => ModWithPositions[]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/modifications-utils/src/getModPositions.ts)
|
|
34
|
+
|
|
35
|
+
### getModProbabilities
|
|
36
|
+
|
|
37
|
+
Reads the ML tag from a feature and returns per-call modification probabilities
|
|
38
|
+
scaled to 0..1.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
// type signature
|
|
42
|
+
(feature: Feature) => number[] | undefined
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/modifications-utils/src/getModProbabilities.ts)
|
|
46
|
+
|
|
47
|
+
### getModProbabilityBytes
|
|
48
|
+
|
|
49
|
+
The ML tag as its raw 0..255 bytes, without the scaling `getModProbabilities`
|
|
50
|
+
applies.
|
|
51
|
+
|
|
52
|
+
The byte is a LOSSLESS stand-in for the probability — every value on this path
|
|
53
|
+
is exactly `(N + 0.5) / 256` — and it is monotonic in it, so anything that only
|
|
54
|
+
compares probabilities (picking the most likely call at a position, testing a
|
|
55
|
+
threshold) can work in bytes and divide once, at the end, for the few calls that
|
|
56
|
+
survive. A caller that needs the numbers themselves still wants
|
|
57
|
+
`getModProbabilities`.
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
// type signature
|
|
61
|
+
(feature: Feature) => ArrayLike<number> | undefined
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/modifications-utils/src/getModProbabilities.ts)
|
|
65
|
+
|
|
66
|
+
### getModTypes
|
|
67
|
+
|
|
68
|
+
The modification types an MM tag declares, from its headers alone.
|
|
69
|
+
|
|
70
|
+
`getModPositions` answers this too, but on the way to placing every call: it
|
|
71
|
+
walks the delta list against the read sequence, which is the expensive half and
|
|
72
|
+
is only needed to DRAW marks. Anything that just wants to know what is in the
|
|
73
|
+
file — which types to offer in a menu, whether a track carries modifications at
|
|
74
|
+
all — wants this instead, and pays neither the walk nor the sequence decode that
|
|
75
|
+
feeds it.
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
// type signature
|
|
79
|
+
(mm: string) => ModTypeHeader[]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/modifications-utils/src/getModTypes.ts)
|
|
83
|
+
|
|
84
|
+
### getTag
|
|
85
|
+
|
|
86
|
+
Read a single tag by name, using the feature's targeted tag accessor when it has
|
|
87
|
+
one (BAM) and the full tags object otherwise (CRAM/synteny).
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
// type signature
|
|
91
|
+
(feature: Feature, tag: string) => unknown
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/modifications-utils/src/getTagAlt.ts)
|
|
95
|
+
|
|
96
|
+
### getTagAlt
|
|
97
|
+
|
|
98
|
+
Read a tag by its canonical name, falling back to a lowercase-suffixed alias
|
|
99
|
+
(e.g. MM/Mm, ML/Ml) as emitted by some aligners.
|
|
100
|
+
|
|
101
|
+
Prefers the feature's own one-pass alias lookup when it has one. The plain
|
|
102
|
+
`getTag(tag) ?? getTag(alt)` form walks the record's whole tag block TWICE
|
|
103
|
+
whenever neither name is present — which is every read in a file without base
|
|
104
|
+
modifications, and this is called per read on every render. On jb2bench's
|
|
105
|
+
1000x.shortread that pair of walks was 12.9% of the whole BAM query, more than
|
|
106
|
+
the CIGAR/SEQ/MD reads the pileup actually uses.
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
// type signature
|
|
110
|
+
(feature: Feature, tag: string, alt: string) => unknown
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/modifications-utils/src/getTagAlt.ts)
|
|
114
|
+
|
|
115
|
+
### isMethylationFillType
|
|
116
|
+
|
|
117
|
+
The modification types the methylation walk below paints — 5mC and 5hmC, the
|
|
118
|
+
pair that compete for one cytosine.
|
|
119
|
+
|
|
120
|
+
Stated once because two functions have to agree on it and they are in different
|
|
121
|
+
packages: this walk claims those types, and `extractModifications` has to skip
|
|
122
|
+
exactly them in the fill view so a cytosine gets one mark rather than two. A
|
|
123
|
+
second spelling would either double-paint or, as it did, drop every OTHER type
|
|
124
|
+
the read declares.
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
// type signature
|
|
128
|
+
(type: string) => type is "h" | "m"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/modifications-utils/src/getMethBins.ts)
|
|
132
|
+
|
|
133
|
+
### matchesCytosineContext
|
|
134
|
+
|
|
135
|
+
Whether the cytosine at read position `pos` sits in the given context.
|
|
136
|
+
|
|
137
|
+
The pattern is defined on the template (the strand the C is on), read 5'->3'.
|
|
138
|
+
For forward reads the stored sequence IS the template, so we read forward from
|
|
139
|
+
`pos`. getModPositions works reverse-strand reads in stored-sequence space,
|
|
140
|
+
where the template runs backwards and complemented, so we read backwards from
|
|
141
|
+
`pos` and complement each base before matching.
|
|
142
|
+
|
|
143
|
+
**Char codes, not characters, and that is the whole shape of this function.**
|
|
144
|
+
Folding case with `& ~0x20` on the code and comparing numbers keeps a probe to
|
|
145
|
+
arithmetic, where lower-casing the character and the pattern base beside it is
|
|
146
|
+
two string operations per base. It is asked at that volume: the fill-unmarked
|
|
147
|
+
methylation walk asks up to twice for every aligned base of every read
|
|
148
|
+
(getMethBins), and bisulfite asks at every candidate cytosine. What it is worth
|
|
149
|
+
in the phase it sits in — which is not what it is worth on its own — is
|
|
150
|
+
`plugins/alignments/benches/modFillView.bench.ts`.
|
|
151
|
+
|
|
152
|
+
`charCodeAt` past either end of the string is NaN and `NaN & ~0x20` is 0 — an
|
|
153
|
+
index no pattern base equals and the complement table holds -1 at — so the walk
|
|
154
|
+
runs off the read as a non-match with no bounds test of its own.
|
|
155
|
+
`features/modCoverage/readBaseCounts.ts` folds case the same way and says so.
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
// type signature
|
|
159
|
+
(seq: string, pos: number, isReverse: boolean, context: CytosineContext) => boolean
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/modifications-utils/src/cytosineContext.ts)
|
|
163
|
+
|
|
164
|
+
### modProbAt
|
|
165
|
+
|
|
166
|
+
Returns the probability value from the flat ML array for a modification's
|
|
167
|
+
position. `idx` is the position's index within the mod's stored `positions`
|
|
168
|
+
array; we recover its MM-tag order (reverse-strand reads store positions in
|
|
169
|
+
descending order) and step into ML by `probStart + mmOrder * probStride`.
|
|
170
|
+
`probStride` is >1 for combined codes (e.g. 'C+mh'), where ML values are
|
|
171
|
+
interleaved per position.
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
// type signature
|
|
175
|
+
(probabilities: number[] | undefined, probStart: number, probStride: number, isReverse: boolean, idx: number, posLen: number) => number
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/modifications-utils/src/getModProbabilities.ts)
|
|
179
|
+
|
|
180
|
+
### parseModHeader
|
|
181
|
+
|
|
182
|
+
Parses one MM-tag modification header (e.g. `C+m`) into its base, strand, type
|
|
183
|
+
string, and modification code.
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
// type signature
|
|
187
|
+
(basemod: string, fullmod: string) => { base: string; strand: string; typestr: string; mod: string; }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/modifications-utils/src/consts.ts)
|
|
191
|
+
|
|
192
|
+
<!-- API_DOCS_END -->
|
package/esm/consts.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const modificationRegex: RegExp;
|
|
2
|
+
/**
|
|
3
|
+
* #api
|
|
4
|
+
* Parses one MM-tag modification header (e.g. `C+m`) into its base, strand,
|
|
5
|
+
* type string, and modification code.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseModHeader(basemod: string, fullmod: string): {
|
|
8
|
+
base: string;
|
|
9
|
+
strand: string;
|
|
10
|
+
typestr: string;
|
|
11
|
+
mod: string;
|
|
12
|
+
};
|
package/esm/consts.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Regex for MM:Z tag format based on SAM/BAM spec (hts-specs):
|
|
2
|
+
// Spec: ([ACGTUN][-+]([a-z]+|[0-9]+)[.?]?(,[0-9]+)*;)*
|
|
3
|
+
//
|
|
4
|
+
// Our implementation diverges intentionally from the spec:
|
|
5
|
+
// 1. Capture strand as group 2: ([-+]) instead of [-+]
|
|
6
|
+
// - Needed to extract strand info for downstream processing
|
|
7
|
+
// 2. Accept uppercase single letters: ([a-z]+|[A-Z]|[0-9]+) instead of ([a-z]+|[0-9]+)
|
|
8
|
+
// - The spec PROSE defines uppercase ambiguity codes A/C/G/T/U and N (an
|
|
9
|
+
// unspecified modification of the respective canonical base), but the
|
|
10
|
+
// spec's own grammar regex omits them — a known inconsistency in the spec.
|
|
11
|
+
// - Ambiguity codes are always a single uppercase letter (never multi-char
|
|
12
|
+
// or mixed with lowercase), hence [A-Z] rather than [A-Z]+.
|
|
13
|
+
// 3. Capture modifier flag as group 4: ([.?]?) instead of non-capturing
|
|
14
|
+
// - Per spec: '?' = modification status of skipped bases is unknown
|
|
15
|
+
// - Per spec: '.' or absent = skipped bases should be assumed low probability of modification
|
|
16
|
+
// - We capture this to support correct interpretation of skipped positions
|
|
17
|
+
// 4. Don't match delta values or semicolon
|
|
18
|
+
// - We extract just the base modification header (e.g., "C+m?" from "C+m?,2,1;")
|
|
19
|
+
// - Caller splits by ";" and "," to get this header before passing to regex
|
|
20
|
+
//
|
|
21
|
+
// Real-world examples with modifier flags:
|
|
22
|
+
// C+m → skipped bases = low probability modification (default)
|
|
23
|
+
// C+m. → skipped bases = low probability modification (explicit)
|
|
24
|
+
// C+m? → skipped bases = unknown modification status
|
|
25
|
+
export const modificationRegex = /([ACGTUN])([-+])([a-z]+|[A-Z]|[0-9]+)([.?]?)/;
|
|
26
|
+
/**
|
|
27
|
+
* #api
|
|
28
|
+
* Parses one MM-tag modification header (e.g. `C+m`) into its base, strand,
|
|
29
|
+
* type string, and modification code.
|
|
30
|
+
*/
|
|
31
|
+
export function parseModHeader(basemod, fullmod) {
|
|
32
|
+
const matches = modificationRegex.exec(basemod);
|
|
33
|
+
if (!matches) {
|
|
34
|
+
throw new Error(`bad format for MM tag: "${fullmod}"`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
base: matches[1],
|
|
38
|
+
strand: matches[2],
|
|
39
|
+
typestr: matches[3],
|
|
40
|
+
mod: matches[4] !== '' ? matches[4] : '.',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type CytosineContext = 'CG' | 'CHG' | 'CHH' | 'all';
|
|
2
|
+
/**
|
|
3
|
+
* #api
|
|
4
|
+
* Whether the cytosine at read position `pos` sits in the given context.
|
|
5
|
+
*
|
|
6
|
+
* The pattern is defined on the template (the strand the C is on), read 5'->3'.
|
|
7
|
+
* For forward reads the stored sequence IS the template, so we read forward from
|
|
8
|
+
* `pos`. getModPositions works reverse-strand reads in stored-sequence space,
|
|
9
|
+
* where the template runs backwards and complemented, so we read backwards from
|
|
10
|
+
* `pos` and complement each base before matching.
|
|
11
|
+
*
|
|
12
|
+
* **Char codes, not characters, and that is the whole shape of this function.**
|
|
13
|
+
* Folding case with `& ~0x20` on the code and comparing numbers keeps a probe
|
|
14
|
+
* to arithmetic, where lower-casing the character and the pattern base beside
|
|
15
|
+
* it is two string operations per base. It is asked at that volume: the
|
|
16
|
+
* fill-unmarked methylation walk asks up to twice for every aligned base of
|
|
17
|
+
* every read (getMethBins), and bisulfite asks at every candidate cytosine.
|
|
18
|
+
* What it is worth in the phase it sits in — which is not what it is worth on
|
|
19
|
+
* its own — is `plugins/alignments/benches/modFillView.bench.ts`.
|
|
20
|
+
*
|
|
21
|
+
* `charCodeAt` past either end of the string is NaN and `NaN & ~0x20` is 0 — an
|
|
22
|
+
* index no pattern base equals and the complement table holds -1 at — so the
|
|
23
|
+
* walk runs off the read as a non-match with no bounds test of its own.
|
|
24
|
+
* `features/modCoverage/readBaseCounts.ts` folds case the same way and says so.
|
|
25
|
+
*/
|
|
26
|
+
export declare function matchesCytosineContext(seq: string, pos: number, isReverse: boolean, context: CytosineContext): boolean;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const A = 65;
|
|
2
|
+
const C = 67;
|
|
3
|
+
const G = 71;
|
|
4
|
+
const T = 84;
|
|
5
|
+
// The IUPAC A/C/T ambiguity code, which is what the H of CHG/CHH is.
|
|
6
|
+
const H = 72;
|
|
7
|
+
// Template-strand base pattern per context, 5'->3', as upper-case char codes.
|
|
8
|
+
// 'all' constrains only the cytosine itself.
|
|
9
|
+
const CONTEXT_PATTERN = {
|
|
10
|
+
CG: Uint8Array.of(C, G),
|
|
11
|
+
CHG: Uint8Array.of(C, H, G),
|
|
12
|
+
CHH: Uint8Array.of(C, H, H),
|
|
13
|
+
all: Uint8Array.of(C),
|
|
14
|
+
};
|
|
15
|
+
const COMPLEMENT_CODE = new Int16Array(128).fill(-1);
|
|
16
|
+
COMPLEMENT_CODE[A] = T;
|
|
17
|
+
COMPLEMENT_CODE[T] = A;
|
|
18
|
+
COMPLEMENT_CODE[C] = G;
|
|
19
|
+
COMPLEMENT_CODE[G] = C;
|
|
20
|
+
/**
|
|
21
|
+
* #api
|
|
22
|
+
* Whether the cytosine at read position `pos` sits in the given context.
|
|
23
|
+
*
|
|
24
|
+
* The pattern is defined on the template (the strand the C is on), read 5'->3'.
|
|
25
|
+
* For forward reads the stored sequence IS the template, so we read forward from
|
|
26
|
+
* `pos`. getModPositions works reverse-strand reads in stored-sequence space,
|
|
27
|
+
* where the template runs backwards and complemented, so we read backwards from
|
|
28
|
+
* `pos` and complement each base before matching.
|
|
29
|
+
*
|
|
30
|
+
* **Char codes, not characters, and that is the whole shape of this function.**
|
|
31
|
+
* Folding case with `& ~0x20` on the code and comparing numbers keeps a probe
|
|
32
|
+
* to arithmetic, where lower-casing the character and the pattern base beside
|
|
33
|
+
* it is two string operations per base. It is asked at that volume: the
|
|
34
|
+
* fill-unmarked methylation walk asks up to twice for every aligned base of
|
|
35
|
+
* every read (getMethBins), and bisulfite asks at every candidate cytosine.
|
|
36
|
+
* What it is worth in the phase it sits in — which is not what it is worth on
|
|
37
|
+
* its own — is `plugins/alignments/benches/modFillView.bench.ts`.
|
|
38
|
+
*
|
|
39
|
+
* `charCodeAt` past either end of the string is NaN and `NaN & ~0x20` is 0 — an
|
|
40
|
+
* index no pattern base equals and the complement table holds -1 at — so the
|
|
41
|
+
* walk runs off the read as a non-match with no bounds test of its own.
|
|
42
|
+
* `features/modCoverage/readBaseCounts.ts` folds case the same way and says so.
|
|
43
|
+
*/
|
|
44
|
+
export function matchesCytosineContext(seq, pos, isReverse, context) {
|
|
45
|
+
const pattern = CONTEXT_PATTERN[context];
|
|
46
|
+
for (let i = 0, len = pattern.length; i < len; i++) {
|
|
47
|
+
const code = seq.charCodeAt(isReverse ? pos - i : pos + i) & ~0x20;
|
|
48
|
+
// A non-ASCII sequence character folds to something past the complement
|
|
49
|
+
// table rather than into it, and matches no pattern base either way.
|
|
50
|
+
if (code > 127) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const actual = isReverse ? COMPLEMENT_CODE[code] : code;
|
|
54
|
+
const expected = pattern[i];
|
|
55
|
+
if (expected === H
|
|
56
|
+
? actual !== A && actual !== C && actual !== T
|
|
57
|
+
: actual !== expected) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ModificationType } from './types.ts';
|
|
2
|
+
type ModificationTypeList = readonly ModificationType[];
|
|
3
|
+
/**
|
|
4
|
+
* Which modification types were basecalled on only ONE strand ("simplex") vs
|
|
5
|
+
* both ("duplex"). The MM tag reports each mod against a strand, e.g. `C+m` (5mC
|
|
6
|
+
* on the read's strand) or `G-m` (5mC on the opposite strand, reading as a G).
|
|
7
|
+
* A type on '+' with no '-' partner anywhere → simplex; both present → duplex.
|
|
8
|
+
*
|
|
9
|
+
* Callers use this to pick the modification-coverage denominator (see
|
|
10
|
+
* calculateModificationCounts): duplex counts every read with the base, simplex
|
|
11
|
+
* only reads on the examined strand, else the modified fraction is understated.
|
|
12
|
+
*
|
|
13
|
+
* Run over the WHOLE dataset, not one read: simplex-ness is a protocol property,
|
|
14
|
+
* shared by all reads, and a single read can lack the '-' call just by covering
|
|
15
|
+
* no eligible base — a per-read check would misclassify duplex data as simplex.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors IGV: AlignmentDataManager builds the set globally,
|
|
18
|
+
* BaseModificationCoverageRenderer uses it for the detectable count.
|
|
19
|
+
*/
|
|
20
|
+
export declare function detectSimplexModifications(modifications: ModificationTypeList): ReadonlySet<string>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which modification types were basecalled on only ONE strand ("simplex") vs
|
|
3
|
+
* both ("duplex"). The MM tag reports each mod against a strand, e.g. `C+m` (5mC
|
|
4
|
+
* on the read's strand) or `G-m` (5mC on the opposite strand, reading as a G).
|
|
5
|
+
* A type on '+' with no '-' partner anywhere → simplex; both present → duplex.
|
|
6
|
+
*
|
|
7
|
+
* Callers use this to pick the modification-coverage denominator (see
|
|
8
|
+
* calculateModificationCounts): duplex counts every read with the base, simplex
|
|
9
|
+
* only reads on the examined strand, else the modified fraction is understated.
|
|
10
|
+
*
|
|
11
|
+
* Run over the WHOLE dataset, not one read: simplex-ness is a protocol property,
|
|
12
|
+
* shared by all reads, and a single read can lack the '-' call just by covering
|
|
13
|
+
* no eligible base — a per-read check would misclassify duplex data as simplex.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors IGV: AlignmentDataManager builds the set globally,
|
|
16
|
+
* BaseModificationCoverageRenderer uses it for the detectable count.
|
|
17
|
+
*/
|
|
18
|
+
export function detectSimplexModifications(modifications) {
|
|
19
|
+
// types seen on the '-' strand anywhere → those are duplex
|
|
20
|
+
const minusTypes = new Set();
|
|
21
|
+
for (const { strand, type } of modifications) {
|
|
22
|
+
if (strand === '-') {
|
|
23
|
+
minusTypes.add(type);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// a '+' type with no '-' partner was only examined on one strand → simplex
|
|
27
|
+
const simplex = new Set();
|
|
28
|
+
for (const { strand, type } of modifications) {
|
|
29
|
+
if (strand === '+' && !minusTypes.has(type)) {
|
|
30
|
+
simplex.add(type);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return simplex;
|
|
34
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { ModWithPositions } from './getModPositions.ts';
|
|
2
|
+
export declare function forEachModRefPos(modifications: readonly ModWithPositions[], probabilities: number[] | undefined, cigarOps: ArrayLike<number>, isReverse: boolean, cb: (mod: ModWithPositions, ref: number, idx: number, prob: number) => void): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getNextRefPos } from '@jbrowse/cigar-utils';
|
|
2
|
+
import { modProbAt } from './getModProbabilities.js';
|
|
3
|
+
// Walk every modification's read-sequence positions, mapping each to its
|
|
4
|
+
// reference position via the CIGAR and recovering its ML probability. The
|
|
5
|
+
// ML-array offset (probStart/probStride) is precomputed per modification by
|
|
6
|
+
// getModPositions, so this no longer tracks a running index by hand.
|
|
7
|
+
export function forEachModRefPos(modifications, probabilities, cigarOps, isReverse, cb) {
|
|
8
|
+
for (const mod of modifications) {
|
|
9
|
+
const { positions, probStart, probStride } = mod;
|
|
10
|
+
getNextRefPos(cigarOps, positions, (ref, idx) => {
|
|
11
|
+
const prob = modProbAt(probabilities, probStart, probStride, isReverse, idx, positions.length);
|
|
12
|
+
cb(mod, ref, idx, prob);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CytosineContext } from './cytosineContext.ts';
|
|
2
|
+
import type { ModWithPositions } from './getModPositions.ts';
|
|
3
|
+
export interface ParsedModData {
|
|
4
|
+
modifications: ModWithPositions[];
|
|
5
|
+
probabilities: number[] | undefined;
|
|
6
|
+
cigarOps: ArrayLike<number>;
|
|
7
|
+
seq: string;
|
|
8
|
+
fstrand: -1 | 0 | 1;
|
|
9
|
+
flen: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* #api
|
|
13
|
+
* The modification types the methylation walk below paints — 5mC and 5hmC, the
|
|
14
|
+
* pair that compete for one cytosine.
|
|
15
|
+
*
|
|
16
|
+
* Stated once because two functions have to agree on it and they are in
|
|
17
|
+
* different packages: this walk claims those types, and
|
|
18
|
+
* `extractModifications` has to skip exactly them in the fill view so a
|
|
19
|
+
* cytosine gets one mark rather than two. A second spelling would either
|
|
20
|
+
* double-paint or, as it did, drop every OTHER type the read declares.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isMethylationFillType(type: string): type is "h" | "m";
|
|
23
|
+
/**
|
|
24
|
+
* #api
|
|
25
|
+
* Bins per-read base modifications and their probabilities onto reference
|
|
26
|
+
* positions, returning typed arrays for methylated/unmethylated calls. Only
|
|
27
|
+
* cytosines in `context` are considered (default CpG); plants also use CHG/CHH.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getMethBins({ modifications, probabilities, cigarOps, seq, fstrand, flen }: ParsedModData, context?: CytosineContext): {
|
|
30
|
+
methBins: number[];
|
|
31
|
+
hydroxyMethBins: number[];
|
|
32
|
+
methProbs: number[];
|
|
33
|
+
hydroxyMethProbs: number[];
|
|
34
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { CIGAR_D, CIGAR_EQ, CIGAR_I, CIGAR_M, CIGAR_N, CIGAR_S, CIGAR_X, } from '@jbrowse/cigar-utils';
|
|
2
|
+
import { matchesCytosineContext } from './cytosineContext.js';
|
|
3
|
+
import { forEachModRefPos } from './forEachModRefPos.js';
|
|
4
|
+
/**
|
|
5
|
+
* #api
|
|
6
|
+
* The modification types the methylation walk below paints — 5mC and 5hmC, the
|
|
7
|
+
* pair that compete for one cytosine.
|
|
8
|
+
*
|
|
9
|
+
* Stated once because two functions have to agree on it and they are in
|
|
10
|
+
* different packages: this walk claims those types, and
|
|
11
|
+
* `extractModifications` has to skip exactly them in the fill view so a
|
|
12
|
+
* cytosine gets one mark rather than two. A second spelling would either
|
|
13
|
+
* double-paint or, as it did, drop every OTHER type the read declares.
|
|
14
|
+
*/
|
|
15
|
+
export function isMethylationFillType(type) {
|
|
16
|
+
return type === 'm' || type === 'h';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* #api
|
|
20
|
+
* Bins per-read base modifications and their probabilities onto reference
|
|
21
|
+
* positions, returning typed arrays for methylated/unmethylated calls. Only
|
|
22
|
+
* cytosines in `context` are considered (default CpG); plants also use CHG/CHH.
|
|
23
|
+
*/
|
|
24
|
+
export function getMethBins({ modifications, probabilities, cigarOps, seq, fstrand, flen }, context = 'CG') {
|
|
25
|
+
const isReverse = fstrand === -1;
|
|
26
|
+
const methBins = [];
|
|
27
|
+
const hydroxyMethBins = [];
|
|
28
|
+
const methProbs = [];
|
|
29
|
+
const hydroxyMethProbs = [];
|
|
30
|
+
forEachModRefPos(modifications, probabilities, cigarOps, isReverse, ({ type, strand, positions }, ref, idx, prob) => {
|
|
31
|
+
const isMeth = isMethylationFillType(type);
|
|
32
|
+
if (isMeth &&
|
|
33
|
+
ref >= 0 &&
|
|
34
|
+
ref < flen &&
|
|
35
|
+
matchesCytosineContext(seq, positions[idx], isReverse !== (strand === '-'), context)) {
|
|
36
|
+
if (type === 'm') {
|
|
37
|
+
methBins[ref] = 1;
|
|
38
|
+
methProbs[ref] = prob;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
hydroxyMethBins[ref] = 1;
|
|
42
|
+
hydroxyMethProbs[ref] = prob;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
// Only fill undetected sites as unmethylated when the read actually carries a
|
|
47
|
+
// 5mC ('m') call. A read with only e.g. 6mA modifications never assayed
|
|
48
|
+
// methylation, so inventing unmethylated cytosines for it would be wrong.
|
|
49
|
+
// Additionally, per SAMtags the '?' flag means the status of bases not listed
|
|
50
|
+
// in the MM tag is unknown, so we must NOT assume them unmethylated; only the
|
|
51
|
+
// '.'/absent flag (low probability for skipped bases) lets us fill them in.
|
|
52
|
+
const methMods = modifications.filter(m => m.type === 'm');
|
|
53
|
+
const fillUnmethylated = methMods.length > 0 && !methMods.some(m => m.unknownSkip);
|
|
54
|
+
// A '-' MM group assays the cytosines of the strand OPPOSITE the read, so a
|
|
55
|
+
// both-strand modBAM ('C+m;G-m') has its uncalled sites in both directions.
|
|
56
|
+
const fillForward = methMods.some(m => isReverse === (m.strand === '-'));
|
|
57
|
+
const fillReverse = methMods.some(m => isReverse !== (m.strand === '-'));
|
|
58
|
+
// Scan the full read sequence for every cytosine in `context` and mark any not
|
|
59
|
+
// already detected from the MM tag as unmethylated (prob=0).
|
|
60
|
+
if (fillUnmethylated) {
|
|
61
|
+
let readPos = 0;
|
|
62
|
+
let refPos = 0;
|
|
63
|
+
for (let i = 0, l = cigarOps.length; i < l; i++) {
|
|
64
|
+
const packed = cigarOps[i];
|
|
65
|
+
const len = packed >>> 4;
|
|
66
|
+
const op = packed & 0xf;
|
|
67
|
+
if (op === CIGAR_S || op === CIGAR_I) {
|
|
68
|
+
readPos += len;
|
|
69
|
+
}
|
|
70
|
+
else if (op === CIGAR_D || op === CIGAR_N) {
|
|
71
|
+
refPos += len;
|
|
72
|
+
}
|
|
73
|
+
else if (op === CIGAR_M || op === CIGAR_X || op === CIGAR_EQ) {
|
|
74
|
+
for (let j = 0; j < len; j++) {
|
|
75
|
+
const rp = readPos + j;
|
|
76
|
+
const rf = refPos + j;
|
|
77
|
+
if (rf >= 0 &&
|
|
78
|
+
rf < flen &&
|
|
79
|
+
!methBins[rf] &&
|
|
80
|
+
((fillForward && matchesCytosineContext(seq, rp, false, context)) ||
|
|
81
|
+
(fillReverse && matchesCytosineContext(seq, rp, true, context)))) {
|
|
82
|
+
methBins[rf] = 1;
|
|
83
|
+
methProbs[rf] = 0;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
readPos += len;
|
|
87
|
+
refPos += len;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { methBins, hydroxyMethBins, methProbs, hydroxyMethProbs };
|
|
92
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ModWithPositions {
|
|
2
|
+
type: string;
|
|
3
|
+
base: string;
|
|
4
|
+
strand: string;
|
|
5
|
+
unknownSkip: boolean;
|
|
6
|
+
positions: number[];
|
|
7
|
+
probStart: number;
|
|
8
|
+
probStride: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* #api
|
|
12
|
+
* Parse MM tag to extract modification positions on the read sequence.
|
|
13
|
+
*
|
|
14
|
+
* @param mm - MM tag string (e.g., "C+m,2,2,1;A+a,0,3")
|
|
15
|
+
* @param fseq - Read sequence
|
|
16
|
+
* @param fstrand - Read strand (-1, 0, or 1)
|
|
17
|
+
* @returns Array of modification objects with positions
|
|
18
|
+
*/
|
|
19
|
+
export declare function getModPositions(mm: string, fseq: string, fstrand: number): ModWithPositions[];
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { parseModHeader } from './consts.js';
|
|
2
|
+
import { isSingleModType } from './getModTypes.js';
|
|
3
|
+
const COMPLEMENT_CODE = {
|
|
4
|
+
65: 84, // A->T
|
|
5
|
+
84: 65, // T->A
|
|
6
|
+
67: 71, // C->G
|
|
7
|
+
71: 67, // G->C
|
|
8
|
+
78: 78, // N->N
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* #api
|
|
12
|
+
* Parse MM tag to extract modification positions on the read sequence.
|
|
13
|
+
*
|
|
14
|
+
* @param mm - MM tag string (e.g., "C+m,2,2,1;A+a,0,3")
|
|
15
|
+
* @param fseq - Read sequence
|
|
16
|
+
* @param fstrand - Read strand (-1, 0, or 1)
|
|
17
|
+
* @returns Array of modification objects with positions
|
|
18
|
+
*/
|
|
19
|
+
export function getModPositions(mm, fseq, fstrand) {
|
|
20
|
+
const seqLength = fseq.length;
|
|
21
|
+
const isRev = fstrand === -1;
|
|
22
|
+
const mods = mm.split(';');
|
|
23
|
+
const result = [];
|
|
24
|
+
// Running offset into the flat ML probabilities array. Each group consumes
|
|
25
|
+
// (numPositions * numTypes) values, interleaved per position.
|
|
26
|
+
let mlBase = 0;
|
|
27
|
+
// Groups already walked, so a later group calling the same positions can reuse
|
|
28
|
+
// the array instead of walking the sequence again for it. Dorado's 5mCG_5hmCG
|
|
29
|
+
// model emits `C+h?;C+m?` — two groups, one canonical base, identical delta
|
|
30
|
+
// lists — so on real ONT output this drops one of every three sequence walks.
|
|
31
|
+
//
|
|
32
|
+
// The key is what the walk below READS: the canonical base and the delta list
|
|
33
|
+
// (`fseq` and `fstrand` are per read, not per group). It also carries the MM
|
|
34
|
+
// strand, which the walk does not currently read — deliberately stronger than
|
|
35
|
+
// needed, so the test cannot go quietly wrong if the walk ever becomes
|
|
36
|
+
// strand-aware.
|
|
37
|
+
//
|
|
38
|
+
// Parallel arrays rather than a Map: a read carries one to four groups, so a
|
|
39
|
+
// linear scan of char compares beats hashing a multi-kilobyte delta string,
|
|
40
|
+
// and they are allocated on the first walk so a single-group read builds none.
|
|
41
|
+
// `benches/sameBaseMerge.bench.ts` prices the whole test at inside the control
|
|
42
|
+
// on every fixture where it cannot fire, including one where every compare is
|
|
43
|
+
// forced to run to its final byte and then fail.
|
|
44
|
+
let seenKeys;
|
|
45
|
+
let seenDeltas;
|
|
46
|
+
let seenPositions;
|
|
47
|
+
for (const mod of mods) {
|
|
48
|
+
if (mod === '') {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const split = mod.split(',');
|
|
52
|
+
const basemod = split[0];
|
|
53
|
+
const { base, strand, typestr, mod: skipFlag, } = parseModHeader(basemod, mod);
|
|
54
|
+
const unknownSkip = skipFlag === '?';
|
|
55
|
+
// typestr can be multi-char lowercase e.g. 'mh' (5mC + 5hmC at same
|
|
56
|
+
// positions) or a ChEBI code e.g. '16061'. The rule is `isSingleModType`,
|
|
57
|
+
// shared with `getModTypes` so the two cannot disagree about what a tag
|
|
58
|
+
// declares.
|
|
59
|
+
const isSingleType = isSingleModType(typestr);
|
|
60
|
+
const nTypes = isSingleType ? 1 : typestr.length;
|
|
61
|
+
// ONE walk per GROUP, not per type. Every type in a combined code is called
|
|
62
|
+
// at the same positions — only probStart differs — so the walk below and the
|
|
63
|
+
// array it fills are shared, and the entries pushed after it point at the
|
|
64
|
+
// same array. `C+mh` used to walk the read sequence twice and allocate two
|
|
65
|
+
// identical arrays; `benches/modCombinedCode.bench.ts` (in the alignments
|
|
66
|
+
// plugin, where the fixture lives) prices that at **2.07x** on a synthesized
|
|
67
|
+
// `C+mh`, and at **1.16x** even on a single-type tag, where nothing is
|
|
68
|
+
// deduplicated and the win is the per-group closure this loop replaced.
|
|
69
|
+
//
|
|
70
|
+
// A combined code is the RARER of the two shapes this deduplicates, though.
|
|
71
|
+
// The reuse test below is the common one — see `seenKeys`.
|
|
72
|
+
//
|
|
73
|
+
// this logic based on parse_mm.pl from hts-specs
|
|
74
|
+
const splitLength = split.length;
|
|
75
|
+
const nPositions = splitLength - 1;
|
|
76
|
+
// Everything after the header — `,2,2,1`. V8 slices a long string in O(1),
|
|
77
|
+
// and the compare rejects on length before it reads a byte.
|
|
78
|
+
const deltas = mod.slice(basemod.length);
|
|
79
|
+
const key = base + strand;
|
|
80
|
+
let positions;
|
|
81
|
+
if (seenKeys !== undefined) {
|
|
82
|
+
for (let s = 0, n = seenKeys.length; s < n; s++) {
|
|
83
|
+
if (seenKeys[s] === key && seenDeltas[s] === deltas) {
|
|
84
|
+
positions = seenPositions[s];
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (positions === undefined) {
|
|
90
|
+
const isN = base === 'N';
|
|
91
|
+
let currPos = 0;
|
|
92
|
+
// An MM tag may declare more calls of a base than the read has left. When
|
|
93
|
+
// that happens the walk has nowhere to put them, and every emitted value
|
|
94
|
+
// still has to be a valid index into the read — `getMethBins` indexes the
|
|
95
|
+
// sequence with these, and the CIGAR walk requires them ascending, so a
|
|
96
|
+
// position outside the read is read as a real one somewhere wrong rather
|
|
97
|
+
// than dropped. So the exhausted case clamps to the nearest valid index
|
|
98
|
+
// and stays there.
|
|
99
|
+
const endClamp = isRev ? 0 : Math.max(0, seqLength - 1);
|
|
100
|
+
// Pre-allocate and fill backwards on reverse strand to avoid a final
|
|
101
|
+
// reverse(). Forward stays a growing literal on purpose: filling
|
|
102
|
+
// `new Array(n)` leaves holey elements, and this array is read in the
|
|
103
|
+
// CIGAR walk's inner loop.
|
|
104
|
+
positions = isRev ? new Array(nPositions) : [];
|
|
105
|
+
let writeIndex = isRev ? nPositions - 1 : 0;
|
|
106
|
+
if (isRev) {
|
|
107
|
+
// Avoid revcom(fseq) by reading fseq from the back and complementing the
|
|
108
|
+
// expected char-code.
|
|
109
|
+
const baseCode = base.charCodeAt(0);
|
|
110
|
+
const targetCode = COMPLEMENT_CODE[baseCode] ?? baseCode;
|
|
111
|
+
for (let i = 1; i < splitLength; i++) {
|
|
112
|
+
if (currPos >= seqLength) {
|
|
113
|
+
positions[writeIndex--] = endClamp;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
let delta = +split[i];
|
|
117
|
+
do {
|
|
118
|
+
if (isN ||
|
|
119
|
+
fseq.charCodeAt(seqLength - 1 - currPos) === targetCode) {
|
|
120
|
+
delta--;
|
|
121
|
+
}
|
|
122
|
+
currPos++;
|
|
123
|
+
} while (delta >= 0 && currPos < seqLength);
|
|
124
|
+
positions[writeIndex--] = seqLength - currPos;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// **Forward jumps, reverse steps, and the asymmetry is measured rather
|
|
129
|
+
// than assumed.** `indexOf` for a single character is a native scan, so
|
|
130
|
+
// finding the (delta+1)-th occurrence is delta+1 searches instead of one
|
|
131
|
+
// step per base — 1.560x on the sparse fixture and 1.247x on the dense
|
|
132
|
+
// one, parse phase, in `benches/mmDeltaJump.bench.ts`.
|
|
133
|
+
//
|
|
134
|
+
// `lastIndexOf` is NOT the mirror image: the same change on reverse
|
|
135
|
+
// reads measures **0.786x**, i.e. materially slower than stepping, so
|
|
136
|
+
// reverse keeps the loop above. Applying it to both, which is the
|
|
137
|
+
// obvious version, nets 1.054x where branching on strand nets 1.263x.
|
|
138
|
+
for (let i = 1; i < splitLength; i++) {
|
|
139
|
+
const delta = +split[i];
|
|
140
|
+
let at = -1;
|
|
141
|
+
if (isN) {
|
|
142
|
+
// 'N' matches every base, so the (delta+1)-th is delta ahead and
|
|
143
|
+
// there is nothing to search for.
|
|
144
|
+
at = currPos + delta;
|
|
145
|
+
if (at >= seqLength) {
|
|
146
|
+
at = -1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
for (let k = 0; k <= delta; k++) {
|
|
151
|
+
at = fseq.indexOf(base, currPos);
|
|
152
|
+
if (at < 0) {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
currPos = at + 1;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (at < 0) {
|
|
159
|
+
currPos = seqLength;
|
|
160
|
+
positions[writeIndex++] = endClamp;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
currPos = at + 1;
|
|
164
|
+
positions[writeIndex++] = at;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (seenKeys === undefined) {
|
|
169
|
+
seenKeys = [key];
|
|
170
|
+
seenDeltas = [deltas];
|
|
171
|
+
seenPositions = [positions];
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
seenKeys.push(key);
|
|
175
|
+
seenDeltas.push(deltas);
|
|
176
|
+
seenPositions.push(positions);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (isSingleType) {
|
|
180
|
+
result.push({
|
|
181
|
+
type: typestr,
|
|
182
|
+
base,
|
|
183
|
+
strand,
|
|
184
|
+
unknownSkip,
|
|
185
|
+
positions,
|
|
186
|
+
probStart: mlBase,
|
|
187
|
+
probStride: 1,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// Multi-char lowercase: each character is a separate type
|
|
192
|
+
for (let j = 0, len = typestr.length; j < len; j++) {
|
|
193
|
+
result.push({
|
|
194
|
+
type: typestr[j],
|
|
195
|
+
base,
|
|
196
|
+
strand,
|
|
197
|
+
unknownSkip,
|
|
198
|
+
positions,
|
|
199
|
+
probStart: mlBase + j,
|
|
200
|
+
probStride: nTypes,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Unaffected by the reuse above. Two separate groups' ML values are
|
|
205
|
+
// consecutive, not interleaved, so each group consumes its own
|
|
206
|
+
// nPositions * nTypes whether or not it walked for them — which is why
|
|
207
|
+
// sharing the array leaves probStart/probStride alone.
|
|
208
|
+
mlBase += nPositions * nTypes;
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Feature } from '@jbrowse/core/util';
|
|
2
|
+
/**
|
|
3
|
+
* #api
|
|
4
|
+
* Returns the probability value from the flat ML array for a modification's
|
|
5
|
+
* position. `idx` is the position's index within the mod's stored `positions`
|
|
6
|
+
* array; we recover its MM-tag order (reverse-strand reads store positions in
|
|
7
|
+
* descending order) and step into ML by `probStart + mmOrder * probStride`.
|
|
8
|
+
* `probStride` is >1 for combined codes (e.g. 'C+mh'), where ML values are
|
|
9
|
+
* interleaved per position.
|
|
10
|
+
*/
|
|
11
|
+
export declare function modProbAt(probabilities: number[] | undefined, probStart: number, probStride: number, isReverse: boolean, idx: number, posLen: number): number;
|
|
12
|
+
/**
|
|
13
|
+
* #api
|
|
14
|
+
* Reads the ML tag from a feature and returns per-call modification
|
|
15
|
+
* probabilities scaled to 0..1.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getModProbabilities(feature: Feature): number[] | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* #api
|
|
20
|
+
* The ML tag as its raw 0..255 bytes, without the scaling `getModProbabilities`
|
|
21
|
+
* applies.
|
|
22
|
+
*
|
|
23
|
+
* The byte is a LOSSLESS stand-in for the probability — every value on this
|
|
24
|
+
* path is exactly `(N + 0.5) / 256` — and it is monotonic in it, so anything
|
|
25
|
+
* that only compares probabilities (picking the most likely call at a
|
|
26
|
+
* position, testing a threshold) can work in bytes and divide once, at the end,
|
|
27
|
+
* for the few calls that survive. A caller that needs the numbers themselves
|
|
28
|
+
* still wants `getModProbabilities`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getModProbabilityBytes(feature: Feature): ArrayLike<number> | undefined;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getTagAlt } from './getTagAlt.js';
|
|
2
|
+
/**
|
|
3
|
+
* #api
|
|
4
|
+
* Returns the probability value from the flat ML array for a modification's
|
|
5
|
+
* position. `idx` is the position's index within the mod's stored `positions`
|
|
6
|
+
* array; we recover its MM-tag order (reverse-strand reads store positions in
|
|
7
|
+
* descending order) and step into ML by `probStart + mmOrder * probStride`.
|
|
8
|
+
* `probStride` is >1 for combined codes (e.g. 'C+mh'), where ML values are
|
|
9
|
+
* interleaved per position.
|
|
10
|
+
*/
|
|
11
|
+
export function modProbAt(probabilities, probStart, probStride, isReverse, idx, posLen) {
|
|
12
|
+
const mmOrder = isReverse ? posLen - 1 - idx : idx;
|
|
13
|
+
return probabilities?.[probStart + mmOrder * probStride] ?? 0;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* #api
|
|
17
|
+
* Reads the ML tag from a feature and returns per-call modification
|
|
18
|
+
* probabilities scaled to 0..1.
|
|
19
|
+
*/
|
|
20
|
+
export function getModProbabilities(feature) {
|
|
21
|
+
// ML is an 8-bit scaled probability. Per SAMtags, integer N covers the
|
|
22
|
+
// continuous range N/256..(N+1)/256, so the representative value is the
|
|
23
|
+
// midpoint (N + 0.5) / 256.
|
|
24
|
+
const ml = getTagAlt(feature, 'ML', 'Ml');
|
|
25
|
+
if (ml === undefined) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
// BAM returns ML:B:C as a Uint8Array; mapping with TypedArray.prototype.map
|
|
29
|
+
// would coerce each float result back to a uint8 (truncating every value to
|
|
30
|
+
// 0). Array.from(values, fn) always produces a plain number[]. A string ML
|
|
31
|
+
// (htsget/SAM text) is split on commas first.
|
|
32
|
+
const values = typeof ml === 'string' ? ml.split(',') : ml;
|
|
33
|
+
return Array.from(values, v => (+v + 0.5) / 256);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* #api
|
|
37
|
+
* The ML tag as its raw 0..255 bytes, without the scaling `getModProbabilities`
|
|
38
|
+
* applies.
|
|
39
|
+
*
|
|
40
|
+
* The byte is a LOSSLESS stand-in for the probability — every value on this
|
|
41
|
+
* path is exactly `(N + 0.5) / 256` — and it is monotonic in it, so anything
|
|
42
|
+
* that only compares probabilities (picking the most likely call at a
|
|
43
|
+
* position, testing a threshold) can work in bytes and divide once, at the end,
|
|
44
|
+
* for the few calls that survive. A caller that needs the numbers themselves
|
|
45
|
+
* still wants `getModProbabilities`.
|
|
46
|
+
*/
|
|
47
|
+
export function getModProbabilityBytes(feature) {
|
|
48
|
+
const ml = getTagAlt(feature, 'ML', 'Ml');
|
|
49
|
+
if (ml === undefined) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (typeof ml !== 'string') {
|
|
53
|
+
return ml;
|
|
54
|
+
}
|
|
55
|
+
// htsget/SAM text gives a comma-separated string; parse it once into bytes
|
|
56
|
+
// rather than making every read of it coerce.
|
|
57
|
+
const parts = ml.split(',');
|
|
58
|
+
const out = new Uint8Array(parts.length);
|
|
59
|
+
for (let i = 0; i < parts.length; i++) {
|
|
60
|
+
out[i] = +parts[i];
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How one MM header's type string splits into modification types.
|
|
3
|
+
*
|
|
4
|
+
* A multi-character LOWERCASE type string is a combined code — `C+mh` is 5mC
|
|
5
|
+
* and 5hmC called at the same positions — so each character is its own type. A
|
|
6
|
+
* single character, or anything starting outside a-z (a ChEBI number like
|
|
7
|
+
* `C+16061`, an uppercase ambiguity code like `C+C`), is one type whole.
|
|
8
|
+
*
|
|
9
|
+
* Stated once because `getModPositions` and `getModTypes` both have to split
|
|
10
|
+
* the same way: the second exists to answer "which types are here" without the
|
|
11
|
+
* first's work, and a disagreement would show as a modification the menu offers
|
|
12
|
+
* and the display cannot draw.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isSingleModType(typestr: string): boolean;
|
|
15
|
+
export interface ModTypeHeader {
|
|
16
|
+
type: string;
|
|
17
|
+
base: string;
|
|
18
|
+
strand: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* #api
|
|
22
|
+
* The modification types an MM tag declares, from its headers alone.
|
|
23
|
+
*
|
|
24
|
+
* `getModPositions` answers this too, but on the way to placing every call: it
|
|
25
|
+
* walks the delta list against the read sequence, which is the expensive half
|
|
26
|
+
* and is only needed to DRAW marks. Anything that just wants to know what is in
|
|
27
|
+
* the file — which types to offer in a menu, whether a track carries
|
|
28
|
+
* modifications at all — wants this instead, and pays neither the walk nor the
|
|
29
|
+
* sequence decode that feeds it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getModTypes(mm: string): ModTypeHeader[];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { parseModHeader } from './consts.js';
|
|
2
|
+
/**
|
|
3
|
+
* How one MM header's type string splits into modification types.
|
|
4
|
+
*
|
|
5
|
+
* A multi-character LOWERCASE type string is a combined code — `C+mh` is 5mC
|
|
6
|
+
* and 5hmC called at the same positions — so each character is its own type. A
|
|
7
|
+
* single character, or anything starting outside a-z (a ChEBI number like
|
|
8
|
+
* `C+16061`, an uppercase ambiguity code like `C+C`), is one type whole.
|
|
9
|
+
*
|
|
10
|
+
* Stated once because `getModPositions` and `getModTypes` both have to split
|
|
11
|
+
* the same way: the second exists to answer "which types are here" without the
|
|
12
|
+
* first's work, and a disagreement would show as a modification the menu offers
|
|
13
|
+
* and the display cannot draw.
|
|
14
|
+
*/
|
|
15
|
+
export function isSingleModType(typestr) {
|
|
16
|
+
return typestr.charCodeAt(0) < 97 || typestr.length === 1;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* #api
|
|
20
|
+
* The modification types an MM tag declares, from its headers alone.
|
|
21
|
+
*
|
|
22
|
+
* `getModPositions` answers this too, but on the way to placing every call: it
|
|
23
|
+
* walks the delta list against the read sequence, which is the expensive half
|
|
24
|
+
* and is only needed to DRAW marks. Anything that just wants to know what is in
|
|
25
|
+
* the file — which types to offer in a menu, whether a track carries
|
|
26
|
+
* modifications at all — wants this instead, and pays neither the walk nor the
|
|
27
|
+
* sequence decode that feeds it.
|
|
28
|
+
*/
|
|
29
|
+
export function getModTypes(mm) {
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const group of mm.split(';')) {
|
|
32
|
+
if (group === '') {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const comma = group.indexOf(',');
|
|
36
|
+
const basemod = comma === -1 ? group : group.slice(0, comma);
|
|
37
|
+
const { base, strand, typestr } = parseModHeader(basemod, group);
|
|
38
|
+
if (isSingleModType(typestr)) {
|
|
39
|
+
out.push({ type: typestr, base, strand });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
for (let i = 0, l = typestr.length; i < l; i++) {
|
|
43
|
+
out.push({ type: typestr[i], base, strand });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Feature } from '@jbrowse/core/util';
|
|
2
|
+
/**
|
|
3
|
+
* #api
|
|
4
|
+
* Read a single tag by name, using the feature's targeted tag accessor when it
|
|
5
|
+
* has one (BAM) and the full tags object otherwise (CRAM/synteny).
|
|
6
|
+
*/
|
|
7
|
+
export declare function getTag(feature: Feature, tag: string): unknown;
|
|
8
|
+
/**
|
|
9
|
+
* #api
|
|
10
|
+
* Read a tag by its canonical name, falling back to a lowercase-suffixed alias
|
|
11
|
+
* (e.g. MM/Mm, ML/Ml) as emitted by some aligners.
|
|
12
|
+
*
|
|
13
|
+
* Prefers the feature's own one-pass alias lookup when it has one. The plain
|
|
14
|
+
* `getTag(tag) ?? getTag(alt)` form walks the record's whole tag block TWICE
|
|
15
|
+
* whenever neither name is present — which is every read in a file without base
|
|
16
|
+
* modifications, and this is called per read on every render. On jb2bench's
|
|
17
|
+
* 1000x.shortread that pair of walks was 12.9% of the whole BAM query, more
|
|
18
|
+
* than the CIGAR/SEQ/MD reads the pileup actually uses.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getTagAlt(feature: Feature, tag: string, alt: string): unknown;
|
package/esm/getTagAlt.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #api
|
|
3
|
+
* Read a single tag by name, using the feature's targeted tag accessor when it
|
|
4
|
+
* has one (BAM) and the full tags object otherwise (CRAM/synteny).
|
|
5
|
+
*/
|
|
6
|
+
export function getTag(feature, tag) {
|
|
7
|
+
const getter = feature.getTag;
|
|
8
|
+
if (getter) {
|
|
9
|
+
return getter.call(feature, tag);
|
|
10
|
+
}
|
|
11
|
+
return feature.get('tags')?.[tag];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* #api
|
|
15
|
+
* Read a tag by its canonical name, falling back to a lowercase-suffixed alias
|
|
16
|
+
* (e.g. MM/Mm, ML/Ml) as emitted by some aligners.
|
|
17
|
+
*
|
|
18
|
+
* Prefers the feature's own one-pass alias lookup when it has one. The plain
|
|
19
|
+
* `getTag(tag) ?? getTag(alt)` form walks the record's whole tag block TWICE
|
|
20
|
+
* whenever neither name is present — which is every read in a file without base
|
|
21
|
+
* modifications, and this is called per read on every render. On jb2bench's
|
|
22
|
+
* 1000x.shortread that pair of walks was 12.9% of the whole BAM query, more
|
|
23
|
+
* than the CIGAR/SEQ/MD reads the pileup actually uses.
|
|
24
|
+
*/
|
|
25
|
+
export function getTagAlt(feature, tag, alt) {
|
|
26
|
+
const both = feature.getTagAlt;
|
|
27
|
+
if (both) {
|
|
28
|
+
return both.call(feature, tag, alt);
|
|
29
|
+
}
|
|
30
|
+
return getTag(feature, tag) ?? getTag(feature, alt);
|
|
31
|
+
}
|
package/esm/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { modificationRegex, parseModHeader } from './consts.ts';
|
|
2
|
+
export { matchesCytosineContext } from './cytosineContext.ts';
|
|
3
|
+
export type { CytosineContext } from './cytosineContext.ts';
|
|
4
|
+
export { detectSimplexModifications } from './detectSimplexModifications.ts';
|
|
5
|
+
export { forEachModRefPos } from './forEachModRefPos.ts';
|
|
6
|
+
export { getMethBins, isMethylationFillType } from './getMethBins.ts';
|
|
7
|
+
export type { ParsedModData } from './getMethBins.ts';
|
|
8
|
+
export { getModPositions } from './getModPositions.ts';
|
|
9
|
+
export type { ModWithPositions } from './getModPositions.ts';
|
|
10
|
+
export { getModTypes, isSingleModType } from './getModTypes.ts';
|
|
11
|
+
export type { ModTypeHeader } from './getModTypes.ts';
|
|
12
|
+
export { getModProbabilities, getModProbabilityBytes, modProbAt, } from './getModProbabilities.ts';
|
|
13
|
+
export { getTag, getTagAlt } from './getTagAlt.ts';
|
|
14
|
+
export type { ModificationType } from './types.ts';
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { modificationRegex, parseModHeader } from './consts.js';
|
|
2
|
+
export { matchesCytosineContext } from './cytosineContext.js';
|
|
3
|
+
export { detectSimplexModifications } from './detectSimplexModifications.js';
|
|
4
|
+
export { forEachModRefPos } from './forEachModRefPos.js';
|
|
5
|
+
export { getMethBins, isMethylationFillType } from './getMethBins.js';
|
|
6
|
+
export { getModPositions } from './getModPositions.js';
|
|
7
|
+
export { getModTypes, isSingleModType } from './getModTypes.js';
|
|
8
|
+
export { getModProbabilities, getModProbabilityBytes, modProbAt, } from './getModProbabilities.js';
|
|
9
|
+
export { getTag, getTagAlt } from './getTagAlt.js';
|
package/esm/types.d.ts
ADDED
package/esm/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jbrowse/modifications-utils",
|
|
3
|
+
"version": "4.1.14",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Pure MM/ML base-modification tag parsers (methylation, etc.)",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"jbrowse",
|
|
8
|
+
"jbrowse2",
|
|
9
|
+
"modifications",
|
|
10
|
+
"methylation",
|
|
11
|
+
"sam",
|
|
12
|
+
"bam"
|
|
13
|
+
],
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"homepage": "https://jbrowse.org",
|
|
16
|
+
"bugs": "https://github.com/GMOD/jbrowse-components/issues",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/GMOD/jbrowse-components.git",
|
|
20
|
+
"directory": "packages/modifications-utils"
|
|
21
|
+
},
|
|
22
|
+
"author": "JBrowse Team",
|
|
23
|
+
"main": "esm/index.js",
|
|
24
|
+
"files": [
|
|
25
|
+
"esm"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@jbrowse/cigar-utils": "^4.1.14",
|
|
29
|
+
"@jbrowse/core": "^4.3.0"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build:esm": "node ../../node_modules/typescript7/bin/tsc --build tsconfig.build.esm.json",
|
|
37
|
+
"build": "pnpm build:esm",
|
|
38
|
+
"test": "cd ../..; jest --passWithNoTests packages/modifications-utils",
|
|
39
|
+
"clean": "rimraf esm *.tsbuildinfo",
|
|
40
|
+
"prebuild": "pnpm clean"
|
|
41
|
+
},
|
|
42
|
+
"types": "esm/index.d.ts",
|
|
43
|
+
"exports": {
|
|
44
|
+
".": "./esm/index.js"
|
|
45
|
+
}
|
|
46
|
+
}
|