Skip to content

Commit be25041

Browse files
committed
gaia: cover short-alias exact-IN branch + harden null bbox handling
- gaia.sql: add gene 34G (3-char alias) test group with the exact OCR word 34g, a boundary decoy 34g/x and a substring decoy x34gy on their own figures, plus a malformed OCR entry (imageName with no bbox). Pins that short aliases (<=3 chars) match exact-only, never via the word-boundary regex or a LIKE. This is because prod db only has gene aliases of at least 3 len. - resources/gaia.py: unpack records the image name even when an OCR entry has no bbox (so the figure still displays) but skips the None, keeping each figure's bbox list clean for the frontend's box-drawing. - test_gaia.py: add the 34G short-alias test (gated MySQL-only via the skip guard) asserting exact match, decoy exclusion, and a None-free bbox list.
1 parent 70a57d0 commit be25041

3 files changed

Lines changed: 83 additions & 43 deletions

File tree

‎api/resources/gaia.py‎

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -182,16 +182,19 @@ def post(self):
182182
class GaiaPublicationFiguresByGene(Resource):
183183
@gaia.param("identifier", _in="path", default="ABI3")
184184
def get(self, identifier=""):
185-
# Escape input and validate
185+
186+
# Escape input
186187
identifier = escape(identifier)
188+
189+
# Is it valid
187190
if not BARUtils.is_gaia_alias(identifier):
188191
return BARUtils.error_exit("Invalid identifier"), 400
189192

190-
# --- Resolve identifier -> gene id(s) -> the gene's full alias set ---
193+
# Resolve to gene ids: try alias first, then locus / ncbi id
191194
rows = db.session.execute(db.select(Aliases.genes_id).filter(Aliases.alias == identifier)).fetchall()
192195
gene_ids = [r.genes_id for r in rows]
193196

194-
if not gene_ids: # not an alias - try locus / ncbi geneid
197+
if not gene_ids:
195198
rows = db.session.execute(
196199
db.select(Genes.id).filter(or_(Genes.locus == identifier, Genes.geneid == identifier))
197200
).fetchall()
@@ -200,22 +203,20 @@ def get(self, identifier=""):
200203
if not gene_ids:
201204
return BARUtils.error_exit("Nothing found"), 404
202205

206+
# Get the gene's full alias set
203207
aliases = [
204208
r.alias.lower()
205209
for r in db.session.execute(db.select(Aliases.alias).filter(Aliases.genes_id.in_(gene_ids))).fetchall()
206210
]
207211

208-
# --- Build the OCR word-boundary match (V1-resolved policy, no gene:true filter) ---
209-
# Aliases >= 4 chars: whole-token regex match (catches "abi3/vp1", rejects "gabi390_r").
210-
# Aliases <= 3 chars: exact IN(...) - a boundary match on a short token over-matches.
212+
# Match OCR words: word-boundary regex for long aliases, exact match for short ones
211213
long_aliases = sorted({re.escape(a) for a in aliases if len(a) >= 4})
212214
short_aliases = sorted({a for a in aliases if len(a) < 4})
213215

214-
# Gene resolved but no usable aliases -> empty payload (frontend falls back to PubMed feed).
216+
# No usable aliases, nothing to match on
215217
if not long_aliases and not short_aliases:
216218
return BARUtils.success_exit({"figures": {}, "allImageWords": {}})
217219

218-
# --- Step 1: match on the scalar OCR word (pure Core; no array explosion) ---
219220
word_expr = func.lower(func.json_unquote(func.json_extract(FigureModels.data, "$.word")))
220221
match_conds = []
221222
if long_aliases:
@@ -228,26 +229,31 @@ def get(self, identifier=""):
228229
if not matched_rows:
229230
return BARUtils.success_exit({"figures": {}, "allImageWords": {}})
230231

231-
# --- Step 2: unpack matched rows' image[] in Python -> name -> [bbox] (data: dict or str) ---
232+
# Collect each matched image and its boxes (keep the image even if a box is missing)
232233
bbox_by_name = {}
233234
for row in matched_rows:
234235
d = row.data if isinstance(row.data, dict) else json.loads(row.data)
235236
for img in d.get("image", []):
236237
name = (img.get("imageName") or "").lstrip("/")
237-
if name:
238-
bbox_by_name.setdefault(name, []).append(img.get("bbox"))
238+
if not name:
239+
continue
240+
bbox_list = bbox_by_name.setdefault(name, [])
241+
bbox = img.get("bbox")
242+
if bbox is not None:
243+
bbox_list.append(bbox)
239244

240245
stripped_names = list(bbox_by_name.keys())
241246
if not stripped_names:
242247
return BARUtils.success_exit({"figures": {}, "allImageWords": {}})
243248

244-
# --- Step 3: figures + publication metadata (pure Core; bbox now comes from Python) ---
245-
# Bare-name guard: exclude any img_name mapping to >1 publication (un-attributable detections).
249+
# Drop image names used by more than one publication, we can't attribute those
246250
collision = (
247251
db.select(Figures.img_name)
248252
.group_by(Figures.img_name)
249253
.having(func.count(func.distinct(Figures.publication_figures_id)) > 1)
250254
)
255+
256+
# Pull the figures and their publication info, skip null urls, newest pubmed first
251257
core_stmt = (
252258
db.select(
253259
PubIds.pmc,
@@ -269,21 +275,16 @@ def get(self, identifier=""):
269275
)
270276
fig_rows = db.session.execute(core_stmt).fetchall()
271277

272-
# Valid gene, but no OCR-matched figures -> empty payload (NOT 404).
273278
if not fig_rows:
274279
return BARUtils.success_exit({"figures": {}, "allImageWords": {}})
275280

276-
# --- Step 4: group by PMC; dedupe figures by img_name; attach the full accumulated bbox list ---
277-
# Dedup guard: the collision guard only excludes names spanning >1 publication, so two figures
278-
# rows with the same name under one pf_id could still duplicate. Surviving img_name -> pmc is
279-
# 1:1, so a seen-set on img_name is sufficient; bbox_by_name[name] already holds the full list.
281+
# Group figures by PMC, one entry per image name
280282
figures_by_pmc, pmc_to_pf, pf_ids, seen_names = {}, {}, set(), set()
281283
for r in fig_rows:
282-
pmc = r.pmc
283284
pf_ids.add(r.pf_id)
284-
pmc_to_pf[pmc] = r.pf_id
285-
if pmc not in figures_by_pmc:
286-
figures_by_pmc[pmc] = {
285+
pmc_to_pf[r.pmc] = r.pf_id
286+
if r.pmc not in figures_by_pmc:
287+
figures_by_pmc[r.pmc] = {
287288
"title": r.title,
288289
"abstract": r.abstract,
289290
"authors": [],
@@ -293,7 +294,7 @@ def get(self, identifier=""):
293294
if r.img_name in seen_names:
294295
continue
295296
seen_names.add(r.img_name)
296-
figures_by_pmc[pmc]["figures"].append(
297+
figures_by_pmc[r.pmc]["figures"].append(
297298
{
298299
"img_name": r.img_name,
299300
"img_url": r.img_url,
@@ -302,7 +303,7 @@ def get(self, identifier=""):
302303
}
303304
)
304305

305-
# --- Step 5: authors per publication (one bulk query; db.select infers the gaia bind) ---
306+
# Attach authors to each publication
306307
authors_by_pf = {}
307308
for r in db.session.execute(
308309
db.select(AuthorList.publication_figures_id, AuthorList.author).filter(
@@ -313,13 +314,11 @@ def get(self, identifier=""):
313314
for pmc, pf_id in pmc_to_pf.items():
314315
figures_by_pmc[pmc]["authors"] = authors_by_pf.get(pf_id, [])
315316

316-
# --- Step 6: allImageWords (pure Core JSON_OVERLAPS pre-filter + Python unpack) ---
317-
# gene:true words detected on the displayed figures. JSON_OVERLAPS pre-filters rows that share
318-
# any displayed image; the Python pass keeps only the displayed names. All gaia-bound -> bind inferred.
317+
# allImageWords: gene words detected on the shown figures, for the gene-name filter
319318
displayed_names = list({r.img_name for r in fig_rows})
320319
all_image_words = {}
321320
if displayed_names:
322-
displayed_slashed = json.dumps(["/" + n for n in displayed_names]) # re-add slash to match stored
321+
displayed_slashed = json.dumps(["/" + n for n in displayed_names]) # stored names keep a leading /
323322
words_rows = db.session.execute(
324323
db.select(FigureModels.data)
325324
.where(func.json_unquote(func.json_extract(FigureModels.data, "$.gene")) == "true")
@@ -337,6 +336,8 @@ def get(self, identifier=""):
337336
for img in d.get("image", []):
338337
name = (img.get("imageName") or "").lstrip("/")
339338
if name in displayed_set:
340-
all_image_words.setdefault(word, {})[name] = img.get("bbox")
339+
bbox = img.get("bbox")
340+
all_image_words.setdefault(word, {})[name] = bbox if bbox is not None else []
341341

342+
# Return final data
342343
return BARUtils.success_exit({"figures": figures_by_pmc, "allImageWords": all_image_words})

0 commit comments

Comments
 (0)