jbrowse-plugin-msaview 2.6.3 → 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.
Files changed (31) hide show
  1. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +8 -9
  2. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +0 -2
  3. package/dist/MsaViewPanel/afterCreateAutoruns.js +1 -63
  4. package/dist/MsaViewPanel/model.d.ts +3 -28
  5. package/dist/MsaViewPanel/model.js +5 -103
  6. package/dist/MsaViewPanel/structureConnection.d.ts +0 -38
  7. package/dist/MsaViewPanel/structureConnection.js +0 -20
  8. package/dist/MsaViewPanel/structureConnection.test.js +1 -35
  9. package/dist/jbrowse-plugin-msaview.umd.production.min.js +26 -26
  10. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  11. package/dist/version.d.ts +1 -1
  12. package/dist/version.js +1 -1
  13. package/package.json +1 -1
  14. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +8 -9
  15. package/src/MsaViewPanel/afterCreateAutoruns.ts +1 -84
  16. package/src/MsaViewPanel/model.ts +4 -135
  17. package/src/MsaViewPanel/structureConnection.test.ts +1 -53
  18. package/src/MsaViewPanel/structureConnection.ts +0 -47
  19. package/src/version.ts +1 -1
  20. package/dist/MsaViewPanel/autoConnectStructures.test.d.ts +0 -1
  21. package/dist/MsaViewPanel/autoConnectStructures.test.js +0 -60
  22. package/dist/MsaViewPanel/components/ConnectStructureDialog.d.ts +0 -7
  23. package/dist/MsaViewPanel/components/ConnectStructureDialog.js +0 -60
  24. package/dist/MsaViewPanel/pairwiseAlignment.d.ts +0 -20
  25. package/dist/MsaViewPanel/pairwiseAlignment.js +0 -138
  26. package/dist/MsaViewPanel/pairwiseAlignment.test.d.ts +0 -1
  27. package/dist/MsaViewPanel/pairwiseAlignment.test.js +0 -111
  28. package/src/MsaViewPanel/autoConnectStructures.test.ts +0 -76
  29. package/src/MsaViewPanel/components/ConnectStructureDialog.tsx +0 -154
  30. package/src/MsaViewPanel/pairwiseAlignment.test.ts +0 -140
  31. 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,76 +0,0 @@
1
- import { getSession } from '@jbrowse/core/util'
2
- import { beforeEach, describe, expect, test, vi } from 'vitest'
3
-
4
- import { autoConnectStructures } from './afterCreateAutoruns'
5
-
6
- import type { JBrowsePluginMsaViewModel } from './model'
7
- import type { ProteinView } from './structureConnection'
8
-
9
- // Integration coverage for the autorun itself — the structure-matching matrix
10
- // lives in structureConnection.test.ts (structureMatchesMsa). Here we check the
11
- // autorun wires a match through to connectToStructure and respects its guards.
12
-
13
- // Mock only getSession; keep the rest of the util module real so the
14
- // afterCreateAutoruns import graph still loads.
15
- vi.mock('@jbrowse/core/util', async importOriginal => ({
16
- ...(await importOriginal<Record<string, unknown>>()),
17
- getSession: vi.fn(),
18
- }))
19
-
20
- const mockGetSession = vi.mocked(getSession)
21
-
22
- function makeModel(opts: { connectedViewId?: string; uniprotId?: string }) {
23
- const connected: { proteinViewId: string; structureIdx: number }[] = []
24
- const model = {
25
- connectedViewId: opts.connectedViewId,
26
- uniprotId: opts.uniprotId,
27
- rows: [['hg38', 'MKATEST']],
28
- connectedStructures: connected,
29
- connectToStructure: (proteinViewId: string, structureIdx: number) => {
30
- connected.push({ proteinViewId, structureIdx })
31
- },
32
- } as unknown as JBrowsePluginMsaViewModel
33
- return { model, connected }
34
- }
35
-
36
- function withStructure(structure: {
37
- connectedViewId?: string
38
- uniprotId?: string
39
- }) {
40
- const view: ProteinView = {
41
- type: 'ProteinView',
42
- id: 'pv1',
43
- structures: [{ ...structure, structureSequences: ['MKATEST'] }],
44
- }
45
- mockGetSession.mockReturnValue({
46
- views: [view],
47
- } as unknown as ReturnType<typeof getSession>)
48
- }
49
-
50
- describe('autoConnectStructures', () => {
51
- beforeEach(() => {
52
- vi.clearAllMocks()
53
- })
54
-
55
- test('connects a matching structure (genome-view link, no UniProt id)', () => {
56
- const { model, connected } = makeModel({ connectedViewId: 'lgv-TP53' })
57
- withStructure({ connectedViewId: 'lgv-TP53' })
58
- autoConnectStructures(model)
59
- expect(connected).toEqual([{ proteinViewId: 'pv1', structureIdx: 0 }])
60
- })
61
-
62
- test('does not connect a non-matching structure', () => {
63
- const { model, connected } = makeModel({ connectedViewId: 'lgv-TP53' })
64
- withStructure({ connectedViewId: 'lgv-OTHER' })
65
- autoConnectStructures(model)
66
- expect(connected).toEqual([])
67
- })
68
-
69
- test('does not connect before the alignment has loaded (no rows)', () => {
70
- const { model, connected } = makeModel({ connectedViewId: 'lgv-TP53' })
71
- ;(model as unknown as { rows: unknown[] }).rows = []
72
- withStructure({ connectedViewId: 'lgv-TP53' })
73
- autoConnectStructures(model)
74
- expect(connected).toEqual([])
75
- })
76
- })
@@ -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
- })