Skill Tracer
Skills › Scientific Agent Skills › medchem

medchem

skill · 355 lines · Apache-2.0 license · skills/medchem/SKILL.md

The facts

Its description

Applies medicinal chemistry filters for compound triage, using drug-likeness rules (Lipinski, Veber, CNS), structural alert catalogs (PAINS, NIBR, ChEMBL), complexity metrics, and the medchem query language for library filtering.

The description the agent reads to decide when to use it.

From its settingsline 3

Tools it may use, or may not

allowed-tools: Read Write Edit Bash

Tools it may use while it runs without asking you first.

From its settingsline 5

Runs scripts

scripts/filter_molecules.py, python scripts/filter_molecules.py

Scripts the text names. Found by matching words, so treat them as pointers.

Found by matching wordsline 326 line 330

Licence

Apache-2.0 license, from the skill file

The terms its author shares it under, which let it be shown here.

From its settingsline 4 licence text

Other settings

compatibility: Requires Python 3.11+ with medchem, datamol, and RDKit. Optional Lilly demerits require native tools built with medchem install-lilly, a C++ compiler, make, zlib, and Ruby; installation needs network access. Other filters run locally without credentials. metadata: version: "1.4" last-reviewed: "2026-10-01" skill-author: K-Dense Inc.

More settings at the top of the file, shown as written.

From its settingsline 6 line 7
Other files in this skill (3) Close

The text

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1---
2name: medchem
3description: Applies medicinal chemistry filters for compound triage, using drug-likeness rules (Lipinski, Veber, CNS), structural alert catalogs (PAINS, NIBR, ChEMBL), complexity metrics, and the medchem query language for library filtering.
4license: Apache-2.0 license
5allowed-tools: Read Write Edit Bash
6compatibility: Requires Python 3.11+ with medchem, datamol, and RDKit. Optional Lilly demerits require native tools built with medchem install-lilly, a C++ compiler, make, zlib, and Ruby; installation needs network access. Other filters run locally without credentials.
7metadata:
8 version: "1.4"
9 last-reviewed: "2026-10-01"
10 skill-author: K-Dense Inc.
11---
13# Medchem
15## Overview
17Medchem is a Python library from [datamol-io](https://github.com/datamol-io/medchem) for molecular filtering and prioritization in drug discovery. Apply literature-derived drug-likeness rules, named alert catalogs, complexity thresholds, chemical-group detection, and a custom query language to triage compound libraries at scale. Filters are context-specific guidelines — combine with domain expertise and target knowledge.
19**Verified runtime:** medchem **2.1.1**, datamol **0.13.0**, RDKit **2026.3.6**; Python **3.11+**. `RuleFilters` and structural classes return pandas DataFrames. Filters run locally; installation and documentation lookup need network access. Lilly native execution was not tested in this review.

Overview

Medchem is a Python library from datamol-io for molecular filtering and prioritization in drug discovery. Apply literature-derived drug-likeness rules, named alert catalogs, complexity thresholds, chemical-group detection, and a custom query language to triage compound libraries at scale. Filters are context-specific guidelines — combine with domain expertise and target knowledge.

Verified runtime: medchem 2.1.1, datamol 0.13.0, RDKit 2026.3.6; Python 3.11+. RuleFilters and structural classes return pandas DataFrames. Filters run locally; installation and documentation lookup need network access. Lilly native execution was not tested in this review.

Raw lines 15 to 20
21## When to Use This Skill
23This skill should be used when:
24- Applying drug-likeness rules (Lipinski, Veber, CNS, lead-like) to compound libraries
25- Filtering molecules by structural alerts, PAINS, or NIBR screening-deck rules
26- Prioritizing compounds for hit-to-lead or lead optimization
27- Calculating complexity metrics against ZINC-derived thresholds
28- Detecting functional groups or named substructure catalogs
29- Building multi-criteria filters with the medchem query language

When to Use This Skill

This skill should be used when:

  • Applying drug-likeness rules (Lipinski, Veber, CNS, lead-like) to compound libraries
  • Filtering molecules by structural alerts, PAINS, or NIBR screening-deck rules
  • Prioritizing compounds for hit-to-lead or lead optimization
  • Calculating complexity metrics against ZINC-derived thresholds
  • Detecting functional groups or named substructure catalogs
  • Building multi-criteria filters with the medchem query language
Raw lines 21 to 30
31## Installation
33```bash
34uv pip install "medchem==2.1.1" "datamol==0.13.0" "rdkit==2026.3.6"
35```
37Optional Lilly integration: install the upstream checksum-pinned native tools beside the active Python. This downloads source, builds executables, and runs native regression tests (C++ compiler, make, zlib, Ruby; WSL on Windows). This installation command is documented upstream, not executed here:
39```bash
40medchem install-lilly
41```

Installation

uv pip install "medchem==2.1.1" "datamol==0.13.0" "rdkit==2026.3.6"

Optional Lilly integration: install the upstream checksum-pinned native tools beside the active Python. This downloads source, builds executables, and runs native regression tests (C++ compiler, make, zlib, Ruby; WSL on Windows). This installation command is documented upstream, not executed here:

medchem install-lilly
Raw lines 31 to 42
43## Core Capabilities

Core Capabilities

Raw lines 43 to 44
45### 1. Medicinal Chemistry Rules
47Apply established drug-likeness rules via `medchem.rules`.
49**List available rules:**
51```python
52import medchem as mc
54mc.rules.RuleFilters.list_available_rules_names()
55# ['rule_of_five', 'rule_of_five_beyond', 'rule_of_four', 'rule_of_three', ...]
56```
58**Single rule on one molecule:**
60```python
61import datamol as dm
62import medchem as mc
64smiles = "CC(=O)OC1=CC=CC=C1C(=O)O" # aspirin
65mc.rules.basic_rules.rule_of_five(smiles) # True
66mc.rules.basic_rules.rule_of_cns(smiles) # True
67mc.rules.basic_rules.rule_of_veber(smiles) # True
68```
70**Multiple rules with `RuleFilters` (returns a DataFrame):**
72```python
73import datamol as dm
74import medchem as mc
76mols = [dm.to_mol(s) for s in smiles_list]
78rfilter = mc.rules.RuleFilters(
79 rule_list=["rule_of_five", "rule_of_oprea", "rule_of_cns", "rule_of_leadlike_soft"]
81df = rfilter(mols=mols, n_jobs=-1, progress=True, keep_props=False)
83# Columns: mol, pass_all, pass_any, rule_of_five, rule_of_oprea, ...
84passing = df[df["pass_all"]]
85```
87Examples below use already parsed `mol_list`, `smiles_list`, and `candidates` supplied by the caller; reject missing/empty structures before filtering. Use `keep_props=True` to include computed descriptors (`mw`, `clogp`, `tpsa`, etc.) in the result.

1. Medicinal Chemistry Rules

Apply established drug-likeness rules via medchem.rules.

List available rules:

import medchem as mc

mc.rules.RuleFilters.list_available_rules_names()
# ['rule_of_five', 'rule_of_five_beyond', 'rule_of_four', 'rule_of_three', ...]

Single rule on one molecule:

import datamol as dm
import medchem as mc

smiles = "CC(=O)OC1=CC=CC=C1C(=O)O"  # aspirin
mc.rules.basic_rules.rule_of_five(smiles)   # True
mc.rules.basic_rules.rule_of_cns(smiles)    # True
mc.rules.basic_rules.rule_of_veber(smiles)  # True

**Multiple rules with RuleFilters (returns a DataFrame):**

import datamol as dm
import medchem as mc

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", "rule_of_leadlike_soft"]
)
df = rfilter(mols=mols, n_jobs=-1, progress=True, keep_props=False)

# Columns: mol, pass_all, pass_any, rule_of_five, rule_of_oprea, ...
passing = df[df["pass_all"]]

Examples below use already parsed mol_list, smiles_list, and candidates supplied by the caller; reject missing/empty structures before filtering. Use keep_props=True to include computed descriptors (mw, clogp, tpsa, etc.) in the result.

Raw lines 45 to 88
89### 2. Structural Alert Filters
91Detect problematic patterns with `medchem.structural`. Both classes return **DataFrames** with `pass_filter`, `status`, and `reasons` columns.
93**Common alerts (ChEMBL-derived rule sets):**
95```python
96import medchem as mc
98alert_filter = mc.structural.CommonAlertsFilters(alerts_set=["BMS", "Dundee", "Glaxo"])
99df = alert_filter(mols=mol_list, n_jobs=-1, progress=True)
100# df columns: mol, pass_filter, status, reasons
102clean = df[df["pass_filter"]]
103```
105**NIBR filters (Novartis screening-deck curation):**
107```python
108nibr_filter = mc.structural.NIBRFilters()
109df = nibr_filter(mols=mol_list, n_jobs=-1, progress=True)
110# df columns: mol, pass_filter, status, severity, reasons, n_covalent_motif, special_mol
111```
113The class rejects explicit exclusion alerts. To also reject accumulated flag severity ≥10, use `df["pass_filter"] & (df["severity"] < 10)`, or `mc.functional.nibr_filter(..., max_severity=10)`. The functional cutoff is strict `< 10` and assumes valid molecules: it checks severity alone and can admit parse failures with severity zero. Prevalidate inputs.

2. Structural Alert Filters

Detect problematic patterns with medchem.structural. Both classes return DataFrames with pass_filter, status, and reasons columns.

Common alerts (ChEMBL-derived rule sets):

import medchem as mc

alert_filter = mc.structural.CommonAlertsFilters(alerts_set=["BMS", "Dundee", "Glaxo"])
df = alert_filter(mols=mol_list, n_jobs=-1, progress=True)
# df columns: mol, pass_filter, status, reasons

clean = df[df["pass_filter"]]

NIBR filters (Novartis screening-deck curation):

nibr_filter = mc.structural.NIBRFilters()
df = nibr_filter(mols=mol_list, n_jobs=-1, progress=True)
# df columns: mol, pass_filter, status, severity, reasons, n_covalent_motif, special_mol

The class rejects explicit exclusion alerts. To also reject accumulated flag severity ≥10, use df["pass_filter"] & (df["severity"] < 10), or mc.functional.nibr_filter(..., max_severity=10). The functional cutoff is strict < 10 and assumes valid molecules: it checks severity alone and can admit parse failures with severity zero. Prevalidate inputs.

Raw lines 89 to 114
115### 3. Named Catalog Filters (PAINS, Brenk, etc.)
117Use `medchem.catalogs.NamedCatalogs` for RDKit `FilterCatalog` instances, or the functional API:
119```python
120import medchem as mc
122# List available named catalogs
123mc.catalogs.list_named_catalogs()
124# ['tox', 'pains', 'pains_a', 'brenk', 'nibr', 'zinc', ...]
126# Functional API — True means molecule passes (no alert match)
127passes = mc.functional.catalog_filter(mols=mol_list, catalogs=["pains"], n_jobs=-1)
129# Or via catalog objects
130passes = mc.functional.catalog_filter(
131 mols=mol_list,
132 catalogs=[mc.catalogs.NamedCatalogs.pains()],
133 n_jobs=-1,
135```
137`alert_filter` is a different API: it uses the ChEMBL common-alert collection names from `CommonAlertsFilters.list_default_available_alerts()`, not every `NamedCatalogs` name. For example, `brenk` and `pains_a` belong in `catalog_filter`. Set common-alert sets explicitly; the current class implementation defaults to BMS only. `catalog_filter` rejects the string names `nibr` and `bredt`; use their dedicated functional filters. Raw NIBR catalog matches include annotations, regardless of severity.

3. Named Catalog Filters (PAINS, Brenk, etc.)

Use medchem.catalogs.NamedCatalogs for RDKit FilterCatalog instances, or the functional API:

import medchem as mc

# List available named catalogs
mc.catalogs.list_named_catalogs()
# ['tox', 'pains', 'pains_a', 'brenk', 'nibr', 'zinc', ...]

# Functional API — True means molecule passes (no alert match)
passes = mc.functional.catalog_filter(mols=mol_list, catalogs=["pains"], n_jobs=-1)

# Or via catalog objects
passes = mc.functional.catalog_filter(
    mols=mol_list,
    catalogs=[mc.catalogs.NamedCatalogs.pains()],
    n_jobs=-1,
)

alert_filter is a different API: it uses the ChEMBL common-alert collection names from CommonAlertsFilters.list_default_available_alerts(), not every NamedCatalogs name. For example, brenk and pains_a belong in catalog_filter. Set common-alert sets explicitly; the current class implementation defaults to BMS only. catalog_filter rejects the string names nibr and bredt; use their dedicated functional filters. Raw NIBR catalog matches include annotations, regardless of severity.

Raw lines 115 to 138
139### 4. Functional API
141`medchem.functional` provides one-call wrappers that return boolean masks (True = passes):
143```python
144import medchem as mc
146mc.functional.rules_filter(mols=mol_list, rules=["rule_of_five", "rule_of_cns"], n_jobs=-1)
147mc.functional.nibr_filter(mols=mol_list, max_severity=10, n_jobs=-1)
148mc.functional.catalog_filter(mols=mol_list, catalogs=["pains", "brenk"], n_jobs=-1)
149mc.functional.complexity_filter(mols=mol_list, complexity_metric="bertz", limit="99", n_jobs=-1)
150```
152Other helpers: `catalog_filter`, `chemical_group_filter`, `lilly_demerit_filter` (requires optional binaries), `macrocycle_filter`, `bredt_filter`, `protecting_groups_filter`, and more. Pass copies to `bredt_filter` (`[Chem.Mol(m) for m in mol_list]`, after `from rdkit import Chem`): its in-place kekulization changes later aromatic alert matches in 2.1.1.

4. Functional API

medchem.functional provides one-call wrappers that return boolean masks (True = passes):

import medchem as mc

mc.functional.rules_filter(mols=mol_list, rules=["rule_of_five", "rule_of_cns"], n_jobs=-1)
mc.functional.nibr_filter(mols=mol_list, max_severity=10, n_jobs=-1)
mc.functional.catalog_filter(mols=mol_list, catalogs=["pains", "brenk"], n_jobs=-1)
mc.functional.complexity_filter(mols=mol_list, complexity_metric="bertz", limit="99", n_jobs=-1)

Other helpers: catalog_filter, chemical_group_filter, lilly_demerit_filter (requires optional binaries), macrocycle_filter, bredt_filter, protecting_groups_filter, and more. Pass copies to bredt_filter ([Chem.Mol(m) for m in mol_list], after from rdkit import Chem): its in-place kekulization changes later aromatic alert matches in 2.1.1.

Raw lines 139 to 153
154### 5. Chemical Groups
156Detect functional groups and curated pattern collections via `medchem.groups`:
158```python
159import medchem as mc
161# Browse available group collections
162mc.groups.list_default_chemical_groups()
163# ['privileged_scaffolds', 'common_warhead_covalent_inhibitors', 'rings_in_drugs', ...]
165group = mc.groups.ChemicalGroup(groups=["privileged_scaffolds"])
166group.has_match(mol) # bool
167group.get_matches(mol) # DataFrame, including a matches column
168matching_mols = [mol for mol in mol_list if group.has_match(mol)]
169# group.filter(names=[...]) narrows pattern names in place; it does not filter molecules.
171# Returns a boolean mask: True means the molecule does NOT match the group
172mc.functional.chemical_group_filter(mols=mol_list, chemical_group=group, n_jobs=-1)
173```
175Custom groups use `groups_db` CSV with both `smiles` and `smarts`, plus `name` and `group` columns. SMILES and SMARTS matching can differ; record the representation and `exact_match` setting.

5. Chemical Groups

Detect functional groups and curated pattern collections via medchem.groups:

import medchem as mc

# Browse available group collections
mc.groups.list_default_chemical_groups()
# ['privileged_scaffolds', 'common_warhead_covalent_inhibitors', 'rings_in_drugs', ...]

group = mc.groups.ChemicalGroup(groups=["privileged_scaffolds"])
group.has_match(mol)                          # bool
group.get_matches(mol)                        # DataFrame, including a matches column
matching_mols = [mol for mol in mol_list if group.has_match(mol)]
# group.filter(names=[...]) narrows pattern names in place; it does not filter molecules.

# Returns a boolean mask: True means the molecule does NOT match the group
mc.functional.chemical_group_filter(mols=mol_list, chemical_group=group, n_jobs=-1)

Custom groups use groups_db CSV with both smiles and smarts, plus name and group columns. SMILES and SMARTS matching can differ; record the representation and exact_match setting.

Raw lines 154 to 176
177### 6. Molecular Complexity
179Compare complexity metrics to precomputed, molecular-weight-binned ZINC-15 thresholds. Valid default limit labels are `median`, `90`, `99`, `999` (99.9th percentile), and `max`; `95` is not provided. `spacialscore` needs a custom threshold file. All metrics use an upper cutoff, including QED; do not interpret that as selecting high QED.
181```python
182import medchem as mc
184# Single molecule
185cf = mc.complexity.ComplexityFilter(limit="99", complexity_metric="bertz")
186cf(mol) # True if below 99th-percentile threshold
188# Batch via functional API
189mc.functional.complexity_filter(
190 mols=mol_list,
191 complexity_metric="bertz", # also: sas, qed, whitlock, barone, smcm, twc
192 limit="99",
193 n_jobs=-1,
196# Direct metric functions
197mc.complexity.WhitlockCT(mol)
198mc.complexity.BaroneCT(mol)
199```

6. Molecular Complexity

Compare complexity metrics to precomputed, molecular-weight-binned ZINC-15 thresholds. Valid default limit labels are median, 90, 99, 999 (99.9th percentile), and max; 95 is not provided. spacialscore needs a custom threshold file. All metrics use an upper cutoff, including QED; do not interpret that as selecting high QED.

import medchem as mc

# Single molecule
cf = mc.complexity.ComplexityFilter(limit="99", complexity_metric="bertz")
cf(mol)  # True if below 99th-percentile threshold

# Batch via functional API
mc.functional.complexity_filter(
    mols=mol_list,
    complexity_metric="bertz",  # also: sas, qed, whitlock, barone, smcm, twc
    limit="99",
    n_jobs=-1,
)

# Direct metric functions
mc.complexity.WhitlockCT(mol)
mc.complexity.BaroneCT(mol)
Raw lines 177 to 200
201### 7. Scaffold Constraints
203`medchem.constraints.Constraints` matches a core scaffold and applies per-atom constraint functions — not simple MW/LogP ranges. For property bounds, use `RuleFilters`, descriptors via `mc.rules.list_descriptors()`, or the query language.
205```python
206import datamol as dm
207import medchem as mc
209core = dm.from_smarts("c1cncc([*:1])c1")
210for atom in core.GetAtoms():
211 if atom.GetAtomMapNum() == 1:
212 atom.SetProp("query", "aromatic_sidechain")
213constraints = mc.constraints.Constraints(
214 core=core,
215 constraint_fns={"aromatic_sidechain": lambda fragment: dm.descriptors.n_aromatic_atoms(fragment) > 0},
217assert not constraints(dm.to_mol("CN(C)C(=O)c1cncc(C)c1"))
218assert constraints(dm.to_mol("c1ccc(cc1)-c1cccnc1"))
219```

7. Scaffold Constraints

medchem.constraints.Constraints matches a core scaffold and applies per-atom constraint functions — not simple MW/LogP ranges. For property bounds, use RuleFilters, descriptors via mc.rules.list_descriptors(), or the query language.

import datamol as dm
import medchem as mc

core = dm.from_smarts("c1cncc([*:1])c1")
for atom in core.GetAtoms():
    if atom.GetAtomMapNum() == 1:
        atom.SetProp("query", "aromatic_sidechain")
constraints = mc.constraints.Constraints(
    core=core,
    constraint_fns={"aromatic_sidechain": lambda fragment: dm.descriptors.n_aromatic_atoms(fragment) > 0},
)
assert not constraints(dm.to_mol("CN(C)C(=O)c1cncc(C)c1"))
assert constraints(dm.to_mol("c1ccc(cc1)-c1cccnc1"))
Raw lines 201 to 220
221### 8. Medchem Query Language
223Build multi-criteria filters with `medchem.query.QueryFilter`:
225```python
226import medchem as mc
228# Rule + alert combination
229qf = mc.query.QueryFilter('MATCHRULE("rule_of_five") AND NOT HASALERT("pains")')
230mask = qf(mols=mol_list, n_jobs=-1) # list[bool]
232# CNS-like with property bounds
233qf = mc.query.QueryFilter('MATCHRULE("rule_of_cns") AND HASPROP("tpsa", <=, 90)')
234mask = qf(mols=mol_list, n_jobs=-1)
235```
237**Query syntax:**
238- `MATCHRULE("rule_of_five")` — apply a named rule
239- `HASALERT("pains")` — match a named catalog (`pains`, `brenk`, `nibr`, `tox`, …)
240- `HASPROP("mw", <, 500)` — compare a descriptor (unquoted comparator)
241- `HASGROUP("Primary amines")` — match a functional-group name from `mc.groups.get_functional_group_map()`; collection names such as `privileged_scaffolds` are not valid here
242- `HASSUBSTRUCTURE("c1ccccc1")` — substructure match
243- Operators: `AND`, `OR`, `NOT`
245List available descriptors: `mc.rules.list_descriptors()`

8. Medchem Query Language

Build multi-criteria filters with medchem.query.QueryFilter:

import medchem as mc

# Rule + alert combination
qf = mc.query.QueryFilter('MATCHRULE("rule_of_five") AND NOT HASALERT("pains")')
mask = qf(mols=mol_list, n_jobs=-1)  # list[bool]

# CNS-like with property bounds
qf = mc.query.QueryFilter('MATCHRULE("rule_of_cns") AND HASPROP("tpsa", <=, 90)')
mask = qf(mols=mol_list, n_jobs=-1)

Query syntax:

  • MATCHRULE("rule_of_five") — apply a named rule
  • HASALERT("pains") — match a named catalog (pains, brenk, nibr, tox, …)
  • HASPROP("mw", <, 500) — compare a descriptor (unquoted comparator)
  • HASGROUP("Primary amines") — match a functional-group name from mc.groups.get_functional_group_map(); collection names such as privileged_scaffolds are not valid here
  • HASSUBSTRUCTURE("c1ccccc1") — substructure match
  • Operators: AND, OR, NOT

List available descriptors: mc.rules.list_descriptors()

Raw lines 221 to 246
247## Workflow Patterns

Workflow Patterns

Raw lines 247 to 248
249### Pattern 1: Initial Triage of a Compound Library
251Before filtering, assign stable source-row IDs and separate failed SMILES/SDF
252parses from valid molecules that fail a chemical rule. Retain original structure
253text and a rejected-input table; report input, parsed, rule-failed, and retained
254counts. The bundled loader removes invalid molecules (and resets tabular indices),
255so do not align results back to the original file by row position. The example
256below assumes all supplied structures parse successfully.
258```python
259import datamol as dm
260import medchem as mc
261import pandas as pd
263df = pd.read_csv("compounds.csv")
264mols = [dm.to_mol(s) for s in df["smiles"]]
266# Drug-likeness rules
267rules_df = mc.rules.RuleFilters(rule_list=["rule_of_five", "rule_of_veber"])(mols=mols, n_jobs=-1)
269# PAINS + common alerts via query
270qf = mc.query.QueryFilter('MATCHRULE("rule_of_five") AND NOT HASALERT("pains")')
271pass_mask = qf(mols=mols, n_jobs=-1)
273df["passes_rules"] = rules_df["pass_all"].values
274df["drug_like"] = pass_mask
275filtered_df = df[df["drug_like"]]
276filtered_df.to_csv("filtered_compounds.csv", index=False)
277```

Pattern 1: Initial Triage of a Compound Library

Before filtering, assign stable source-row IDs and separate failed SMILES/SDF parses from valid molecules that fail a chemical rule. Retain original structure text and a rejected-input table; report input, parsed, rule-failed, and retained counts. The bundled loader removes invalid molecules (and resets tabular indices), so do not align results back to the original file by row position. The example below assumes all supplied structures parse successfully.

import datamol as dm
import medchem as mc
import pandas as pd

df = pd.read_csv("compounds.csv")
mols = [dm.to_mol(s) for s in df["smiles"]]

# Drug-likeness rules
rules_df = mc.rules.RuleFilters(rule_list=["rule_of_five", "rule_of_veber"])(mols=mols, n_jobs=-1)

# PAINS + common alerts via query
qf = mc.query.QueryFilter('MATCHRULE("rule_of_five") AND NOT HASALERT("pains")')
pass_mask = qf(mols=mols, n_jobs=-1)

df["passes_rules"] = rules_df["pass_all"].values
df["drug_like"] = pass_mask
filtered_df = df[df["drug_like"]]
filtered_df.to_csv("filtered_compounds.csv", index=False)
Raw lines 249 to 278
279### Pattern 2: Lead Optimization Filtering
281```python
282import medchem as mc
284rules_df = mc.rules.RuleFilters(rule_list=["rule_of_leadlike_soft"])(mols=candidates, n_jobs=-1)
285nibr_df = mc.structural.NIBRFilters()(mols=candidates, n_jobs=-1)
286complex_mask = mc.functional.complexity_filter(
287 mols=candidates, complexity_metric="bertz", limit="90", n_jobs=-1
290passes = (
291 rules_df["pass_all"]
292 & nibr_df["pass_filter"]
293 & (nibr_df["severity"] < 10)
294 & complex_mask
296```

Pattern 2: Lead Optimization Filtering

import medchem as mc

rules_df = mc.rules.RuleFilters(rule_list=["rule_of_leadlike_soft"])(mols=candidates, n_jobs=-1)
nibr_df = mc.structural.NIBRFilters()(mols=candidates, n_jobs=-1)
complex_mask = mc.functional.complexity_filter(
    mols=candidates, complexity_metric="bertz", limit="90", n_jobs=-1
)

passes = (
    rules_df["pass_all"]
    & nibr_df["pass_filter"]
    & (nibr_df["severity"] < 10)
    & complex_mask
)
Raw lines 279 to 297
298### Pattern 3: Detect Functional Groups
300```python
301import medchem as mc
303group = mc.groups.ChemicalGroup(groups=["common_warhead_covalent_inhibitors"])
304matches = [group.has_match(mol) for mol in mol_list]
305warhead_mols = [mol for mol, m in zip(mol_list, matches) if m]
306```

Pattern 3: Detect Functional Groups

import medchem as mc

group = mc.groups.ChemicalGroup(groups=["common_warhead_covalent_inhibitors"])
matches = [group.has_match(mol) for mol in mol_list]
warhead_mols = [mol for mol, m in zip(mol_list, matches) if m]
Raw lines 298 to 307
308## Best Practices
3101. **Context matters** — marketed drugs often violate Ro5; prodrugs and natural products are common exceptions.
3112. **Combine filters** — rules, alert catalogs, and complexity thresholds work best together.
3123. **Use parallelization** — pass `n_jobs=-1` for libraries >1000 molecules.
3134. **Check return types** — `RuleFilters` and structural classes return DataFrames; functional helpers return boolean arrays.
3145. **Lilly demerits are optional** — run `medchem install-lilly` in the active environment; default max demerits is 160 in the functional API.
3156. **Document decisions** — retain `status`, `reasons`, and `severity` columns and record salt handling, protonation, tautomer, stereochemistry, and package versions. No normalization is automatic in the bundled loader.
3167. **Interpret alerts cautiously** — PAINS and reactive motifs indicate review priorities, not measured assay interference or toxicity; require assay-specific controls. Complexity is a library-relative heuristic, not a synthesis feasibility assessment. The upstream complexity filter admits NaN scores, so calculate and validate finite scores when a metric can be undefined.

Best Practices

  1. Context matters — marketed drugs often violate Ro5; prodrugs and natural products are common exceptions.
  2. Combine filters — rules, alert catalogs, and complexity thresholds work best together.
  3. Use parallelization — pass n_jobs=-1 for libraries >1000 molecules.
  4. Check return types — RuleFilters and structural classes return DataFrames; functional helpers return boolean arrays.
  5. Lilly demerits are optional — run medchem install-lilly in the active environment; default max demerits is 160 in the functional API.
  6. Document decisions — retain status, reasons, and severity columns and record salt handling, protonation, tautomer, stereochemistry, and package versions. No normalization is automatic in the bundled loader.
  7. Interpret alerts cautiously — PAINS and reactive motifs indicate review priorities, not measured assay interference or toxicity; require assay-specific controls. Complexity is a library-relative heuristic, not a synthesis feasibility assessment. The upstream complexity filter admits NaN scores, so calculate and validate finite scores when a metric can be undefined.
Raw lines 308 to 317
318## Resources
320### references/api_guide.md
321Module-by-module API reference with signatures, return types, and patterns.

references/api_guide.md

Module-by-module API reference with signatures, return types, and patterns.

Raw lines 320 to 322
323### references/rules_catalog.md
324Catalog of available rules, alert sets, complexity metrics, and filter selection guidelines.

references/rules_catalog.md

Catalog of available rules, alert sets, complexity metrics, and filter selection guidelines.

Raw lines 323 to 325
326### scripts/filter_molecules.py
327Batch filtering script for CSV/TSV/SDF or one-SMILES-per-line TXT inputs with configurable rules, named catalogs, and complexity thresholds. Run the command from the skill directory in the installed environment. `--groups` adds annotations; it does not exclude matches. `--filter-output` retains all-filter passes, while its summary covers the full parsed library. Unknown group/catalog names and unavailable requested Lilly filters fail. Existing `passes_*` input annotations do not act as newly evaluated filters. The loader requires RDKit-valid molecules even for Lilly; use raw SMILES with the native wrapper separately when LillyMol-specific valence handling matters. Invalid inputs are removed; retain stable IDs and a separate rejected-input table before invoking the script.
329```bash
330uv run python scripts/filter_molecules.py input.csv \
331 --rules rule_of_five,rule_of_cns --pains --nibr --output filtered.csv
332```

scripts/filter_molecules.py

Batch filtering script for CSV/TSV/SDF or one-SMILES-per-line TXT inputs with configurable rules, named catalogs, and complexity thresholds. Run the command from the skill directory in the installed environment. --groups adds annotations; it does not exclude matches. --filter-output retains all-filter passes, while its summary covers the full parsed library. Unknown group/catalog names and unavailable requested Lilly filters fail. Existing passes_* input annotations do not act as newly evaluated filters. The loader requires RDKit-valid molecules even for Lilly; use raw SMILES with the native wrapper separately when LillyMol-specific valence handling matters. Invalid inputs are removed; retain stable IDs and a separate rejected-input table before invoking the script.

uv run python scripts/filter_molecules.py input.csv \
  --rules rule_of_five,rule_of_cns --pains --nibr --output filtered.csv
Raw lines 326 to 333
Runs code scripts/filter_molecules.py :326
Runs code python scripts/filter_molecules.py :330
334## Documentation
336- Official docs: https://medchem-docs.datamol.io/
337- GitHub: https://github.com/datamol-io/medchem
338- PyPI: https://pypi.org/project/medchem/ (2.1.1)

Documentation

  • Official docs: https://medchem-docs.datamol.io/
  • GitHub: https://github.com/datamol-io/medchem
  • PyPI: https://pypi.org/project/medchem/ (2.1.1)
Raw lines 334 to 339
Boilerplate, lines 340 to 355. The same text appears in 125 other files here. Close
340## Citing Scientific Agent Skills
342This skill is part of Scientific Agent Skills by K-Dense. If it materially contributed to a
343manuscript, report, presentation, or code release, add the paper to the references or
344software section and tell the user you did so:
346> Kassis, T., Agarwal, V., He, Y., Patel, D., & Brueckner, A. M. (2026). Scientific Agent
347> Skills: A Library of Procedural Knowledge for Research Agents. arXiv:2609.00065.
348> https://doi.org/10.48550/arXiv.2609.00065
350Always cite the current version. The DOI and https://arxiv.org/abs/2609.00065 resolve to the
351latest arXiv version, so never append a version suffix such as `v1`. When network access is
352available, fetch https://arxiv.org/abs/2609.00065 (or
353http://export.arxiv.org/api/query?id_list=2609.00065) before writing the reference and take
354the author list, year, and version from that record. If the record lists a journal reference
355or publisher DOI, cite the published version instead.

From K-Dense-AI/scientific-agent-skills at 5f2bc03, 2026-10-26. Not read by a model yet. Summary and techniques by Claude Opus 5.5 on 2026-11-06.