Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
# Changelog

## [Unreleased]

### Added

- `ReactionClassifier`: template-grounded reaction-name classification. Given a
reaction SMILES it returns every reaction label whose template can reproduce
the observed product from a compatible pair of supplied reactants, with full
evidence (template IDs, reactants used, generated outcome, match policy).
Supports multi-component inputs, spectators, an opt-in agent-candidate mode,
and opt-in lenient matching (`connectivity`, `parent_fragment`, `tautomer`).
- `parse_reaction_smiles` / `ParsedReaction`: reaction SMILES parser supporting
`reactants>>products` and `reactants>agents>products`.
- Public classification types: `ClassificationResult`, `ReactionCandidate`,
`TemplateMatch`, `ClassificationDiagnostic`, plus outcome-preserving internal
types `ReactionOutcome` and `TemplateApplication`.
Comment on lines +15 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use “public” rather than “internal” for re-exported types.

Lines [15]-[17] describe ReactionOutcome and TemplateApplication as internal, but src/smartreact/__init__.py imports both and lists them in __all__. Update the changelog wording so users are not misled about their supported import surface.

Proposed wording
- plus outcome-preserving internal
+ plus outcome-preserving public
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Public classification types: `ClassificationResult`, `ReactionCandidate`,
`TemplateMatch`, `ClassificationDiagnostic`, plus outcome-preserving internal
types `ReactionOutcome` and `TemplateApplication`.
- Public classification types: `ClassificationResult`, `ReactionCandidate`,
`TemplateMatch`, `ClassificationDiagnostic`, plus outcome-preserving public
types `ReactionOutcome` and `TemplateApplication`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 15 - 17, Update the changelog entry’s
classification type description to call ReactionOutcome and TemplateApplication
public types, matching their re-exported status in smartreact.__init__ and
__all__; remove the misleading “internal” wording while preserving the rest of
the list.

- Template-ID provenance on `ReactionResult` via a new `template_id` field.
- Opt-in `main_product` matching policy: compares only the principal (largest)
product of each side, so a reaction whose dataset records byproducts the
template does not generate (e.g. `...>>ester.O`, `...>>biphenyl.Br`) can still
be classified. Being the most lossy policy it is never reported as an exact
match and is off by default.
- `reactant_indices` on `TemplateMatch` and `TemplateApplication`: the original
left-side component index of each reactant, so duplicate reactants that share a
SMILES string stay distinguishable by position and are no longer collapsed
during evidence de-duplication.

### Changed

- Product-set matching is now fragment-aware: every product is split into its
connected components before comparison, so a template that emits a single
disconnected product (`X.Y`) matches an observed side written as two
dot-separated products (`X`, `Y`).
- A `no_match` whose observed product set could not be normalized under any
enabled policy now reports `incomplete` (inconclusive) rather than a confident
`no_match`.

## [1.0.0] 2026-06-08

### Added
Expand Down
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,64 @@ for result in results:

See [`notebooks/examples.ipynb`](notebooks/examples.ipynb) for more detailed examples.

### Reaction-name classification

`ReactionClassifier` runs the enumeration in reverse *as a question*: given a
reaction SMILES, which named templates can reproduce the observed product from a
compatible pair of the supplied reactants? It returns **every** matching label —
multiple labels are a normal, valid result, and it never invents a single winner.

```python
from smartreact import ReactionClassifier

with ReactionClassifier() as classifier:
result = classifier.classify("Brc1ccccc1.OB(O)c1ccccc1>>c1ccc(-c2ccccc2)cc1")

print(result.status) # "matched"
print(result.reaction_labels) # ["suzuki"]

for candidate in result.candidates:
print(candidate.reaction_name, candidate.template_ids, candidate.best_match_type)
for evidence in candidate.matches:
print(evidence.reactants_used, "->", evidence.generated_products)
```

Every candidate is traceable to the template IDs, the exact reactants used, the
generated outcome, and the named matching policy — so a label is always evidence,
never a bare guess.

- **Status** is one of `matched`, `no_match`, `invalid_input`, `unsupported`, or
`incomplete`. A `no_match` means no *installed* template reproduced the
reaction — not that the chemistry is impossible. `incomplete` means an
execution failure prevented a conclusive `no_match`.
- **Multi-component inputs** are handled: spectators (bases, solvents, catalysts,
counterions) can stay on the reactant side. Every positional reactant pair is
tried, and functional-group keys are computed once per unique component.
- **Agents** (the `reactants>agents>products` middle section) are metadata by
default. Pass `include_agents_as_candidates=True` for datasets that place true
reactants among the agents.
- **Matching** defaults to strict `product_set_exact` (atom maps removed, full
product-set equality on canonical isomeric SMILES). Comparison is fragment-aware,
so a template that emits a single disconnected product (`X.Y`) still matches an
observed side written as two dot-separated products (`X`, `Y`). Lenient policies
are opt-in and never silently replace exact matching:

```python
result = classifier.classify(
rxn_smiles,
match_policies=[
"product_set_exact", "connectivity", "parent_fragment", "tautomer", "main_product",
],
)
```

When several policies match the same evidence, only the strongest
(`product_set_exact` > `connectivity` > `parent_fragment` > `tautomer` >
`main_product`) is kept. `main_product` compares only the principal (largest)
product of each side, so it tolerates byproducts (water, HX, salts) recorded on
one side but not the other; being the most lossy, it is opt-in and never reported
as an exact match.

### Usage

The `ReactionEnumerator` class generates products from reactant pairs using curated SMARTS templates. It uses SMARTS-RX functional group keys to filter compatible reactant pairs before applying reactions.
Expand Down
30 changes: 29 additions & 1 deletion src/smartreact/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
"""SmartReact: SMARTS-based reaction enumeration with SMARTS-RX functional group filtering."""

from .classifier import ReactionClassifier
from .cleaning import clean_smiles
from .conversion import detect_format, read_sdf_block, read_sdf_file, to_smiles
from .enumerator import ReactionEnumerator
from .keygen import KeyGenerator, KeysLibrary, load_rules
from .keys import extract_key_strings, orders_for_template
from .preprocessing import load_preprocessed, preprocess_smiles, save_preprocessed
from .reaction_smiles import ParsedReaction, ReactionParseError, parse_reaction_smiles
from .templates import load_templates
from .types import KeyMatch, KeysResult, ReactionResult, ReactionTemplate, Rule
from .types import (
ClassificationDiagnostic,
ClassificationResult,
ClassificationStatus,
KeyMatch,
KeysResult,
MatchType,
ReactionCandidate,
ReactionOutcome,
ReactionResult,
ReactionTemplate,
Rule,
TemplateApplication,
TemplateMatch,
)

try:
from ._version import __version__, __version_tuple__ # noqa: F401
Expand All @@ -16,6 +32,7 @@

__all__ = [
"ReactionEnumerator",
"ReactionClassifier",
"KeyGenerator",
"KeysLibrary",
"load_templates",
Expand All @@ -25,6 +42,17 @@
"KeyMatch",
"KeysResult",
"Rule",
"ReactionOutcome",
"TemplateApplication",
"ClassificationResult",
"ClassificationStatus",
"ClassificationDiagnostic",
"ReactionCandidate",
"TemplateMatch",
"MatchType",
"ParsedReaction",
"ReactionParseError",
"parse_reaction_smiles",
"extract_key_strings",
"orders_for_template",
"clean_smiles",
Expand Down
Loading