A Python implementation of the Cramer decision tree for toxicological hazard classification of chemical substances. This tool classifies chemicals into one of three risk classes (I-III) based on structural and chemical properties relevant to systemic toxicity assessment.
Original Reference: Cramer, G. M., Ford, R. A., & Hall, R. L. (1978). Estimation of toxic hazard—a decision tree approach. Food and Cosmetics Toxicology, 16(3), 255-276.
Implementation derived from: Toxtree (https://toxtree.sourceforge.net/), licensed under the GNU General Public License v2.0 or later (GPLv2+)
- Full decision tree implementation: All 39 rules (33 main + 6 auxiliary) from the original Cramer tree
- RDKit-based: Uses RDKit for molecular analysis and SMARTS pattern matching
- Automatic hydrogen handling: Explicit hydrogens added automatically—users don't need to manage this
- Salt fragmentation: Automatically extracts and analyzes organic residues from salt compounds
- Transparent reasoning: Full trace of decision steps for interpretability
- InChI lookups: Q1 (body constituents) and Q22 (food components) use exact InChI matching against reference databases
Validated against the Munro dataset:
| Dataset | Compounds | Accuracy | Context |
|---|---|---|---|
| Munro (Expert Baseline) | 613 | 88.91% | Expert-curated toxicity reference. Matches Toxtree baseline. |
- Munro validation: 88.91% agreement with Toxtree confirms correct implementation of the decision tree logic.
The package requires RDKit:
uv add cdt
from rdkit import Chem
from cdt.cdt.decision_tree import classify
# Create molecule from SMILES (explicit hydrogens added automatically)
mol = Chem.MolFromSmiles("CCO") # Ethanol
# Classify
result = classify(mol)
print(f"Cramer Class: {result.cramer_class}") # 1, 2, or 3
print(f"Category: {result.category_name}") # "Low", "Intermediate", "High"
print(f"Description: {result.category_description}")
# View decision trace
for entry in result.trace:
print(f"Q{entry.question_id}: {entry.answer}")Note: Explicit hydrogens are automatically added during classification. Users do not need to call Chem.AddHs() manually.
@dataclass
class CramerResult:
cramer_class: int # 1 (Low), 2 (Intermediate), or 3 (High)
category_name: str # "Low (Class I)", "Intermediate (Class II)", etc.
category_description: str # Full text description
trace: list[TraceEntry] # Decision steps taken
notes: list[str] # Warnings about stubs/simplifications@dataclass
class TraceEntry:
question_id: str # "Q1", "Q2", etc.
question_text: str # Human-readable question
answer: bool # True or False__init__.py: Public API (mainclassify()function)mol_flags.py: Preprocessing - molecular feature extraction (MolFlags, analyse())functional_groups.py: SMARTS pattern catalogue and group detection helpersrules.py: All 39 decision rule functionsdecision_tree.py: Transition matrix and tree execution enginelookup.py: InChI lookup functionalityresult.py: Result and trace data structures
- Layer 1 - Preprocessing (
mol_flags.py): Extract boolean flags and ring information - Layer 2 - Functional Groups (
functional_groups.py): SMARTS pattern matching - Layer 3 - Lookups (
lookup.py): InChI database queries - Layer 4 - Rules (
rules.py): 39 decision functions - Layer 5 - Engine (
decision_tree.py): Transition matrix and execution
- Q3: Elemental composition (C, H, O, N, S(II))
- Q7: Heterocyclic ring
- Q10: 3-membered heterocyclic ring
- Q12: Heteroaromatic ring
- Q13: Ring with non-ring substituent
- Q14: Multiple aromatic rings
- Q19: Open-chain (acyclic)
- Q23: Aromatic ring
- Q27: Ring with non-ring substituent (same as Q13)
- Q28: Multiple aromatic rings (same as Q14)
- Q2: Restricted N/S groups (nitro, cyano, N-nitroso, diazo, triazeno, quaternary N)
- Q5: Simple aliphatic hydrocarbon or carbohydrate
- Q6: Simple benzene ring with limited substituents
- Q8: Lactone ring
- Q16: Isoprene unit with simple groups (OH, CHO, COOH)
- Q18: Unsaturated aldehyde (acrolein, methacrolein, etc.)
- Q20: Functional group count limits
- Q21: Multiple (>3) distinct functional groups
- Q24: Single 5+ membered alicyclic ring with allowed substituents
- Q25: Single 3-4 membered alicyclic ring with allowed substituents
- Q26: Atoms covered by allowed groups (1-2 rings)
- Q30: Aromatic substituents with chain length ≤5
- Q32: Atoms covered by Q30 allowed group list
- Q4: Salt/sulphonate detection
- Q9: Unstable lactone (simplified)
- Q11: Allowed heterocyclic substituents
- Q15: Hydrolysable groups (ester/acetal/lactone)
- Q17: Terpene ester
- Q29: Aromatic ring with hydrolysable ester
- Q31: Acetal/ester residue (simplified)
- Q33: Sulphonate/sulphamate with fragmentation limits
Reuse logic from main rules (Q7, Q12, Q16, Q15, Q17, Q30)
The implementation includes several simplifications documented with notes in the output:
| Rule | Limitation |
|---|---|
| Q5 | "Simply branched" not verified - all non-acetylenic aliphatics pass |
| Q9 | Lactone stability not simulated - returns conservative result |
| Q15 | Hydrolysis not simulated - presence of groups taken as proxy |
| Q17 | Terpene ester check simplified |
| Q20 | Group count limits not fully enforced |
| Q24/Q25 | Substituent types not completely verified |
| Q26 | Atom coverage not fully verified |
| Q30 | Substituent chain length not fully verified |
| Q31 | Hydrolysis products not simulated |
| Q33 | Metabolic fragmentation not simulated |
These match the limitations in the original Java implementation or are acceptable simplifications for the Python port.
Three test files are included:
# Basic functionality test
python test_phase1.py
# Comprehensive validation (10 compounds)
python test_validation.py
# Unit tests for individual rules
python test_rules.py- ✅ 10/10 validation tests pass
- ✅ 7/7 unit tests pass
- ✅ 100% rule implementation coverage
- ✅ All stub rules properly documented
data/bodymol.inchi(51 KB): InChI strings for body constituents (Q1)data/foodmol.inchi(9.1 KB): InChI strings for food components (Q22)
Copied from ToxTree-core resources.
This implementation is licensed under GPLv2, or (at your option) any later version, to align with the Toxtree source implementation, which is also licensed under these terms. Attribution: This code implements the Cramer decision tree algorithm from Toxtree (https://toxtree.sourceforge.net/), originally developed by Ideaconsult Ltd. and licensed under GPLv2. See LICENSE file for full GPLv2 text.
Multi-fragment molecules (salts) are fragmented using fragment_salt(). The largest organic residue is extracted and analyzed through the full tree, while inorganic counterions are discarded. This allows organic compounds that are present as salts to be correctly classified based on their organic structure.
Distinguishes between:
- Simple aliphatic hydrocarbons (C, H only)
- Cyclic carbohydrates (C, H, O with oxygen in ring)
- Acyclic carbohydrates (C, H, O with multiple alcohols + single aldehyde/ketone)
Ring oxygen in carbohydrates is treated as structural, not a functional group.
Glucose correctly returns heterocyclic=False because the ring oxygen is structural, not an ether functional group.
Per-substituent validation ensures only OH, OCH3, OC2H5 substituents are allowed on benzene rings.
- Cramer et al. (1978): Estimation of toxic hazard—a decision tree approach. Food and Cosmetics Toxicology, 16(3), 255-276.
- Roberts et al. (2015): A practical guidance for Cramer class determination. Regulatory Toxicology and Pharmacology, 81, S2-S5.
- Toxtree: https://toxtree.sourceforge.net/
- RDKit: https://www.rdkit.org/
This implementation is described in: Patlewicz G, Mihalchik AL. Revisiting the Threshold of Toxicological Concern: a computational framework for structural classification, cross-scheme comparison and opportunities for refinement. Computational Toxicology 39: 100443 https://doi.org/10.1016/j.comtox.2026.100443
0.1.0 - Python implementation of Cramer decision tree (validated against Toxtree 88.91% on Munro dataset)