Skip to content

Commit eb7eb50

Browse files
committed
Init
1 parent 6965211 commit eb7eb50

File tree

7 files changed

+392
-24
lines changed

7 files changed

+392
-24
lines changed

packages/core/configuration/configurationSchema.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,6 @@ function makeConfigurationSchemaModel<
204204
completeModel = completeModel.postProcessSnapshot(snap => {
205205
const newSnap: SnapshotOut<typeof completeModel> = {}
206206
let matchesDefault = true
207-
// let keyCount = 0
208207
for (const [key, value] of Object.entries(snap)) {
209208
if (matchesDefault) {
210209
if (typeof defaultSnap[key] === 'object' && typeof value === 'object') {
@@ -221,14 +220,10 @@ function makeConfigurationSchemaModel<
221220
!isEmptyObject(value) &&
222221
!isEmptyArray(value)
223222
) {
224-
// keyCount += 1
225223
newSnap[key] = value
226224
}
227225
}
228-
if (matchesDefault) {
229-
return {}
230-
}
231-
return newSnap
226+
return matchesDefault ? {} : newSnap
232227
})
233228

234229
if (options.preProcessSnapshot) {
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { readConfObject } from '@jbrowse/core/configuration'
2+
import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter'
3+
import { fetchAndMaybeUnzip } from '@jbrowse/core/util'
4+
import { openLocation } from '@jbrowse/core/util/io'
5+
import { doesIntersect2 } from '@jbrowse/core/util/range'
6+
import { ObservableCreate } from '@jbrowse/core/util/rxjs'
7+
import { MismatchParser } from '@jbrowse/plugin-alignments'
8+
9+
import SyntenyFeature from '../SyntenyFeature'
10+
import {
11+
flipCigar,
12+
parseLineByLine,
13+
parsePAFLine,
14+
swapIndelCigar,
15+
} from '../util'
16+
import { getWeightedMeans } from './util'
17+
18+
import type { PAFRecord } from './util'
19+
import type { AnyConfigurationModel } from '@jbrowse/core/configuration'
20+
import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter'
21+
import type { Feature } from '@jbrowse/core/util'
22+
import type { Region } from '@jbrowse/core/util/types'
23+
24+
const { parseCigar } = MismatchParser
25+
26+
interface PAFOptions extends BaseOptions {
27+
config?: AnyConfigurationModel
28+
}
29+
30+
export default class AllVsAllPAFAdapter extends BaseFeatureDataAdapter {
31+
private setupP?: Promise<PAFRecord[]>
32+
33+
public static capabilities = ['getFeatures', 'getRefNames']
34+
35+
async setup(opts?: BaseOptions) {
36+
if (!this.setupP) {
37+
this.setupP = this.setupPre(opts).catch((e: unknown) => {
38+
this.setupP = undefined
39+
throw e
40+
})
41+
}
42+
return this.setupP
43+
}
44+
45+
async setupPre(opts?: BaseOptions) {
46+
return parseLineByLine(
47+
await fetchAndMaybeUnzip(
48+
openLocation(this.getConf('pafLocation'), this.pluginManager),
49+
opts,
50+
),
51+
parsePAFLine,
52+
opts,
53+
)
54+
}
55+
56+
async hasDataForRefName() {
57+
// determining this properly is basically a call to getFeatures so is not
58+
// really that important, and has to be true or else getFeatures is never
59+
// called (BaseAdapter filters it out)
60+
return true
61+
}
62+
63+
getAssemblyNames() {
64+
return this.getConf('assemblyNames') as string[]
65+
}
66+
67+
async getRefNames(opts: BaseOptions = {}) {
68+
// @ts-expect-error
69+
const r1 = opts.regions?.[0].assemblyName
70+
const feats = await this.setup(opts)
71+
72+
const idx = this.getAssemblyNames().indexOf(r1)
73+
if (idx !== -1) {
74+
const set = new Set<string>()
75+
for (const feat of feats) {
76+
set.add(idx === 0 ? feat.qname : feat.tname)
77+
}
78+
return [...set]
79+
}
80+
console.warn('Unable to do ref renaming on adapter')
81+
return []
82+
}
83+
84+
getFeatures(query: Region, opts: PAFOptions = {}) {
85+
return ObservableCreate<Feature>(async observer => {
86+
let pafRecords = await this.setup(opts)
87+
const { config } = opts
88+
89+
// note: this is not the adapter config, it is responding to a display
90+
// setting passed in via the opts parameter
91+
if (config && readConfObject(config, 'colorBy') === 'meanQueryIdentity') {
92+
pafRecords = getWeightedMeans(pafRecords)
93+
}
94+
const assemblyNames = this.getAssemblyNames()
95+
96+
// The index of the assembly name in the query list corresponds to the
97+
// adapter in the subadapters list
98+
const { start: qstart, end: qend, refName: qref, assemblyName } = query
99+
const index = assemblyNames.indexOf(assemblyName)
100+
101+
// if the getFeatures::query is on the query assembly, flip orientation
102+
// of data
103+
const flip = index === 0
104+
if (index === -1) {
105+
console.warn(`${assemblyName} not found in this adapter`)
106+
observer.complete()
107+
}
108+
109+
// eslint-disable-next-line unicorn/no-for-loop
110+
for (let i = 0; i < pafRecords.length; i++) {
111+
const r = pafRecords[i]!
112+
let start = 0
113+
let end = 0
114+
let refName = ''
115+
let mateName = ''
116+
let mateStart = 0
117+
let mateEnd = 0
118+
119+
if (flip) {
120+
start = r.qstart
121+
end = r.qend
122+
refName = r.qname
123+
mateName = r.tname
124+
mateStart = r.tstart
125+
mateEnd = r.tend
126+
} else {
127+
start = r.tstart
128+
end = r.tend
129+
refName = r.tname
130+
mateName = r.qname
131+
mateStart = r.qstart
132+
mateEnd = r.qend
133+
}
134+
const { extra, strand } = r
135+
if (refName === qref && doesIntersect2(qstart, qend, start, end)) {
136+
const { numMatches = 0, blockLen = 1, cg, ...rest } = extra
137+
138+
let CIGAR = extra.cg
139+
if (extra.cg) {
140+
if (flip && strand === -1) {
141+
CIGAR = flipCigar(parseCigar(extra.cg)).join('')
142+
} else if (flip) {
143+
CIGAR = swapIndelCigar(extra.cg)
144+
}
145+
}
146+
147+
observer.next(
148+
new SyntenyFeature({
149+
uniqueId: i + assemblyName,
150+
assemblyName,
151+
start,
152+
end,
153+
type: 'match',
154+
refName,
155+
strand,
156+
...rest,
157+
CIGAR,
158+
syntenyId: i,
159+
identity: numMatches / blockLen,
160+
numMatches,
161+
blockLen,
162+
mate: {
163+
start: mateStart,
164+
end: mateEnd,
165+
refName: mateName,
166+
assemblyName: assemblyNames[+flip],
167+
},
168+
}),
169+
)
170+
}
171+
}
172+
173+
observer.complete()
174+
})
175+
}
176+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { ConfigurationSchema } from '@jbrowse/core/configuration'
2+
3+
/**
4+
* #config AllVsAllPAFAdapter
5+
*/
6+
function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars
7+
8+
const AllVsAllPAFAdapter = ConfigurationSchema(
9+
'AllVsAllPAFAdapter',
10+
{
11+
/**
12+
* #slot
13+
*/
14+
assemblyNames: {
15+
type: 'stringArray',
16+
defaultValue: [],
17+
description:
18+
'Array of assembly names to use for this file. The query assembly name is the first value in the array, target assembly name is the second',
19+
},
20+
/**
21+
* #slot
22+
* can be optionally gzipped
23+
*/
24+
pafLocation: {
25+
type: 'fileLocation',
26+
defaultValue: {
27+
uri: '/path/to/file.paf',
28+
locationType: 'UriLocation',
29+
},
30+
},
31+
},
32+
{
33+
explicitlyTyped: true,
34+
35+
/**
36+
* #preProcessSnapshot
37+
*
38+
*
39+
* preprocessor to allow minimal config:
40+
* ```json
41+
* {
42+
* "type": "AllVsAllPAFAdapter",
43+
* "uri": "file.paf.gz"
44+
* }
45+
* ```
46+
*/
47+
preProcessSnapshot: snap => {
48+
return snap.uri
49+
? {
50+
...snap,
51+
pafLocation: {
52+
uri: snap.uri,
53+
baseUri: snap.baseUri,
54+
},
55+
}
56+
: snap
57+
},
58+
},
59+
)
60+
61+
export default AllVsAllPAFAdapter
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType'
2+
3+
import configSchema from './configSchema'
4+
5+
import type PluginManager from '@jbrowse/core/PluginManager'
6+
7+
export default function AllVsAllPAFAdapterF(pluginManager: PluginManager) {
8+
pluginManager.addAdapterType(
9+
() =>
10+
new AdapterType({
11+
name: 'AllVsAllPAFAdapter',
12+
displayName: 'AllVsAllPAF adapter',
13+
configSchema,
14+
adapterMetadata: {
15+
category: 'Synteny adapters',
16+
},
17+
getAdapterClass: () =>
18+
import('./AllVsAllPAFAdapter').then(r => r.default),
19+
}),
20+
)
21+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { zip } from '../util'
2+
3+
export interface PAFRecord {
4+
qname: string
5+
qstart: number
6+
qend: number
7+
tname: string
8+
tstart: number
9+
tend: number
10+
strand: number
11+
extra: {
12+
cg?: string
13+
blockLen?: number
14+
mappingQual?: number
15+
numMatches?: number
16+
meanScore?: number
17+
}
18+
}
19+
// based on "weighted mean" method from https://github.com/tpoorten/dotPlotly
20+
// License reproduced here
21+
//
22+
// MIT License
23+
24+
// Copyright (c) 2017 Tom Poorten
25+
26+
// Permission is hereby granted, free of charge, to any person obtaining a copy
27+
// of this software and associated documentation files (the "Software"), to deal
28+
// in the Software without restriction, including without limitation the rights
29+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
30+
// copies of the Software, and to permit persons to whom the Software is
31+
// furnished to do so, subject to the following conditions:
32+
33+
// The above copyright notice and this permission notice shall be included in all
34+
// copies or substantial portions of the Software.
35+
36+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
37+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
38+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
39+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
40+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
41+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
42+
// SOFTWARE.
43+
//
44+
// Notes: in the weighted mean longer alignments factor in more heavily of all
45+
// the fragments of a query vs the reference that it mapped to
46+
//
47+
// this uses a combined key query+'-'+ref to iteratively map all the alignments
48+
// that match a particular ref from a particular query (so 1d array of what
49+
// could be a 2d map)
50+
//
51+
// the result is a single number that says e.g. chr5 from human mapped to chr5
52+
// on mouse with 0.8 quality, and that0.8 is then attached to all the pieces of
53+
// chr5 on human that mapped to chr5 on mouse. if chr5 on human also more
54+
// weakly mapped to chr6 on mouse, then it would have another value e.g. 0.6.
55+
// this can show strong and weak levels of synteny, especially in polyploidy
56+
// situations
57+
58+
export function getWeightedMeans(ret: PAFRecord[]) {
59+
const scoreMap: Record<string, { quals: number[]; len: number[] }> = {}
60+
for (const entry of ret) {
61+
const query = entry.qname
62+
const target = entry.tname
63+
const key = `${query}-${target}`
64+
if (!scoreMap[key]) {
65+
scoreMap[key] = { quals: [], len: [] }
66+
}
67+
scoreMap[key].quals.push(entry.extra.mappingQual || 1)
68+
scoreMap[key].len.push(entry.extra.blockLen || 1)
69+
}
70+
71+
const meanScoreMap = Object.fromEntries(
72+
Object.entries(scoreMap).map(([key, val]) => {
73+
const vals = zip(val.quals, val.len)
74+
return [key, weightedMean(vals)]
75+
}),
76+
)
77+
for (const entry of ret) {
78+
const query = entry.qname
79+
const target = entry.tname
80+
const key = `${query}-${target}`
81+
entry.extra.meanScore = meanScoreMap[key]
82+
}
83+
84+
let min = 10000
85+
let max = 0
86+
for (const entry of ret) {
87+
min = Math.min(entry.extra.meanScore || 0, min)
88+
max = Math.max(entry.extra.meanScore || 0, max)
89+
}
90+
for (const entry of ret) {
91+
const b = entry.extra.meanScore || 0
92+
entry.extra.meanScore = (b - min) / (max - min)
93+
}
94+
95+
return ret
96+
}
97+
98+
// https://gist.github.com/stekhn/a12ed417e91f90ecec14bcfa4c2ae16a
99+
function weightedMean(tuples: [number, number][]) {
100+
const [valueSum, weightSum] = tuples.reduce(
101+
([valueSum, weightSum], [value, weight]) => [
102+
valueSum + value * weight,
103+
weightSum + weight,
104+
],
105+
[0, 0],
106+
)
107+
return valueSum / weightSum
108+
}

0 commit comments

Comments
 (0)