Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Medicinal chemistry filters for compound triage. Drug-likeness rules (Lipinski Ro5, Veber, Oprea, CNS, leadlike, REOS, Golden Triangle, Ro3), structural alerts (PAINS, NIBR, Lilly Demerits), chemical group detectors, complexity metrics, and filter composition query language. Built on RDKit/datamol. For hit-to-lead filtering, library design, ADMET pre-screening. For molecular I/O use rdkit-cheminformatics or datamol.
.claude/skills/jaechang-hits-medchem/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 75% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 108% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 215% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 197% | 0% |
Medchem is a Python library for molecular filtering and prioritization in drug discovery. It provides hundreds of established medicinal chemistry rules, structural alerts, and chemical group detectors to triage compound libraries at scale. All filters support parallel execution and return structured results.
bashpip install medchem datamol
Medchem depends on RDKit and datamol. All molecule inputs are RDKit Chem.Mol objects; use datamol.to_mol() to convert from SMILES.
pythonimport datamol as dm import medchem as mc # Convert SMILES to molecules smiles_list = ["CC(=O)OC1=CC=CC=C1C(=O)O", "c1ccccc1N", "O=C(O)c1ccccc1"] mols = [dm.to_mol(s) for s in smiles_list] # Apply Rule of Five + structural alerts in one pass rule_filter = mc.rules.RuleFilters(rule_list=["rule_of_five"]) alert_filter = mc.structural.CommonAlertsFilters() rule_results = rule_filter(mols=mols, n_jobs=-1) alert_results = alert_filter(mols=mols, n_jobs=-1) print(f"Rule results: {rule_results}") print(f"Alert results: {[r['has_alerts'] for r in alert_results]}")
Apply established medicinal chemistry rules via mc.rules. Individual rules return bool; RuleFilters applies multiple rules in batch.
pythonimport medchem as mc # Single rule on a SMILES string passes = mc.rules.basic_rules.rule_of_five("CC(=O)OC1=CC=CC=C1C(=O)O") print(f"Passes Ro5: {passes}") # True # Available individual rules: # rule_of_five, rule_of_three, rule_of_oprea, rule_of_cns, # rule_of_leadlike_soft, rule_of_leadlike_strict, rule_of_veber, # rule_of_reos, rule_of_drug, golden_triangle, pains_filter
pythonimport datamol as dm import medchem as mc # Batch application with RuleFilters mols = [dm.to_mol(s) for s in smiles_list] rfilter = mc.rules.RuleFilters( rule_list=["rule_of_five", "rule_of_oprea", "rule_of_cns"] ) results = rfilter(mols=mols, n_jobs=-1, progress=True) # Returns list of dicts: [{"rule_of_five": True, "rule_of_oprea": False, ...}, ...] print(f"First molecule: {results[0]}")
Detect problematic structural patterns via mc.structural. Three filter sets cover different scope and stringency.
pythonimport datamol as dm import medchem as mc mol = dm.to_mol("c1ccc(N)cc1") mols = [dm.to_mol(s) for s in smiles_list] # Common Alerts — general structural alerts from ChEMBL / literature alert_filter = mc.structural.CommonAlertsFilters() has_alerts, details = alert_filter.check_mol(mol) # single molecule batch_results = alert_filter(mols=mols, n_jobs=-1, progress=True) # Each result: {"has_alerts": bool, "alert_details": [...], "num_alerts": int} print(f"Alerts: {batch_results[0]}")
pythonimport medchem as mc # NIBR Filters — Novartis industrial filter set (returns bool list) nibr_filter = mc.structural.NIBRFilters() nibr_results = nibr_filter(mols=mols, n_jobs=-1) print(f"NIBR pass: {nibr_results}") # [True, False, ...] # Lilly Demerits — 275 patterns, molecules rejected at >100 demerits lilly_filter = mc.structural.LillyDemeritsFilters() lilly_results = lilly_filter(mols=mols, n_jobs=-1) # Each result: {"demerits": int, "passes": bool, "matched_patterns": [...]} print(f"Lilly: {lilly_results[0]}")
Detect specific functional group motifs via mc.groups.ChemicalGroup.
Predefined groups: hinge_binders, phosphate_binders, michael_acceptors, reactive_groups.
pythonimport medchem as mc # Check for kinase hinge binders and Michael acceptors group = mc.groups.ChemicalGroup( groups=["hinge_binders", "michael_acceptors"] ) has_matches = group.has_match(mols) # List[bool] match_info = group.get_matches(mols[0]) # {group_name: [(atom_indices), ...]} all_matches = group.get_all_matches(mols) # List[Dict] print(f"Has hinge binder: {has_matches}") # Custom SMARTS patterns custom = mc.groups.ChemicalGroup( groups=["reactive_groups"], custom_smarts={"trifluoromethyl_ketone": "[C;H0](=O)C(F)(F)F"} )
Access curated chemical structure catalogs via mc.catalogs.
Available catalogs: functional_groups, protecting_groups, reagents, fragments.
pythonimport medchem as mc catalog = mc.catalogs.NamedCatalogs.get("functional_groups") matches = catalog.get_matches(mol) print(f"Functional group matches: {matches}")
Calculate synthetic accessibility proxies via mc.complexity.
Methods: bertz (topological), whitlock, barone.
pythonimport datamol as dm import medchem as mc mol = dm.to_mol("CC(=O)OC1=CC=CC=C1C(=O)O") # Single molecule complexity score = mc.complexity.calculate_complexity(mol, method="bertz") print(f"Bertz complexity: {score:.1f}") # Batch filtering by complexity threshold cfilter = mc.complexity.ComplexityFilter(max_complexity=500, method="bertz") results = cfilter(mols=mols, n_jobs=-1) print(f"Passes complexity: {results}") # List[bool]
Apply custom property-based constraints via mc.constraints.Constraints.
pythonimport medchem as mc constraints = mc.constraints.Constraints( mw_range=(200, 500), logp_range=(-2, 5), tpsa_max=140, rotatable_bonds_max=10, hbd_max=5, hba_max=10, rings_range=(1, 5), aromatic_rings_max=3, ) results = constraints(mols=mols, n_jobs=-1) # Each result: {"passes": bool, "violations": ["mw_range", ...]} print(f"Violations: {results[0]}")
Compose complex filter logic with Boolean expressions via mc.query.
pythonimport medchem as mc # Parse a query combining rules, alerts, and property checks query = mc.query.parse("rule_of_five AND NOT common_alerts") results = query.apply(mols=mols, n_jobs=-1) # List[bool] print(f"Passing: {sum(results)}/{len(results)}") # More complex queries q2 = mc.query.parse("rule_of_cns AND complexity < 400") q3 = mc.query.parse("(rule_of_five OR rule_of_oprea) AND NOT pains_filter") q4 = mc.query.parse("mw > 200 AND mw < 500 AND logp < 5")
Shortcut functions in mc.functional and utilities in mc.utils.
pythonimport medchem as mc # Functional API — one-liner filters nibr_ok = mc.functional.nibr_filter(mols=mols, n_jobs=-1) # List[bool] alerts = mc.functional.common_alerts_filter(mols=mols, n_jobs=-1) lilly = mc.functional.lilly_demerits_filter(mols=mols, n_jobs=-1) # Utilities standardized = mc.utils.standardize_mol(mol) # sanitize, neutralize charges batch_out = mc.utils.batch_process( mols=mols, func=mc.complexity.calculate_complexity, n_jobs=-1, progress=True, batch_size=100 )
| Rule | MW | LogP | HBD | HBA | RotBonds | TPSA | Other | |------|----|----- |-----|-----|----------|------|-------| | Ro5 (Lipinski) | ≤500 | ≤5 | ≤5 | ≤10 | — | — | — | | Veber | — | — | — | — | ≤10 | ≤140 | — | | Oprea (lead) | 200-350 | -2 to 4 | — | — | ≤7 | — | Rings ≤4 | | Leadlike Soft | 250-450 | -3 to 4 | — | — | ≤10 | — | — | | Leadlike Strict | 200-350 | -2 to 3.5 | — | — | ≤7 | — | Rings 1-3 | | CNS | ≤450 | -1 to 5 | ≤2 | — | — | ≤90 | — | | REOS | 200-500 | -5 to 5 | 0-5 | 0-10 | — | — | — | | Ro3 (fragment) | ≤300 | ≤3 | ≤3 | ≤3 | ≤3 | ≤60 | — | | Golden Triangle | 200-50LogP+400 | -2 to 5 | — | — | — | — | — | | Rule of Drug | Ro5 + Veber + no PAINS | | | | | | |
| Stage | Recommended Filters | Rationale | |-------|-------------------|-----------| | Initial screening | Ro5, PAINS, Common Alerts | Broad triage, remove obvious liabilities | | Hit-to-lead | Oprea or Leadlike Soft, NIBR, Lilly | Lead-like space, industrial filters | | Lead optimization | Rule of Drug, Leadlike Strict, Complexity | Strict drug-likeness + synthetic feasibility | | CNS targets | Rule of CNS, TPSA ≤90, HBD ≤2 | BBB permeability requirements | | Fragment-based | Ro3, low complexity (≤250) | Fragments have "room to grow" |
~10% of marketed drugs violate Ro5. Exceptions are common for natural products, antibiotics, PROTACs, and prodrugs. Always combine rule-based filtering with domain expertise and target-class knowledge. Different modalities (oral, IV, topical) and target classes (kinases, GPCRs, ion channels) have distinct optimal property spaces.
Full pipeline from raw SMILES to filtered drug-like candidates.
pythonimport pandas as pd import datamol as dm import medchem as mc # Load compound library df = pd.read_csv("compounds.csv") mols = [dm.to_mol(s) for s in df["smiles"]] print(f"Input: {len(mols)} molecules") # Step 1: Drug-likeness rules rfilter = mc.rules.RuleFilters(rule_list=["rule_of_five", "rule_of_veber"]) rule_results = rfilter(mols=mols, n_jobs=-1, progress=True) # Step 2: Structural alerts alert_filter = mc.structural.CommonAlertsFilters() alert_results = alert_filter(mols=mols, n_jobs=-1, progress=True) # Step 3: Combine results df["passes_rules"] = [all(r.values()) for r in rule_results] df["has_alerts"] = [r["has_alerts"] for r in alert_results] df["drug_like"] = df["passes_rules"] & ~df["has_alerts"] filtered = df[df["drug_like"]] print(f"Output: {len(filtered)} drug-like molecules ({len(filtered)/len(df)*100:.1f}%)") filtered.to_csv("filtered_compounds.csv", index=False)
Apply progressively stricter filters with detailed reporting.
pythonimport datamol as dm import medchem as mc import pandas as pd mols = [dm.to_mol(s) for s in smiles_list] # Cascade: rules → structural alerts → complexity → Lilly demerits filters = [ ("Leadlike Strict", mc.rules.RuleFilters(rule_list=["rule_of_leadlike_strict"])), ("NIBR Alerts", mc.structural.NIBRFilters()), ("Complexity ≤400", mc.complexity.ComplexityFilter(max_complexity=400)), ("Lilly Demerits", mc.structural.LillyDemeritsFilters()), ] surviving = list(range(len(mols))) for name, filt in filters: results = filt(mols=[mols[i] for i in surviving], n_jobs=-1) # Handle different result formats if isinstance(results[0], dict): passed = [i for i, r in zip(surviving, results) if r.get("passes", not r.get("has_alerts", True))] else: passed = [i for i, r in zip(surviving, results) if r] print(f"{name}: {len(surviving)} → {len(passed)}") surviving = passed print(f"Final candidates: {len(surviving)} / {len(mols)}")
| Parameter | Module | Default | Range | Effect | |-----------|--------|---------|-------|--------| | rule_list | RuleFilters | — | See rules table | Which drug-likeness rules to apply | | n_jobs | All filters | 1 | -1 to N | Parallel workers (-1 = all cores) | | progress | All filters | False | bool | Show progress bar | | max_complexity | ComplexityFilter | — | 0-1000+ | Bertz complexity threshold | | method | calculate_complexity | "bertz" | bertz/whitlock/barone | Complexity metric | | mw_range | Constraints | None | tuple(float, float) | Molecular weight range (Da) | | logp_range | Constraints | None | tuple(float, float) | LogP range | | tpsa_max | Constraints | None | 0-200+ | Max topological polar surface area | | groups | ChemicalGroup | — | list of names | Predefined chemical groups to detect | | custom_smarts | ChemicalGroup | None | dict | Custom SMARTS patterns {name: SMARTS} |
n_jobs=-1 to use all CPU cores.pythonimport pandas as pd import datamol as dm import medchem as mc df = pd.read_csv("molecules.csv") df["mol"] = df["smiles"].apply(dm.to_mol) rfilter = mc.rules.RuleFilters(rule_list=["rule_of_five", "rule_of_cns"]) results = rfilter(mols=df["mol"].tolist(), n_jobs=-1) df["passes_ro5"] = [r["rule_of_five"] for r in results] df["passes_cns"] = [r["rule_of_cns"] for r in results] filtered = df[df["passes_ro5"] & df["passes_cns"]] print(f"CNS drug-like: {len(filtered)}")
pythonimport medchem as mc # Rule-based pre-filter rule_results = mc.rules.RuleFilters(rule_list=["rule_of_five"])(mols, n_jobs=-1) filtered_mols = [mol for mol, r in zip(mols, rule_results) if r["rule_of_five"]] # ML model scoring on filtered set (reduces compute cost) ml_scores = ml_model.predict(filtered_mols) candidates = [mol for mol, score in zip(filtered_mols, ml_scores) if score > 0.8] print(f"ML-scored candidates: {len(candidates)}")
pythonimport medchem as mc # Define project-specific warheads for covalent inhibitor screening custom_warheads = { "acrylamide": "[C;H1](=O)[CH]=[CH2]", "vinyl_sulfonamide": "[NH]S(=O)(=O)[CH]=[CH2]", "chloroacetamide": "ClCC(=O)N", } group = mc.groups.ChemicalGroup(groups=[], custom_smarts=custom_warheads) has_warhead = group.has_match(mols) warhead_mols = [mol for mol, match in zip(mols, has_warhead) if match] print(f"Covalent warhead candidates: {len(warhead_mols)}")
| Problem | Cause | Solution | |---------|-------|----------| | None in molecule list | Invalid SMILES in input | Pre-filter: mols = [m for m in mols if m is not None] | | All molecules fail Ro5 | Library is fragment-like or PPI space | Use rule_of_three or rule_of_leadlike_soft instead | | No PAINS alerts found | Molecules are simple/fragment-like | Expected — PAINS patterns target screening-hit-size molecules | | Lilly demerits all >100 | Highly functionalized molecules | Check individual patterns; consider raising threshold or using NIBR instead | | Slow processing | Large library without parallelization | Set n_jobs=-1 for parallel execution | | ImportError: medchem | Missing dependency | pip install medchem datamol (requires RDKit) | | Query parse error | Invalid query syntax | Check operators: AND, OR, NOT, comparisons: <, >, == | | Inconsistent result formats | Different filter classes return different types | Check docs: RuleFilters → dict, NIBRFilters → bool, LillyDemeritsFilters → dict |
Complete catalog of all medicinal chemistry rules and structural alert filters with literature references, threshold criteria, chemical group patterns, custom SMARTS examples, and stage-specific filter selection guidelines. API function signatures were consolidated into Core API code blocks above.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 25,246 | 17,778 | -30% | 1 | 1 | 0% | 5,552 | 9,697 | +75% | 0 | 0 | — |
case-02 | fail→fail | 34,618 | 10,523 | -70% | 1 | 1 | 0% | 2,635 | 7,687 | +192% | 0 | 0 | — |
case-03 | fail→fail | 29,196 | 10,865 | -63% | 1 | 1 | 0% | 4,224 | 7,842 | +86% | 0 | 0 | — |
case-04 | fail→fail | 11,376 | 11,141 | -2% | 1 | 1 | 0% | 2,386 | 7,682 | +222% | 0 | 0 | — |
case-05 | fail→pass | 16,826 | 21,986 | +31% | 1 | 1 | 0% | 3,485 | 7,241 | +108% | 0 | 0 | — |
case-06 | pass→pass | 13,639 | 12,938 | -5% | 1 | 1 | 0% | 2,885 | 8,179 | +184% | 0 | 0 | — |
case-07 | fail→pass | 15,920 | 4,132 | -74% | 1 | 1 | 0% | 3,276 | 6,325 | +93% | 0 | 0 | — |
case-08 | fail→pass | 10,252 | 2,946 | -71% | 1 | 1 | 0% | 1,888 | 5,939 | +215% | 0 | 0 | — |
case-09 | fail→pass | 11,825 | 4,557 | -61% | 1 | 1 | 0% | 2,115 | 6,277 | +197% | 0 | 0 | — |
case-10 | fail→pass | 12,823 | 3,000 | -77% | 1 | 1 | 0% | 2,288 | 5,961 | +161% | 0 | 0 | — |
case-11 | fail→pass | 12,884 | 3,956 | -69% | 1 | 1 | 0% | 2,409 | 6,061 | +152% | 0 | 0 | — |
case-12 | pass→pass | 5,710 | 4,107 | -28% | 1 | 1 | 0% | 1,081 | 5,885 | +444% | 0 | 0 | — |
case-13 | fail→pass | 14,846 | 2,152 | -86% | 1 | 1 | 0% | 2,891 | 5,756 | +99% | 0 | 0 | — |
case-14 | fail→pass | 5,204 | 3,058 | -41% | 1 | 1 | 0% | 1,073 | 6,069 | +466% | 0 | 0 | — |
case-15 | fail→pass | 10,998 | 4,268 | -61% | 1 | 1 | 0% | 2,208 | 6,267 | +184% | 0 | 0 | — |
case-16 | pass→pass | 7,598 | 2,308 | -70% | 1 | 1 | 0% | 1,653 | 5,872 | +255% | 0 | 0 | — |
case-17 | fail→pass | 13,152 | 9,111 | -31% | 1 | 1 | 0% | 2,544 | 6,045 | +138% | 0 | 0 | — |
case-18 | pass→pass | 6,879 | 5,176 | -25% | 1 | 1 | 0% | 1,349 | 5,922 | +339% | 0 | 0 | — |
case-19 | fail→pass | 15,153 | 3,272 | -78% | 1 | 1 | 0% | 3,052 | 6,045 | +98% | 0 | 0 | — |
case-20 | fail→pass | 8,831 | 2,832 | -68% | 1 | 1 | 0% | 1,912 | 5,992 | +213% | 0 | 0 | — |
case-21 | fail→pass | 20,007 | 7,961 | -60% | 1 | 1 | 0% | 3,776 | 6,010 | +59% | 0 | 0 | — |
case-22 | fail→pass | 7,291 | 1,913 | -74% | 1 | 1 | 0% | 1,428 | 5,762 | +304% | 0 | 0 | — |
case-23 | fail→pass | 10,315 | 4,501 | -56% | 1 | 1 | 0% | 2,164 | 6,306 | +191% | 0 | 0 | — |
case-24 | fail→pass | 12,133 | 2,523 | -79% | 1 | 1 | 0% | 2,522 | 5,886 | +133% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of +71 percentage points is the difference between those two pass rates over the 24 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.