jbrowse-plugin-msaview 2.6.2 → 2.6.4
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/dist/AddHighlightModel/MsaToGenomeHighlight.js +8 -9
- package/dist/LaunchMsaViewExtensionPoint/index.js +12 -5
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +0 -2
- package/dist/MsaViewPanel/afterCreateAutoruns.js +8 -81
- package/dist/MsaViewPanel/model.d.ts +3 -28
- package/dist/MsaViewPanel/model.js +5 -103
- package/dist/MsaViewPanel/structureConnection.d.ts +0 -18
- package/dist/MsaViewPanel/types.d.ts +1 -4
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +26 -26
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +8 -9
- package/src/LaunchMsaViewExtensionPoint/index.ts +12 -5
- package/src/MsaViewPanel/afterCreateAutoruns.ts +8 -109
- package/src/MsaViewPanel/model.ts +4 -135
- package/src/MsaViewPanel/structureConnection.ts +0 -17
- package/src/MsaViewPanel/types.ts +14 -9
- package/src/version.ts +1 -1
- package/dist/MsaViewPanel/components/ConnectStructureDialog.d.ts +0 -7
- package/dist/MsaViewPanel/components/ConnectStructureDialog.js +0 -60
- package/dist/MsaViewPanel/pairwiseAlignment.d.ts +0 -20
- package/dist/MsaViewPanel/pairwiseAlignment.js +0 -138
- package/dist/MsaViewPanel/pairwiseAlignment.test.d.ts +0 -1
- package/dist/MsaViewPanel/pairwiseAlignment.test.js +0 -111
- package/src/MsaViewPanel/components/ConnectStructureDialog.tsx +0 -154
- package/src/MsaViewPanel/pairwiseAlignment.test.ts +0 -140
- package/src/MsaViewPanel/pairwiseAlignment.ts +0 -182
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import BLOSUM62 from './blosum62';
|
|
2
|
-
function getScore(a, b) {
|
|
3
|
-
return BLOSUM62[a.toUpperCase()]?.[b.toUpperCase()] ?? -4;
|
|
4
|
-
}
|
|
5
|
-
const GAP_OPEN = -10;
|
|
6
|
-
const GAP_EXTEND = -0.5;
|
|
7
|
-
export function needlemanWunsch(seq1, seq2, gapOpen = GAP_OPEN, gapExtend = GAP_EXTEND) {
|
|
8
|
-
const m = seq1.length;
|
|
9
|
-
const n = seq2.length;
|
|
10
|
-
const M = [];
|
|
11
|
-
const Ix = [];
|
|
12
|
-
const Iy = [];
|
|
13
|
-
for (let i = 0; i <= m; i++) {
|
|
14
|
-
M[i] = [];
|
|
15
|
-
Ix[i] = [];
|
|
16
|
-
Iy[i] = [];
|
|
17
|
-
for (let j = 0; j <= n; j++) {
|
|
18
|
-
M[i][j] = -Infinity;
|
|
19
|
-
Ix[i][j] = -Infinity;
|
|
20
|
-
Iy[i][j] = -Infinity;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
M[0][0] = 0;
|
|
24
|
-
for (let i = 1; i <= m; i++) {
|
|
25
|
-
Ix[i][0] = gapOpen + (i - 1) * gapExtend;
|
|
26
|
-
}
|
|
27
|
-
for (let j = 1; j <= n; j++) {
|
|
28
|
-
Iy[0][j] = gapOpen + (j - 1) * gapExtend;
|
|
29
|
-
}
|
|
30
|
-
for (let i = 1; i <= m; i++) {
|
|
31
|
-
for (let j = 1; j <= n; j++) {
|
|
32
|
-
const matchScore = getScore(seq1[i - 1], seq2[j - 1]);
|
|
33
|
-
M[i][j] =
|
|
34
|
-
Math.max(M[i - 1][j - 1], Ix[i - 1][j - 1], Iy[i - 1][j - 1]) +
|
|
35
|
-
matchScore;
|
|
36
|
-
Ix[i][j] = Math.max(M[i - 1][j] + gapOpen, Ix[i - 1][j] + gapExtend);
|
|
37
|
-
Iy[i][j] = Math.max(M[i][j - 1] + gapOpen, Iy[i][j - 1] + gapExtend);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
let alignedSeq1 = '';
|
|
41
|
-
let alignedSeq2 = '';
|
|
42
|
-
let i = m;
|
|
43
|
-
let j = n;
|
|
44
|
-
const mScore = M[m][n];
|
|
45
|
-
const ixScore = Ix[m][n];
|
|
46
|
-
const iyScore = Iy[m][n];
|
|
47
|
-
const score = Math.max(mScore, ixScore, iyScore);
|
|
48
|
-
let currentMatrix = score === mScore ? 'M' : score === ixScore ? 'Ix' : 'Iy';
|
|
49
|
-
while (i > 0 || j > 0) {
|
|
50
|
-
if (currentMatrix === 'M' && i > 0 && j > 0) {
|
|
51
|
-
alignedSeq1 = seq1[i - 1] + alignedSeq1;
|
|
52
|
-
alignedSeq2 = seq2[j - 1] + alignedSeq2;
|
|
53
|
-
const matchScore = getScore(seq1[i - 1], seq2[j - 1]);
|
|
54
|
-
const prevM = M[i - 1][j - 1];
|
|
55
|
-
const prevIx = Ix[i - 1][j - 1];
|
|
56
|
-
const currentScore = M[i][j];
|
|
57
|
-
if (currentScore === prevM + matchScore) {
|
|
58
|
-
currentMatrix = 'M';
|
|
59
|
-
}
|
|
60
|
-
else if (currentScore === prevIx + matchScore) {
|
|
61
|
-
currentMatrix = 'Ix';
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
currentMatrix = 'Iy';
|
|
65
|
-
}
|
|
66
|
-
i--;
|
|
67
|
-
j--;
|
|
68
|
-
}
|
|
69
|
-
else if (currentMatrix === 'Ix' && i > 0) {
|
|
70
|
-
alignedSeq1 = seq1[i - 1] + alignedSeq1;
|
|
71
|
-
alignedSeq2 = '-' + alignedSeq2;
|
|
72
|
-
const ixScore = Ix[i][j];
|
|
73
|
-
const mScore = M[i - 1][j] + gapOpen;
|
|
74
|
-
currentMatrix = ixScore === mScore ? 'M' : 'Ix';
|
|
75
|
-
i--;
|
|
76
|
-
}
|
|
77
|
-
else if (j > 0) {
|
|
78
|
-
alignedSeq1 = '-' + alignedSeq1;
|
|
79
|
-
alignedSeq2 = seq2[j - 1] + alignedSeq2;
|
|
80
|
-
const iyScore = Iy[i][j];
|
|
81
|
-
const mScore = M[i][j - 1] + gapOpen;
|
|
82
|
-
currentMatrix = iyScore === mScore ? 'M' : 'Iy';
|
|
83
|
-
j--;
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return { alignedSeq1, alignedSeq2, score };
|
|
90
|
-
}
|
|
91
|
-
function buildConsensus(alignedSeq1, alignedSeq2) {
|
|
92
|
-
let consensus = '';
|
|
93
|
-
for (let i = 0; i < alignedSeq1.length; i++) {
|
|
94
|
-
const a = alignedSeq1[i];
|
|
95
|
-
const b = alignedSeq2[i];
|
|
96
|
-
const match = a !== '-' && b !== '-' && a.toUpperCase() === b.toUpperCase();
|
|
97
|
-
consensus += match ? '|' : ' ';
|
|
98
|
-
}
|
|
99
|
-
return consensus;
|
|
100
|
-
}
|
|
101
|
-
export function runPairwiseAlignment(seq1, seq2) {
|
|
102
|
-
const { alignedSeq1, alignedSeq2 } = needlemanWunsch(seq1, seq2);
|
|
103
|
-
return {
|
|
104
|
-
consensus: buildConsensus(alignedSeq1, alignedSeq2),
|
|
105
|
-
alns: [
|
|
106
|
-
{ id: 'msa', seq: alignedSeq1 },
|
|
107
|
-
{ id: 'structure', seq: alignedSeq2 },
|
|
108
|
-
],
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
export function buildAlignmentMaps(pairwiseAlignment) {
|
|
112
|
-
const seq1 = pairwiseAlignment.alns[0].seq;
|
|
113
|
-
const seq2 = pairwiseAlignment.alns[1].seq;
|
|
114
|
-
if (seq1.length !== seq2.length) {
|
|
115
|
-
throw new Error('Aligned sequences must have same length');
|
|
116
|
-
}
|
|
117
|
-
let pos1 = 0;
|
|
118
|
-
let pos2 = 0;
|
|
119
|
-
const seq1ToSeq2 = new Map();
|
|
120
|
-
const seq2ToSeq1 = new Map();
|
|
121
|
-
for (let i = 0; i < seq1.length; i++) {
|
|
122
|
-
const c1 = seq1[i];
|
|
123
|
-
const c2 = seq2[i];
|
|
124
|
-
if (c1 !== '-' && c2 !== '-') {
|
|
125
|
-
seq1ToSeq2.set(pos1, pos2);
|
|
126
|
-
seq2ToSeq1.set(pos2, pos1);
|
|
127
|
-
pos1++;
|
|
128
|
-
pos2++;
|
|
129
|
-
}
|
|
130
|
-
else if (c1 === '-') {
|
|
131
|
-
pos2++;
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
pos1++;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return { seq1ToSeq2, seq2ToSeq1 };
|
|
138
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import { buildAlignmentMaps, needlemanWunsch, runPairwiseAlignment, } from './pairwiseAlignment';
|
|
3
|
-
describe('needlemanWunsch', () => {
|
|
4
|
-
test('identical sequences align perfectly', () => {
|
|
5
|
-
const result = needlemanWunsch('MKAA', 'MKAA');
|
|
6
|
-
expect(result.alignedSeq1).toBe('MKAA');
|
|
7
|
-
expect(result.alignedSeq2).toBe('MKAA');
|
|
8
|
-
expect(result.score).toBeGreaterThan(0);
|
|
9
|
-
});
|
|
10
|
-
test('aligned sequences have same length', () => {
|
|
11
|
-
const result = needlemanWunsch('MKAAYLSMFG', 'MKAYLSMFG');
|
|
12
|
-
expect(result.alignedSeq1.length).toBe(result.alignedSeq2.length);
|
|
13
|
-
});
|
|
14
|
-
test('aligned sequences preserve original characters', () => {
|
|
15
|
-
const seq1 = 'MKAAYLSMFG';
|
|
16
|
-
const seq2 = 'MKAYLSMFG';
|
|
17
|
-
const result = needlemanWunsch(seq1, seq2);
|
|
18
|
-
expect(result.alignedSeq1.replaceAll('-', '')).toBe(seq1);
|
|
19
|
-
expect(result.alignedSeq2.replaceAll('-', '')).toBe(seq2);
|
|
20
|
-
});
|
|
21
|
-
test('handles empty sequences', () => {
|
|
22
|
-
const result = needlemanWunsch('', '');
|
|
23
|
-
expect(result.alignedSeq1).toBe('');
|
|
24
|
-
expect(result.alignedSeq2).toBe('');
|
|
25
|
-
});
|
|
26
|
-
test('handles one empty sequence', () => {
|
|
27
|
-
const result = needlemanWunsch('MKA', '');
|
|
28
|
-
expect(result.alignedSeq1.replaceAll('-', '')).toBe('MKA');
|
|
29
|
-
expect(result.alignedSeq2.replaceAll('-', '')).toBe('');
|
|
30
|
-
expect(result.alignedSeq1.length).toBe(result.alignedSeq2.length);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
describe('runPairwiseAlignment', () => {
|
|
34
|
-
test('returns PairwiseAlignment format', () => {
|
|
35
|
-
const result = runPairwiseAlignment('MKAA', 'MKAA');
|
|
36
|
-
expect(result.consensus).toBeDefined();
|
|
37
|
-
expect(result.alns).toHaveLength(2);
|
|
38
|
-
expect(result.alns[0].id).toBe('msa');
|
|
39
|
-
expect(result.alns[1].id).toBe('structure');
|
|
40
|
-
});
|
|
41
|
-
test('consensus marks matches with pipe', () => {
|
|
42
|
-
const result = runPairwiseAlignment('MKAA', 'MKAA');
|
|
43
|
-
expect(result.consensus).toBe('||||');
|
|
44
|
-
});
|
|
45
|
-
test('consensus marks gaps with space', () => {
|
|
46
|
-
const result = runPairwiseAlignment('MKAA', 'MKA');
|
|
47
|
-
expect(result.consensus).toContain(' ');
|
|
48
|
-
});
|
|
49
|
-
test('consensus marks mismatches with space', () => {
|
|
50
|
-
const result = runPairwiseAlignment('MKAA', 'MKBA');
|
|
51
|
-
// A vs B should be a space in consensus
|
|
52
|
-
expect(result.consensus).toContain(' ');
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
describe('buildAlignmentMaps', () => {
|
|
56
|
-
test('builds bidirectional maps for identical sequences', () => {
|
|
57
|
-
const alignment = runPairwiseAlignment('MKAA', 'MKAA');
|
|
58
|
-
const { seq1ToSeq2, seq2ToSeq1 } = buildAlignmentMaps(alignment);
|
|
59
|
-
expect(seq1ToSeq2.get(0)).toBe(0);
|
|
60
|
-
expect(seq1ToSeq2.get(1)).toBe(1);
|
|
61
|
-
expect(seq1ToSeq2.get(2)).toBe(2);
|
|
62
|
-
expect(seq1ToSeq2.get(3)).toBe(3);
|
|
63
|
-
expect(seq2ToSeq1.get(0)).toBe(0);
|
|
64
|
-
expect(seq2ToSeq1.get(1)).toBe(1);
|
|
65
|
-
expect(seq2ToSeq1.get(2)).toBe(2);
|
|
66
|
-
expect(seq2ToSeq1.get(3)).toBe(3);
|
|
67
|
-
});
|
|
68
|
-
test('maps are inverses of each other for matched positions', () => {
|
|
69
|
-
const alignment = runPairwiseAlignment('MKAAYLSMFG', 'MKAYLSMFG');
|
|
70
|
-
const { seq1ToSeq2, seq2ToSeq1 } = buildAlignmentMaps(alignment);
|
|
71
|
-
// For every mapped position, the inverse should return the original
|
|
72
|
-
for (const [pos1, pos2] of seq1ToSeq2) {
|
|
73
|
-
expect(seq2ToSeq1.get(pos2)).toBe(pos1);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
test('handles gaps correctly - positions without counterpart are not in map', () => {
|
|
77
|
-
// Aligning 'MKAAA' with 'MKA' should result in gaps
|
|
78
|
-
const alignment = runPairwiseAlignment('MKAAA', 'MKA');
|
|
79
|
-
const { seq1ToSeq2 } = buildAlignmentMaps(alignment);
|
|
80
|
-
// seq1 has 5 positions, seq2 has 3
|
|
81
|
-
// Only matched positions should be in the map
|
|
82
|
-
expect(seq1ToSeq2.size).toBeLessThanOrEqual(3);
|
|
83
|
-
});
|
|
84
|
-
test('handles real protein sequence alignment', () => {
|
|
85
|
-
const msaSeq = 'MKAAYLSMFGKEDHKPFGDDEVELFRAVPGLKLKIAG';
|
|
86
|
-
const structureSeq = 'MKAAYLSMFGKEDHKPFGDDEVELFRAVPGLKLKIAG';
|
|
87
|
-
const alignment = runPairwiseAlignment(msaSeq, structureSeq);
|
|
88
|
-
const { seq1ToSeq2, seq2ToSeq1 } = buildAlignmentMaps(alignment);
|
|
89
|
-
// Identical sequences should have 1:1 mapping
|
|
90
|
-
expect(seq1ToSeq2.size).toBe(msaSeq.length);
|
|
91
|
-
expect(seq2ToSeq1.size).toBe(structureSeq.length);
|
|
92
|
-
// Check a few positions
|
|
93
|
-
expect(seq1ToSeq2.get(0)).toBe(0);
|
|
94
|
-
expect(seq1ToSeq2.get(10)).toBe(10);
|
|
95
|
-
expect(seq2ToSeq1.get(20)).toBe(20);
|
|
96
|
-
});
|
|
97
|
-
test('handles sequences with insertions/deletions', () => {
|
|
98
|
-
// MSA sequence has an extra 'X' in the middle
|
|
99
|
-
const msaSeq = 'MKAXYLSMFG';
|
|
100
|
-
const structureSeq = 'MKAYLSMFG';
|
|
101
|
-
const alignment = runPairwiseAlignment(msaSeq, structureSeq);
|
|
102
|
-
const { seq1ToSeq2 } = buildAlignmentMaps(alignment);
|
|
103
|
-
// Positions before the insertion should map correctly
|
|
104
|
-
expect(seq1ToSeq2.get(0)).toBe(0); // M
|
|
105
|
-
expect(seq1ToSeq2.get(1)).toBe(1); // K
|
|
106
|
-
expect(seq1ToSeq2.get(2)).toBe(2); // A
|
|
107
|
-
// The mapping should handle the offset after insertion
|
|
108
|
-
// (exact behavior depends on alignment algorithm)
|
|
109
|
-
expect(seq1ToSeq2.size).toBeLessThanOrEqual(structureSeq.length);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
|
|
3
|
-
import { Dialog, ErrorMessage } from '@jbrowse/core/ui'
|
|
4
|
-
import { getSession } from '@jbrowse/core/util'
|
|
5
|
-
import {
|
|
6
|
-
Button,
|
|
7
|
-
DialogActions,
|
|
8
|
-
DialogContent,
|
|
9
|
-
FormControl,
|
|
10
|
-
InputLabel,
|
|
11
|
-
MenuItem,
|
|
12
|
-
Select,
|
|
13
|
-
Typography,
|
|
14
|
-
} from '@mui/material'
|
|
15
|
-
import { observer } from 'mobx-react'
|
|
16
|
-
import { makeStyles } from 'tss-react/mui'
|
|
17
|
-
|
|
18
|
-
import { getProteinViews } from '../structureConnection'
|
|
19
|
-
|
|
20
|
-
import type { JBrowsePluginMsaViewModel } from '../model'
|
|
21
|
-
|
|
22
|
-
const useStyles = makeStyles()(theme => ({
|
|
23
|
-
formControl: {
|
|
24
|
-
marginBottom: theme.spacing(2),
|
|
25
|
-
},
|
|
26
|
-
}))
|
|
27
|
-
|
|
28
|
-
const ConnectStructureDialog = observer(function ConnectStructureDialog({
|
|
29
|
-
model,
|
|
30
|
-
handleClose,
|
|
31
|
-
}: {
|
|
32
|
-
model: JBrowsePluginMsaViewModel
|
|
33
|
-
handleClose: () => void
|
|
34
|
-
}) {
|
|
35
|
-
const { classes } = useStyles()
|
|
36
|
-
const session = getSession(model)
|
|
37
|
-
const [selectedViewId, setSelectedViewId] = useState('')
|
|
38
|
-
const [selectedStructureIdx, setSelectedStructureIdx] = useState(0)
|
|
39
|
-
const [selectedMsaRow, setSelectedMsaRow] = useState(model.querySeqName)
|
|
40
|
-
const [error, setError] = useState<string>()
|
|
41
|
-
|
|
42
|
-
const proteinViews = getProteinViews(session.views)
|
|
43
|
-
|
|
44
|
-
const selectedView = proteinViews.find(v => v.id === selectedViewId)
|
|
45
|
-
const structures = selectedView?.structures ?? []
|
|
46
|
-
|
|
47
|
-
const msaRowNames = model.rows.map(r => r[0])
|
|
48
|
-
|
|
49
|
-
const handleConnect = () => {
|
|
50
|
-
if (!selectedViewId) {
|
|
51
|
-
setError('Please select a protein view')
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
model.connectToStructure(
|
|
57
|
-
selectedViewId,
|
|
58
|
-
selectedStructureIdx,
|
|
59
|
-
selectedMsaRow,
|
|
60
|
-
)
|
|
61
|
-
handleClose()
|
|
62
|
-
} catch (e) {
|
|
63
|
-
setError(e instanceof Error ? e.message : String(e))
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<Dialog
|
|
69
|
-
maxWidth="sm"
|
|
70
|
-
title="Connect to Protein Structure"
|
|
71
|
-
open
|
|
72
|
-
onClose={handleClose}
|
|
73
|
-
>
|
|
74
|
-
<DialogContent>
|
|
75
|
-
{proteinViews.length === 0 ? (
|
|
76
|
-
<Typography color="textSecondary">
|
|
77
|
-
No protein views are currently open. Please open a protein structure
|
|
78
|
-
view first.
|
|
79
|
-
</Typography>
|
|
80
|
-
) : (
|
|
81
|
-
<>
|
|
82
|
-
<FormControl fullWidth className={classes.formControl}>
|
|
83
|
-
<InputLabel>Protein View</InputLabel>
|
|
84
|
-
<Select
|
|
85
|
-
value={selectedViewId}
|
|
86
|
-
label="Protein View"
|
|
87
|
-
onChange={e => {
|
|
88
|
-
setSelectedViewId(e.target.value)
|
|
89
|
-
setSelectedStructureIdx(0)
|
|
90
|
-
}}
|
|
91
|
-
>
|
|
92
|
-
{proteinViews.map(view => (
|
|
93
|
-
<MenuItem key={view.id} value={view.id}>
|
|
94
|
-
{view.displayName ?? `ProteinView ${view.id}`}
|
|
95
|
-
</MenuItem>
|
|
96
|
-
))}
|
|
97
|
-
</Select>
|
|
98
|
-
</FormControl>
|
|
99
|
-
|
|
100
|
-
{structures.length > 1 ? (
|
|
101
|
-
<FormControl fullWidth className={classes.formControl}>
|
|
102
|
-
<InputLabel>Structure</InputLabel>
|
|
103
|
-
<Select
|
|
104
|
-
value={selectedStructureIdx}
|
|
105
|
-
label="Structure"
|
|
106
|
-
onChange={e => {
|
|
107
|
-
setSelectedStructureIdx(e.target.value)
|
|
108
|
-
}}
|
|
109
|
-
>
|
|
110
|
-
{structures.map((structure, idx) => (
|
|
111
|
-
<MenuItem key={idx} value={idx}>
|
|
112
|
-
{structure.url ?? `Structure ${idx + 1}`}
|
|
113
|
-
</MenuItem>
|
|
114
|
-
))}
|
|
115
|
-
</Select>
|
|
116
|
-
</FormControl>
|
|
117
|
-
) : null}
|
|
118
|
-
|
|
119
|
-
<FormControl fullWidth className={classes.formControl}>
|
|
120
|
-
<InputLabel>MSA Row</InputLabel>
|
|
121
|
-
<Select
|
|
122
|
-
value={selectedMsaRow}
|
|
123
|
-
label="MSA Row"
|
|
124
|
-
onChange={e => {
|
|
125
|
-
setSelectedMsaRow(e.target.value)
|
|
126
|
-
}}
|
|
127
|
-
>
|
|
128
|
-
{msaRowNames.map(name => (
|
|
129
|
-
<MenuItem key={name} value={name}>
|
|
130
|
-
{name}
|
|
131
|
-
</MenuItem>
|
|
132
|
-
))}
|
|
133
|
-
</Select>
|
|
134
|
-
</FormControl>
|
|
135
|
-
|
|
136
|
-
{error ? <ErrorMessage error={error} /> : null}
|
|
137
|
-
</>
|
|
138
|
-
)}
|
|
139
|
-
</DialogContent>
|
|
140
|
-
<DialogActions>
|
|
141
|
-
<Button onClick={handleClose}>Cancel</Button>
|
|
142
|
-
<Button
|
|
143
|
-
onClick={handleConnect}
|
|
144
|
-
variant="contained"
|
|
145
|
-
disabled={proteinViews.length === 0 || !selectedViewId}
|
|
146
|
-
>
|
|
147
|
-
Connect
|
|
148
|
-
</Button>
|
|
149
|
-
</DialogActions>
|
|
150
|
-
</Dialog>
|
|
151
|
-
)
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
export default ConnectStructureDialog
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
buildAlignmentMaps,
|
|
5
|
-
needlemanWunsch,
|
|
6
|
-
runPairwiseAlignment,
|
|
7
|
-
} from './pairwiseAlignment'
|
|
8
|
-
|
|
9
|
-
describe('needlemanWunsch', () => {
|
|
10
|
-
test('identical sequences align perfectly', () => {
|
|
11
|
-
const result = needlemanWunsch('MKAA', 'MKAA')
|
|
12
|
-
expect(result.alignedSeq1).toBe('MKAA')
|
|
13
|
-
expect(result.alignedSeq2).toBe('MKAA')
|
|
14
|
-
expect(result.score).toBeGreaterThan(0)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('aligned sequences have same length', () => {
|
|
18
|
-
const result = needlemanWunsch('MKAAYLSMFG', 'MKAYLSMFG')
|
|
19
|
-
expect(result.alignedSeq1.length).toBe(result.alignedSeq2.length)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('aligned sequences preserve original characters', () => {
|
|
23
|
-
const seq1 = 'MKAAYLSMFG'
|
|
24
|
-
const seq2 = 'MKAYLSMFG'
|
|
25
|
-
const result = needlemanWunsch(seq1, seq2)
|
|
26
|
-
expect(result.alignedSeq1.replaceAll('-', '')).toBe(seq1)
|
|
27
|
-
expect(result.alignedSeq2.replaceAll('-', '')).toBe(seq2)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
test('handles empty sequences', () => {
|
|
31
|
-
const result = needlemanWunsch('', '')
|
|
32
|
-
expect(result.alignedSeq1).toBe('')
|
|
33
|
-
expect(result.alignedSeq2).toBe('')
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('handles one empty sequence', () => {
|
|
37
|
-
const result = needlemanWunsch('MKA', '')
|
|
38
|
-
expect(result.alignedSeq1.replaceAll('-', '')).toBe('MKA')
|
|
39
|
-
expect(result.alignedSeq2.replaceAll('-', '')).toBe('')
|
|
40
|
-
expect(result.alignedSeq1.length).toBe(result.alignedSeq2.length)
|
|
41
|
-
})
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
describe('runPairwiseAlignment', () => {
|
|
45
|
-
test('returns PairwiseAlignment format', () => {
|
|
46
|
-
const result = runPairwiseAlignment('MKAA', 'MKAA')
|
|
47
|
-
expect(result.consensus).toBeDefined()
|
|
48
|
-
expect(result.alns).toHaveLength(2)
|
|
49
|
-
expect(result.alns[0].id).toBe('msa')
|
|
50
|
-
expect(result.alns[1].id).toBe('structure')
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test('consensus marks matches with pipe', () => {
|
|
54
|
-
const result = runPairwiseAlignment('MKAA', 'MKAA')
|
|
55
|
-
expect(result.consensus).toBe('||||')
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test('consensus marks gaps with space', () => {
|
|
59
|
-
const result = runPairwiseAlignment('MKAA', 'MKA')
|
|
60
|
-
expect(result.consensus).toContain(' ')
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test('consensus marks mismatches with space', () => {
|
|
64
|
-
const result = runPairwiseAlignment('MKAA', 'MKBA')
|
|
65
|
-
// A vs B should be a space in consensus
|
|
66
|
-
expect(result.consensus).toContain(' ')
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
describe('buildAlignmentMaps', () => {
|
|
71
|
-
test('builds bidirectional maps for identical sequences', () => {
|
|
72
|
-
const alignment = runPairwiseAlignment('MKAA', 'MKAA')
|
|
73
|
-
const { seq1ToSeq2, seq2ToSeq1 } = buildAlignmentMaps(alignment)
|
|
74
|
-
|
|
75
|
-
expect(seq1ToSeq2.get(0)).toBe(0)
|
|
76
|
-
expect(seq1ToSeq2.get(1)).toBe(1)
|
|
77
|
-
expect(seq1ToSeq2.get(2)).toBe(2)
|
|
78
|
-
expect(seq1ToSeq2.get(3)).toBe(3)
|
|
79
|
-
|
|
80
|
-
expect(seq2ToSeq1.get(0)).toBe(0)
|
|
81
|
-
expect(seq2ToSeq1.get(1)).toBe(1)
|
|
82
|
-
expect(seq2ToSeq1.get(2)).toBe(2)
|
|
83
|
-
expect(seq2ToSeq1.get(3)).toBe(3)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
test('maps are inverses of each other for matched positions', () => {
|
|
87
|
-
const alignment = runPairwiseAlignment('MKAAYLSMFG', 'MKAYLSMFG')
|
|
88
|
-
const { seq1ToSeq2, seq2ToSeq1 } = buildAlignmentMaps(alignment)
|
|
89
|
-
|
|
90
|
-
// For every mapped position, the inverse should return the original
|
|
91
|
-
for (const [pos1, pos2] of seq1ToSeq2) {
|
|
92
|
-
expect(seq2ToSeq1.get(pos2)).toBe(pos1)
|
|
93
|
-
}
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
test('handles gaps correctly - positions without counterpart are not in map', () => {
|
|
97
|
-
// Aligning 'MKAAA' with 'MKA' should result in gaps
|
|
98
|
-
const alignment = runPairwiseAlignment('MKAAA', 'MKA')
|
|
99
|
-
const { seq1ToSeq2 } = buildAlignmentMaps(alignment)
|
|
100
|
-
|
|
101
|
-
// seq1 has 5 positions, seq2 has 3
|
|
102
|
-
// Only matched positions should be in the map
|
|
103
|
-
expect(seq1ToSeq2.size).toBeLessThanOrEqual(3)
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
test('handles real protein sequence alignment', () => {
|
|
107
|
-
const msaSeq = 'MKAAYLSMFGKEDHKPFGDDEVELFRAVPGLKLKIAG'
|
|
108
|
-
const structureSeq = 'MKAAYLSMFGKEDHKPFGDDEVELFRAVPGLKLKIAG'
|
|
109
|
-
|
|
110
|
-
const alignment = runPairwiseAlignment(msaSeq, structureSeq)
|
|
111
|
-
const { seq1ToSeq2, seq2ToSeq1 } = buildAlignmentMaps(alignment)
|
|
112
|
-
|
|
113
|
-
// Identical sequences should have 1:1 mapping
|
|
114
|
-
expect(seq1ToSeq2.size).toBe(msaSeq.length)
|
|
115
|
-
expect(seq2ToSeq1.size).toBe(structureSeq.length)
|
|
116
|
-
|
|
117
|
-
// Check a few positions
|
|
118
|
-
expect(seq1ToSeq2.get(0)).toBe(0)
|
|
119
|
-
expect(seq1ToSeq2.get(10)).toBe(10)
|
|
120
|
-
expect(seq2ToSeq1.get(20)).toBe(20)
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
test('handles sequences with insertions/deletions', () => {
|
|
124
|
-
// MSA sequence has an extra 'X' in the middle
|
|
125
|
-
const msaSeq = 'MKAXYLSMFG'
|
|
126
|
-
const structureSeq = 'MKAYLSMFG'
|
|
127
|
-
|
|
128
|
-
const alignment = runPairwiseAlignment(msaSeq, structureSeq)
|
|
129
|
-
const { seq1ToSeq2 } = buildAlignmentMaps(alignment)
|
|
130
|
-
|
|
131
|
-
// Positions before the insertion should map correctly
|
|
132
|
-
expect(seq1ToSeq2.get(0)).toBe(0) // M
|
|
133
|
-
expect(seq1ToSeq2.get(1)).toBe(1) // K
|
|
134
|
-
expect(seq1ToSeq2.get(2)).toBe(2) // A
|
|
135
|
-
|
|
136
|
-
// The mapping should handle the offset after insertion
|
|
137
|
-
// (exact behavior depends on alignment algorithm)
|
|
138
|
-
expect(seq1ToSeq2.size).toBeLessThanOrEqual(structureSeq.length)
|
|
139
|
-
})
|
|
140
|
-
})
|