Skip to content

Commit eb421a2

Browse files
mpagni12claude
andcommitted
Replace matched-substructure SVG image output with completeSmiles
Remove the SVG depiction feature from substructure search: the func:withImages input, func:matchedImage output, the with_images engine flag / matched_images field, and the highlight_match_svg helper (plus its rdDepictor/rdMolDraw2D imports). Add a func:completeSmiles output instead: the original, complete SMILES string of the matching compound as stored in the service (repeated per matched-fragment row), complementing the per-match matchedSmiles/matchedSmarts. Docs regenerated and tests updated accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f08726d commit eb421a2

5 files changed

Lines changed: 23 additions & 147 deletions

File tree

README.md

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,6 @@ differently by RDKit:
280280
| `func:dbNames` | `UnionType[str, NoneType]` | `None` | Optional database name to limit the search. |
281281
| `func:minMatchCount` | `int` | `1` | Minimum number of substructure matches required. |
282282
| `func:useChirality` | `bool` | `False` | If true, both tetrahedral (R/S) and double-bond (E/Z) stereochemistry are enforced during matching. Defaults to false. Most meaningful for SMILES queries; SMARTS encodes its own stereo in the pattern. |
283-
| `func:withImages` | `bool` | `False` | If true, populate func:matchedImage with an SVG of the database molecule, matched substructure highlighted. Defaults to false (rendering is comparatively expensive). |
284283

285284
**Outputs:**
286285

@@ -290,7 +289,7 @@ differently by RDKit:
290289
| `func:matchCount` | `int` | Number of matches found (1 if boolean match). |
291290
| `func:matchedSmiles` | `str` | SMILES of the matched fragment, rendered from the target (stereo preserved). |
292291
| `func:matchedSmarts` | `str` | SMARTS of the matched fragment, rendered from the target (stereo preserved). |
293-
| `func:matchedImage` | `str` | SVG depiction of the database molecule with the matched substructure highlighted (empty unless func:withImages is true). |
292+
| `func:completeSmiles` | `str` | The original, complete SMILES string of the matching compound as stored in the service. |
294293

295294
**Example 1:**
296295

@@ -310,26 +309,13 @@ SELECT ?result ?matchCount ?matchedSmiles ?matchedSmarts WHERE {
310309

311310
```sparql
312311
PREFIX func: <urn:sparql-function:>
313-
SELECT ?result ?matchCount ?matchedSmiles ?matchedSmarts WHERE {
312+
SELECT ?result ?matchCount ?matchedSmiles ?completeSmiles WHERE {
314313
[] a func:SubstructureSearch ;
315314
func:smiles "c1ccccc1" ;
316315
func:result ?result ;
317316
func:matchCount ?matchCount ;
318317
func:matchedSmiles ?matchedSmiles ;
319-
func:matchedSmarts ?matchedSmarts .
320-
}
321-
```
322-
323-
**Example 3:**
324-
325-
```sparql
326-
PREFIX func: <urn:sparql-function:>
327-
SELECT ?result ?matchedImage WHERE {
328-
[] a func:SubstructureSearch ;
329-
func:smiles "c1ccccc1" ;
330-
func:withImages true ;
331-
func:result ?result ;
332-
func:matchedImage ?matchedImage .
318+
func:completeSmiles ?completeSmiles .
333319
}
334320
```
335321

src/mol_search_sparql_service/rdkit_fingerprints.py

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
MACCSkeys,
1616
PatternFingerprint,
1717
)
18-
from rdkit.Chem import rdDepictor
19-
from rdkit.Chem.Draw import rdMolDraw2D
2018

2119
# NOTE: we need to silence RDKit warnings that magically poped up from nowhere
2220
# even if it was working before, and no libs version have been changed
@@ -138,10 +136,6 @@ class SubstructureResult:
138136
# rendered from the target molecule so its stereochemistry is preserved.
139137
matched_smiles: list[str] = field(default_factory=list)
140138
matched_smarts: list[str] = field(default_factory=list)
141-
# Parallel SVG depiction per match, with the matched atoms/bonds highlighted.
142-
# Only populated when the search is run with with_images=True (rendering is
143-
# comparatively expensive); otherwise left empty.
144-
matched_images: list[str] = field(default_factory=list)
145139

146140

147141
# ---------------------------------------------------------------------------
@@ -443,49 +437,6 @@ def safe_mol_from_smarts(smarts: str) -> Chem.Mol | None:
443437
return Chem.MolFromSmarts(smarts)
444438

445439

446-
def highlight_match_svg(
447-
mol: Chem.Mol, atoms: list[int], width: int = 400, height: int = 400
448-
) -> str:
449-
"""Render ``mol`` to an SVG string with ``atoms`` AND the bonds among them
450-
highlighted — used to depict a matched substructure on the database molecule.
451-
452-
Generates fresh 2D coordinates with the CoordGen algorithm: the legacy
453-
depiction can produce distorted/overlapping layouts on complex molecules
454-
(rings that look "unclosed"), whereas CoordGen yields clean, properly closed
455-
rings. Returns an empty string on any drawing failure so a single bad
456-
depiction never aborts a search.
457-
"""
458-
try:
459-
atom_set = set(atoms)
460-
# Highlight every bond whose both endpoints are in the matched atom set.
461-
bonds = [
462-
b.GetIdx()
463-
for b in mol.GetBonds()
464-
if b.GetBeginAtomIdx() in atom_set and b.GetEndAtomIdx() in atom_set
465-
]
466-
# Lay the molecule out cleanly. Compute once per molecule (reused across
467-
# this molecule's matches); CoordGen gives publication-quality 2D coords.
468-
if mol.GetNumConformers() == 0:
469-
with _silence_stderr():
470-
rdDepictor.SetPreferCoordGen(True)
471-
rdDepictor.Compute2DCoords(mol)
472-
drawer = rdMolDraw2D.MolDraw2DSVG(width, height)
473-
with _silence_stderr():
474-
rdMolDraw2D.PrepareAndDrawMolecule(
475-
drawer, mol, highlightAtoms=list(atoms), highlightBonds=bonds
476-
)
477-
drawer.FinishDrawing()
478-
svg = drawer.GetDrawingText()
479-
# Strip the leading XML declaration (e.g.
480-
# "<?xml version='1.0' encoding='iso-8859-1'?>\n") so the result is a
481-
# bare <svg> element, easier to embed inline.
482-
if svg.startswith("<?xml"):
483-
svg = svg[svg.index("?>") + 2 :].lstrip("\n")
484-
return svg
485-
except Exception:
486-
return ""
487-
488-
489440
# ---------------------------------------------------------------------------
490441
# Search engine
491442
# ---------------------------------------------------------------------------
@@ -593,15 +544,10 @@ def search_substructure(
593544
min_match_count: int = 1,
594545
use_chirality: bool = False,
595546
query_type: str = "smiles",
596-
with_images: bool = False,
597547
) -> list[SubstructureResult]:
598548
"""Executes a substructure search (Screening + Verification).
599549
600550
Args:
601-
with_images: If True, render an SVG depiction of each matched
602-
fragment (highlighted on the database molecule) into
603-
``matched_images``. Off by default as rendering is comparatively
604-
expensive.
605551
query: The query pattern, interpreted according to ``query_type``.
606552
query_type: ``"smiles"`` (default) parses ``query`` with
607553
``MolFromSmiles`` — sanitized, aromatized, stereo perceived.
@@ -668,7 +614,6 @@ def search_substructure(
668614
seen: set[tuple[str, str]] = set()
669615
matched_smiles: list[str] = []
670616
matched_smarts: list[str] = []
671-
matched_images: list[str] = []
672617
for atom_ids in matches:
673618
atoms = list(atom_ids)
674619
try:
@@ -684,10 +629,6 @@ def search_substructure(
684629
seen.add(key)
685630
matched_smiles.append(smi)
686631
matched_smarts.append(sma)
687-
if with_images:
688-
matched_images.append(
689-
highlight_match_svg(target_mol, atoms)
690-
)
691632

692633
results.append(
693634
SubstructureResult(
@@ -698,7 +639,6 @@ def search_substructure(
698639
match_count=len(matches),
699640
matched_smiles=matched_smiles,
700641
matched_smarts=matched_smarts,
701-
matched_images=matched_images,
702642
)
703643
)
704644

src/mol_search_sparql_service/sparql_service.py

Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ class SubstructureSearchResult:
2323
"""SMILES of the matched fragment, rendered from the target (stereo preserved)."""
2424
matchedSmarts: str
2525
"""SMARTS of the matched fragment, rendered from the target (stereo preserved)."""
26-
matchedImage: str
27-
"""SVG depiction of the database molecule with the matched substructure highlighted (empty unless func:withImages is true)."""
26+
completeSmiles: str
27+
"""The original, complete SMILES string of the matching compound as stored in the service."""
2828

2929

3030
@dataclass
@@ -178,7 +178,6 @@ def substructure_search(
178178
db_names: str | None = None,
179179
min_match_count: int = 1,
180180
use_chirality: bool = False,
181-
with_images: bool = False,
182181
) -> list[SubstructureSearchResult]:
183182
"""Perform substructure search using a SMARTS or SMILES query pattern.
184183
@@ -199,7 +198,6 @@ def substructure_search(
199198
db_names: Optional database name to limit the search.
200199
min_match_count: Minimum number of substructure matches required.
201200
use_chirality: If true, both tetrahedral (R/S) and double-bond (E/Z) stereochemistry are enforced during matching. Defaults to false. Most meaningful for SMILES queries; SMARTS encodes its own stereo in the pattern.
202-
with_images: If true, populate func:matchedImage with an SVG of the database molecule, matched substructure highlighted. Defaults to false (rendering is comparatively expensive).
203201
204202
Example (SMARTS):
205203
```sparql
@@ -214,28 +212,16 @@ def substructure_search(
214212
}
215213
```
216214
217-
Example (SMILES):
215+
Example (SMILES, with the complete molecule SMILES):
218216
```sparql
219217
PREFIX func: <urn:sparql-function:>
220-
SELECT ?result ?matchCount ?matchedSmiles ?matchedSmarts WHERE {
218+
SELECT ?result ?matchCount ?matchedSmiles ?completeSmiles WHERE {
221219
[] a func:SubstructureSearch ;
222220
func:smiles "c1ccccc1" ;
223221
func:result ?result ;
224222
func:matchCount ?matchCount ;
225223
func:matchedSmiles ?matchedSmiles ;
226-
func:matchedSmarts ?matchedSmarts .
227-
}
228-
```
229-
230-
Example (with highlighted SVG image):
231-
```sparql
232-
PREFIX func: <urn:sparql-function:>
233-
SELECT ?result ?matchedImage WHERE {
234-
[] a func:SubstructureSearch ;
235-
func:smiles "c1ccccc1" ;
236-
func:withImages true ;
237-
func:result ?result ;
238-
func:matchedImage ?matchedImage .
224+
func:completeSmiles ?completeSmiles .
239225
}
240226
```
241227
"""
@@ -259,27 +245,22 @@ def substructure_search(
259245
min_match_count=min_match_count,
260246
use_chirality=use_chirality,
261247
query_type=query_type,
262-
with_images=with_images,
263248
)
264-
# Emit one row per distinct matched fragment so each match's SMILES,
265-
# SMARTS and image are individually bindable. Compounds with no
266-
# renderable fragment still surface once with empty fragment strings.
249+
# Emit one row per distinct matched fragment so each match's SMILES and
250+
# SMARTS are individually bindable. completeSmiles is the full original
251+
# molecule SMILES, repeated on every row for that compound. Compounds
252+
# with no renderable fragment still surface once with empty fragments.
267253
rows = []
268254
for r in results:
269-
# matched_images is parallel to the fragment lists when with_images
270-
# was requested, and empty otherwise.
271-
images = r.matched_images or [""] * len(r.matched_smiles)
272-
fragments = list(zip(r.matched_smiles, r.matched_smarts, images)) or [
273-
("", "", "")
274-
]
275-
for smi, sma, img in fragments:
255+
fragments = list(zip(r.matched_smiles, r.matched_smarts)) or [("", "")]
256+
for smi, sma in fragments:
276257
rows.append(
277258
SubstructureSearchResult(
278259
result=URIRef(r.id),
279260
matchCount=int(r.match_count),
280261
matchedSmiles=smi,
281262
matchedSmarts=sma,
282-
matchedImage=img,
263+
completeSmiles=r.smiles,
283264
)
284265
)
285266
return rows

tests/test_parameters.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -302,39 +302,6 @@ def test_matched_fragments_deduplicated():
302302
os.unlink(path)
303303

304304

305-
def test_matched_images_opt_in():
306-
"""SVG depictions are produced only when with_images=True, parallel to the
307-
matched fragments and highlighting the match."""
308-
import tempfile, os
309-
from mol_search_sparql_service.rdkit_fingerprints import MolSearchEngine
310-
311-
tsv = "?chem\t?smiles\t?db\n<http://ex.org/toluene>\tCc1ccccc1\ttest\n"
312-
with tempfile.NamedTemporaryFile(mode="w", suffix=".tsv", delete=False) as f:
313-
f.write(tsv)
314-
path = f.name
315-
316-
try:
317-
eng = MolSearchEngine()
318-
eng.load_file(path)
319-
320-
# Default: no images rendered.
321-
r = eng.search_substructure("c1ccccc1")[0]
322-
assert r.matched_images == []
323-
324-
# Opt in: one SVG per matched fragment, each a valid highlighted SVG.
325-
r = eng.search_substructure("c1ccccc1", with_images=True)[0]
326-
assert len(r.matched_images) == len(r.matched_smiles) >= 1
327-
svg = r.matched_images[0]
328-
assert "<svg" in svg
329-
# The XML declaration is stripped; the SVG starts at the root element.
330-
assert not svg.startswith("<?xml")
331-
assert svg.lstrip().startswith("<")
332-
# PrepareAndDrawMolecule emits highlight markers (ellipse / fill colour).
333-
assert "ellipse" in svg or "fill:#" in svg
334-
finally:
335-
os.unlink(path)
336-
337-
338305
def test_query_type_smiles_vs_smarts():
339306
"""SMILES and SMARTS queries are parsed differently: a Kekulé string is
340307
aromatized as SMILES but stays literal/aliphatic as SMARTS."""

tests/test_sparql.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,21 +228,23 @@ def test_substructure_search_by_smiles():
228228
assert int(bindings[0]["matchCount"]["value"]) >= 1
229229

230230

231-
def test_substructure_search_with_image():
232-
# func:withImages true populates func:matchedImage with a highlighted SVG.
231+
def test_substructure_search_complete_smiles():
232+
# func:completeSmiles returns the full original SMILES of the matching
233+
# compound (a superset of, and generally different from, the matched fragment).
233234
bindings = sparql_query("""
234235
PREFIX func: <urn:sparql-function:>
235-
SELECT ?result ?matchedImage WHERE {
236+
SELECT ?result ?matchedSmiles ?completeSmiles WHERE {
236237
[] a func:SubstructureSearch ;
237238
func:smiles "c1ccccc1" ;
238239
func:limit 5 ;
239-
func:withImages true ;
240240
func:result ?result ;
241-
func:matchedImage ?matchedImage .
241+
func:matchedSmiles ?matchedSmiles ;
242+
func:completeSmiles ?completeSmiles .
242243
}
243244
""")
244245
assert len(bindings) > 0
245-
assert "<svg" in bindings[0]["matchedImage"]["value"]
246+
# Every row carries a non-empty complete SMILES string.
247+
assert all(b["completeSmiles"]["value"] for b in bindings)
246248

247249

248250
def test_list_fingerprints():

0 commit comments

Comments
 (0)