@datagrok/proteomics 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +52 -3
  2. package/CLAUDE.md +178 -0
  3. package/README.md +195 -3
  4. package/detectors.js +152 -9
  5. package/dist/package-test.js +1 -1744
  6. package/dist/package-test.js.map +1 -1
  7. package/dist/package.js +1 -146
  8. package/dist/package.js.map +1 -1
  9. package/docs/personas-and-capabilities.md +165 -0
  10. package/files/demo/README.md +264 -0
  11. package/files/demo/cptac-spike-in.txt +1571 -0
  12. package/files/demo/enrichment-demo.csv +120 -0
  13. package/files/demo/fragpipe-smoke-test.tsv +11 -0
  14. package/files/demo/proteinGroups.txt +28 -0
  15. package/files/demo/spectronaut-hye-candidates.tsv +94 -0
  16. package/files/demo/spectronaut-hye-demo.tsv +8761 -0
  17. package/files/demo/spectronaut-hye-mix.tsv +8761 -0
  18. package/files/demo/spectronaut-hye-precursor-golden.json +938 -0
  19. package/files/demo/spectronaut-hye-precursor-golden.tsv +235 -0
  20. package/files/demo/spectronaut-hye-precursor.tsv +493 -0
  21. package/images/enrichment-crosslink.png +0 -0
  22. package/images/enrichment-term-selected.png +0 -0
  23. package/images/hero.png +0 -0
  24. package/images/pipeline.svg +80 -0
  25. package/package.json +87 -63
  26. package/scripts/deqms_de.R +60 -0
  27. package/scripts/limma_de.R +42 -0
  28. package/scripts/vsn_normalize.R +19 -0
  29. package/src/analysis/differential-expression.ts +450 -0
  30. package/src/analysis/enrichment-export.ts +101 -0
  31. package/src/analysis/enrichment.ts +602 -0
  32. package/src/analysis/experiment-setup.ts +199 -0
  33. package/src/analysis/imputation.ts +407 -0
  34. package/src/analysis/log2-scale.ts +139 -0
  35. package/src/analysis/normalization.ts +255 -0
  36. package/src/analysis/pca.ts +254 -0
  37. package/src/analysis/spc-storage.ts +515 -0
  38. package/src/analysis/spc.ts +544 -0
  39. package/src/analysis/subcellular-location.ts +431 -0
  40. package/src/demo/enrichment-demo.ts +94 -0
  41. package/src/demo/proteomics-demo.ts +123 -0
  42. package/src/global.d.ts +15 -0
  43. package/src/menu.ts +133 -0
  44. package/src/package-api.ts +136 -14
  45. package/src/package-test.ts +45 -20
  46. package/src/package.g.ts +161 -0
  47. package/src/package.ts +1029 -17
  48. package/src/panels/protein-focus.ts +63 -0
  49. package/src/panels/published-analysis-panel.ts +151 -0
  50. package/src/panels/uniprot-panel.ts +349 -0
  51. package/src/parsers/fragpipe-parser.ts +200 -0
  52. package/src/parsers/generic-parser.ts +197 -0
  53. package/src/parsers/maxquant-parser.ts +162 -0
  54. package/src/parsers/shared-utils.ts +163 -0
  55. package/src/parsers/spectronaut-candidates-parser.ts +307 -0
  56. package/src/parsers/spectronaut-parser.ts +604 -0
  57. package/src/publishing/assert-published-shape.ts +260 -0
  58. package/src/publishing/post-open-recovery.ts +104 -0
  59. package/src/publishing/publish-project.ts +515 -0
  60. package/src/publishing/publish-settings.ts +59 -0
  61. package/src/publishing/publish-state.ts +316 -0
  62. package/src/publishing/share-dialog.ts +171 -0
  63. package/src/publishing/trim-dataframe.ts +247 -0
  64. package/src/tests/analysis.ts +658 -0
  65. package/src/tests/enrichment-export.ts +61 -0
  66. package/src/tests/enrichment-visualization.ts +340 -0
  67. package/src/tests/enrichment.ts +224 -0
  68. package/src/tests/fragpipe-e2e.ts +74 -0
  69. package/src/tests/fragpipe-parser.ts +147 -0
  70. package/src/tests/gene-label-resolver.ts +387 -0
  71. package/src/tests/generic-parser.ts +152 -0
  72. package/src/tests/group-mean-correlation.ts +139 -0
  73. package/src/tests/log2-scale.ts +93 -0
  74. package/src/tests/organisms.ts +56 -0
  75. package/src/tests/parsers.ts +182 -0
  76. package/src/tests/publish-roundtrip.ts +584 -0
  77. package/src/tests/publish-spike.ts +377 -0
  78. package/src/tests/qc-dashboard.ts +210 -0
  79. package/src/tests/smart-pathway-filter.ts +193 -0
  80. package/src/tests/spc-formula-lines-spike.ts +129 -0
  81. package/src/tests/spc.ts +640 -0
  82. package/src/tests/spectronaut-candidates-e2e.ts +140 -0
  83. package/src/tests/spectronaut-candidates-parser.ts +398 -0
  84. package/src/tests/spectronaut-parser.ts +668 -0
  85. package/src/tests/subcellular-location.ts +361 -0
  86. package/src/tests/uniprot-panel.ts +202 -0
  87. package/src/tests/volcano.ts +603 -0
  88. package/src/utils/column-detection.ts +28 -0
  89. package/src/utils/gene-label-resolver.ts +443 -0
  90. package/src/utils/organisms.ts +82 -0
  91. package/src/utils/proteomics-types.ts +30 -0
  92. package/src/viewers/enrichment-viewers.ts +274 -0
  93. package/src/viewers/group-mean-correlation.ts +218 -0
  94. package/src/viewers/heatmap.ts +168 -0
  95. package/src/viewers/pca-plot.ts +169 -0
  96. package/src/viewers/qc-computations.ts +266 -0
  97. package/src/viewers/qc-dashboard.ts +176 -0
  98. package/src/viewers/spc-dashboard.ts +755 -0
  99. package/src/viewers/volcano.ts +690 -0
  100. package/test-console-output-1.log +2055 -0
  101. package/test-record-1.mp4 +0 -0
  102. package/tools/derive-precursor-golden-sidecar.mjs +81 -0
  103. package/tools/generate-enrichment-fixture.sh +160 -0
  104. package/tools/generate-spectronaut-candidates-fixture.mjs +212 -0
  105. package/tools/generate-spectronaut-precursor-fixture.mjs +128 -0
  106. package/tools/spectronaut-aggregate.sh +46 -0
  107. package/tools/spectronaut-aggregate.sql +80 -0
  108. package/tsconfig.json +18 -71
  109. package/webpack.config.js +86 -45
  110. package/.eslintignore +0 -1
  111. package/.eslintrc.json +0 -56
  112. package/LICENSE +0 -674
  113. package/package.png +0 -0
  114. package/scripts/number_antibody.py +0 -190
  115. package/scripts/number_antibody_abnumber.py +0 -177
  116. package/scripts/number_antibody_anarci.py +0 -200
@@ -1,190 +0,0 @@
1
- #name: Antpack Antibody Numbering
2
- #description: Assigns antibody numbering (IMGT/Kabat/Chothia/AHo) using AntPack
3
- #language: python
4
- #environment: channels: [conda-forge, defaults], dependencies: [python=3.9, pip, pip: [antpack==0.3.8.6,pyside6,qt_material]]
5
- #meta.role: antibodyNumbering
6
- #input: dataframe df
7
- #input: column seqCol {semType: Macromolecule}
8
- #input: string scheme {choices: ["imgt", "kabat", "chothia", "aho"]} [Numbering scheme]
9
- #output: dataframe result
10
-
11
- import json
12
- import pandas as pd
13
-
14
- try:
15
- from antpack import SingleChainAnnotator
16
- except ImportError:
17
- raise ImportError(
18
- 'AntPack is not installed. Please install it with:\n'
19
- ' pip install antpack==0.3.8.5\n'
20
- )
21
-
22
- # Antpack github: https://gidthub.com/jlparkI/AntPack
23
- # While antpack is currently under non-commercial licence, the version used here (0.3.8.6 as specified in environment) is released unde GPL, allowing us to distribute this plugin with same GPL.
24
-
25
- # AntPack uses "martin" for modern Chothia numbering
26
- SCHEME_MAP = {
27
- 'imgt': 'imgt',
28
- 'kabat': 'kabat',
29
- 'chothia': 'martin',
30
- 'aho': 'aho',
31
- }
32
-
33
- # Region definitions per scheme, keyed by (scheme, chain_group).
34
- # chain_group is 'Heavy' or 'Light'.
35
- # Each region: (name, start_position, end_position).
36
- IMGT_REGIONS = {
37
- 'Heavy': [
38
- ('FR1', 1, 26), ('CDR1', 27, 38), ('FR2', 39, 55),
39
- ('CDR2', 56, 65), ('FR3', 66, 104), ('CDR3', 105, 117), ('FR4', 118, 128),
40
- ],
41
- 'Light': [
42
- ('FR1', 1, 26), ('CDR1', 27, 38), ('FR2', 39, 55),
43
- ('CDR2', 56, 65), ('FR3', 66, 104), ('CDR3', 105, 117), ('FR4', 118, 127),
44
- ],
45
- }
46
-
47
- KABAT_REGIONS = {
48
- 'Heavy': [
49
- ('FR1', 1, 30), ('CDR1', 31, 35), ('FR2', 36, 49),
50
- ('CDR2', 50, 65), ('FR3', 66, 94), ('CDR3', 95, 102), ('FR4', 103, 113),
51
- ],
52
- 'Light': [
53
- ('FR1', 1, 23), ('CDR1', 24, 34), ('FR2', 35, 49),
54
- ('CDR2', 50, 56), ('FR3', 57, 88), ('CDR3', 89, 97), ('FR4', 98, 107),
55
- ],
56
- }
57
-
58
- CHOTHIA_REGIONS = {
59
- 'Heavy': [
60
- ('FR1', 1, 25), ('CDR1', 26, 32), ('FR2', 33, 51),
61
- ('CDR2', 52, 56), ('FR3', 57, 94), ('CDR3', 95, 102), ('FR4', 103, 113),
62
- ],
63
- 'Light': [
64
- ('FR1', 1, 25), ('CDR1', 26, 32), ('FR2', 33, 49),
65
- ('CDR2', 50, 52), ('FR3', 53, 90), ('CDR3', 91, 96), ('FR4', 97, 107),
66
- ],
67
- }
68
-
69
- AHO_REGIONS = {
70
- 'Heavy': [
71
- ('FR1', 1, 24), ('CDR1', 25, 40), ('FR2', 41, 55),
72
- ('CDR2', 56, 78), ('FR3', 79, 108), ('CDR3', 109, 138), ('FR4', 139, 149),
73
- ],
74
- 'Light': [
75
- ('FR1', 1, 24), ('CDR1', 25, 40), ('FR2', 41, 55),
76
- ('CDR2', 56, 78), ('FR3', 79, 108), ('CDR3', 109, 138), ('FR4', 139, 149),
77
- ],
78
- }
79
-
80
- SCHEME_REGIONS = {
81
- 'imgt': IMGT_REGIONS,
82
- 'kabat': KABAT_REGIONS,
83
- 'chothia': CHOTHIA_REGIONS,
84
- 'aho': AHO_REGIONS,
85
- }
86
-
87
-
88
- def extract_sequence(raw_seq):
89
- """Extract single-letter amino acid sequence from various formats."""
90
- if not raw_seq or not isinstance(raw_seq, str):
91
- return ''
92
- s = raw_seq.strip()
93
- s = s.replace('-', '').replace('.', '')
94
- valid = set('ACDEFGHIKLMNPQRSTVWY')
95
- return ''.join(c for c in s.upper() if c in valid)
96
-
97
-
98
- # Map user-facing scheme to AntPack scheme name
99
- antpack_scheme = SCHEME_MAP.get(scheme, 'imgt')
100
-
101
- # Create the annotator once for all sequences
102
- annotator = SingleChainAnnotator(chains=['H', 'K', 'L'], scheme=antpack_scheme)
103
-
104
- # Extract sequences from the input column
105
- col_name = seqCol
106
- sequences = []
107
- for i in range(df.shape[0]):
108
- raw = df[col_name].iloc[i]
109
- sequences.append(extract_sequence(str(raw) if raw is not None else ''))
110
-
111
- # Process each sequence
112
- position_names_list = []
113
- chain_types = []
114
- region_annotations_list = []
115
- numbering_results = []
116
- numbering_maps = []
117
-
118
- regions_def = SCHEME_REGIONS.get(scheme, IMGT_REGIONS)
119
-
120
- for seq in sequences:
121
- if not seq:
122
- position_names_list.append('')
123
- chain_types.append('')
124
- region_annotations_list.append('[]')
125
- numbering_results.append('')
126
- numbering_maps.append('')
127
- continue
128
-
129
- numbering, pct_identity, chain_type_code, err = annotator.analyze_seq(seq)
130
-
131
- if err or pct_identity < 0.5:
132
- position_names_list.append('')
133
- chain_types.append('')
134
- region_annotations_list.append('[]')
135
- numbering_results.append('')
136
- numbering_maps.append('')
137
- continue
138
-
139
- # Determine chain type
140
- if chain_type_code == 'H':
141
- chain_type = 'Heavy'
142
- elif chain_type_code in ('K', 'L'):
143
- chain_type = 'Light'
144
- else:
145
- chain_type = 'Unknown'
146
-
147
- # Build position names, numbering detail, and position-to-character-index map.
148
- # numbering is a list with the same length as the input sequence;
149
- # each element is a position code string or '-' for residues outside the variable region.
150
- pos_names = []
151
- numbering_detail = []
152
- pos_to_char_idx = {}
153
- for i, pos_code in enumerate(numbering):
154
- if pos_code != '-':
155
- pos_names.append(pos_code)
156
- numbering_detail.append({'position': pos_code, 'aa': seq[i]})
157
- pos_to_char_idx[pos_code] = i
158
-
159
- position_names_list.append(', '.join(pos_names))
160
- chain_types.append(chain_type)
161
-
162
- # Build region annotations
163
- chain_key = chain_type if chain_type in regions_def else 'Heavy'
164
- region_defs = regions_def.get(chain_key, [])
165
- annotations = []
166
- for region_name, start, end in region_defs:
167
- annotations.append({
168
- 'id': f'{scheme}-{chain_type}-{region_name}'.lower(),
169
- 'name': region_name,
170
- 'description': f'{region_name} ({scheme.upper()} {start}-{end})',
171
- 'start': str(start),
172
- 'end': str(end),
173
- 'visualType': 'region',
174
- 'category': 'structure',
175
- 'sourceScheme': scheme.upper(),
176
- 'autoGenerated': True,
177
- })
178
-
179
- region_annotations_list.append(json.dumps(annotations))
180
- numbering_results.append(json.dumps(numbering_detail))
181
- numbering_maps.append(json.dumps(pos_to_char_idx))
182
-
183
- # Build result DataFrame
184
- result = pd.DataFrame({
185
- 'position_names': position_names_list,
186
- 'chain_type': chain_types,
187
- 'annotations_json': region_annotations_list,
188
- 'numbering_detail': numbering_results,
189
- 'numbering_map': numbering_maps,
190
- })
@@ -1,177 +0,0 @@
1
- #name: Number Antibody Sequences (AbNumber)
2
- #description: [Legacy] Assigns antibody numbering (IMGT/Kabat/Chothia/AHo) using AbNumber
3
- #language: python
4
- #environment: channels: [conda-forge, bioconda, defaults], dependencies: [python=3.9, pip, hmmer=3.3.2, pip: [abnumber]]
5
- #input: dataframe df
6
- #input: column seqCol {semType: Macromolecule}
7
- #input: string scheme {choices: ["imgt", "kabat", "chothia", "aho"]} [Numbering scheme]
8
- #output: dataframe result
9
-
10
- import json
11
- import pandas as pd
12
-
13
- try:
14
- from abnumber import Chain
15
- except ImportError:
16
- raise ImportError(
17
- 'AbNumber is not installed. Please install it with:\n'
18
- ' conda install -c bioconda abnumber\n'
19
- 'or:\n'
20
- ' pip install abnumber\n'
21
- 'HMMER must also be available on PATH.'
22
- )
23
-
24
- # Region definitions per scheme, keyed by (scheme, chain_group).
25
- # chain_group is 'Heavy' or 'Light'.
26
- # Each region: (name, start_position, end_position).
27
- IMGT_REGIONS = {
28
- 'Heavy': [
29
- ('FR1', 1, 26), ('CDR1', 27, 38), ('FR2', 39, 55),
30
- ('CDR2', 56, 65), ('FR3', 66, 104), ('CDR3', 105, 117), ('FR4', 118, 128),
31
- ],
32
- 'Light': [
33
- ('FR1', 1, 26), ('CDR1', 27, 38), ('FR2', 39, 55),
34
- ('CDR2', 56, 65), ('FR3', 66, 104), ('CDR3', 105, 117), ('FR4', 118, 127),
35
- ],
36
- }
37
-
38
- KABAT_REGIONS = {
39
- 'Heavy': [
40
- ('FR1', 1, 30), ('CDR1', 31, 35), ('FR2', 36, 49),
41
- ('CDR2', 50, 65), ('FR3', 66, 94), ('CDR3', 95, 102), ('FR4', 103, 113),
42
- ],
43
- 'Light': [
44
- ('FR1', 1, 23), ('CDR1', 24, 34), ('FR2', 35, 49),
45
- ('CDR2', 50, 56), ('FR3', 57, 88), ('CDR3', 89, 97), ('FR4', 98, 107),
46
- ],
47
- }
48
-
49
- CHOTHIA_REGIONS = {
50
- 'Heavy': [
51
- ('FR1', 1, 25), ('CDR1', 26, 32), ('FR2', 33, 51),
52
- ('CDR2', 52, 56), ('FR3', 57, 94), ('CDR3', 95, 102), ('FR4', 103, 113),
53
- ],
54
- 'Light': [
55
- ('FR1', 1, 25), ('CDR1', 26, 32), ('FR2', 33, 49),
56
- ('CDR2', 50, 52), ('FR3', 53, 90), ('CDR3', 91, 96), ('FR4', 97, 107),
57
- ],
58
- }
59
-
60
- AHO_REGIONS = {
61
- 'Heavy': [
62
- ('FR1', 1, 24), ('CDR1', 25, 40), ('FR2', 41, 55),
63
- ('CDR2', 56, 78), ('FR3', 79, 108), ('CDR3', 109, 138), ('FR4', 139, 149),
64
- ],
65
- 'Light': [
66
- ('FR1', 1, 24), ('CDR1', 25, 40), ('FR2', 41, 55),
67
- ('CDR2', 56, 78), ('FR3', 79, 108), ('CDR3', 109, 138), ('FR4', 139, 149),
68
- ],
69
- }
70
-
71
- SCHEME_REGIONS = {
72
- 'imgt': IMGT_REGIONS,
73
- 'kabat': KABAT_REGIONS,
74
- 'chothia': CHOTHIA_REGIONS,
75
- 'aho': AHO_REGIONS,
76
- }
77
-
78
-
79
- def extract_sequence(raw_seq):
80
- """Extract single-letter amino acid sequence from various formats."""
81
- if not raw_seq or not isinstance(raw_seq, str):
82
- return ''
83
- s = raw_seq.strip()
84
- s = s.replace('-', '').replace('.', '')
85
- valid = set('ACDEFGHIKLMNPQRSTVWY')
86
- return ''.join(c for c in s.upper() if c in valid)
87
-
88
-
89
- def number_single_sequence(seq, scheme_name):
90
- """Number a single sequence using AbNumber's Chain object.
91
-
92
- Returns (chain, error_msg) where chain is an abnumber.Chain or None.
93
- """
94
- if not seq:
95
- return None, 'empty sequence'
96
- try:
97
- chain = Chain(seq, scheme=scheme_name)
98
- return chain, None
99
- except Exception as e:
100
- return None, str(e)
101
-
102
-
103
- # Extract sequences from the input column
104
- col_name = seqCol
105
- sequences = []
106
- for i in range(df.shape[0]):
107
- raw = df[col_name].iloc[i]
108
- sequences.append(extract_sequence(str(raw) if raw is not None else ''))
109
-
110
- # Process each sequence
111
- position_names_list = []
112
- chain_types = []
113
- region_annotations_list = []
114
- numbering_results = []
115
-
116
- regions_def = SCHEME_REGIONS.get(scheme, IMGT_REGIONS)
117
-
118
- for seq in sequences:
119
- chain, err = number_single_sequence(seq, scheme)
120
-
121
- if chain is None:
122
- position_names_list.append('')
123
- chain_types.append('')
124
- region_annotations_list.append('[]')
125
- numbering_results.append('')
126
- continue
127
-
128
- # Determine chain type
129
- ct = chain.chain_type # 'H', 'K', or 'L'
130
- if ct == 'H':
131
- chain_type = 'Heavy'
132
- elif ct in ('K', 'L'):
133
- chain_type = 'Light'
134
- else:
135
- chain_type = 'Unknown'
136
-
137
- # Build position names from numbering
138
- pos_names = []
139
- numbering_detail = []
140
- for position, aa in chain:
141
- pos_name = str(position.number)
142
- if position.letter and position.letter.strip():
143
- pos_name += position.letter.strip()
144
- pos_names.append(pos_name)
145
- numbering_detail.append({'position': pos_name, 'aa': aa})
146
-
147
- position_names_list.append(', '.join(pos_names))
148
-
149
- chain_types.append(chain_type)
150
-
151
- # Build region annotations
152
- chain_key = chain_type if chain_type in regions_def else 'Heavy'
153
- region_defs = regions_def.get(chain_key, [])
154
- annotations = []
155
- for region_name, start, end in region_defs:
156
- annotations.append({
157
- 'id': f'{scheme}-{chain_type}-{region_name}'.lower(),
158
- 'name': region_name,
159
- 'description': f'{region_name} ({scheme.upper()} {start}-{end})',
160
- 'start': str(start),
161
- 'end': str(end),
162
- 'visualType': 'region',
163
- 'category': 'structure',
164
- 'sourceScheme': scheme.upper(),
165
- 'autoGenerated': True,
166
- })
167
-
168
- region_annotations_list.append(json.dumps(annotations))
169
- numbering_results.append(json.dumps(numbering_detail))
170
-
171
- # Build result DataFrame
172
- result = pd.DataFrame({
173
- 'position_names': position_names_list,
174
- 'chain_type': chain_types,
175
- 'annotations_json': region_annotations_list,
176
- 'numbering_detail': numbering_results,
177
- })
@@ -1,200 +0,0 @@
1
- #name: Number Antibody Sequences (ANARCI)
2
- #description: [Legacy] Assigns antibody numbering (IMGT/Kabat/Chothia/AHo) using ANARCI directly
3
- #language: python
4
- #environment: channels: [conda-forge, bioconda, defaults], dependencies: [python=3.9, pip, hmmer=3.3.2, anarci]
5
- #input: dataframe df
6
- #input: column seqCol {semType: Macromolecule}
7
- #input: string scheme {choices: ["imgt", "kabat", "chothia", "aho"]} [Numbering scheme]
8
- #output: dataframe result
9
-
10
- import json
11
- import pandas as pd
12
-
13
- try:
14
- from anarci import anarci, number
15
- except ImportError:
16
- raise ImportError(
17
- 'ANARCI is not installed. Please install it with:\n'
18
- ' conda install -c bioconda anarci hmmer\n'
19
- 'or:\n'
20
- ' pip install anarci\n'
21
- 'HMMER must also be available on PATH.'
22
- )
23
-
24
- # IMGT region boundaries (used for all schemes after numbering)
25
- IMGT_REGIONS = {
26
- 'Heavy': [
27
- ('FR1', 1, 26), ('CDR1', 27, 38), ('FR2', 39, 55),
28
- ('CDR2', 56, 65), ('FR3', 66, 104), ('CDR3', 105, 117), ('FR4', 118, 128),
29
- ],
30
- 'Light': [
31
- ('FR1', 1, 26), ('CDR1', 27, 38), ('FR2', 39, 55),
32
- ('CDR2', 56, 65), ('FR3', 66, 104), ('CDR3', 105, 117), ('FR4', 118, 127),
33
- ],
34
- }
35
-
36
- KABAT_REGIONS = {
37
- 'Heavy': [
38
- ('FR1', 1, 30), ('CDR1', 31, 35), ('FR2', 36, 49),
39
- ('CDR2', 50, 65), ('FR3', 66, 94), ('CDR3', 95, 102), ('FR4', 103, 113),
40
- ],
41
- 'Light': [
42
- ('FR1', 1, 23), ('CDR1', 24, 34), ('FR2', 35, 49),
43
- ('CDR2', 50, 56), ('FR3', 57, 88), ('CDR3', 89, 97), ('FR4', 98, 107),
44
- ],
45
- }
46
-
47
- CHOTHIA_REGIONS = {
48
- 'Heavy': [
49
- ('FR1', 1, 25), ('CDR1', 26, 32), ('FR2', 33, 51),
50
- ('CDR2', 52, 56), ('FR3', 57, 94), ('CDR3', 95, 102), ('FR4', 103, 113),
51
- ],
52
- 'Light': [
53
- ('FR1', 1, 25), ('CDR1', 26, 32), ('FR2', 33, 49),
54
- ('CDR2', 50, 52), ('FR3', 53, 90), ('CDR3', 91, 96), ('FR4', 97, 107),
55
- ],
56
- }
57
-
58
- AHO_REGIONS = {
59
- 'Heavy': [
60
- ('FR1', 1, 24), ('CDR1', 25, 40), ('FR2', 41, 55),
61
- ('CDR2', 56, 78), ('FR3', 79, 108), ('CDR3', 109, 138), ('FR4', 139, 149),
62
- ],
63
- 'Light': [
64
- ('FR1', 1, 24), ('CDR1', 25, 40), ('FR2', 41, 55),
65
- ('CDR2', 56, 78), ('FR3', 79, 108), ('CDR3', 109, 138), ('FR4', 139, 149),
66
- ],
67
- }
68
-
69
- SCHEME_REGIONS = {
70
- 'imgt': IMGT_REGIONS,
71
- 'kabat': KABAT_REGIONS,
72
- 'chothia': CHOTHIA_REGIONS,
73
- 'aho': AHO_REGIONS,
74
- }
75
-
76
-
77
- def extract_sequence(raw_seq):
78
- """Extract single-letter amino acid sequence from various formats."""
79
- if not raw_seq or not isinstance(raw_seq, str):
80
- return ''
81
- s = raw_seq.strip()
82
- # If it looks like FASTA single-letter (only AA chars and gaps)
83
- s = s.replace('-', '').replace('.', '')
84
- # Filter to valid amino acid characters
85
- valid = set('ACDEFGHIKLMNPQRSTVWY')
86
- return ''.join(c for c in s.upper() if c in valid)
87
-
88
-
89
- def number_sequences(sequences, scheme_name):
90
- """Number a list of sequences using ANARCI."""
91
- # Prepare input for ANARCI
92
- seq_list = [(f'seq_{i}', seq) for i, seq in enumerate(sequences) if seq]
93
-
94
- if not seq_list:
95
- return [], [], []
96
-
97
- # Run ANARCI
98
- numbered_seqs, alignment_details, hit_tables = anarci(
99
- seq_list,
100
- scheme=scheme_name,
101
- output=False,
102
- allow=set(['H', 'K', 'L']),
103
- )
104
-
105
- return numbered_seqs, alignment_details, hit_tables
106
-
107
-
108
- # Extract sequences from the input column
109
- col_name = seqCol
110
- sequences = []
111
- for i in range(df.shape[0]):
112
- raw = df[col_name].iloc[i]
113
- sequences.append(extract_sequence(str(raw) if raw is not None else ''))
114
-
115
- # Number all sequences
116
- numbered_seqs, alignment_details, hit_tables = number_sequences(sequences, scheme)
117
-
118
- # Process results
119
- position_names_list = []
120
- chain_types = []
121
- region_annotations_list = []
122
- numbering_results = []
123
-
124
- regions_def = SCHEME_REGIONS.get(scheme, IMGT_REGIONS)
125
-
126
- for idx in range(len(sequences)):
127
- if numbered_seqs[idx] is None or len(numbered_seqs[idx]) == 0:
128
- position_names_list.append('')
129
- chain_types.append('')
130
- region_annotations_list.append('[]')
131
- numbering_results.append('')
132
- continue
133
-
134
- # Get the best hit (first domain)
135
- numbering = numbered_seqs[idx][0]
136
- chain_info = alignment_details[idx][0]
137
-
138
- if numbering is None or chain_info is None:
139
- position_names_list.append('')
140
- chain_types.append('')
141
- region_annotations_list.append('[]')
142
- numbering_results.append('')
143
- continue
144
-
145
- # Extract chain type
146
- chain_type_code = chain_info['chain_type']
147
- if chain_type_code == 'H':
148
- chain_type = 'Heavy'
149
- elif chain_type_code in ('K', 'L'):
150
- chain_type = 'Light'
151
- else:
152
- chain_type = 'Unknown'
153
-
154
- # Build position names from numbering
155
- pos_names = []
156
- for (pos_num, insertion_code), aa in numbering:
157
- if aa != '-':
158
- pos_name = str(pos_num)
159
- if insertion_code != ' ':
160
- pos_name += insertion_code
161
- pos_names.append(pos_name)
162
-
163
- position_names_list.append(', '.join(pos_names))
164
- chain_types.append(chain_type)
165
-
166
- # Build region annotations
167
- chain_key = chain_type if chain_type in regions_def else 'Heavy'
168
- region_defs = regions_def.get(chain_key, [])
169
- annotations = []
170
- for region_name, start, end in region_defs:
171
- region_type = 'CDR' if 'CDR' in region_name else 'FR'
172
- annotations.append({
173
- 'id': f'{scheme}-{chain_type}-{region_name}'.lower(),
174
- 'name': region_name,
175
- 'description': f'{region_name} ({scheme.upper()} {start}-{end})',
176
- 'start': str(start),
177
- 'end': str(end),
178
- 'visualType': 'region',
179
- 'category': 'structure',
180
- 'sourceScheme': scheme.upper(),
181
- 'autoGenerated': True,
182
- })
183
-
184
- region_annotations_list.append(json.dumps(annotations))
185
-
186
- # Full numbering as JSON for debugging/reference
187
- numbering_json = json.dumps([
188
- {'position': f'{pos_num}{insertion_code.strip()}', 'aa': aa}
189
- for (pos_num, insertion_code), aa in numbering
190
- if aa != '-'
191
- ])
192
- numbering_results.append(numbering_json)
193
-
194
- # Build result DataFrame
195
- result = pd.DataFrame({
196
- 'position_names': position_names_list,
197
- 'chain_type': chain_types,
198
- 'annotations_json': region_annotations_list,
199
- 'numbering_detail': numbering_results,
200
- })