wenruifan commited on
Commit
c15d304
·
1 Parent(s): 9b67c39

Make chain search use lazy node lookups

Browse files
Files changed (1) hide show
  1. backend/app/protein/tsv_loader.py +32 -16
backend/app/protein/tsv_loader.py CHANGED
@@ -8,6 +8,7 @@ key the first occurrence is kept.
8
  import ast
9
  import sqlite3
10
  import logging
 
11
  from typing import Dict, List, Optional, Tuple, Any
12
 
13
  from app.protein.config import get_database_path
@@ -74,6 +75,27 @@ def reset_index() -> None:
74
  """Force reload of node index."""
75
  global _index
76
  _index = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
 
79
  def _parse_functions(raw: str) -> List[str]:
@@ -90,7 +112,7 @@ def _parse_functions(raw: str) -> List[str]:
90
 
91
 
92
  def _parse_float(val: str) -> Optional[float]:
93
- val = val.strip()
94
  if not val:
95
  return None
96
  try:
@@ -101,8 +123,7 @@ def _parse_float(val: str) -> Optional[float]:
101
 
102
  def get_energy_scores(pdb_id: str, auth_asym_id: str) -> Optional[Dict[str, Any]]:
103
  """Return energy scores (Rosetta, FoldX, EvoEF2, RM, RM+) or None if not found."""
104
- key = (pdb_id.lower(), auth_asym_id.upper())
105
- row = _get_index().get(key)
106
  if row is None:
107
  return None
108
  return {
@@ -116,13 +137,13 @@ def get_energy_scores(pdb_id: str, auth_asym_id: str) -> Optional[Dict[str, Any]
116
 
117
  def get_node_row(pdb_id: str, auth_asym_id: str) -> Optional[_Row]:
118
  """Return the cached node row for a chain."""
119
- return _get_index().get((pdb_id.lower(), auth_asym_id.upper()))
 
120
 
121
 
122
  def get_binding_status(pdb_id: str, auth_asym_id: str) -> Optional[str]:
123
  """Return binding status (base_label: apo/holo) or None if not found."""
124
- key = (pdb_id.lower(), auth_asym_id.upper())
125
- row = _get_index().get(key)
126
  if row is None:
127
  return None
128
  return row.get("base_label") or None
@@ -130,8 +151,7 @@ def get_binding_status(pdb_id: str, auth_asym_id: str) -> Optional[str]:
130
 
131
  def get_cath_id(pdb_id: str, auth_asym_id: str) -> Optional[str]:
132
  """Return CATH_ID or 'uncategorized' if not found."""
133
- key = (pdb_id.lower(), auth_asym_id.upper())
134
- row = _get_index().get(key)
135
  if row is None:
136
  return "uncategorized"
137
  val = row.get("CATH_ID", "").strip()
@@ -140,8 +160,7 @@ def get_cath_id(pdb_id: str, auth_asym_id: str) -> Optional[str]:
140
 
141
  def get_cath_superfamily(pdb_id: str, auth_asym_id: str) -> Optional[str]:
142
  """Return CATH superfamily code (e.g. '3.40.190.10') or None if not found."""
143
- key = (pdb_id.lower(), auth_asym_id.upper())
144
- row = _get_index().get(key)
145
  if row is None:
146
  return None
147
  val = row.get("cath_superfamily", "").strip()
@@ -150,8 +169,7 @@ def get_cath_superfamily(pdb_id: str, auth_asym_id: str) -> Optional[str]:
150
 
151
  def get_sequence(pdb_id: str, auth_asym_id: str) -> Tuple[Optional[str], Optional[int]]:
152
  """Return (seq_can, sequence_length) from node table, or (None, None) if not found."""
153
- key = (pdb_id.lower(), auth_asym_id.upper())
154
- row = _get_index().get(key)
155
  if row is None:
156
  return None, None
157
  seq = row["sequence"] or None
@@ -164,8 +182,7 @@ def get_sequence(pdb_id: str, auth_asym_id: str) -> Tuple[Optional[str], Optiona
164
 
165
  def get_functions(pdb_id: str, auth_asym_id: str) -> Optional[List[str]]:
166
  """Return ranked function list for a chain, or None if not in node table."""
167
- key = (pdb_id.lower(), auth_asym_id.upper())
168
- row = _get_index().get(key)
169
  if row is None:
170
  return None
171
  return _parse_functions(row["ranked_functions"])
@@ -173,8 +190,7 @@ def get_functions(pdb_id: str, auth_asym_id: str) -> Optional[List[str]]:
173
 
174
  def get_state_id(pdb_id: str, auth_asym_id: str) -> Optional[str]:
175
  """Return state_id for a chain, or None if not found."""
176
- key = (pdb_id.lower(), auth_asym_id.upper())
177
- row = _get_index().get(key)
178
  if row is None:
179
  return None
180
  val = row.get("state_id", "").strip()
 
8
  import ast
9
  import sqlite3
10
  import logging
11
+ from functools import lru_cache
12
  from typing import Dict, List, Optional, Tuple, Any
13
 
14
  from app.protein.config import get_database_path
 
75
  """Force reload of node index."""
76
  global _index
77
  _index = None
78
+ _lookup_node_row.cache_clear()
79
+
80
+
81
+ @lru_cache(maxsize=4096)
82
+ def _lookup_node_row(pdb_id: str, auth_asym_id: str) -> Optional[_Row]:
83
+ """Read one node row without blocking on a full-table in-memory index."""
84
+ conn = connect_readonly(get_database_path())
85
+ try:
86
+ conn.row_factory = sqlite3.Row
87
+ row = conn.execute(
88
+ 'SELECT base_label, sequence, sequence_length, CATH_ID, cath_superfamily,'
89
+ ' Rosetta, FoldX, EvoEF2, RM, "RM+", ranked_functions,'
90
+ ' state_id, experimental_method, pH, temp_K'
91
+ ' FROM node WHERE LOWER(pdb_id) = ? AND UPPER(auth_asym_id) = ? LIMIT 1',
92
+ (pdb_id.lower(), auth_asym_id.upper()),
93
+ ).fetchone()
94
+ if row is None:
95
+ return None
96
+ return {key: row[key] or "" for key in row.keys()}
97
+ finally:
98
+ conn.close()
99
 
100
 
101
  def _parse_functions(raw: str) -> List[str]:
 
112
 
113
 
114
  def _parse_float(val: str) -> Optional[float]:
115
+ val = str(val).strip()
116
  if not val:
117
  return None
118
  try:
 
123
 
124
  def get_energy_scores(pdb_id: str, auth_asym_id: str) -> Optional[Dict[str, Any]]:
125
  """Return energy scores (Rosetta, FoldX, EvoEF2, RM, RM+) or None if not found."""
126
+ row = get_node_row(pdb_id, auth_asym_id)
 
127
  if row is None:
128
  return None
129
  return {
 
137
 
138
  def get_node_row(pdb_id: str, auth_asym_id: str) -> Optional[_Row]:
139
  """Return the cached node row for a chain."""
140
+ key = (pdb_id.lower(), auth_asym_id.upper())
141
+ return _index.get(key) if _index is not None else _lookup_node_row(*key)
142
 
143
 
144
  def get_binding_status(pdb_id: str, auth_asym_id: str) -> Optional[str]:
145
  """Return binding status (base_label: apo/holo) or None if not found."""
146
+ row = get_node_row(pdb_id, auth_asym_id)
 
147
  if row is None:
148
  return None
149
  return row.get("base_label") or None
 
151
 
152
  def get_cath_id(pdb_id: str, auth_asym_id: str) -> Optional[str]:
153
  """Return CATH_ID or 'uncategorized' if not found."""
154
+ row = get_node_row(pdb_id, auth_asym_id)
 
155
  if row is None:
156
  return "uncategorized"
157
  val = row.get("CATH_ID", "").strip()
 
160
 
161
  def get_cath_superfamily(pdb_id: str, auth_asym_id: str) -> Optional[str]:
162
  """Return CATH superfamily code (e.g. '3.40.190.10') or None if not found."""
163
+ row = get_node_row(pdb_id, auth_asym_id)
 
164
  if row is None:
165
  return None
166
  val = row.get("cath_superfamily", "").strip()
 
169
 
170
  def get_sequence(pdb_id: str, auth_asym_id: str) -> Tuple[Optional[str], Optional[int]]:
171
  """Return (seq_can, sequence_length) from node table, or (None, None) if not found."""
172
+ row = get_node_row(pdb_id, auth_asym_id)
 
173
  if row is None:
174
  return None, None
175
  seq = row["sequence"] or None
 
182
 
183
  def get_functions(pdb_id: str, auth_asym_id: str) -> Optional[List[str]]:
184
  """Return ranked function list for a chain, or None if not in node table."""
185
+ row = get_node_row(pdb_id, auth_asym_id)
 
186
  if row is None:
187
  return None
188
  return _parse_functions(row["ranked_functions"])
 
190
 
191
  def get_state_id(pdb_id: str, auth_asym_id: str) -> Optional[str]:
192
  """Return state_id for a chain, or None if not found."""
193
+ row = get_node_row(pdb_id, auth_asym_id)
 
194
  if row is None:
195
  return None
196
  val = row.get("state_id", "").strip()