debase 0.4.2__py3-none-any.whl → 0.4.3__py3-none-any.whl
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.
- debase/_version.py +1 -1
- debase/cleanup_sequence.py +151 -1
- debase/enzyme_lineage_extractor.py +100 -12
- {debase-0.4.2.dist-info → debase-0.4.3.dist-info}/METADATA +1 -1
- {debase-0.4.2.dist-info → debase-0.4.3.dist-info}/RECORD +9 -9
- {debase-0.4.2.dist-info → debase-0.4.3.dist-info}/WHEEL +0 -0
- {debase-0.4.2.dist-info → debase-0.4.3.dist-info}/entry_points.txt +0 -0
- {debase-0.4.2.dist-info → debase-0.4.3.dist-info}/licenses/LICENSE +0 -0
- {debase-0.4.2.dist-info → debase-0.4.3.dist-info}/top_level.txt +0 -0
debase/_version.py
CHANGED
debase/cleanup_sequence.py
CHANGED
@@ -11,6 +11,7 @@ Usage:
|
|
11
11
|
|
12
12
|
import argparse
|
13
13
|
import logging
|
14
|
+
import os
|
14
15
|
import re
|
15
16
|
import sys
|
16
17
|
from dataclasses import dataclass, field
|
@@ -19,11 +20,20 @@ from typing import Dict, List, Optional, Set, Tuple, Union
|
|
19
20
|
|
20
21
|
import pandas as pd
|
21
22
|
|
23
|
+
try:
|
24
|
+
import google.generativeai as genai # type: ignore
|
25
|
+
GEMINI_OK = True
|
26
|
+
except ImportError: # pragma: no cover
|
27
|
+
GEMINI_OK = False
|
28
|
+
|
22
29
|
|
23
30
|
# === 1. CONFIGURATION & CONSTANTS === ----------------------------------------
|
24
31
|
|
25
32
|
VALID_AMINO_ACIDS = set("ACDEFGHIKLMNPQRSTVWY*") # Include * for stop codons
|
26
33
|
|
34
|
+
# Gemini API configuration
|
35
|
+
GEMINI_API_KEY: str = os.environ.get("GEMINI_API_KEY", "")
|
36
|
+
|
27
37
|
# Configure module logger
|
28
38
|
log = logging.getLogger(__name__)
|
29
39
|
|
@@ -565,7 +575,136 @@ class SequenceGenerator:
|
|
565
575
|
return None
|
566
576
|
|
567
577
|
|
568
|
-
# === 7.
|
578
|
+
# === 7. GEMINI PARENT IDENTIFICATION === ------------------------------------
|
579
|
+
|
580
|
+
def identify_parents_with_gemini(df: pd.DataFrame) -> pd.DataFrame:
|
581
|
+
"""Use Gemini API to identify parent enzymes for entries with missing parent information."""
|
582
|
+
if not GEMINI_OK:
|
583
|
+
log.warning("Gemini API not available (missing google.generativeai). Skipping parent identification.")
|
584
|
+
return df
|
585
|
+
|
586
|
+
if not GEMINI_API_KEY:
|
587
|
+
log.warning("GEMINI_API_KEY not set. Skipping parent identification.")
|
588
|
+
return df
|
589
|
+
|
590
|
+
try:
|
591
|
+
genai.configure(api_key=GEMINI_API_KEY)
|
592
|
+
model = genai.GenerativeModel('gemini-1.5-flash')
|
593
|
+
except Exception as e:
|
594
|
+
log.warning(f"Failed to configure Gemini API: {e}. Skipping parent identification.")
|
595
|
+
return df
|
596
|
+
|
597
|
+
# Find entries with empty sequences but missing parent information
|
598
|
+
entries_needing_parents = []
|
599
|
+
for idx, row in df.iterrows():
|
600
|
+
protein_seq = str(row.get("protein_sequence", "")).strip()
|
601
|
+
parent_id = str(row.get("parent_enzyme_id", "")).strip()
|
602
|
+
|
603
|
+
# Only process entries that have empty sequences AND no parent info
|
604
|
+
if (not protein_seq or protein_seq == "nan") and (not parent_id or parent_id == "nan"):
|
605
|
+
enzyme_id = str(row.get("enzyme_id", ""))
|
606
|
+
campaign_id = str(row.get("campaign_id", ""))
|
607
|
+
generation = str(row.get("generation", ""))
|
608
|
+
|
609
|
+
entries_needing_parents.append({
|
610
|
+
"idx": idx,
|
611
|
+
"enzyme_id": enzyme_id,
|
612
|
+
"campaign_id": campaign_id,
|
613
|
+
"generation": generation
|
614
|
+
})
|
615
|
+
|
616
|
+
if not entries_needing_parents:
|
617
|
+
log.info("No entries need parent identification from Gemini")
|
618
|
+
return df
|
619
|
+
|
620
|
+
log.info(f"Found {len(entries_needing_parents)} entries needing parent identification. Querying Gemini...")
|
621
|
+
|
622
|
+
# Create a lookup of all available enzyme IDs for context
|
623
|
+
available_enzymes = {}
|
624
|
+
for idx, row in df.iterrows():
|
625
|
+
enzyme_id = str(row.get("enzyme_id", ""))
|
626
|
+
campaign_id = str(row.get("campaign_id", ""))
|
627
|
+
protein_seq = str(row.get("protein_sequence", "")).strip()
|
628
|
+
generation = str(row.get("generation", ""))
|
629
|
+
|
630
|
+
if enzyme_id and enzyme_id != "nan":
|
631
|
+
available_enzymes[enzyme_id] = {
|
632
|
+
"campaign_id": campaign_id,
|
633
|
+
"has_sequence": bool(protein_seq and protein_seq != "nan"),
|
634
|
+
"generation": generation
|
635
|
+
}
|
636
|
+
|
637
|
+
identified_count = 0
|
638
|
+
for entry in entries_needing_parents:
|
639
|
+
enzyme_id = entry["enzyme_id"]
|
640
|
+
campaign_id = entry["campaign_id"]
|
641
|
+
generation = entry["generation"]
|
642
|
+
|
643
|
+
# Create context for Gemini
|
644
|
+
context_info = []
|
645
|
+
context_info.append(f"Enzyme ID: {enzyme_id}")
|
646
|
+
context_info.append(f"Campaign ID: {campaign_id}")
|
647
|
+
if generation:
|
648
|
+
context_info.append(f"Generation: {generation}")
|
649
|
+
|
650
|
+
# Add available enzymes from the same campaign for context
|
651
|
+
campaign_enzymes = []
|
652
|
+
for enz_id, enz_data in available_enzymes.items():
|
653
|
+
if enz_data["campaign_id"] == campaign_id:
|
654
|
+
status = "with sequence" if enz_data["has_sequence"] else "without sequence"
|
655
|
+
gen_info = f"(gen {enz_data['generation']})" if enz_data["generation"] else ""
|
656
|
+
campaign_enzymes.append(f" - {enz_id} {status} {gen_info}")
|
657
|
+
|
658
|
+
if campaign_enzymes:
|
659
|
+
context_info.append("Available enzymes in same campaign:")
|
660
|
+
context_info.extend(campaign_enzymes[:10]) # Limit to first 10 for context
|
661
|
+
|
662
|
+
context_text = "\n".join(context_info)
|
663
|
+
|
664
|
+
prompt = f"""
|
665
|
+
Based on the enzyme information provided, can you identify the parent enzyme for this enzyme?
|
666
|
+
|
667
|
+
{context_text}
|
668
|
+
|
669
|
+
This enzyme currently has no sequence data and no parent information. Based on the enzyme ID and the available enzymes in the same campaign, can you identify which enzyme is likely the parent?
|
670
|
+
|
671
|
+
Please provide your response in this format:
|
672
|
+
Parent: [parent_enzyme_id or "Unknown"]
|
673
|
+
|
674
|
+
If you cannot identify a parent enzyme, just respond with "Parent: Unknown".
|
675
|
+
"""
|
676
|
+
|
677
|
+
try:
|
678
|
+
response = model.generate_content(prompt)
|
679
|
+
response_text = response.text.strip()
|
680
|
+
|
681
|
+
# Parse the response
|
682
|
+
parent_match = re.search(r'Parent:\s*([^\n]+)', response_text)
|
683
|
+
|
684
|
+
if parent_match:
|
685
|
+
parent = parent_match.group(1).strip()
|
686
|
+
if parent and parent != "Unknown" and parent != "No parent identified":
|
687
|
+
# Verify the parent exists in our available enzymes
|
688
|
+
if parent in available_enzymes:
|
689
|
+
df.at[entry["idx"], "parent_enzyme_id"] = parent
|
690
|
+
identified_count += 1
|
691
|
+
log.info(f"Identified parent for {enzyme_id}: {parent}")
|
692
|
+
else:
|
693
|
+
log.warning(f"Gemini suggested parent {parent} for {enzyme_id}, but it's not in available enzymes")
|
694
|
+
|
695
|
+
except Exception as e:
|
696
|
+
log.warning(f"Failed to identify parent for {enzyme_id} from Gemini: {e}")
|
697
|
+
continue
|
698
|
+
|
699
|
+
if identified_count > 0:
|
700
|
+
log.info(f"Successfully identified {identified_count} parent enzymes using Gemini API")
|
701
|
+
else:
|
702
|
+
log.info("No parent enzymes were identified using Gemini API")
|
703
|
+
|
704
|
+
return df
|
705
|
+
|
706
|
+
|
707
|
+
# === 8. MAIN PROCESSOR === ---------------------------------------------------
|
569
708
|
|
570
709
|
class SequenceProcessor:
|
571
710
|
"""Main processor for handling the complete workflow."""
|
@@ -866,6 +1005,17 @@ class SequenceProcessor:
|
|
866
1005
|
self.process_remaining()
|
867
1006
|
self.backward_pass()
|
868
1007
|
|
1008
|
+
# Use Gemini to identify parent enzymes for entries with missing sequences
|
1009
|
+
log.info(f"Identifying parents with Gemini for campaign: {campaign_id}")
|
1010
|
+
self.df = identify_parents_with_gemini(self.df)
|
1011
|
+
|
1012
|
+
# Rebuild relationships after parent identification
|
1013
|
+
self.generator = SequenceGenerator(self.df)
|
1014
|
+
|
1015
|
+
# Try to fill sequences again after parent identification
|
1016
|
+
log.info(f"Attempting to fill sequences after parent identification for campaign: {campaign_id}")
|
1017
|
+
self.process_remaining()
|
1018
|
+
|
869
1019
|
# Update the original dataframe with results
|
870
1020
|
original_df.loc[campaign_mask, :] = self.df
|
871
1021
|
|
@@ -142,21 +142,36 @@ def extract_text(pdf_path: str | Path | bytes) -> str:
|
|
142
142
|
|
143
143
|
|
144
144
|
def extract_captions(pdf_path: str | Path | bytes, max_chars: int = MAX_CHARS) -> str:
|
145
|
-
"""Extract figure/table captions
|
145
|
+
"""Extract ALL figure/table captions with extensive surrounding context.
|
146
146
|
|
147
147
|
The function scans every text line on every page and keeps lines whose first
|
148
148
|
token matches `_CAPTION_PREFIX_RE`. This covers labels such as:
|
149
|
-
* Fig. 1, Figure 2A,
|
149
|
+
* Fig. 1, Figure 2A, Figure 2B, Figure 2C (ALL sub-captions)
|
150
150
|
* Table S1, Table 4, Scheme 2, Chart 1B
|
151
|
-
* Supplementary Fig.
|
151
|
+
* Supplementary Fig. S5A, S5B, S5C (ALL variations)
|
152
|
+
|
153
|
+
For SI documents, includes extensive context since understanding what each
|
154
|
+
section contains is crucial for accurate location identification.
|
152
155
|
"""
|
153
156
|
|
154
157
|
doc = _open_doc(pdf_path)
|
155
158
|
captions: list[str] = []
|
156
159
|
try:
|
157
|
-
for page in doc:
|
160
|
+
for page_num, page in enumerate(doc):
|
158
161
|
page_dict = page.get_text("dict")
|
162
|
+
|
163
|
+
# Get all text blocks on this page for broader context
|
164
|
+
page_text_blocks = []
|
159
165
|
for block in page_dict.get("blocks", []):
|
166
|
+
block_text = ""
|
167
|
+
for line in block.get("lines", []):
|
168
|
+
text_line = "".join(span["text"] for span in line.get("spans", []))
|
169
|
+
if text_line.strip():
|
170
|
+
block_text += text_line.strip() + " "
|
171
|
+
if block_text.strip():
|
172
|
+
page_text_blocks.append(block_text.strip())
|
173
|
+
|
174
|
+
for block_idx, block in enumerate(page_dict.get("blocks", [])):
|
160
175
|
# Get all lines in this block
|
161
176
|
block_lines = []
|
162
177
|
for line in block.get("lines", []):
|
@@ -166,21 +181,94 @@ def extract_captions(pdf_path: str | Path | bytes, max_chars: int = MAX_CHARS) -
|
|
166
181
|
# Check if any line starts with a caption prefix
|
167
182
|
for i, line in enumerate(block_lines):
|
168
183
|
if _CAPTION_PREFIX_RE.match(line):
|
169
|
-
|
170
|
-
|
184
|
+
context_parts = []
|
185
|
+
|
186
|
+
# Add page context for SI documents (more critical there)
|
187
|
+
context_parts.append(f"Page {page_num + 1}")
|
188
|
+
|
189
|
+
# Add extensive context before the caption (5-7 lines for SI context)
|
190
|
+
context_before = []
|
191
|
+
|
192
|
+
# First try to get context from current block
|
193
|
+
for k in range(max(0, i-7), i):
|
194
|
+
if k < len(block_lines) and block_lines[k].strip():
|
195
|
+
if not _CAPTION_PREFIX_RE.match(block_lines[k]):
|
196
|
+
context_before.append(block_lines[k])
|
197
|
+
|
198
|
+
# If not enough context, look at previous text blocks on the page
|
199
|
+
if len(context_before) < 3 and block_idx > 0:
|
200
|
+
prev_block_text = page_text_blocks[block_idx - 1] if block_idx < len(page_text_blocks) else ""
|
201
|
+
if prev_block_text:
|
202
|
+
# Get last few sentences from previous block
|
203
|
+
sentences = prev_block_text.split('. ')
|
204
|
+
context_before = sentences[-2:] + context_before if len(sentences) > 1 else [prev_block_text] + context_before
|
205
|
+
|
206
|
+
if context_before:
|
207
|
+
# Include more extensive context for better understanding
|
208
|
+
context_text = " ".join(context_before[-5:]) # Last 5 lines/sentences of context
|
209
|
+
context_parts.append("Context: " + context_text)
|
210
|
+
|
211
|
+
# Extract the COMPLETE caption including all sub-parts
|
171
212
|
caption_parts = [line]
|
172
|
-
|
213
|
+
j = i + 1
|
214
|
+
|
215
|
+
# Continue collecting caption text until we hit a clear break
|
216
|
+
while j < len(block_lines):
|
173
217
|
next_line = block_lines[j]
|
174
|
-
|
175
|
-
|
176
|
-
|
218
|
+
|
219
|
+
# Stop if we hit an empty line followed by non-caption text
|
220
|
+
if not next_line:
|
221
|
+
# Check if the line after empty is a new caption
|
222
|
+
if j + 1 < len(block_lines) and _CAPTION_PREFIX_RE.match(block_lines[j + 1]):
|
223
|
+
break
|
224
|
+
# If next non-empty line is not a caption, continue collecting
|
225
|
+
elif j + 1 < len(block_lines):
|
226
|
+
j += 1
|
227
|
+
continue
|
228
|
+
else:
|
229
|
+
break
|
230
|
+
|
231
|
+
# Stop if we hit a new caption
|
177
232
|
if _CAPTION_PREFIX_RE.match(next_line):
|
178
233
|
break
|
234
|
+
|
235
|
+
# Include this line as part of the caption
|
179
236
|
caption_parts.append(next_line)
|
237
|
+
j += 1
|
180
238
|
|
181
|
-
# Join the parts
|
239
|
+
# Join the caption parts
|
182
240
|
full_caption = " ".join(caption_parts)
|
183
|
-
|
241
|
+
context_parts.append("Caption: " + full_caption)
|
242
|
+
|
243
|
+
# Add extensive context after the caption (especially important for SI)
|
244
|
+
context_after = []
|
245
|
+
|
246
|
+
# Look for descriptive text following the caption
|
247
|
+
for k in range(j, min(len(block_lines), j + 10)): # Look ahead up to 10 lines
|
248
|
+
if k < len(block_lines) and block_lines[k].strip():
|
249
|
+
if not _CAPTION_PREFIX_RE.match(block_lines[k]):
|
250
|
+
context_after.append(block_lines[k])
|
251
|
+
|
252
|
+
# If not enough context, look at next text blocks
|
253
|
+
if len(context_after) < 3 and block_idx + 1 < len(page_text_blocks):
|
254
|
+
next_block_text = page_text_blocks[block_idx + 1]
|
255
|
+
if next_block_text:
|
256
|
+
# Get first few sentences from next block
|
257
|
+
sentences = next_block_text.split('. ')
|
258
|
+
context_after.extend(sentences[:3] if len(sentences) > 1 else [next_block_text])
|
259
|
+
|
260
|
+
if context_after:
|
261
|
+
# Include extensive following context
|
262
|
+
following_text = " ".join(context_after[:7]) # First 7 lines of following context
|
263
|
+
context_parts.append("Following: " + following_text)
|
264
|
+
|
265
|
+
# For SI documents, add section context if this appears to be a section header
|
266
|
+
if any(keyword in full_caption.lower() for keyword in ['supplementary', 'supporting', 'si ', 's1', 's2', 's3']):
|
267
|
+
context_parts.append("SI_SECTION: This appears to be supplementary material content")
|
268
|
+
|
269
|
+
# Combine all parts with proper separation
|
270
|
+
full_caption_with_context = " | ".join(context_parts)
|
271
|
+
captions.append(full_caption_with_context)
|
184
272
|
finally:
|
185
273
|
doc.close()
|
186
274
|
|
@@ -1,16 +1,16 @@
|
|
1
1
|
debase/__init__.py,sha256=YeKveGj_8fwuu5ozoK2mUU86so_FjiCwsvg1d_lYVZU,586
|
2
2
|
debase/__main__.py,sha256=LbxYt2x9TG5Ced7LpzzX_8gkWyXeZSlVHzqHfqAiPwQ,160
|
3
|
-
debase/_version.py,sha256=
|
3
|
+
debase/_version.py,sha256=r0b4fvQcrrvOScFMddjVgWAGNt17iQxCJH2xYW06jio,49
|
4
4
|
debase/build_db.py,sha256=bW574GxsL1BJtDwM19urLbciPcejLzfraXZPpzm09FQ,7167
|
5
|
-
debase/cleanup_sequence.py,sha256=
|
6
|
-
debase/enzyme_lineage_extractor.py,sha256=
|
5
|
+
debase/cleanup_sequence.py,sha256=4qZrSXInyJKEJqcgcONp4IX24ALEj5lf7E0XaOZVxZ0,40329
|
6
|
+
debase/enzyme_lineage_extractor.py,sha256=tFyrcWkNKKr8T9xq0tIXUDNfcX0tbdWGrLhgo5m7lmA,129804
|
7
7
|
debase/lineage_format.py,sha256=Q6kpqKPUxJsMYpb0Yt8IbVlp6VDYX2vkITuGhT9MEbw,47056
|
8
8
|
debase/reaction_info_extractor.py,sha256=q-iHgfVLXP4r2Se8yA9I0AvtnAhHBltTztrXspl3EKU,151949
|
9
9
|
debase/substrate_scope_extractor.py,sha256=eaVimhxmmaRj-9dRN6RKK4yStCmZAuX8xBaarsIsmUo,114212
|
10
10
|
debase/wrapper.py,sha256=r0xxoiBvmMIktiGPOD4w9hne8m0SLzZ03WeWnBuDW0A,25236
|
11
|
-
debase-0.4.
|
12
|
-
debase-0.4.
|
13
|
-
debase-0.4.
|
14
|
-
debase-0.4.
|
15
|
-
debase-0.4.
|
16
|
-
debase-0.4.
|
11
|
+
debase-0.4.3.dist-info/licenses/LICENSE,sha256=5sk9_tcNmr1r2iMIUAiioBo7wo38u8BrPlO7f0seqgE,1075
|
12
|
+
debase-0.4.3.dist-info/METADATA,sha256=UT8ymX3oothXvgA9ayr74_Bd-St7I0Pj7CoEg8LlKg8,10789
|
13
|
+
debase-0.4.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
14
|
+
debase-0.4.3.dist-info/entry_points.txt,sha256=hUcxA1b4xORu-HHBFTe9u2KTdbxPzt0dwz95_6JNe9M,48
|
15
|
+
debase-0.4.3.dist-info/top_level.txt,sha256=2BUeq-4kmQr0Rhl06AnRzmmZNs8WzBRK9OcJehkcdk8,7
|
16
|
+
debase-0.4.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|