From cceb0ff5042b8803ed46a1fe2b55a541ea5140c6 Mon Sep 17 00:00:00 2001 From: Tieu Long Phan <125431507+TieuLongPhan@users.noreply.github.com> Date: Fri, 18 Jul 2025 12:37:42 +0200 Subject: [PATCH 1/6] Refractor (#26) * update refractor * fix reactor * fix lint * prepare bechmark * refractor MODReactor and SynReactor * update crn * refractor cluster, change to matcher, fix code synreactor, now resnow comparable to modreactor * test 3 os * test * test * fix workflow * fix lint * fix win * fix win * fix win again * update smart * add synreactor implicit hydrogen * fix mcsmatcher * refractor visualization * fix conflict rdkit, upgrade to 2025.3.1 * fix lint * move aam_validator to Chem submodule * fix lint * prepare benchmark matcher * change backend rule to mod * prepare doc * add doc * update fih * update graph module doc * update doc * prepare release * . * fix doc * clean doc * fix docstring * fix tutorial * update fig * update explicit_hydrogen for its * prepare release * build doc * fix lint * fix bug in explicit hydrogen for ITS * fix build * fix * fix * fix * fix doc * update nauty canon, rule filters, change benchmark * prepare release * fix bug in nauty alg * update doc * add features for expanding its * add rule_matcher.py * add testcase rule matcher * add wildcard for smiles * add partial engine * update new features * update document * add data * update Chem features * format docstring and refractor Chem module * add auto-test pypi * create dependabot * test run yml * test * test docker * add docker * add docker * . * add readme * rename * release docker * remove redundant file --- .github/dependabot.yml | 10 + .github/workflows/docker-publish.yml | 46 + .github/workflows/verify-pypi-install.yml | 59 ++ .gitignore | 1 + Dockerfile | 45 + Makefile | 20 - README.md | 34 +- Test/Chem/Fingerprint/test_fp_calculator.py | 98 ++- .../Fingerprint/test_transformation_fp.py | 8 +- Test/Chem/Reaction/test_aam_utils.py | 52 -- Test/Chem/Reaction/test_cleanning.py | 4 +- .../test_rsmi_utils.py => test_utils.py} | 49 +- doc/api.rst | 16 + doc/conf.py | 9 +- doc/getting_started.rst | 34 + make.bat | 35 - pyproject.toml | 3 +- synkit/Chem/Cluster/__init__.py | 0 synkit/Chem/Cluster/butina.py | 139 +++ synkit/Chem/Fingerprint/fp_calculator.py | 231 +++-- synkit/Chem/Fingerprint/smiles_featurizer.py | 295 ++++--- synkit/Chem/Fingerprint/transformation_fp.py | 158 ++-- synkit/Chem/Molecule/standardize.py | 197 ++--- synkit/Chem/Reaction/__init__.py | 7 +- synkit/Chem/Reaction/aam_utils.py | 97 --- synkit/Chem/Reaction/aam_validator.py | 53 +- synkit/Chem/Reaction/balance_check.py | 190 ++-- synkit/Chem/Reaction/canon_rsmi.py | 26 +- synkit/Chem/Reaction/cleaning.py | 66 ++ synkit/Chem/Reaction/cleanning.py | 67 -- synkit/Chem/Reaction/deionize.py | 342 +++----- synkit/Chem/Reaction/fix_aam.py | 105 +-- synkit/Chem/Reaction/neutralize.py | 325 +++---- synkit/Chem/Reaction/radical_wildcard.py | 45 +- synkit/Chem/Reaction/rsmi_utils.py | 126 --- synkit/Chem/Reaction/standardize.py | 90 +- synkit/Chem/Reaction/tautomerize.py | 164 ++-- synkit/Chem/utils.py | 363 +++++--- synkit/Data/gen_partial_aam.py | 13 +- synkit/Graph/Canon/canon_algs.py | 21 +- synkit/Graph/Canon/canon_graph.py | 34 +- synkit/Graph/Canon/nauty.py | 34 +- synkit/Graph/Context/hier_context.py | 42 +- synkit/Graph/Context/radius_expand.py | 50 +- synkit/Graph/Feature/graph_descriptors.py | 46 +- synkit/Graph/Feature/graph_fps.py | 14 +- synkit/Graph/Feature/graph_signature.py | 39 +- synkit/Graph/Feature/hash_fps.py | 24 +- synkit/Graph/Feature/morgan_fps.py | 18 +- synkit/Graph/Feature/path_fps.py | 15 +- synkit/Graph/Feature/wl_hash.py | 22 +- synkit/Graph/Hyrogen/_misc.py | 48 +- synkit/Graph/Hyrogen/hcomplete.py | 30 +- synkit/Graph/Hyrogen/hextend.py | 17 +- synkit/Graph/ITS/its_builder.py | 20 +- synkit/Graph/ITS/its_construction.py | 33 +- synkit/Graph/ITS/its_decompose.py | 29 +- synkit/Graph/ITS/its_expand.py | 24 +- synkit/Graph/ITS/its_relabel.py | 37 +- synkit/Graph/ITS/normalize_aam.py | 33 +- synkit/Graph/MTG/group_comp.py | 3 +- synkit/Graph/MTG/groupoid.py | 7 +- synkit/Graph/MTG/mcs_matcher.py | 3 +- synkit/Graph/MTG/mtg.py | 3 +- synkit/Graph/Matcher/batch_cluster.py | 24 +- synkit/Graph/Matcher/graph_cluster.py | 21 +- synkit/Graph/Matcher/graph_morphism.py | 808 ++++++++++-------- synkit/Graph/Matcher/multi_turbo_iso.py | 22 +- synkit/Graph/Matcher/sing.py | 22 +- synkit/Graph/Matcher/subgraph_matcher.py | 36 +- synkit/Graph/Matcher/turbo_iso.py | 4 +- synkit/Graph/Wildcard/fuse_graph.py | 12 +- synkit/Graph/canon_graph.py | 34 +- synkit/Graph/syn_graph.py | 31 +- synkit/Graph/utils.py | 16 +- synkit/IO/chem_converter.py | 139 +-- synkit/IO/data_io.py | 109 +-- synkit/IO/data_process.py | 7 +- synkit/IO/debug.py | 22 +- synkit/IO/dg_to_gml.py | 5 +- synkit/IO/gml_to_nx.py | 39 +- synkit/IO/graph_to_mol.py | 61 +- synkit/IO/mol_to_graph.py | 79 +- synkit/IO/nx_to_gml.py | 70 +- synkit/IO/smiles_to_id.py | 11 +- synkit/Rule/Apply/reactor_rule.py | 21 +- synkit/Rule/Apply/retro_reactor.py | 20 +- synkit/Rule/Apply/rule_matcher.py | 40 +- synkit/Rule/Apply/rule_rbl.py | 6 +- synkit/Rule/Compose/compose_rule.py | 34 +- synkit/Rule/Compose/rule_compose.py | 28 +- synkit/Rule/Compose/rule_mapping.py | 29 +- synkit/Rule/Compose/seq_comp.py | 19 +- synkit/Rule/Compose/valence_constrain.py | 14 +- synkit/Rule/Modify/implict_rule.py | 5 +- synkit/Rule/Modify/longest_path.py | 13 +- synkit/Rule/Modify/molecule_rule.py | 23 +- synkit/Rule/Modify/prune_templates.py | 12 +- synkit/Rule/Modify/rule_utils.py | 36 +- synkit/Rule/Modify/strip_rule.py | 17 +- synkit/Rule/syn_rule.py | 8 +- synkit/Synthesis/CRN/crn.py | 16 +- synkit/Synthesis/CRN/dcrn.py | 32 +- synkit/Synthesis/CRN/mod_crn.py | 17 +- synkit/Synthesis/MSR/multi_steps.py | 16 +- synkit/Synthesis/MSR/path_finder.py | 40 +- synkit/Synthesis/Metrics/_base.py | 3 +- synkit/Synthesis/Metrics/_plot.py | 11 +- synkit/Synthesis/Metrics/_ranking.py | 41 +- synkit/Synthesis/Reactor/batch_reactor.py | 98 +-- synkit/Synthesis/Reactor/core_engine.py | 212 ----- synkit/Synthesis/Reactor/mod_aam.py | 12 +- synkit/Synthesis/Reactor/mod_reactor.py | 56 +- synkit/Synthesis/Reactor/partial_engine.py | 19 +- synkit/Synthesis/Reactor/rbl_engine.py | 41 +- synkit/Synthesis/Reactor/rule_filter.py | 56 +- synkit/Synthesis/Reactor/single_predictor.py | 24 +- synkit/Synthesis/Reactor/strategy.py | 14 +- synkit/Synthesis/Reactor/syn_reactor.py | 62 +- synkit/Synthesis/reactor_utils.py | 39 +- synkit/Utils/utils.py | 21 +- synkit/Vis/embedding.py | 25 +- synkit/Vis/graph_visualizer.py | 12 +- synkit/Vis/pdf_writer.py | 18 +- synkit/Vis/rule_vis.py | 22 +- synkit/Vis/rxn_vis.py | 12 +- 126 files changed, 3453 insertions(+), 3631 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/docker-publish.yml create mode 100644 .github/workflows/verify-pypi-install.yml create mode 100644 Dockerfile delete mode 100644 Makefile delete mode 100644 Test/Chem/Reaction/test_aam_utils.py rename Test/Chem/{Reaction/test_rsmi_utils.py => test_utils.py} (57%) delete mode 100644 make.bat create mode 100644 synkit/Chem/Cluster/__init__.py create mode 100644 synkit/Chem/Cluster/butina.py delete mode 100644 synkit/Chem/Reaction/aam_utils.py create mode 100644 synkit/Chem/Reaction/cleaning.py delete mode 100644 synkit/Chem/Reaction/cleanning.py delete mode 100644 synkit/Chem/Reaction/rsmi_utils.py delete mode 100644 synkit/Synthesis/Reactor/core_engine.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c0c0eb1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +# .github/dependabot.yml +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" # location of requirements.txt or pyproject.toml + target-branch: "staging" # open PRs against staging instead of main + schedule: + interval: "weekly" # check for updates once a week + open-pull-requests-limit: 5 # max concurrent Dependabot PRs + rebase-strategy: "auto" # auto-rebase PRs when they fall out of date diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..363e899 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,46 @@ +# .github/workflows/docker-publish.yml +name: Publish SynKit Docker Package + +on: + push: + # Fire on semver tags for real releases… + tags: + - 'v*.*.*' + # …and on any push to the refractor branch for testing + branches: + - 'staging' + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up QEMU (optional, for multi‑arch) + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + file: Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: | + tieulongphan/synkit:${{ github.ref_name }} + tieulongphan/synkit:latest diff --git a/.github/workflows/verify-pypi-install.yml b/.github/workflows/verify-pypi-install.yml new file mode 100644 index 0000000..55e3e2e --- /dev/null +++ b/.github/workflows/verify-pypi-install.yml @@ -0,0 +1,59 @@ +# .github/workflows/verify-synkit-pypi-install.yml +name: Verify SynKit PyPI install + +on: + workflow_dispatch: + inputs: + branches: + type: string + required: true + default: refractor + + # Scheduled test every Monday at 03:00 UTC + schedule: + - cron: '0 3 * * 1' + +jobs: + verify: + runs-on: ubuntu-latest + + steps: + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Create & activate virtualenv, upgrade pip, install SynKit + run: | + python -m venv venv + source venv/bin/activate + python -m pip install --upgrade pip + pip install synkit[all] + + - name: Show installed SynKit version + run: | + source venv/bin/activate + python -c "import importlib.metadata as m; print('SynKit version:', m.version('synkit'))" + + - name: Write smoke-test script + run: | + cat << 'EOF' > test_synkit.py + from synkit.IO import rsmi_to_rsmarts + + template = ( + '[C:2]=[O:3].[C:4]([H:7])[H:8]' + '>>' + '[C:2]=[C:4].[O:3]([H:7])[H:8]' + ) + + smart = rsmi_to_rsmarts(template) + print("Reaction SMARTS:", smart) + EOF + + - name: Run smoke-test + run: | + source venv/bin/activate + python test_synkit.py + + - name: Success message + run: echo "✅ synkit[all] installed and smoke-test passed" diff --git a/.gitignore b/.gitignore index c85f0f8..07ad7b7 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ Data/Benchmark/* run.sh docs/* run_rdcanon.py +Data/Fragment/* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7bfb459 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +############################################ +# STAGE 1: Build your package wheel +############################################ +FROM python:3.11-slim AS builder + +# 1. Install system build tools (for any C extensions) +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential \ + && rm -rf /var/lib/apt/lists/* + +# 2. Upgrade pip/setuptools/wheel and install PEP 517 tooling + Hatchling backend +RUN pip install --upgrade pip setuptools wheel \ + && pip install --no-cache-dir build hatchling + +# 3. Set working directory inside builder +WORKDIR /build + +# 4. Copy project metadata (including README so Hatchling can find it) +COPY pyproject.toml README.md ./ +# If you have a lockfile, uncomment: +# COPY poetry.lock ./ + +# 5. Copy your library source +COPY synkit/ ./synkit + +# 6. Build the wheel +RUN python -m build --wheel --no-isolation + +############################################ +# STAGE 2: Create the “release” image +############################################ +FROM python:3.11-slim + +# 7. Set a clean workdir +WORKDIR /opt/synkit + +# 8. Copy in the built wheel from the builder stage +COPY --from=builder /build/dist/*.whl ./ + +# 9. Install your package (and its dependencies), then remove the wheel +RUN pip install --no-cache-dir *.whl \ + && rm *.whl + +# 10. Sanity check: print the installed synkit version +CMD ["python", "-c", "import importlib.metadata as m; print(m.version('synkit'))"] diff --git a/Makefile b/Makefile deleted file mode 100644 index 269cadc..0000000 --- a/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/README.md b/README.md index d33bd0d..47d57fe 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,14 @@ Our tools are tailored to assist researchers and chemists in navigating complex For more details on each utility within the repository, please refer to the documentation provided in the respective folders. -## Step-by-Step Installation Guide +## Table of Contents +- [Installation](#installation) +- [Contribute to `SynKit`](#contribute) +- [Contributing](#contributing) +- [License](#license) +- [Acknowledgments](#acknowledgments) + +## Installation 1. **Python Installation:** Ensure that Python 3.11 or later is installed on your system. You can download it from [python.org](https://www.python.org/downloads/). @@ -41,7 +48,20 @@ For more details on each utility within the repository, please refer to the docu pip install synkit[all] ``` -## For contributors +4. **Install via Docker** + Pull the image: + + ```bash + docker pull tieulongphan/synkit:latest + # or a specific version: + docker pull tieulongphan/synkit:0.1.0 + ``` + Run a container (sanity check): + ``` + docker run --rm tieulongphan/synkit:latest + ``` + +## Contribute We're welcoming new contributors to build this project better. Please not hesitate to inquire me via [email][tieu@bioinf.uni-leipzig.de]. @@ -52,7 +72,7 @@ git checkout main git pull ``` -## Working on New Features +### Working on New Features 1. **Create a New Branch**: For every new feature or bug fix, create a new branch from the `main` branch. Name your branch meaningfully, related to the feature or fix you are working on. @@ -78,7 +98,7 @@ git pull Fix any issues or errors highlighted by these checks. -## Integrating Changes +### Integrating Changes 1. **Rebase onto Staging**: Once your feature is complete and tests pass, rebase your changes onto the `staging` branch to prepare for integration. @@ -98,16 +118,16 @@ git pull ``` 3. **Create a Pull Request**: - Open a pull request from your feature branch to the `stagging` branch. Ensure the pull request description clearly describes the changes and any additional context necessary for review. + Open a pull request from your feature branch to the `staging` branch. Ensure the pull request description clearly describes the changes and any additional context necessary for review. ## Contributing - [Tieu-Long Phan](https://tieulongphan.github.io/) - [Klaus Weinbauer](https://github.com/klausweinbauer) - [Phuoc-Chung Nguyen Van](https://github.com/phuocchung123) -## Deployment timeline +## Publication -We plan to update new version quarterly. +[**SynKit**: An Advanced Cheminformatics Python Library for Efficient Manipulation and Analysis of Chemical Reaction Data]() ## License diff --git a/Test/Chem/Fingerprint/test_fp_calculator.py b/Test/Chem/Fingerprint/test_fp_calculator.py index c98babc..47bcb3e 100644 --- a/Test/Chem/Fingerprint/test_fp_calculator.py +++ b/Test/Chem/Fingerprint/test_fp_calculator.py @@ -1,58 +1,68 @@ +import io import unittest +from contextlib import redirect_stdout + from synkit.Chem.Fingerprint.fp_calculator import FPCalculator class TestFPCalculator(unittest.TestCase): def setUp(self): - # Sample data setup - self.data = [ - { - "smiles": [ - ( - "C1CCCCC1.CCO.CS(=O)(=O)N1CCN(Cc2ccccc2)CC1.[OH-].[OH-].[Pd+2]" - + ">>CS(=O)(=O)N1CCNCC1" - ), - ( - "CCOC(C)=O.Cc1cc([N+](=O)[O-])ccc1NC(=O)c1ccccc1.Cl[Sn]Cl.O.O.O=C([O-])O.[Na+]" - + ">>Cc1cc(N)ccc1NC(=O)c1ccccc1" - ), - ( - "COc1ccc(-c2coc3ccc(-c4nnc(S)o4)cc23)cc1.COc1ccc(CCl)cc1F" - + ">>COc1ccc(-c2coc3ccc(-c4nnc(SCc5ccc(OC)c(F)c5)o4)cc23)cc1" - ), - ], - "ID": [1, 2, 3], - } + # Sample single reaction dict + self.single = {"rsmi": "CCO>>CC=O"} + # List of dicts for parallel + self.batch = [ + {"rsmi": "CCO>>CC=O"}, + {"rsmi": "CC(Cl)C>>CCCl"}, ] - self.smiles_key = "smiles" - self.fp_type = "drfp" - self.n_jobs = 2 - self.verbose = 0 - - # Instantiate the FPCalculator - self.fp_calculator = FPCalculator( - smiles_key=self.smiles_key, - fp_type=self.fp_type, - n_jobs=self.n_jobs, - verbose=self.verbose, - ) + self.rsmi_key = "rsmi" + self.fp_type = "ecfp4" + self.calc = FPCalculator(n_jobs=2, verbose=0) + + def test_constructor_assigns_attributes(self): + self.assertEqual(self.calc.n_jobs, 2) + self.assertEqual(self.calc.verbose, 0) - def test_init_invalid_fp_type(self): + def test_validate_fp_type_accepts_supported(self): + # Should not raise + for ft in FPCalculator.VALID_FP_TYPES: + self.calc._validate_fp_type(ft) + + def test_validate_fp_type_rejects_unsupported(self): with self.assertRaises(ValueError): - FPCalculator(smiles_key=self.smiles_key, fp_type="invalid_type") + self.calc._validate_fp_type("invalid_fp") - def test_fit_missing_column(self): + def test_dict_process_missing_key_raises(self): with self.assertRaises(ValueError): - fp_calculator = FPCalculator( - smiles_key=self.smiles_key, - fp_type=self.fp_type, - ) - fp_calculator.dict_process({"not_smiles": ["C"]}, "smiles") - - def test_constructor_and_attribute_assignment(self): - self.assertEqual(self.fp_calculator.smiles_key, "smiles") - self.assertEqual(self.fp_calculator.fp_type, "drfp") - self.assertEqual(self.fp_calculator.n_jobs, 2) + FPCalculator.dict_process({}, self.rsmi_key, fp_type=self.fp_type) + + def test_dict_process_adds_fingerprint(self): + data = {"rsmi": "CCO>>CC=O"} + out = FPCalculator.dict_process(data, "rsmi", fp_type="ecfp4") + self.assertIn("ecfp4", out) + # Check it's a list/vector (not None) + self.assertIsNotNone(out["ecfp4"]) + + def test_parallel_process_returns_list_of_dicts(self): + results = self.calc.parallel_process(self.batch, "rsmi", fp_type="ecfp4") + self.assertIsInstance(results, list) + self.assertEqual(len(results), 2) + for d in results: + self.assertIn("ecfp4", d) + + def test_str_and_help_output(self): + s = str(self.calc) + self.assertIn("FPCalculator", s) + buf = io.StringIO() + with redirect_stdout(buf): + self.calc.help() + help_out = buf.getvalue() + + # The help text starts with this exact line + self.assertIn( + "FPCalculator supports the following fingerprint types:", help_out + ) + # And lists our parallel jobs config + self.assertIn(f"Configured for {self.calc.n_jobs} parallel jobs", help_out) if __name__ == "__main__": diff --git a/Test/Chem/Fingerprint/test_transformation_fp.py b/Test/Chem/Fingerprint/test_transformation_fp.py index aad0552..8332c6e 100644 --- a/Test/Chem/Fingerprint/test_transformation_fp.py +++ b/Test/Chem/Fingerprint/test_transformation_fp.py @@ -22,13 +22,13 @@ def test_fit(self): abs_val = True # Test with return_array=True - reaction_fp_array = TransformationFP.fit( + reaction_fp_array = TransformationFP().fit( reaction_smiles, symbols, fp_type, abs_val ) self.assertIsInstance(reaction_fp_array, np.ndarray) # Test with return_array=False - reaction_fp_bitvect = TransformationFP.fit( + reaction_fp_bitvect = TransformationFP().fit( reaction_smiles, symbols, fp_type, abs_val, return_array=False ) self.assertIsInstance(reaction_fp_bitvect, cDataStructs.ExplicitBitVect) @@ -40,7 +40,7 @@ def test_fit_invalid_smiles(self): fp_type = "maccs" abs_val = True with self.assertRaises(Exception): - _ = TransformationFP.fit(reaction_smiles, symbols, fp_type, abs_val) + _ = TransformationFP().fit(reaction_smiles, symbols, fp_type, abs_val) def test_fit_reaction_split(self): """Test handling of SMILES split by symbols and impact on results""" @@ -48,7 +48,7 @@ def test_fit_reaction_split(self): symbols = ">>" fp_type = "maccs" abs_val = False # without taking absolute values - reaction_fp = TransformationFP.fit(reaction_smiles, symbols, fp_type, abs_val) + reaction_fp = TransformationFP().fit(reaction_smiles, symbols, fp_type, abs_val) self.assertIsInstance(reaction_fp, np.ndarray) diff --git a/Test/Chem/Reaction/test_aam_utils.py b/Test/Chem/Reaction/test_aam_utils.py deleted file mode 100644 index 1baf9f0..0000000 --- a/Test/Chem/Reaction/test_aam_utils.py +++ /dev/null @@ -1,52 +0,0 @@ -import unittest -from rdkit import Chem -from synkit.Chem.Reaction.aam_utils import enumerate_tautomers, mapping_success_rate - - -class TestChemUtils(unittest.TestCase): - def test_enumerate_tautomers_simple(self): - # A simple keto-enol tautomerism: acetylacetone (CC(=O)CC=O) -> same product - reaction = "CC(=O)CC=O>>O" - tautomers = enumerate_tautomers(reaction) - # Should return a list with at least the original reaction - self.assertIsInstance(tautomers, list) - self.assertIn(reaction, tautomers) - # Each entry should be a valid reaction SMILES - for rsmi in tautomers: - self.assertIsInstance(rsmi, str) - parts = rsmi.split(">>") - self.assertEqual(len(parts), 2) - # Reactant and product part parseable by RDKit - self.assertIsNotNone(Chem.MolFromSmiles(parts[0])) - self.assertIsNotNone(Chem.MolFromSmiles(parts[1])) - - def test_enumerate_tautomers_invalid(self): - # Invalid SMILES input - bad = "INVALID>>SMILES" - result = enumerate_tautomers(bad) - # Should return list with original - self.assertEqual(result, [bad]) - - def test_mapping_success_rate_normal(self): - data = ["C:1CC", "CCC", "O:3=O", ":5", "N"] - rate = mapping_success_rate(data) - # Entries with mapping: 'C:1CC', 'O:3=O', ':5' => 3/5 = 60.0% - self.assertEqual(rate, 60.0) - - def test_mapping_success_rate_empty(self): - with self.assertRaises(ValueError): - mapping_success_rate([]) - - def test_mapping_success_rate_all(self): - data = [":1C", ":2", "N:3"] - rate = mapping_success_rate(data) - self.assertEqual(rate, 100.0) - - def test_mapping_success_rate_none(self): - data = ["C", "O", "N"] - rate = mapping_success_rate(data) - self.assertEqual(rate, 0.0) - - -if __name__ == "__main__": - unittest.main() diff --git a/Test/Chem/Reaction/test_cleanning.py b/Test/Chem/Reaction/test_cleanning.py index 0f902ca..d065fe3 100644 --- a/Test/Chem/Reaction/test_cleanning.py +++ b/Test/Chem/Reaction/test_cleanning.py @@ -1,11 +1,11 @@ import unittest -from synkit.Chem.Reaction.cleanning import Cleanning +from synkit.Chem.Reaction.cleaning import Cleaning class TestCleaning(unittest.TestCase): def setUp(self): - self.cleaner = Cleanning() + self.cleaner = Cleaning() def test_remove_duplicates(self): input_smiles = ["CC>>CC", "CC>>CC"] diff --git a/Test/Chem/Reaction/test_rsmi_utils.py b/Test/Chem/test_utils.py similarity index 57% rename from Test/Chem/Reaction/test_rsmi_utils.py rename to Test/Chem/test_utils.py index 2ffd091..98fe9e8 100644 --- a/Test/Chem/Reaction/test_rsmi_utils.py +++ b/Test/Chem/test_utils.py @@ -1,5 +1,8 @@ import unittest -from synkit.Chem.Reaction.rsmi_utils import ( +from rdkit import Chem +from synkit.Chem.utils import ( + enumerate_tautomers, + mapping_success_rate, remove_common_reagents, reverse_reaction, remove_duplicates, @@ -8,7 +11,49 @@ ) -class TestChemicalReactions(unittest.TestCase): +class TestChemUtils(unittest.TestCase): + def test_enumerate_tautomers_simple(self): + # A simple keto-enol tautomerism: acetylacetone (CC(=O)CC=O) -> same product + reaction = "CC(=O)CC=O>>O" + tautomers = enumerate_tautomers(reaction) + # Should return a list with at least the original reaction + self.assertIsInstance(tautomers, list) + self.assertIn(reaction, tautomers) + # Each entry should be a valid reaction SMILES + for rsmi in tautomers: + self.assertIsInstance(rsmi, str) + parts = rsmi.split(">>") + self.assertEqual(len(parts), 2) + # Reactant and product part parseable by RDKit + self.assertIsNotNone(Chem.MolFromSmiles(parts[0])) + self.assertIsNotNone(Chem.MolFromSmiles(parts[1])) + + def test_enumerate_tautomers_invalid(self): + # Invalid SMILES input should raise ValueError + bad = "INVALID>>SMILES" + with self.assertRaises(ValueError) as cm: + enumerate_tautomers(bad) + self.assertIn("Invalid reactant or product SMILES", str(cm.exception)) + + def test_mapping_success_rate_normal(self): + data = ["C:1CC", "CCC", "O:3=O", ":5", "N"] + rate = mapping_success_rate(data) + # Entries with mapping: 'C:1CC', 'O:3=O', ':5' => 3/5 = 60.0% + self.assertEqual(rate, 60.0) + + def test_mapping_success_rate_empty(self): + with self.assertRaises(ValueError): + mapping_success_rate([]) + + def test_mapping_success_rate_all(self): + data = [":1C", ":2", "N:3"] + rate = mapping_success_rate(data) + self.assertEqual(rate, 100.0) + + def test_mapping_success_rate_none(self): + data = ["C", "O", "N"] + rate = mapping_success_rate(data) + self.assertEqual(rate, 0.0) def test_remove_common_reagents_no_common(self): reaction = "A.B.C>>D.E.F" diff --git a/doc/api.rst b/doc/api.rst index 7c0af53..1e38a3b 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -21,6 +21,22 @@ The `Chem` module provides tools for handling input and output operations relate :undoc-members: :show-inheritance: +.. automodule:: synkit.Chem.Reaction.balance_check + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: synkit.Chem.Fingerprint.fp_calculator + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: synkit.Chem.Cluster.butina + :members: + :undoc-members: + :show-inheritance: + + Synthesis Module ================ diff --git a/doc/conf.py b/doc/conf.py index 915c62a..509d687 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,5 +1,6 @@ import os import sys +import importlib.metadata as m from importlib.metadata import version as _get_version, PackageNotFoundError # -- Path setup -------------------------------------------------------------- @@ -15,12 +16,10 @@ release = _get_version("synkit") except PackageNotFoundError: try: - import synkit - - release = synkit.__version__ + release = m.version("synkit") except (ImportError, AttributeError): # Fallback default - release = "0.0.10" + release = "0.0.11" # Use only major.minor for short version version = ".".join(release.split(".")[:2]) @@ -46,4 +45,4 @@ # -- Options for HTML output ------------------------------------------------- html_theme = "sphinx_rtd_theme" -html_static_path = ["_static"] +html_static_path = ["_static"] \ No newline at end of file diff --git a/doc/getting_started.rst b/doc/getting_started.rst index 2ebef8e..d179319 100644 --- a/doc/getting_started.rst +++ b/doc/getting_started.rst @@ -1,5 +1,9 @@ .. _getting-started-synkit: +.. image:: https://img.shields.io/pypi/v/synkit.svg + :alt: PyPI version + :align: right + Getting Started =============== @@ -69,10 +73,40 @@ After installation, verify that **synkit** is available and check its version: python -c "import importlib.metadata as m; print(m.version('synkit'))" # Should print the installed synkit version +Docker Installation +------------------- + +Install **SynKit** using Docker. + +Pull the image: + +.. code-block:: bash + + docker pull tieulongphan/synkit:latest + +Run a quick version check: + +.. code-block:: bash + + docker run --rm tieulongphan/synkit:latest \ + python -c "import importlib.metadata as m; print(m.version('synkit'))" + + +Use as a base image in your own Dockerfile: + +.. code-block:: dockerfile + + FROM tieulongphan/synkit:latest + WORKDIR /app + COPY . . + CMD ["python", "your_script.py"] + + Further Resources ----------------- - Official documentation: `SynKit Docs `_ - Tutorials and examples: :doc:`Tutorials and Examples ` +- Support ------- diff --git a/make.bat b/make.bat deleted file mode 100644 index dafd057..0000000 --- a/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b74faf1..688cc3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "synkit" -version = "0.0.10" +version = "0.0.11" authors = [ {name="Tieu Long Phan", email="tieu@bioinf.uni-leipzig.de"} ] @@ -35,6 +35,7 @@ docs = [ "sphinx-rtd-theme", "sphinxcontrib-bibtex", ] + [project.urls] homepage = "https://github.com/TieuLongPhan/SynKit" source = "https://github.com/TieuLongPhan/SynKit" diff --git a/synkit/Chem/Cluster/__init__.py b/synkit/Chem/Cluster/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/synkit/Chem/Cluster/butina.py b/synkit/Chem/Cluster/butina.py new file mode 100644 index 0000000..ef13fb1 --- /dev/null +++ b/synkit/Chem/Cluster/butina.py @@ -0,0 +1,139 @@ +from __future__ import annotations +from typing import List, Optional + +import numpy as np +from rdkit.DataStructs import cDataStructs, CreateFromBitString, BulkTanimotoSimilarity +from rdkit.ML.Cluster import Butina +from sklearn.manifold import TSNE +import matplotlib.pyplot as plt + + +class ButinaCluster: + """Cluster chemical fingerprint vectors using the Butina algorithm from + RDKit, with integrated t-SNE visualization of clusters. + + Key features + ------------ + * **Butina clustering** – fast hierarchical clustering with a similarity cutoff. + * **t-SNE visualization** – 2D embedding of fingerprints, highlighting top‑k clusters. + * **NumPy support** – accepts 2D arrays of 0/1 fingerprint data. + * **Configurable** – user‑defined cutoff, perplexity, and top‑k highlight. + + Quick start + ----------- + >>> from synkit.Chem.Fingerprint.fingerprint_clusterer import ButinaCluster + >>> clusters = ButinaCluster.cluster(arr, cutoff=0.3) + >>> ButinaCluster.visualize(arr, clusters, k=5) + """ + + @staticmethod + def cluster(arr: np.ndarray, cutoff: float = 0.2) -> List[List[int]]: + """Perform Butina clustering on fingerprint bit-vectors. + + :param arr: 2D array of shape (n_samples, n_bits) with 0/1 + dtype. + :type arr: np.ndarray + :param cutoff: Distance cutoff (1 – similarity) to form + clusters. Defaults to 0.2. + :type cutoff: float + :returns: List of clusters, each a list of sample indices. + :rtype: list of list of int + """ + # Convert rows to RDKit ExplicitBitVect + fps: List[cDataStructs.ExplicitBitVect] = [] + for row in arr: + bitstr = "".join(str(int(b)) for b in row.tolist()) + fps.append(CreateFromBitString(bitstr)) + + n = len(fps) + # Build flattened upper‐triangular distance list + distances: List[float] = [] + for i in range(n): + # fmt: off + sims = BulkTanimotoSimilarity(fps[i], fps[i + 1:]) + # fmt: on + distances.extend((1.0 - np.array(sims, dtype=float)).tolist()) + + # Cluster: ClusterData(distanceList, nPts, cutoff, isDistData) + clusters = Butina.ClusterData(distances, n, cutoff, True) + return clusters + + @staticmethod + def visualize( + arr: np.ndarray, + clusters: List[List[int]], + k: Optional[int] = None, + perplexity: float = 30.0, + random_state: int = 42, + ) -> None: + """Visualize clusters in 2D via t-SNE embedding. + + :param arr: 2D array of shape (n_samples, n_features) with fingerprint data. + :type arr: np.ndarray + :param clusters: Clusters as returned by `cluster()`. + :type clusters: list of list of int + :param k: If provided, highlight only the top‑k largest clusters; others shown as 'Other'. + :type k: int or None + :param perplexity: t-SNE perplexity parameter. Defaults to 30.0. + :type perplexity: float + :param random_state: Random seed for reproducibility. Defaults to 42. + :type random_state: int + :returns: None + :rtype: NoneType + + :example: + >>> clusters = ButinaCluster.cluster(arr, cutoff=0.3) + >>> ButinaCluster.visualize(arr, clusters, k=5) + """ + n = arr.shape[0] + # assign labels: cluster idx or -1 for 'Other' + labels = np.full(n, -1, dtype=int) + # sort clusters by size + sorted_idx = sorted( + range(len(clusters)), key=lambda i: len(clusters[i]), reverse=True + ) + top = set(sorted_idx[:k]) if k is not None else set(sorted_idx) + for idx, cluster in enumerate(clusters): + for i in cluster: + labels[i] = idx if idx in top else -1 + + # compute t-SNE embedding + tsne = TSNE(n_components=2, perplexity=perplexity, random_state=random_state) + emb = tsne.fit_transform(arr) + + # plot + plt.figure(figsize=(8, 6)) + unique = sorted(set(labels)) + for lab in unique: + mask = labels == lab + if lab == -1: + plt.scatter( + emb[mask, 0], emb[mask, 1], color="gray", alpha=0.3, label="Other" + ) + else: + plt.scatter( + emb[mask, 0], emb[mask, 1], alpha=0.7, label=f"Cluster {lab}" + ) + plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left") + plt.title("t-SNE visualization of Butina clusters") + plt.xlabel("t-SNE dim 1") + plt.ylabel("t-SNE dim 2") + plt.tight_layout() + plt.show() + + def __str__(self) -> str: + """Short description of the clusterer. + + :returns: Class name. + :rtype: str + """ + return "" + + def help(self) -> None: + """Print usage summary for clustering and visualization. + + :returns: None + :rtype: NoneType + """ + print("ButinaCluster.cluster(arr, cutoff=0.2)") + print("ButinaCluster.visualize(arr, clusters, k=None, perplexity=30.0)") diff --git a/synkit/Chem/Fingerprint/fp_calculator.py b/synkit/Chem/Fingerprint/fp_calculator.py index d164b7b..dd1d56b 100644 --- a/synkit/Chem/Fingerprint/fp_calculator.py +++ b/synkit/Chem/Fingerprint/fp_calculator.py @@ -1,158 +1,155 @@ +from __future__ import annotations +from typing import Any, Dict, List from joblib import Parallel, delayed -from typing import Dict, List + from synkit.IO.debug import configure_warnings_and_logs from synkit.Chem.Fingerprint.transformation_fp import TransformationFP -# Configure warnings and logging configure_warnings_and_logs(True, True) class FPCalculator: + """Calculate fingerprint vectors for chemical reactions represented by + SMILES strings. + + :cvar fps: Shared fingerprint engine instance. + :vartype fps: TransformationFP + :cvar VALID_FP_TYPES: Supported fingerprint type identifiers. + :vartype VALID_FP_TYPES: List[str] + :param n_jobs: Number of parallel jobs to use for batch processing. + :type n_jobs: int + :param verbose: Verbosity level for parallel execution. + :type verbose: int """ - Class to calculate fingerprint vectors for chemical compounds represented by - SMILES strings. This class provides methods to process SMILES strings - into various types of fingerprint vectors, - either individually or in batches, and supports parallel processing. - - Attributes: - - smiles_key (str): Key in the dictionary corresponding to the SMILES string. - - fp_type (str): Type of fingerprint to calculate; supports various cheminformatics fingerprint types. - - n_jobs (int): Number of parallel jobs to run for performance enhancement. - - verbose (int): Verbosity level of parallel computation. - """ - - # Class-level instance to be used in static methods. - fps = TransformationFP() - def __init__( - self, - smiles_key: str, - fp_type: str, - n_jobs: int = 1, - verbose: int = 0, - ): - """ - Initialize the FPCalculator with specific settings for SMILES string processing and fingerprint generation. - - Parameters: - - smiles_key (str): The key in a dictionary corresponding to the SMILES string. - - fp_type (str): The type of fingerprint to generate. - - n_jobs (int): Number of parallel jobs. - Default is 1. - - verbose (int): Verbosity level for parallel processing. - Default is 0. + fps: TransformationFP = TransformationFP() + VALID_FP_TYPES: List[str] = [ + "drfp", + "avalon", + "maccs", + "torsion", + "pharm2D", + "ecfp2", + "ecfp4", + "ecfp6", + "fcfp2", + "fcfp4", + "fcfp6", + "rdk5", + "rdk6", + "rdk7", + "ap", + ] + + def __init__(self, n_jobs: int = 1, verbose: int = 0) -> None: + """Initialize the FPCalculator. + + :param n_jobs: Number of parallel jobs to use for fingerprint + computation. + :type n_jobs: int + :param verbose: Verbosity level for the parallel processing. + :type verbose: int """ - self.smiles_key = smiles_key - self.fp_type = fp_type self.n_jobs = n_jobs self.verbose = verbose - self._validate_fp_type(fp_type) def _validate_fp_type(self, fp_type: str) -> None: - """ - Validate if the provided fingerprint type is supported. + """Ensure the requested fingerprint type is supported. - Parameters: - - fp_type (str): The type of fingerprint to be validated. - - Raises: - ValueError: If the fingerprint type is not supported. + :param fp_type: Fingerprint type identifier to validate. + :type fp_type: str + :raises ValueError: If `fp_type` is not in VALID_FP_TYPES. """ - valid_fps = [ - "drfp", - "avalon", - "maccs", - "torsion", - "pharm2D", - "ecfp2", - "ecfp4", - "ecfp6", - "fcfp2", - "fcfp4", - "fcfp6", - "rdk5", - "rdk6", - "rdk7", - ] - if fp_type not in valid_fps: + if fp_type not in self.VALID_FP_TYPES: + valid = ", ".join(self.VALID_FP_TYPES) raise ValueError( - f"Unsupported fingerprint type '{fp_type}'. Currently supported: {', '.join(valid_fps)}." + f"Unsupported fingerprint type '{fp_type}'. Supported types: {valid}." ) @staticmethod def dict_process( - data_dict: Dict, + data_dict: Dict[str, Any], rsmi_key: str, symbol: str = ">>", - fp_type: str = "ap", + fp_type: str = "ecfp4", absolute: bool = True, - ) -> Dict: - """ - Convert a reaction SMILES string to a fingerprint vector based on the - specified fingerprint type. - - Parameters: - - data_dict (Dict): A dictionary containing reaction SMILES. - - rsmi_key (str): The key in the dictionary for the reaction SMILES. - - symbol (str): The symbol used to separate reactants and products. - Default is '>>'. - - fp_type (str): The type of fingerprint to generate. - Default is 'ap'. - - absolute (bool): Whether to use absolute values. - Default is True. - - Returns: - - Dict: The updated dictionary with the fingerprint added - under the key `fp_type`. - - Raises: - - ValueError: If an unsupported fingerprint type is specified or - the reaction SMILES key does not exist. + ) -> Dict[str, Any]: + """Compute a fingerprint for a single reaction SMILES entry and add it + to the dict. + + :param data_dict: Dictionary containing reaction data. + :type data_dict: dict + :param rsmi_key: Key in `data_dict` for the reaction SMILES string. + :type rsmi_key: str + :param symbol: Delimiter between reactant and product in the SMILES. + :type symbol: str + :param fp_type: Fingerprint type to compute. + :type fp_type: str + :param absolute: Whether to take absolute values of the fingerprint difference. + :type absolute: bool + :returns: The input dictionary with a new key `fp_{fp_type}` holding the fingerprint vector. + :rtype: dict + :raises ValueError: If `rsmi_key` is missing in `data_dict`. """ if rsmi_key not in data_dict: - raise ValueError(f"Key '{rsmi_key}' does not exist in the dictionary.") - data_dict[fp_type] = FPCalculator.fps.fit( + raise ValueError(f"Key '{rsmi_key}' not found in data dictionary.") + # compute and insert fingerprint + vec = FPCalculator.fps.fit( data_dict[rsmi_key], symbols=symbol, fp_type=fp_type, abs=absolute ) + data_dict[f"{fp_type}"] = vec return data_dict def parallel_process( self, - data_dicts: List[Dict], + data_dicts: List[Dict[str, Any]], rsmi_key: str, symbol: str = ">>", - fp_type: str = "ap", + fp_type: str = "ecfp4", absolute: bool = True, - ) -> List[Dict]: - """ - Convert a list of SMILES strings to fingerprint vectors in parallel - based on the specified fingerprint type. This method processes - multiple dictionaries containing SMILES strings simultaneously - using multiple workers. - - Parameters: - - data_dicts (List[Dict]): A list of dictionaries, each containing reaction data. - - rsmi_key (str): The key to access the reaction SMILES in each dictionary. - - symbol (str): The symbol used to separate reactants and products. - Default is '>>'. - - fp_type (str): The type of fingerprint to generate. - Default is 'ap'. - - absolute (bool): Whether to use absolute values. - Default is True. - - Returns: - - List[Dict]: A list of dictionaries with updated fingerprint data, - where each dictionary includes a fingerprint vector. - - Raises: - - ValueError: If an unsupported fingerprint type is specified or the - reaction SMILES key does not exist in any dictionary. + ) -> List[Dict[str, Any]]: + """Compute fingerprints for a batch of reaction dictionaries in + parallel. + + :param data_dicts: List of dictionaries, each containing a reaction SMILES. + :type data_dicts: list of dict + :param rsmi_key: Key in each dict for the reaction SMILES string. + :type rsmi_key: str + :param symbol: Delimiter between reactant and product in the SMILES. + :type symbol: str + :param fp_type: Fingerprint type to compute. + :type fp_type: str + :param absolute: Whether to take absolute values of the fingerprint difference. + :type absolute: bool + :returns: A list of dictionaries augmented with `fp_{fp_type}` entries. + :rtype: list of dict + :raises ValueError: If `fp_type` is unsupported or any dict is missing `rsmi_key`. """ + # Validate fingerprint type once + self._validate_fp_type(fp_type) + # Process in parallel results = Parallel(n_jobs=self.n_jobs, verbose=self.verbose)( - delayed(FPCalculator.safe_dict_process)( - data_dict, rsmi_key, symbol, fp_type, absolute - ) - for data_dict in data_dicts + delayed(self.dict_process)(dd, rsmi_key, symbol, fp_type, absolute) + for dd in data_dicts ) return results + + def __str__(self) -> str: + """Short string summarizing the calculator configuration. + + :returns: A summary of n_jobs and verbosity. + :rtype: str + """ + return f"" + + def help(self) -> None: + """Print details about supported fingerprint types and usage. + + :returns: None + :rtype: NoneType + """ + print("FPCalculator supports the following fingerprint types:") + for t in self.VALID_FP_TYPES: + print(" -", t) + print(f"Configured for {self.n_jobs} parallel jobs, verbose={self.verbose}") diff --git a/synkit/Chem/Fingerprint/smiles_featurizer.py b/synkit/Chem/Fingerprint/smiles_featurizer.py index 2c450a5..93fe024 100644 --- a/synkit/Chem/Fingerprint/smiles_featurizer.py +++ b/synkit/Chem/Fingerprint/smiles_featurizer.py @@ -1,3 +1,24 @@ +"""smiles_featurizer.py +======================= +Utility for converting SMILES strings into various cheminformatics fingerprints, +with optional NumPy‐array conversion. + +Key features +------------ +* **Multi‐fingerprint support** – MACCS, Avalon, ECFP/FCFP, RDKit, AtomPair, Torsion, Pharm2D +* **SMILES validation** – raises on invalid input +* **Array conversion** – output as NumPy arrays for ML pipelines +* **Extensible** – add new methods or override via subclassing + +Quick start +----------- +>>> from synkit.Chem.Fingerprint.smiles_featurizer import SmilesFeaturizer +>>> arr = SmilesFeaturizer.featurize_smiles("CCO", "ecfp4", convert_to_array=True) +""" + +from __future__ import annotations +from typing import Any + import numpy as np from rdkit import Chem, DataStructs from rdkit.Chem import AllChem, MACCSkeys @@ -7,71 +28,86 @@ class SmilesFeaturizer: - def __init__(self): - """ - Initializes the SmilesFeaturizer class without any specific parameters for fingerprint generation. + """Convert SMILES strings into chemical fingerprint vectors. + + :cvar None: This class only provides static/​class methods and holds no state. + + Supported fingerprint methods: + - MACCS keys + - Avalon + - ECFP/FCFP (Morgan) + - RDKit topological + - AtomPair + - Torsion + - 2D Pharmacophore + + Use `featurize_smiles` for one‑line access. + """ + + def __init__(self) -> None: + """Initialize SmilesFeaturizer. + + This class has no instance state; all methods are static or + class‑level. """ pass @staticmethod def smiles_to_mol(smiles: str) -> Chem.Mol: - """ - Converts a SMILES string to an RDKit Mol object. - - Parameters: - - smiles (str): The SMILES string to be converted. + """Convert a SMILES string to an RDKit Mol object. - Returns: - - Chem.Mol: The corresponding RDKit Mol object. + :param smiles: The SMILES string to convert. + :type smiles: str + :returns: RDKit Mol object corresponding to the SMILES. + :rtype: Chem.Mol + :raises ValueError: If the SMILES string is invalid. """ mol = Chem.MolFromSmiles(smiles) if mol is None: - raise ValueError("Invalid SMILES string provided.") + raise ValueError(f"Invalid SMILES string: {smiles!r}") return mol @staticmethod - def get_maccs_keys(mol: Chem.Mol): - """ - Generates MACCS keys fingerprint from an RDKit Mol object. + def get_maccs_keys(mol: Chem.Mol) -> Any: + """Generate the MACCS keys fingerprint for a molecule. - Parameters: - - mol (Chem.Mol): The Mol object to be featurized. - - Returns: - - RDKit ExplicitBitVect: The MACCS keys fingerprint of the Mol object. + :param mol: RDKit Mol object. + :type mol: Chem.Mol + :returns: MACCS keys fingerprint bit vector. + :rtype: ExplicitBitVect """ return MACCSkeys.GenMACCSKeys(mol) @staticmethod - def get_avalon_fp(mol: Chem.Mol, nBits: int = 1024): - """ - Generates Avalon fingerprint from an RDKit Mol object. + def get_avalon_fp(mol: Chem.Mol, nBits: int = 1024) -> Any: + """Generate the Avalon fingerprint for a molecule. - Parameters: - - mol (Chem.Mol): The Mol object to be featurized. - - nBits (int): The number of bits in the generated fingerprint. - - Returns: - - RDKit ExplicitBitVect: The Avalon fingerprint of the Mol object. + :param mol: RDKit Mol object. + :type mol: Chem.Mol + :param nBits: Length of the fingerprint vector. + :type nBits: int + :returns: Avalon fingerprint bit vector. + :rtype: ExplicitBitVect """ return fpAvalon.GetAvalonFP(mol, nBits) @staticmethod def get_ecfp( mol: Chem.Mol, radius: int, nBits: int = 2048, useFeatures: bool = False - ): - """ - Generates Extended-Connectivity Fingerprints (ECFP) or - Feature-Class Fingerprints (FCFP) from an RDKit Mol object. - - Parameters: - - mol (Chem.Mol): The Mol object to be featurized. - - radius (int): The radius of the fingerprint. - - nBits (int): The number of bits in the generated fingerprint. - - useFeatures (bool): Whether to use atom features instead of atom identities. + ) -> Any: + """Generate a Morgan fingerprint (ECFP or FCFP) for a molecule. - Returns: - - RDKit ExplicitBitVect: The ECFP or FCFP fingerprint of the Mol object. + :param mol: RDKit Mol object. + :type mol: Chem.Mol + :param radius: Radius for the Morgan algorithm. + :type radius: int + :param nBits: Length of the fingerprint vector. + :type nBits: int + :param useFeatures: If True, generate a Feature‑Class + fingerprint (FCFP). + :type useFeatures: bool + :returns: Morgan fingerprint bit vector. + :rtype: ExplicitBitVect """ return AllChem.GetMorganFingerprintAsBitVect( mol, radius, nBits=nBits, useFeatures=useFeatures @@ -80,106 +116,143 @@ def get_ecfp( @staticmethod def get_rdk_fp( mol: Chem.Mol, maxPath: int, fpSize: int = 2048, nBitsPerHash: int = 2 - ): - """ - Generates RDKit fingerprint from an RDKit Mol object. - - Parameters: - - mol (Chem.Mol): The Mol object to be featurized. - - maxPath (int): The maximum path length (in bonds) to be included. - - fpSize (int): The size of the fingerprint. - - nBitsPerHash (int): The number of bits per hash. + ) -> Any: + """Generate an RDKit topological fingerprint for a molecule. - Returns: - - RDKit ExplicitBitVect: The RDKit fingerprint of the Mol object. + :param mol: RDKit Mol object. + :type mol: Chem.Mol + :param maxPath: Maximum path length (bonds) to include. + :type maxPath: int + :param fpSize: Length of the fingerprint vector. + :type fpSize: int + :param nBitsPerHash: Bits per hash for path hashing. + :type nBitsPerHash: int + :returns: RDKit topological fingerprint bit vector. + :rtype: ExplicitBitVect """ return Chem.RDKFingerprint( mol, maxPath=maxPath, fpSize=fpSize, nBitsPerHash=nBitsPerHash ) @staticmethod - def mol_to_ap(mol: Chem.Mol) -> np.ndarray: - """ - Generates an Atom Pair fingerprint as a NumPy array from an RDKit Mol object. - - Parameters: - - mol (Chem.Mol): The Mol object to be featurized. + def mol_to_ap(mol: Chem.Mol) -> Any: + """Generate an Atom Pair fingerprint for a molecule. - Returns: - - RDKit ExplicitBitVect: The RDKit fingerprint of the Mol object. + :param mol: RDKit Mol object. + :type mol: Chem.Mol + :returns: Atom Pair fingerprint as an integer vector. + :rtype: ExplicitBitVect """ return Pairs.GetAtomPairFingerprint(mol) @staticmethod - def mol_to_torsion(mol: Chem.Mol) -> np.ndarray: - """ - Generates a Topological Torsion fingerprint as a NumPy array from an RDKit Mol object. + def mol_to_torsion(mol: Chem.Mol) -> Any: + """Generate a Topological Torsion fingerprint for a molecule. - Parameters: - - mol (Chem.Mol): The Mol object to be featurized. - - Returns: - - RDKit ExplicitBitVect: The RDKit fingerprint of the Mol object. + :param mol: RDKit Mol object. + :type mol: Chem.Mol + :returns: Torsion fingerprint as an integer vector. + :rtype: ExplicitBitVect """ return Torsions.GetTopologicalTorsionFingerprintAsIntVect(mol) @staticmethod - def mol_to_pharm2d(mol: Chem.Mol) -> np.ndarray: - """ - Generates a 2D Pharmacophore fingerprint as a NumPy array from an RDKit Mol object. + def mol_to_pharm2d(mol: Chem.Mol) -> Any: + """Generate a 2D Pharmacophore fingerprint for a molecule. - Parameters: - - mol (Chem.Mol): The Mol object to be featurized. - - Returns: - - RDKit ExplicitBitVect: The RDKit fingerprint of the Mol object. + :param mol: RDKit Mol object. + :type mol: Chem.Mol + :returns: 2D pharmacophore fingerprint bit vector. + :rtype: ExplicitBitVect """ return Generate.Gen2DFingerprint(mol, Gobbi_Pharm2D.factory) @classmethod def featurize_smiles( - cls, smiles: str, fingerprint_type: str, convert_to_array: bool = True, **kwargs - ) -> np.ndarray: - """ - Featurizes a SMILES string into the specified type of fingerprint, optionally converting it to a NumPy array. - - Parameters: - - smiles (str): The SMILES string to be featurized. - - fingerprint_type (str): The type of fingerprint to generate. - - convert_to_array (bool): Whether to convert the fingerprint to a NumPy array. Defaults to True. - - **kwargs: Additional keyword arguments for the fingerprint function. + cls, + smiles: str, + fingerprint_type: str, + convert_to_array: bool = True, + **kwargs: Any, + ) -> Any: + """Featurize a SMILES string into a chosen fingerprint, optionally + converting to a NumPy array. - Returns: - - np.ndarray or RDKit ExplicitBitVect: The requested type of fingerprint for the SMILES string, - either as a NumPy array or as an RDKit bit vector, depending on `convert_to_array`. + :param smiles: The SMILES string to featurize. + :type smiles: str + :param fingerprint_type: One of 'maccs', 'avalon', 'ecfp#', 'fcfp#', + 'rdk#', 'ap', 'torsion', 'pharm2d'. + :type fingerprint_type: str + :param convert_to_array: If True, convert the result to a NumPy array. + :type convert_to_array: bool + :param kwargs: Additional parameters passed to the chosen method: + - `nBits` for Avalon/ECFP/FCFP + - `radius` for ECFP/FCFP + - `maxPath`, `fpSize`, `nBitsPerHash` for RDKit FP + :type kwargs: dict + :returns: Fingerprint as a NumPy array (if `convert_to_array`) or RDKit bit vector. + :rtype: np.ndarray or ExplicitBitVect + :raises ValueError: If `fingerprint_type` is unsupported. """ mol = cls.smiles_to_mol(smiles) - if fingerprint_type == "maccs": + + ft = fingerprint_type.lower() + if ft == "maccs": fp = cls.get_maccs_keys(mol) - elif fingerprint_type == "avalon": - fp = cls.get_avalon_fp(mol, **kwargs) - elif fingerprint_type.startswith("ecfp") or fingerprint_type.startswith("fcfp"): - radius = int(fingerprint_type[4]) - useFeatures = fingerprint_type.startswith("fcfp") - nBits = kwargs.get("nBits", 2048) - fp = cls.get_ecfp(mol, radius, nBits=nBits, useFeatures=useFeatures) - elif fingerprint_type.startswith("rdk"): - maxPath = int(fingerprint_type[3]) - fp = cls.get_rdk_fp(mol, maxPath, **kwargs) - elif fingerprint_type == "ap": + elif ft == "avalon": + fp = cls.get_avalon_fp(mol, nBits=kwargs.get("nBits", 1024)) + elif ft.startswith("ecfp") or ft.startswith("fcfp"): + radius = int(ft[4]) + use_features = ft.startswith("fcfp") + fp = cls.get_ecfp( + mol, + radius, + nBits=kwargs.get("nBits", 2048), + useFeatures=use_features, + ) + elif ft.startswith("rdk"): + max_path = int(ft[3]) + fp = cls.get_rdk_fp( + mol, + maxPath=max_path, + fpSize=kwargs.get("fpSize", 2048), + nBitsPerHash=kwargs.get("nBitsPerHash", 2), + ) + elif ft == "ap": fp = cls.mol_to_ap(mol) - elif fingerprint_type == "torsion": + elif ft == "torsion": fp = cls.mol_to_torsion(mol) - elif fingerprint_type == "pharm2d": + elif ft == "pharm2d": fp = cls.mol_to_pharm2d(mol) else: - raise ValueError(f"Unsupported fingerprint type: {fingerprint_type}") + raise ValueError(f"Unsupported fingerprint type: {fingerprint_type!r}") + if convert_to_array: - if fingerprint_type == "pharm2d": - return np.frombuffer(fp.ToBitString().encode(), "u1") - ord("0") - else: - ar = np.zeros((1,), dtype=np.int8) - DataStructs.ConvertToNumpyArray(fp, ar) - return ar - else: - return fp + if ft == "pharm2d": + bitstr = fp.ToBitString() + return np.array([int(b) for b in bitstr], dtype=np.int8) + arr = np.zeros((fp.GetNumBits(),), dtype=np.int8) + DataStructs.ConvertToNumpyArray(fp, arr) + return arr + + return fp + + def __str__(self) -> str: + """Short description of the featurizer. + + :returns: Class name. + :rtype: str + """ + return "" + + def help(self) -> None: + """Print supported fingerprint types and usage summary. + + :returns: None + :rtype: NoneType + """ + print("SmilesFeaturizer supports the following fingerprint types:") + print(" - maccs, avalon, ecfp#, fcfp#, rdk#, ap, torsion, pharm2d") + print( + "Usage: SmilesFeaturizer.featurize_smiles(smiles, fingerprint_type, **kwargs)" + ) diff --git a/synkit/Chem/Fingerprint/transformation_fp.py b/synkit/Chem/Fingerprint/transformation_fp.py index b05eded..991b0f7 100644 --- a/synkit/Chem/Fingerprint/transformation_fp.py +++ b/synkit/Chem/Fingerprint/transformation_fp.py @@ -1,39 +1,53 @@ +"""transformation_fp.py +======================= +Compute reaction‐level fingerprints by combining molecular fingerprints +of reactants and products, with optional absolute mode and bit‐vector conversion. + +Quick start +----------- +>>> from synkit.Chem.Fingerprint.transformation_fp import TransformationFP +>>> arr = TransformationFP().fit('CCO>>CC=O', symbols='>>', fp_type='ecfp4', abs=True) +>>> bv = TransformationFP().fit('CCO>>CC=O', symbols='>>', fp_type='ecfp4', abs=True, return_array=False) +""" + +from __future__ import annotations +from typing import Any, Union + import numpy as np -from typing import Union, Any from rdkit.DataStructs import cDataStructs + from synkit.Chem.Fingerprint.smiles_featurizer import SmilesFeaturizer class TransformationFP: - """ - A class for handling the transformation of chemical reactions into reaction fingerprints - based on SMILES strings. + """Calculate reaction fingerprints by featurizing individual molecules and + combining them via vector subtraction. + + :cvar None: Stateless utility class. """ def __init__(self) -> None: - """ - Initializes the TransformationFP object. Currently, this constructor does not - perform any operations. + """Initialize TransformationFP. + + This class has no instance state; all methods are static or + class‐level. """ pass @staticmethod def convert_arr2vec(arr: np.ndarray) -> cDataStructs.ExplicitBitVect: - """ - Converts a numpy array to a RDKit ExplicitBitVect. + """Convert a NumPy array of bits into an RDKit ExplicitBitVect. - Parameters: - - arr (np.ndarray): The input array. - - Returns: - - cDataStructs.ExplicitBitVect: The converted bit vector. + :param arr: Array of 0/1 values representing a fingerprint. + :type arr: np.ndarray + :returns: RDKit bit vector constructed from the bit string. + :rtype: cDataStructs.ExplicitBitVect """ - arr_tostring = "".join(arr.astype(str)) - EBitVect = cDataStructs.CreateFromBitString(arr_tostring) - return EBitVect + bitstr = "".join(str(int(x)) for x in arr.flatten()) + return cDataStructs.CreateFromBitString(bitstr) - @staticmethod def fit( + self, reaction_smiles: str, symbols: str, fp_type: str, @@ -41,39 +55,81 @@ def fit( return_array: bool = True, **kwargs: Any, ) -> Union[np.ndarray, cDataStructs.ExplicitBitVect]: + """Generate a reaction fingerprint by subtracting reactant from product + fingerprints. + + :param reaction_smiles: Reaction SMILES, reactant and product separated by `symbols`. + :type reaction_smiles: str + :param symbols: Delimiter between reactants and products in the SMILES string. + :type symbols: str + :param fp_type: Fingerprint type to use for individual molecules (e.g., 'ecfp4'). + :type fp_type: str + :param abs: If True, take absolute value of the difference vector. + :type abs: bool + :param return_array: If True, return a NumPy array; otherwise convert to an RDKit bit vector. + :type return_array: bool + :param kwargs: Additional keyword arguments passed to `SmilesFeaturizer.featurize_smiles`. + :type kwargs: Any + :returns: Reaction fingerprint as a NumPy array or RDKit bit vector. + :rtype: Union[np.ndarray, cDataStructs.ExplicitBitVect] + :raises ValueError: If `reaction_smiles` is not correctly formatted. """ - Generates a reaction fingerprint for a given reaction represented by a SMILES string. - - Parameters: - - reaction_smiles (str): The SMILES string of the reaction, separated by `symbols`. - - symbols (str): The symbol used to separate reactants and products in the SMILES string. - - fp_type (str): The type of fingerprint to generate (e.g., 'maccs', 'ecfp'). - - abs (bool): Whether to take the absolute value of the reaction fingerprint difference. - - return_array (bool): Whether to return the reaction fingerprint as a numpy array or as a bit vector. - - Returns: - - Union[np.ndarray, cDataStructs.ExplicitBitVect]: The reaction fingerprint either as an array - or a bit vector, depending on the value of `return_array`. - """ - react, prod = reaction_smiles.split(symbols) - react_fps = None - for s in react.split("."): - if react_fps is None: - react_fps = SmilesFeaturizer.featurize_smiles(s, fp_type, **kwargs) - else: - react_fps += SmilesFeaturizer.featurize_smiles(s, fp_type, **kwargs) - - prod_fps = None - for s in prod.split("."): - if prod_fps is None: - prod_fps = SmilesFeaturizer.featurize_smiles(s, fp_type, **kwargs) - else: - prod_fps += SmilesFeaturizer.featurize_smiles(s, fp_type, **kwargs) - - reaction_fp = np.subtract(prod_fps, react_fps) + if symbols not in reaction_smiles: + raise ValueError(f"Reaction SMILES must contain separator '{symbols}'") + react_part, prod_part = reaction_smiles.split(symbols) + + def sum_fps(parts: list[str]) -> np.ndarray: + total = None + for smi in parts: + vec = SmilesFeaturizer.featurize_smiles(smi, fp_type, **kwargs) + if total is None: + total = vec.copy() if isinstance(vec, np.ndarray) else vec + else: + total = total + vec # type: ignore + return total # type: ignore + + react_vec = sum_fps(react_part.split(".")) + prod_vec = sum_fps(prod_part.split(".")) + + diff = prod_vec - react_vec # type: ignore if abs: - reaction_fp = np.abs(reaction_fp) + diff = np.abs(diff) + if return_array: - return reaction_fp - else: - return TransformationFP.convert_arr2vec(reaction_fp) + return diff # type: ignore + return TransformationFP.convert_arr2vec(diff) # type: ignore + + def help(self) -> None: + """Print usage summary for the TransformationFP class. + + :returns: None + :rtype: NoneType + """ + print("TransformationFP: compute reaction fingerprints via vector subtraction.") + print( + " fit(reaction_smiles, symbols, fp_type, abs, return_array=True, **kwargs)" + ) + print(" reaction_smiles: 'R1.R2>>P1.P2' SMILES string") + print(" symbols: separator between reactants and products (e.g. '>>')") + print( + " fp_type: one of 'maccs', 'avalon', 'ecfp#', 'fcfp#', 'rdk#', 'ap', 'torsion', 'pharm2d'" + ) + print(" abs: take absolute difference (True/False)") + print(" return_array: return NumPy array (True) or RDKit bit vector (False)") + print(" convert_arr2vec(arr: np.ndarray) -> ExplicitBitVect") + print("Example:") + print(" tfp = TransformationFP()") + print(" arr = tfp.fit('CCO>>CC=O', '>>', 'ecfp4', abs=True)") + print( + " bv = tfp.fit('CCO>>CC=O', '>>', 'ecfp4', abs=True, return_array=False)" + ) + + def __str__(self) -> str: + """Short description of the transformer. + + :returns: Class name. + :rtype: str + """ + return "" + + __repr__ = __str__ diff --git a/synkit/Chem/Molecule/standardize.py b/synkit/Chem/Molecule/standardize.py index b8d9f88..7a8b6ae 100644 --- a/synkit/Chem/Molecule/standardize.py +++ b/synkit/Chem/Molecule/standardize.py @@ -5,148 +5,125 @@ from typing import Optional -def sanitize_and_canonicalize_smiles(smiles: str) -> str | None: - """ - Sanitize and canonicalize a SMILES string using RDKit. - - Parameters - ---------- - smiles : str - Input SMILES string. +def sanitize_and_canonicalize_smiles(smiles: str) -> Optional[str]: + """Sanitize and canonicalize a SMILES string. - Returns - ------- - str or None - Canonical SMILES if valid and sanitizable, else None. + :param smiles: Input SMILES string. + :type smiles: str + :returns: Canonical SMILES if valid, otherwise None. + :rtype: Optional[str] """ try: mol = Chem.MolFromSmiles(smiles, sanitize=True) if mol is None: return None - Chem.SanitizeMol(mol) # additional safety + Chem.SanitizeMol(mol) return Chem.MolToSmiles(mol, canonical=True) except Exception: return None def normalize_molecule(mol: Chem.Mol) -> Chem.Mol: - """ - Normalize a molecule using RDKit's Normalizer. + """Normalize a molecule using RDKit's Normalizer. - Parameters: - - mol (Chem.Mol): RDKit Mol object to be normalized. - - Returns: - - Chem.Mol: Normalized RDKit Mol object. + :param mol: RDKit Mol object to normalize. + :type mol: Chem.Mol + :returns: Normalized RDKit Mol object. + :rtype: Chem.Mol """ normalizer = rdMolStandardize.Normalizer() return normalizer.normalize(mol) def canonicalize_tautomer(mol: Chem.Mol) -> Chem.Mol: - """ - Canonicalize the tautomer of a molecule using RDKit's TautomerCanonicalizer. - - Parameters: - - mol (Chem.Mol): RDKit Mol object. + """Canonicalize the tautomeric form of a molecule. - Returns: - - Chem.Mol: Mol object with canonicalized tautomer. + :param mol: RDKit Mol object to canonicalize. + :type mol: Chem.Mol + :returns: Mol object with a canonical tautomer. + :rtype: Chem.Mol """ - tautomer_canonicalizer = rdMolStandardize.TautomerEnumerator() - return tautomer_canonicalizer.Canonicalize(mol) + tautomer_enumerator = rdMolStandardize.TautomerEnumerator() + return tautomer_enumerator.Canonicalize(mol) def salts_remover(mol: Chem.Mol) -> Chem.Mol: - """ - Remove salt fragments from a molecule using RDKit's SaltRemover. - - Parameters: - - mol (Chem.Mol): RDKit Mol object. + """Remove salt fragments from a molecule. - Returns: - - Chem.Mol: Mol object with salts removed. + :param mol: RDKit Mol object to process. + :type mol: Chem.Mol + :returns: Mol object with salts removed. + :rtype: Chem.Mol """ remover = SaltRemover() return remover.StripMol(mol) def uncharge_molecule(mol: Chem.Mol) -> Chem.Mol: - """ - Neutralize a molecule by removing counter-ions using RDKit's Uncharger. + """Neutralize a molecule by removing charges. - Parameters: - - mol (Chem.Mol): RDKit Mol object. - - Returns: - - Chem.Mol: Neutralized Mol object. + :param mol: RDKit Mol object to neutralize. + :type mol: Chem.Mol + :returns: Neutralized Mol object. + :rtype: Chem.Mol """ uncharger = rdMolStandardize.Uncharger() return uncharger.uncharge(mol) -def fragments_remover(mol: Chem.Mol) -> Chem.Mol: - """ - Remove small fragments from a molecule, keeping only the largest one. - - Parameters: - - mol (Chem.Mol): RDKit Mol object. +def fragments_remover(mol: Chem.Mol) -> Optional[Chem.Mol]: + """Keep only the largest fragment of a molecule. - Returns: - - Chem.Mol: Mol object with small fragments removed. + :param mol: RDKit Mol object to fragment. + :type mol: Chem.Mol + :returns: Mol object of the largest fragment, or None if input is + empty. + :rtype: Optional[Chem.Mol] """ frags = Chem.GetMolFrags(mol, asMols=True, sanitizeFrags=True) return max(frags, default=None, key=lambda m: m.GetNumAtoms()) def remove_explicit_hydrogens(mol: Chem.Mol) -> Chem.Mol: - """ - Remove explicit hydrogens from a molecule to leave only the heavy atoms. - - Parameters: - - mol (Chem.Mol): RDKit Mol object. + """Remove all explicit hydrogens from a molecule. - Returns: - - Chem.Mol: Mol object with explicit hydrogens removed. + :param mol: RDKit Mol object to process. + :type mol: Chem.Mol + :returns: Mol object without explicit hydrogens. + :rtype: Chem.Mol """ return Chem.RemoveHs(mol) def remove_radicals_and_add_hydrogens( - mol: Chem.Mol, removeH=True + mol: Chem.Mol, removeH: bool = True ) -> Optional[Chem.Mol]: - """ - Remove radicals from a molecule by setting radical electrons to zero and adding hydrogens where needed. + """Replace radical electrons by hydrogens and optionally remove explicit H. - Parameters: - - mol (Chem.Mol): RDKit Mol object. - - Returns: - - Chem.Mol: Mol object with radicals removed and necessary hydrogens added. + :param mol: RDKit Mol object with possible radicals. + :type mol: Chem.Mol + :param removeH: If True, remove explicit hydrogens after addition. + :type removeH: bool + :returns: Mol object with radicals neutralized and hydrogens + adjusted. + :rtype: Optional[Chem.Mol] """ - # mol = Chem.RemoveHs(mol) # Remove explicit hydrogens first for atom in mol.GetAtoms(): - if atom.GetNumRadicalElectrons() > 0: - atom.SetNumExplicitHs( - atom.GetNumExplicitHs() + atom.GetNumRadicalElectrons() - ) - atom.SetNumRadicalElectrons(0) - mol = rdmolops.AddHs(mol) # Add hydrogens back - if removeH: - return remove_explicit_hydrogens(mol) - else: - return mol + rad = atom.GetNumRadicalElectrons() + if rad > 0: + atom.SetNumExplicitHs(atom.GetNumExplicitHs() + rad) + atom.SetNumRadicalElectrons(0) + mol = rdmolops.AddHs(mol) + return remove_explicit_hydrogens(mol) if removeH else mol def remove_isotopes(mol: Chem.Mol) -> Chem.Mol: - """ - Remove isotopic information from a molecule. - - Parameters: - - mol (Chem.Mol): RDKit Mol object. + """Remove all isotope labels from a molecule. - Returns: - - Chem.Mol: Mol object with isotopes removed. + :param mol: RDKit Mol object to process. + :type mol: Chem.Mol + :returns: Mol object with isotopes cleared. + :rtype: Chem.Mol """ for atom in mol.GetAtoms(): atom.SetIsotope(0) @@ -154,41 +131,37 @@ def remove_isotopes(mol: Chem.Mol) -> Chem.Mol: def clear_stereochemistry(mol: Chem.Mol) -> Chem.Mol: - """ - Clear all stereochemical information from a molecule. - - Parameters: - - mol (Chem.Mol): RDKit Mol object. + """Remove stereochemical annotations from a molecule. - Returns: - - Chem.Mol: Mol object with stereochemistry cleared. + :param mol: RDKit Mol object to process. + :type mol: Chem.Mol + :returns: Mol object with stereochemistry removed. + :rtype: Chem.Mol """ Chem.RemoveStereochemistry(mol) return mol -def fix_radical_rsmi(rsmi: str, removeH=True) -> str: - """ - Takes a reaction SMILES string with potential radicals and returns a new reaction SMILES string - where all radicals have been replaced by adding hydrogen atoms. +def fix_radical_rsmi(rsmi: str, removeH: bool = True) -> str: + """Fix radicals in a reaction SMILES by converting them to hydrogens. - Parameters: - - rsmi (str): A reaction SMILES string containing reactants and products. - - Returns: - - str: A reaction SMILES string with radicals replaced by hydrogen atoms. + :param rsmi: Reaction SMILES string, format 'reactant>>product'. + :type rsmi: str + :param removeH: If True, remove explicit hydrogens after addition. + :type removeH: bool + :returns: Corrected reaction SMILES with radicals replaced. + :rtype: str """ - r, p = rsmi.split(">>") - r_mol = Chem.MolFromSmiles(r, sanitize=False) - p_mol = Chem.MolFromSmiles(p, sanitize=False) + react_smiles, prod_smiles = rsmi.split(">>") + r_mol = Chem.MolFromSmiles(react_smiles, sanitize=False) + p_mol = Chem.MolFromSmiles(prod_smiles, sanitize=False) Chem.SanitizeMol(r_mol) Chem.SanitizeMol(p_mol) - if r_mol is not None and p_mol is not None: - r_mol = remove_radicals_and_add_hydrogens(r_mol, removeH) - p_mol = remove_radicals_and_add_hydrogens(p_mol, removeH) - - r_smiles = Chem.MolToSmiles(r_mol) if r_mol else r - p_smiles = Chem.MolToSmiles(p_mol) if p_mol else p - return f"{r_smiles}>>{p_smiles}" - else: - return f"{r}>>{p}" # + + if r_mol and p_mol: + r_fixed = remove_radicals_and_add_hydrogens(r_mol, removeH) + p_fixed = remove_radicals_and_add_hydrogens(p_mol, removeH) + r_out = Chem.MolToSmiles(r_fixed) if r_fixed else react_smiles + p_out = Chem.MolToSmiles(p_fixed) if p_fixed else prod_smiles + return f"{r_out}>>{p_out}" + return rsmi diff --git a/synkit/Chem/Reaction/__init__.py b/synkit/Chem/Reaction/__init__.py index e168770..db24749 100644 --- a/synkit/Chem/Reaction/__init__.py +++ b/synkit/Chem/Reaction/__init__.py @@ -1,4 +1,9 @@ from .aam_validator import AAMValidator from .standardize import Standardize from .canon_rsmi import CanonRSMI -from .rsmi_utils import * + +__all__ = [ + "AAMValidator", + "Standardize", + "CanonRSMI", +] diff --git a/synkit/Chem/Reaction/aam_utils.py b/synkit/Chem/Reaction/aam_utils.py deleted file mode 100644 index 2866f1c..0000000 --- a/synkit/Chem/Reaction/aam_utils.py +++ /dev/null @@ -1,97 +0,0 @@ -import re -from rdkit import Chem -from rdkit.Chem.MolStandardize import rdMolStandardize - -from typing import Optional, List - - -def enumerate_tautomers(reaction_smiles: str) -> Optional[List[str]]: - """ - Enumerates possible tautomers for reactants while canonicalizing the products in a - reaction SMILES string. This function first splits the reaction SMILES string into - reactants and products. It then generates all possible tautomers for the reactants and - canonicalizes the product molecule. The function returns a list of reaction SMILES - strings for each tautomer of the reactants combined with the canonical product. - - Parameters: - - reaction_smiles (str): A SMILES string of the reaction formatted as - 'reactants>>products'. - - Returns: - - List[str] | None: A list of SMILES strings for the reaction, with each string - representing a different - - tautomer of the reactants combined with the canonicalized products. Returns None if - an error occurs or if invalid SMILES strings are provided. - - Raises: - - ValueError: If the provided SMILES strings cannot be converted to molecule objects, - indicating invalid input. - """ - try: - # Split the input reaction SMILES string into reactants and products - reactants_smiles, products_smiles = reaction_smiles.split(">>") - - # Convert SMILES strings to molecule objects - reactants_mol = Chem.MolFromSmiles(reactants_smiles) - products_mol = Chem.MolFromSmiles(products_smiles) - - if reactants_mol is None or products_mol is None: - raise ValueError( - "Invalid SMILES string provided for reactants or products." - ) - - # Initialize tautomer enumerator - - enumerator = rdMolStandardize.TautomerEnumerator() - - # Enumerate tautomers for the reactants and canonicalize the products - try: - reactants_can = enumerator.Enumerate(reactants_mol) - except Exception as e: - print(f"An error occurred: {e}") - reactants_can = [reactants_mol] - products_can = products_mol - - # Convert molecule objects back to SMILES strings - reactants_can_smiles = [Chem.MolToSmiles(i) for i in reactants_can] - products_can_smiles = Chem.MolToSmiles(products_can) - - # Combine each reactant tautomer with the canonical product in SMILES format - rsmi_list = [i + ">>" + products_can_smiles for i in reactants_can_smiles] - if len(rsmi_list) == 0: - return [reaction_smiles] - else: - # rsmi_list.remove(reaction_smiles) - rsmi_list.insert(0, reaction_smiles) - return rsmi_list - - except Exception as e: - print(f"An error occurred: {e}") - return [reaction_smiles] - - -def mapping_success_rate(list_mapping_data): - """ - Calculate the success rate of entries containing atom mappings in a list of data - strings. - - Parameters: - - list_mapping_in_data (list of str): List containing strings to be searched for atom - mappings. - - Returns: - - float: The success rate of finding atom mappings in the list as a percentage. - - Raises: - - ValueError: If the input list is empty. - """ - atom_map_pattern = re.compile(r":\d+") - if not list_mapping_data: - raise ValueError("The input list is empty, cannot calculate success rate.") - - success = sum( - 1 for entry in list_mapping_data if re.search(atom_map_pattern, entry) - ) - rate = 100 * (success / len(list_mapping_data)) - - return round(rate, 2) diff --git a/synkit/Chem/Reaction/aam_validator.py b/synkit/Chem/Reaction/aam_validator.py index f48cd53..6662cee 100644 --- a/synkit/Chem/Reaction/aam_validator.py +++ b/synkit/Chem/Reaction/aam_validator.py @@ -9,30 +9,43 @@ from synkit.IO.chem_converter import rsmi_to_graph from synkit.Graph.ITS.its_decompose import get_rc from synkit.Graph.ITS.its_construction import ITSConstruction -from .aam_utils import enumerate_tautomers, mapping_success_rate +from synkit.Chem.utils import enumerate_tautomers, mapping_success_rate class AAMValidator: - """ - A utility class for validating atom‐atom mappings (AAM) in reaction SMILES. + """A utility class for validating atom‐atom mappings (AAM) in reaction + SMILES. Provides methods to compare mapped SMILES against ground truth by using reaction‐center (RC) or ITS‐graph isomorphism checks, including tautomer enumeration support and batch validation over tabular data. + + Quick start + ----------- + >>> from synkit.Chem.Reaction import AAMValidator + >>> validator = AAMValidator() + >>> rsmi_1 = ( + '[CH3:1][C:2](=[O:3])[OH:4].[CH3:5][OH:6]' + '>>' + '[CH3:1][C:2](=[O:3])[O:6][CH3:5].[OH2:4]') + >>> rsmi_2 = ( + '[CH3:5][C:1](=[O:2])[OH:3].[CH3:6][OH:4]' + '>>' + '[CH3:5][C:1](=[O:2])[O:4][CH3:6].[OH2:3]') + >>> is_eq = validator.smiles_check(rsmi_1, rsmi_2, check_method='ITS') + >>> print(is_eq) + >>> True """ def __init__(self) -> None: - """ - Initialize the AAMValidator. - """ + """Initialize the AAMValidator.""" pass @staticmethod def check_equivariant_graph( its_graphs: List[nx.Graph], ) -> Tuple[List[Tuple[int, int]], int]: - """ - Identify all pairs of isomorphic ITS graphs. + """Identify all pairs of isomorphic ITS graphs. :param its_graphs: A list of ITS graphs to compare. :type its_graphs: list of networkx.Graph @@ -66,20 +79,21 @@ def smiles_check( check_method: str = "RC", ignore_aromaticity: bool = False, ) -> bool: - """ - Validate a single mapped SMILES string against ground truth. + """Validate a single mapped SMILES string against ground truth. :param mapped_smile: The mapped SMILES to validate. :type mapped_smile: str :param ground_truth: The reference SMILES string. :type ground_truth: str - :param check_method: Which method to use: - `"RC"` for reaction‐center graph or - `"ITS"` for full ITS‐graph isomorphism. + :param check_method: Which method to use: `"RC"` for + reaction‐center graph or `"ITS"` for full ITS‐graph + isomorphism. :type check_method: str - :param ignore_aromaticity: If True, ignore aromaticity differences in ITS construction. + :param ignore_aromaticity: If True, ignore aromaticity + differences in ITS construction. :type ignore_aromaticity: bool - :returns: True if exactly one isomorphic match is found; False otherwise. + :returns: True if exactly one isomorphic match is found; False + otherwise. :rtype: bool """ its_graphs, rc_graphs = [], [] @@ -105,8 +119,7 @@ def smiles_check_tautomer( check_method: str = "RC", ignore_aromaticity: bool = False, ) -> Optional[bool]: - """ - Validate against all tautomers of a ground truth SMILES. + """Validate against all tautomers of a ground truth SMILES. :param mapped_smile: The mapped SMILES to test. :type mapped_smile: str @@ -142,8 +155,7 @@ def check_pair( ignore_aromaticity: bool = False, ignore_tautomers: bool = True, ) -> bool: - """ - Validate a single record (dict) entry for equivalence. + """Validate a single record (dict) entry for equivalence. :param mapping: A record containing both mapped and ground‐truth SMILES. :type mapping: dict of str→str @@ -186,8 +198,7 @@ def validate_smiles( verbose: int = 0, ignore_tautomers: bool = True, ) -> List[Dict[str, Union[str, float, List[bool]]]]: - """ - Batch-validate mapped SMILES in tabular or list-of-dicts form. + """Batch-validate mapped SMILES in tabular or list-of-dicts form. :param data: A pandas DataFrame or list of dicts, each row containing at least `ground_truth_col` and each entry in `mapped_cols`. diff --git a/synkit/Chem/Reaction/balance_check.py b/synkit/Chem/Reaction/balance_check.py index 76e0f1c..f21754b 100644 --- a/synkit/Chem/Reaction/balance_check.py +++ b/synkit/Chem/Reaction/balance_check.py @@ -1,48 +1,38 @@ from rdkit import Chem from rdkit.Chem.rdMolDescriptors import CalcMolFormula - from joblib import Parallel, delayed -from typing import List, Dict, Union, Tuple +from typing import List, Dict, Union, Tuple, Any class BalanceReactionCheck: - """ - A class to check the balance of chemical reactions given in SMILES format. - It supports parallel execution and maintains the input format in the output. + """Check elemental balance of chemical reactions in SMILES format. + + Supports checking single reactions, reaction dictionaries, or lists + in parallel. + + :ivar n_jobs: Number of parallel jobs for batch checking. + :ivar verbose: Verbosity level for joblib. """ - def __init__( - self, - n_jobs: int = 4, - verbose: int = 0, - ): + def __init__(self, n_jobs: int = 4, verbose: int = 0) -> None: """ - Initializes the class with given input data, the column name - for reactions in the input, number of jobs for - parallel processing, and verbosity level. - - Parameters: - - input_data (Union[str, List[Union[str, Dict[str, str]]]]): A single SMILES - string, a list of SMILES strings, or a list of dictionaries with 'reactions' keys. - - rsmi_column (str): The key/column name for reaction SMILES strings - in the input data. - - n_jobs (int): The number of parallel jobs to run for balance checking. - - verbose (int): The verbosity level of joblib parallel execution. + :param n_jobs: Number of parallel jobs for batch balance checks. Defaults to 4. + :type n_jobs: int + :param verbose: Verbosity level passed to joblib. Defaults to 0. + :type verbose: int """ - self.n_jobs = n_jobs self.verbose = verbose @staticmethod def get_combined_molecular_formula(smiles: str) -> str: - """ - Computes the molecular formula for a molecule represented by a SMILES string. + """Compute the molecular formula of a SMILES. - Parameters: - - smiles (str): The SMILES string of the molecule. - - Returns: - - str: The molecular formula, or an empty string if the molecule is invalid. + :param smiles: SMILES string of the molecule. + :type smiles: str + :returns: Elemental formula (e.g., "C6H6") or empty string if + invalid. + :rtype: str """ mol = Chem.MolFromSmiles(smiles) if not mol: @@ -54,109 +44,95 @@ def parse_input( input_data: Union[str, List[Union[str, Dict[str, str]]]], rsmi_column: str = "reactions", ) -> List[Dict[str, str]]: + """Normalize input into a list of reaction‐dicts. + + :param input_data: A single SMILES, list of SMILES, or list of dicts containing `rsmi_column`. + :type input_data: str or List[Union[str, Dict[str, str]]] + :param rsmi_column: Key in dicts for the reaction SMILES. Defaults to "reactions". + :type rsmi_column: str + :returns: List of dicts with a single key `rsmi_column` mapping to each reaction. + :rtype: List[Dict[str, str]] + :raises ValueError: If `input_data` is neither str nor list. """ - Parses the input data into a standardized list containing - dictionaries for each reaction. - - Parameters: - - input_data (Union[str, List[Union[str, Dict[str, str]]]]): - The input data to be processed. - - Returns: - - List[Dict[str, str]]: A list of dictionaries with reaction SMILES strings. - """ - standardized_input = [] + standardized: List[Dict[str, str]] = [] if isinstance(input_data, str): - standardized_input.append({rsmi_column: input_data}) + standardized.append({rsmi_column: input_data}) elif isinstance(input_data, list): for item in input_data: if isinstance(item, str): - standardized_input.append({rsmi_column: item}) + standardized.append({rsmi_column: item}) elif isinstance(item, dict) and rsmi_column in item: - standardized_input.append(item) + standardized.append(item) else: - raise ValueError("Unsupported input type") - return standardized_input + raise ValueError("Unsupported input type for balance checking") + return standardized @staticmethod - def parse_reaction(reaction_smiles: str) -> Tuple[List[str], List[str]]: - """ - Splits a reaction SMILES string into reactants and products. - - Parameters: - - reaction_smiles (str): A SMILES string representing a chemical reaction. - - Returns: - - Tuple[List[str], List[str]]: Lists of SMILES strings for reactants and products. + def parse_reaction(reaction_smiles: str) -> Tuple[str, str]: + """Split a reaction SMILES into reactant and product SMILES strings. + + :param reaction_smiles: Reaction SMILES in 'reactants>>products' + format. + :type reaction_smiles: str + :returns: Tuple of (reactants, products) SMILES. + :rtype: Tuple[str, str] """ - reactants_smiles, products_smiles = reaction_smiles.split(">>") - return reactants_smiles, products_smiles + return tuple(reaction_smiles.split(">>")) @staticmethod def rsmi_balance_check(reaction_smiles: str) -> bool: + """Determine if a reaction SMILES is elementally balanced. + + :param reaction_smiles: Reaction SMILES in 'reactants>>products' + format. + :type reaction_smiles: str + :returns: True if reactant and product formulas match, else + False. + :rtype: bool """ - Checks if a reaction SMILES string is balanced. - - Parameters: - - reaction_smiles (str): A SMILES string representing a chemical reaction. - - Returns: - - bool: True if the reaction is balanced, False otherwise. - """ - reactants_smiles, products_smiles = BalanceReactionCheck.parse_reaction( - reaction_smiles - ) - reactants_forumula = BalanceReactionCheck.get_combined_molecular_formula( - reactants_smiles - ) - products_forumula = BalanceReactionCheck.get_combined_molecular_formula( - products_smiles - ) - return reactants_forumula == products_forumula + react, prod = BalanceReactionCheck.parse_reaction(reaction_smiles) + react_formula = BalanceReactionCheck.get_combined_molecular_formula(react) + prod_formula = BalanceReactionCheck.get_combined_molecular_formula(prod) + return react_formula == prod_formula @staticmethod def dict_balance_check( reaction_dict: Dict[str, str], rsmi_column: str - ) -> Dict[str, Union[bool, str]]: - """ - Checks if a single reaction (in SMILES format) is balanced, maintaining - the input format. - - Parameters: - - reaction_dict (Dict[str, str]): A dictionary containing the - reaction SMILES string. - - Returns: - - Dict[str, Union[bool, str]]: A dictionary indicating if the reaction is - balanced, along with the original reaction data. + ) -> Dict[str, Any]: + """Check balance for a single reaction dict, preserving original keys. + + :param reaction_dict: Dict containing at least a `rsmi_column` key. + :type reaction_dict: Dict[str, str] + :param rsmi_column: Key for reaction SMILES in `reaction_dict`. + :type rsmi_column: str + :returns: Original dict augmented with `"balanced": bool`. + :rtype: Dict[str, Any] """ - reaction_smiles = reaction_dict[rsmi_column] - balance = BalanceReactionCheck.rsmi_balance_check(reaction_smiles) - return {"balanced": balance, **reaction_dict} + rsmi = reaction_dict[rsmi_column] + balanced = BalanceReactionCheck.rsmi_balance_check(rsmi) + return {"balanced": balanced, **reaction_dict} def dicts_balance_check( self, input_data: Union[str, List[Union[str, Dict[str, str]]]], rsmi_column: str = "reactions", - ) -> Tuple[List[Dict[str, Union[bool, str]]], List[Dict[str, Union[bool, str]]]]: - """ - Checks the balance of all reactions in the input data. - - Returns: - - Tuple[List[Dict[str, Union[bool, str]]], List[Dict[str, Union[bool, str]]]]: - Two lists containing dictionaries of balanced and unbalanced reactions, - respectively. + ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: + """Batch‐check balance for multiple reactions, in parallel. + + :param input_data: Single reaction SMILES, list of SMILES, or + list of dicts. + :type input_data: Union[str, List[Union[str, Dict[str, str]]]] + :param rsmi_column: Key for reaction SMILES in each dict. + Defaults to "reactions". + :type rsmi_column: str + :returns: Tuple (balanced_list, unbalanced_list) of dicts each + including `"balanced"`. + :rtype: Tuple[List[Dict[str, Any]], List[Dict[str, Any]]] """ - reactions = self.parse_input(input_data, rsmi_column) results = Parallel(n_jobs=self.n_jobs, verbose=self.verbose)( - delayed(self.dict_balance_check)(reaction, rsmi_column) - for reaction in reactions + delayed(self.dict_balance_check)(rd, rsmi_column) for rd in reactions ) - - balanced_reactions = [reaction for reaction in results if reaction["balanced"]] - unbalanced_reactions = [ - reaction for reaction in results if not reaction["balanced"] - ] - - return balanced_reactions, unbalanced_reactions + balanced = [r for r in results if r["balanced"]] + unbalanced = [r for r in results if not r["balanced"]] + return balanced, unbalanced diff --git a/synkit/Chem/Reaction/canon_rsmi.py b/synkit/Chem/Reaction/canon_rsmi.py index 27a9417..0abb0c8 100644 --- a/synkit/Chem/Reaction/canon_rsmi.py +++ b/synkit/Chem/Reaction/canon_rsmi.py @@ -9,9 +9,9 @@ class CanonRSMI: - """ - A **pure-Python / pure-NetworkX** utility for canonicalizing reaction SMILES - by expanding atom-maps and deterministically reindexing reaction graphs. + """A **pure-Python / pure-NetworkX** utility for canonicalizing reaction + SMILES by expanding atom-maps and deterministically reindexing reaction + graphs. Workflow -------- @@ -61,16 +61,15 @@ def __init__( @staticmethod def _mol_from_smiles(smi: str) -> Chem.Mol: - """ - RDKit MolFromSmiles with explicit sanitize step. - """ + """RDKit MolFromSmiles with explicit sanitize step.""" mol = _rdkit_MolFromSmiles(smi, sanitize=False) Chem.SanitizeMol(mol) return mol def expand_aam(self, rsmi: str) -> str: - """ - Assign new atom-map IDs to unmapped reactant atoms in 'reactants>>products' SMILES. + """Assign new atom-map IDs to unmapped reactant atoms in + 'reactants>>products' SMILES. + New IDs start at max(existing maps)+1. """ try: @@ -110,9 +109,7 @@ def sync_atom_map_with_index(G: nx.Graph) -> None: def get_aam_pairwise_indices( G: nx.Graph, H: nx.Graph, aam_key: str = "atom_map" ) -> List[Tuple[int, int]]: - """ - Return sorted list of (G_node, H_node) for shared atom-map IDs. - """ + """Return sorted list of (G_node, H_node) for shared atom-map IDs.""" gmap = { data[aam_key]: n for n, data in G.nodes(data=True) @@ -223,7 +220,8 @@ def canonical_product_graph(self) -> Optional[nx.Graph]: @property def canonical_hash(self) -> Optional[str]: - """Reaction-level hash combining reactant and product canonical hashes.""" + """Reaction-level hash combining reactant and product canonical + hashes.""" if not self._canon_reactant_graph or not self._canon_product_graph: return None h_reac = self._canon.canonical_signature(self._canon_reactant_graph) @@ -236,9 +234,7 @@ def mapping_pairs(self) -> Optional[List[Tuple[int, int]]]: return self._mapping_pairs def help(self) -> None: # pragma: no cover - """ - Pretty-print the class doc and public methods with signatures. - """ + """Pretty-print the class doc and public methods with signatures.""" print(inspect.getdoc(self.__class__)) for meth in ( "expand_aam", diff --git a/synkit/Chem/Reaction/cleaning.py b/synkit/Chem/Reaction/cleaning.py new file mode 100644 index 0000000..6bdd370 --- /dev/null +++ b/synkit/Chem/Reaction/cleaning.py @@ -0,0 +1,66 @@ +from typing import List +from synkit.Chem.Reaction.standardize import Standardize +from synkit.Chem.Reaction.balance_check import BalanceReactionCheck + + +class Cleaning: + """Utilities for cleaning and filtering reaction SMILES lists. + + Methods + ------- + remove_duplicates(smiles_list) + Remove duplicate SMILES while preserving input order. + clean_smiles(smiles_list) + Standardize, balance‑check, and deduplicate a list of reaction SMILES. + """ + + def __init__(self) -> None: + """Initialize the Cleaning helper. + + No instance attributes are used. + """ + pass + + @staticmethod + def remove_duplicates(smiles_list: List[str]) -> List[str]: + """Remove duplicate SMILES strings, preserving first occurrences. + + :param smiles_list: List of reaction SMILES strings. + :type smiles_list: List[str] + :returns: List of unique SMILES in original order. + :rtype: List[str] + """ + seen = set() + return [smi for smi in smiles_list if not (smi in seen or seen.add(smi))] + + @staticmethod + def clean_smiles(smiles_list: List[str]) -> List[str]: + """Standardize, balance‑check, and deduplicate reaction SMILES. + + Steps: + 1. Standardize each SMILES via `Standardize.standardize_rsmi`. + 2. Keep only those that pass `BalanceReactionCheck.rsmi_balance_check`. + 3. Remove duplicates preserving order. + + :param smiles_list: List of reaction SMILES strings to clean. + :type smiles_list: List[str] + :returns: Cleaned list of standardized, balanced, unique SMILES. + :rtype: List[str] + """ + standardizer = Standardize() + balance_checker = BalanceReactionCheck() + + standardized: List[str] = [] + for smi in smiles_list: + try: + std = standardizer.standardize_rsmi(smi, stereo=True) + if std: + standardized.append(std) + except Exception: + continue + + balanced = [ + smi for smi in standardized if balance_checker.rsmi_balance_check(smi) + ] + + return Cleaning.remove_duplicates(balanced) diff --git a/synkit/Chem/Reaction/cleanning.py b/synkit/Chem/Reaction/cleanning.py deleted file mode 100644 index 1f4b6bb..0000000 --- a/synkit/Chem/Reaction/cleanning.py +++ /dev/null @@ -1,67 +0,0 @@ -from typing import List -from synkit.Chem.Reaction.standardize import Standardize -from synkit.Chem.Reaction.balance_check import BalanceReactionCheck - - -class Cleanning: - def __init__(self) -> None: - pass - - @staticmethod - def remove_duplicates(smiles_list: List[str]) -> List[str]: - """ - Removes duplicate SMILES strings from a list, maintaining the order of - first occurrences. Uses a set to track seen SMILES for efficiency. - - Parameters: - - smiles_list (List[str]): A list of SMILES strings representing - chemical reactions. - - Returns: - - List[str]: A list with unique SMILES strings, preserving the original order. - """ - seen = set() - unique_smiles = [ - smiles for smiles in smiles_list if not (smiles in seen or seen.add(smiles)) - ] - return unique_smiles - - @staticmethod - def clean_smiles(smiles_list: List[str]) -> List[str]: - """ - Cleans a list of SMILES strings by standardizing them, checking their chemical - balance, and removing duplicates. Each SMILES is first checked for validity and - then standardized. Only balanced reactions are kept. - - Parameters: - - smiles_list (List[str]): A list of SMILES strings representing chemical reactions. - - Returns: - - List[str]: A list of cleaned and standardized SMILES strings. - """ - # Standardize and check balance in separate list comprehensions - standardizer = Standardize() - balance_checker = BalanceReactionCheck() - - standardized_smiles = [] - for smiles in smiles_list: - try: - r = standardizer.standardize_rsmi(smiles, True) - standardized_smiles.append(r) - except Exception as e: - print(e) - pass - # standardized_smiles = [ - # standardizer.standardize_rsmi(smiles, True) - # for smiles in smiles_list - # if smiles - # ] - balanced_smiles = [ - smiles - for smiles in standardized_smiles - if balance_checker.rsmi_balance_check(smiles) - ] - - # Remove duplicates from the balanced SMILES list - clean_smiles = Cleanning.remove_duplicates(balanced_smiles) - return clean_smiles diff --git a/synkit/Chem/Reaction/deionize.py b/synkit/Chem/Reaction/deionize.py index 02cb3dd..98c02fc 100644 --- a/synkit/Chem/Reaction/deionize.py +++ b/synkit/Chem/Reaction/deionize.py @@ -1,8 +1,7 @@ import random -from itertools import permutations -from itertools import combinations +from itertools import combinations, permutations from joblib import Parallel, delayed -from typing import List, Tuple, Callable, Dict +from typing import List, Tuple, Callable, Dict, Any from rdkit import Chem from rdkit.Chem.MolStandardize import rdMolStandardize @@ -11,279 +10,190 @@ class Deionize: - """ - A class to deionize reactions. + """Neutralize ionic species and mixtures of ions in reactions. + + Provides methods to group ions into neutral combinations, uncharge + individual anions/cations, and apply these corrections to SMILES + strings or entire reaction dictionaries. """ @staticmethod def random_pair_ions( charges: List[int], smiles: List[str] ) -> Tuple[List[List[str]], List[List[int]]]: - """ - Generates non-overlapping groups of ions (2, 3, or 4) based on - their charges and corresponding SMILES representations, - aiming to maximize the total number of ions used by preferring - multiple smaller groups over fewer larger groups. - - Parameters: - - charges (List[int]): A list of integer charges of the ions. - - smiles (List[str]): A list of SMILES strings representing the ions. - - Returns: - - Tuple[List[List[str]], List[List[int]]]: A tuple containing two lists: - - The first list contains the groups of SMILES strings. - - The second list contains the groups of charges. + """Identify non‑overlapping groups of ions whose charges sum to zero. + + :param charges: List of integer formal charges for each ion. + :type charges: List[int] + :param smiles: Corresponding SMILES strings for each ion. + :type smiles: List[str] + :returns: A tuple of two lists: + - groups of SMILES strings forming neutral sets, + - groups of their corresponding charges. + :rtype: Tuple[List[List[str]], List[List[int]]] """ - def find_groups(indices, size): - """Finds and removes groups of a specific size that sum to zero charge.""" + def find_groups(indices: List[int], size: int) -> Tuple[int, ...]: for group in combinations(indices, size): if sum(charges[i] for i in group) == 0: return group - return [] + return () - # Prepare initial variables indices = list(range(len(charges))) - random.shuffle(indices) # Shuffle indices to ensure variety - used_indices = set() - grouped_smiles = [] - grouped_charges = [] + random.shuffle(indices) + used = set() + grouped_smiles: List[List[str]] = [] + grouped_charges: List[List[int]] = [] - for group_size in range( - 2, 5 - ): # Start with pairs, then triples, and finally quads + for group_size in (2, 3, 4): while True: - group = find_groups( - [i for i in indices if i not in used_indices], group_size - ) + available = [i for i in indices if i not in used] + group = find_groups(available, group_size) if not group: - break # No more groups of this size can be formed + break grouped_smiles.append([smiles[i] for i in group]) grouped_charges.append([charges[i] for i in group]) - used_indices.update(group) + used.update(group) return grouped_smiles, grouped_charges @staticmethod def uncharge_anion(smiles: str, charges: int = -1) -> str: - """ - Removes charge from an anionic species represented by a SMILES string. - - This function uses RDKit's standardization tools to neutralize - the charges in the molecule. It returns - the SMILES representation of the uncharged molecule. - - Parameters:: - - smiles (str): A SMILES string representing the anionic species. - - Returns: - - str: The SMILES string of the uncharged molecule. - - Note: - - The function assumes valid SMILES input. + """Neutralize an anionic SMILES string. + + :param smiles: SMILES of the anion to neutralize. + :type smiles: str + :param charges: Formal charge of the ion (negative integer). + Defaults to -1. + :type charges: int + :returns: SMILES of the uncharged molecule. + :rtype: str """ if smiles == "[N-]=[N+]=[N-]": return "[N-]=[N+]=[N]" if charges == -1: - # Convert the SMILES string to an RDKit molecule object mol = Chem.MolFromSmiles(smiles) - - # Initialize the uncharger uncharger = rdMolStandardize.Uncharger() - - # Apply the uncharger to the molecule - uncharged_mol = uncharger.uncharge(mol) - - # Convert the uncharged molecule back to a SMILES string - return Chem.MolToSmiles(uncharged_mol) - - elif charges < -1: - new_smiles = ( - smiles.replace(f"{charges}", "").replace("[", "").replace("]", "") - ) - return new_smiles + uncharged = uncharger.uncharge(mol) + return Chem.MolToSmiles(uncharged) + # for multi‐charged anions + return smiles.replace(f"{charges}", "").replace("[", "").replace("]", "") @staticmethod def uncharge_cation(smiles: str, charges: int = 1) -> str: + """Neutralize a cationic SMILES string. + + :param smiles: SMILES of the cation to neutralize. + :type smiles: str + :param charges: Formal charge of the ion (positive integer). + Defaults to 1. + :type charges: int + :returns: SMILES of the uncharged molecule. + :rtype: str """ - Removes charge from a cationic species represented by a SMILES string. - - This function uses RDKit's standardization tools to neutralize - the charges in the molecule. It returns the - SMILES representation of the uncharged molecule. - - Parameters:: - - smiles (str): A SMILES string representing the cationic species. - - Returns: - - str: The SMILES string of the uncharged molecule. - - Note: - - The function assumes valid SMILES input. - """ - if charges == 1: - new_smiles = smiles.replace("+", "") - elif charges > 1: - # For multiple positive charges, directly modify the SMILES string - new_smiles = smiles.replace(f"+{charges}", "") - return new_smiles + return smiles.replace("+", "") + return smiles.replace(f"+{charges}", "") @staticmethod def uncharge_smiles(charge_smiles: str) -> str: - """ - Processes a SMILES string containing ionic and non-ionic parts, - neutralizes the charges, and returns a modified SMILES string. - - The function splits the input SMILES string into individual components, - identifies ionic and non-ionic parts, - and attempts to neutralize charged ions. - It then creates permutations of the modified ions and combines them into - a single SMILES string, ensuring the molecular structure is valid. + """Neutralize all ionic components in a dot‑separated SMILES string. - Parameters:: - - charge_smiles (str): A SMILES string that may contain ionic and non-ionic parts. + Splits into components, identifies ionic species, groups + them into neutral sets via `random_pair_ions`, then + applies `uncharge_anion` or `uncharge_cation` and recombines. - Returns: - - str: A modified SMILES string with neutralized charges. - - Note: - - This function depends on RDKit for molecular operations. - - The function assumes a valid SMILES input. - - The 'uncharge_anion' and 'random_pair_ions' functions - must be defined and accessible. + :param charge_smiles: SMILES string with ionic and non‑ionic parts. + :type charge_smiles: str + :returns: SMILES string with charges neutralized. + :rtype: str """ - - smiles = charge_smiles.split(".") - charges = [Chem.rdmolops.GetFormalCharge(Chem.MolFromSmiles(i)) for i in smiles] - - if all(charge == 0 for charge in charges): + parts = charge_smiles.split(".") + charges = [Chem.rdmolops.GetFormalCharge(Chem.MolFromSmiles(p)) for p in parts] + if all(c == 0 for c in charges): return charge_smiles - valid_smiles, non_ionic_smiles = [], [] - original_ionic_parts, original_ion_charges = [], [] - - # Splitting the SMILES into ionic and non-ionic parts - for smile, charge in zip(smiles, charges): - if charge == 0: - non_ionic_smiles.append(smile) + non_ionic, ionic_parts, ionic_charges = [], [], [] + for p, c in zip(parts, charges): + if c == 0: + non_ionic.append(p) else: - original_ionic_parts.append(smile) - original_ion_charges.append(charge) - - valid_smiles.extend(non_ionic_smiles) - paired_smiles, paired_charges = Deionize.random_pair_ions( - original_ion_charges, original_ionic_parts - ) - # Processing each pair of ionic parts - for i_smile, i_charge in zip(paired_smiles, paired_charges): - modified_ions = [] - for ion, charge in zip(i_smile, i_charge): - if int(charge) > 0: - new_ion = Deionize.uncharge_cation(ion, charge) - modified_ions.append(new_ion) - elif int(charge) < 0: - new_ion = Deionize.uncharge_anion(ion, charge) - modified_ions.append(new_ion) - # Creating permutations of the modified ions - check_merge = False - for perm in permutations(modified_ions): - combined_ionic = "".join(perm) - if Chem.MolFromSmiles(combined_ionic): - coordinate_pattern = ["->", "<-"] - if all( - pattern not in Chem.CanonSmiles(combined_ionic) - for pattern in coordinate_pattern - ): - valid_smiles.append(Chem.CanonSmiles(combined_ionic)) - check_merge = True - break - if check_merge is False: - valid_smiles.extend(i_smile) - return ".".join(valid_smiles) + ionic_parts.append(p) + ionic_charges.append(c) + + valid = non_ionic.copy() + groups, group_chs = Deionize.random_pair_ions(ionic_charges, ionic_parts) + for smiles_group, charge_group in zip(groups, group_chs): + candidates = [] + for smi, ch in zip(smiles_group, charge_group): + if ch > 0: + candidates.append(Deionize.uncharge_cation(smi, ch)) + else: + candidates.append(Deionize.uncharge_anion(smi, ch)) + # try permutations for valid SMILES + for perm in permutations(candidates): + combo = "".join(perm) + if Chem.MolFromSmiles(combo): + valid.append(Chem.CanonSmiles(combo)) + break + else: + valid.extend(smiles_group) + return ".".join(valid) @staticmethod def ammonia_hydroxide_standardize(reaction_smiles: str) -> str: - """ - Replaces occurrences of ammonium hydroxide (NH4+ and OH-) in a - reaction SMILES string with a simplified representation (N.O or O.N). - - Parameters:: - reaction_smiles (str): The reaction SMILES string to be standardized. + """Simplify ammonium hydroxide pairs in a reaction SMILES. - Returns: - str: The standardized reaction SMILES string with - ammonium hydroxide represented as 'N.O' or 'O.N'. + :param reaction_smiles: Reaction SMILES string. + :type reaction_smiles: str + :returns: Reaction SMILES with '[NH4+].[OH-]' replaced by 'N.O' + or 'O.N'. + :rtype: str """ - # Simplify the representation of ammonium hydroxide in the reaction SMILES - new_smiles = reaction_smiles.replace("[NH4+].[OH-]", "N.O").replace( + return reaction_smiles.replace("[NH4+].[OH-]", "N.O").replace( "[OH-].[NH4+]", "O.N" ) - return new_smiles @classmethod def apply_uncharge_smiles_to_reactions( cls, - reactions: List[Dict[str, str]], + reactions: List[Dict[str, Any]], uncharge_smiles_func: Callable[[str], str], n_jobs: int = 4, - ) -> List[Dict[str, str]]: - """ - Applies a given uncharge SMILES function to the reactants - and products of a list of chemical reactions, - parallelizing the process for improved performance. - Each reaction is expected to be a dictionary - with at least 'reactants' and 'products' keys. - The function adds three new keys to each reaction - dictionary: 'uncharged_reactants', 'uncharged_products', - and 'uncharged_reactions', containing - the uncharged SMILES strings of reactants, products, - and the overall reaction, respectively. - - Parameters:: - - reactions (List[Dict[str, str]]): A list of dictionaries, where each dictionary - represents a chemical reaction with 'reactants' and 'products' keys. - - uncharge_smiles_func (Callable[[str], str]): A function that takes a SMILES - string as input and returns a modified SMILES string with neutralized charges. - - Returns: - - List[Dict[str, str]]: The input list of reaction dictionaries, modified in-place - to include 'uncharged_reactants', 'uncharged_products', and 'uncharged_reactions' - keys. + ) -> List[Dict[str, Any]]: + """Apply a neutralization function to each reaction’s + reactants/products in parallel. + + Adds keys 'new_reactants', 'new_products', and 'standardized_reactions' + based on uncharged SMILES and verifies formula balance. + + :param reactions: List of reaction dicts with 'reactants' and 'products' keys. + :type reactions: List[Dict[str, Any]] + :param uncharge_smiles_func: Function to neutralize a SMILES string. + :type uncharge_smiles_func: Callable[[str], str] + :param n_jobs: Number of parallel jobs to run. Defaults to 4. + :type n_jobs: int + :returns: List of updated reaction dicts with: + - 'success': bool indicating formula match + - 'new_reactants' / 'new_products' + - 'standardized_reactions' + :rtype: List[Dict[str, Any]] """ - # Define a helper function for processing a single reaction - def process_reaction(reaction): - fix_reactants = cls.ammonia_hydroxide_standardize(reaction["reactants"]) - fix_products = cls.ammonia_hydroxide_standardize(reaction["products"]) - - uncharged_reactants = uncharge_smiles_func(fix_reactants) - uncharged_products = uncharge_smiles_func(fix_products) - uncharged_reactants_formula = ( - BalanceReactionCheck().get_combined_molecular_formula( - uncharged_reactants - ) - ) - uncharged_products_formula = ( - BalanceReactionCheck().get_combined_molecular_formula( - uncharged_products - ) - ) - if uncharged_reactants_formula != uncharged_products_formula: - reaction["success"] = False - reaction["new_reactants"] = fix_reactants - reaction["new_products"] = fix_products - else: - reaction["success"] = True - reaction["new_reactants"] = uncharged_reactants - reaction["new_products"] = uncharged_products + def process(reaction: Dict[str, Any]) -> Dict[str, Any]: + # pre‐standardize ammonia hydroxide + r_fix = cls.ammonia_hydroxide_standardize(reaction["reactants"]) + p_fix = cls.ammonia_hydroxide_standardize(reaction["products"]) + ur = uncharge_smiles_func(r_fix) + up = uncharge_smiles_func(p_fix) + r_formula = BalanceReactionCheck().get_combined_molecular_formula(ur) + p_formula = BalanceReactionCheck().get_combined_molecular_formula(up) + reaction["success"] = r_formula == p_formula + reaction["new_reactants"] = ur if reaction["success"] else r_fix + reaction["new_products"] = up if reaction["success"] else p_fix reaction["standardized_reactions"] = ( f"{reaction['new_reactants']}>>{reaction['new_products']}" ) return reaction - # Use joblib to parallelize the processing of reactions - reactions = Parallel(n_jobs=n_jobs)( - delayed(process_reaction)(reaction) for reaction in reactions - ) - return reactions + return Parallel(n_jobs=n_jobs)(delayed(process)(rxn) for rxn in reactions) diff --git a/synkit/Chem/Reaction/fix_aam.py b/synkit/Chem/Reaction/fix_aam.py index 995eab7..8f2b2eb 100644 --- a/synkit/Chem/Reaction/fix_aam.py +++ b/synkit/Chem/Reaction/fix_aam.py @@ -3,89 +3,62 @@ class FixAAM: - """ - A class containing methods for manipulating atom mapping numbers (AAM) in molecules. - It includes functionality for incrementing atom map numbers in a molecule, adjusting - atom mappings in SMILES strings, and fixing atom mappings in reaction SMILES (RSMI) strings. - - Methods: - increment_atom_mapping(mol: Chem.Mol) -> Chem.Mol: - Increments the atom map number for each atom in the molecule by 1. - - fix_aam_smiles(smiles: str) -> str: - Takes a SMILES string, increments all atom mapping numbers by 1, and returns the updated SMILES. + """Utilities for incrementing and correcting atom‐atom mapping (AAM) + numbers in molecules and reaction SMILES. - fix_aam_rsmi(rsmi: str) -> str: - Adjusts atom mapping numbers in both reactant and product parts of a reaction SMILES (RSMI). + Provides methods to: + - Increment AAM on all atoms of an RDKit Mol. + - Adjust AAM numbers in a standalone SMILES string. + - Apply the same adjustment to both sides of a reaction SMILES (RSMI). """ @staticmethod def increment_atom_mapping(mol: Chem.Mol) -> Chem.Mol: - """ - Increments the atom mapping number of each atom in the molecule by 1. - - This method iterates through each atom in the molecule and increments its atom map - number (if it has one). + """Increment the atom‐map number of each atom in an RDKit Mol by 1. - Parameters: - mol (Chem.Mol): The RDKit molecule object that represents the molecule with atom mapping. - - Returns: - Chem.Mol: The updated RDKit molecule with incremented atom mapping numbers for all atoms. + :param mol: RDKit molecule with existing atom‐map annotations. + :type mol: Chem.Mol + :returns: The same Mol object with each atom’s map number + increased by one. + :rtype: Chem.Mol """ - # Iterate through all atoms in the molecule for atom in mol.GetAtoms(): - # Get the current atom map (if it exists) - atom_map = atom.GetAtomMapNum() - - atom.SetAtomMapNum(atom_map + 1) + atom.SetAtomMapNum(atom.GetAtomMapNum() + 1) return mol @staticmethod def fix_aam_smiles(smiles: str) -> str: + """Parse a SMILES string, increment all atom map numbers, and return + updated SMILES. + + :param smiles: SMILES string containing atom‐map annotations. + :type smiles: str + :returns: SMILES string with every atom‐map number increased by + one. + :rtype: str + :raises ValueError: If the input SMILES cannot be parsed into an + RDKit Mol. """ - Takes a SMILES string, increments all atom mapping numbers by 1, and returns the updated SMILES. - - This method converts the SMILES string into an RDKit molecule, increments the atom - mapping numbers, and returns the updated SMILES string. - - Parameters: - smiles (str): A SMILES string containing atom mapping numbers. - - Returns: - str: A new SMILES string with incremented atom mapping numbers for all atoms. - - Raises: - ValueError: If the input SMILES string is invalid and cannot be parsed into a molecule. - """ - # Create the molecule from the SMILES string mol: Optional[Chem.Mol] = Chem.MolFromSmiles(smiles, sanitize=False) if mol is None: - raise ValueError("Invalid SMILES string.") + raise ValueError(f"Invalid SMILES string: {smiles!r}") Chem.SanitizeMol(mol) - # Increment atom mapping numbers - updated_mol = FixAAM.increment_atom_mapping(mol) - - # Return the SMILES string with updated atom mappings - return Chem.MolToSmiles(updated_mol) + FixAAM.increment_atom_mapping(mol) + return Chem.MolToSmiles(mol) @staticmethod def fix_aam_rsmi(rsmi: str) -> str: + """Apply atom‐map increment to both reactant and product sides of a + reaction SMILES. + + :param rsmi: Reaction SMILES in 'reactants>>products' format + with atom‐map tags. + :type rsmi: str + :returns: New reaction SMILES string where each atom‐map number + in both halves is increased by one. + :rtype: str """ - Adjusts atom mapping numbers in both reactant and product parts of a reaction SMILES (RSMI). - - This method splits the reaction SMILES (RSMI) into its reactant and product components, - increments the atom mappings for both parts, and returns the updated reaction SMILES string. - - Parameters: - rsmi (str): A reaction SMILES string with atom mapping numbers. - - Returns: - str: A new reaction SMILES string with incremented atom mapping numbers in both reactant - and product parts. - """ - # Split the reaction SMILES into reactants and products - r, p = rsmi.split(">>") - - # Update both reactant and product SMILES strings - return f"{FixAAM.fix_aam_smiles(r)}>>{FixAAM.fix_aam_smiles(p)}" + react, prod = rsmi.split(">>") + new_react = FixAAM.fix_aam_smiles(react) + new_prod = FixAAM.fix_aam_smiles(prod) + return f"{new_react}>>{new_prod}" diff --git a/synkit/Chem/Reaction/neutralize.py b/synkit/Chem/Reaction/neutralize.py index eed307b..9745fdf 100644 --- a/synkit/Chem/Reaction/neutralize.py +++ b/synkit/Chem/Reaction/neutralize.py @@ -1,23 +1,25 @@ from rdkit import Chem from joblib import Parallel, delayed -from typing import Dict, Any, List, Union, Tuple +from typing import Dict, Any, List, Union, Tuple, Optional class Neutralize: - """ - A class for neutralizing unbalanced charges in a reaction. + """Neutralize unbalanced charges in chemical reactions by adding + counter‑ions. + + Provides utilities to calculate formal charges, parse reaction + SMILES, and adjust reactants/products with [Na+] or [Cl‑] to restore + neutrality. """ @staticmethod def calculate_charge(smiles: str) -> int: - """ - Calculates the formal charge of a given molecule represented by a SMILES string. + """Calculate the formal charge of a molecule. - Parameters: - - smiles (str): A SMILES string representing a molecule. - - Returns: - - int: The formal charge of the molecule. + :param smiles: SMILES string of the molecule. + :type smiles: str + :returns: Formal charge of the molecule (0 if invalid SMILES). + :rtype: int """ mol = Chem.MolFromSmiles(smiles) if mol is None: @@ -25,20 +27,15 @@ def calculate_charge(smiles: str) -> int: return Chem.rdmolops.GetFormalCharge(mol) @staticmethod - def parse_reaction(reaction_smiles: str) -> Tuple[str, str]: - """ - Parses a reaction SMILES string into reactants and products. - - Parameters: - - reaction_smiles (str): A reaction SMILES string of the form - "reactants>>products". - - Returns: - - Tuple[str, str]: A tuple containing the reactants and - products SMILES strings, respectively. - - This function uses a while loop and exception handling to - manage parsing errors and ensure the input is correctly formatted. + def parse_reaction(reaction_smiles: str) -> Tuple[Optional[str], Optional[str]]: + """Split a reaction SMILES into reactants and products. + + :param reaction_smiles: Reaction SMILES in 'reactants>>products' + format. + :type reaction_smiles: str + :returns: Tuple of (reactants, products) SMILES, or (None, None) + if parse fails. + :rtype: Tuple[Optional[str], Optional[str]] """ try: reactants, products = reaction_smiles.split(">>") @@ -48,20 +45,24 @@ def parse_reaction(reaction_smiles: str) -> Tuple[str, str]: @staticmethod def calculate_charge_dict( - reaction: Dict[str, str], reaction_column: str + reaction: Dict[str, Any], reaction_column: str ) -> Dict[str, Union[str, int]]: + """Compute and store the total formal charge of the products in a + reaction dict. + + :param reaction: Dictionary containing at least `reaction_column` with a reaction SMILES. + :type reaction: Dict[str, Any] + :param reaction_column: Key under which the reaction SMILES is stored. + :type reaction_column: str + :returns: The same dictionary updated with: + - 'reactants': reactant SMILES or None + - 'products': product SMILES or None + - 'total_charge_in_products': integer sum of product charges or None + :rtype: Dict[str, Union[str, int]] """ - Calculates and adds the total charge of products in a single reaction. - - Parameters: - - reaction (Dict[str, str]): A dictionary representing a reaction with keys - 'R-id' and 'new_reaction'. - - Returns: - - Dict[str, Union[str, int]]: The same reaction dictionary, with an added key - 'total_charge_in_products' indicating the sum of formal charges in its products. - """ - reactants, products = Neutralize.parse_reaction(reaction[reaction_column]) + reactants, products = Neutralize.parse_reaction( + reaction.get(reaction_column, "") + ) if reactants is None or products is None: reaction.update( {"reactants": None, "products": None, "total_charge_in_products": None} @@ -69,188 +70,126 @@ def calculate_charge_dict( else: reaction["reactants"] = reactants reaction["products"] = products - products = products.split(".") - total_charge = sum( - Neutralize.calculate_charge(product) for product in products - ) - reaction["total_charge_in_products"] = total_charge + total = sum(Neutralize.calculate_charge(p) for p in products.split(".")) + reaction["total_charge_in_products"] = total return reaction @staticmethod def fix_negative_charge( - reaction_dict: Dict[str, any], + reaction_dict: Dict[str, Any], charges_column: str = "total_charge_in_products", id_column: str = "R-id", reaction_column: str = "reactions", - ) -> Dict[str, any]: - """ - Adjusts a reaction dictionary to compensate for a negative charge - in the products by adding [Na+] ions. - - This function calculates the number of sodium ions ([Na+]) needed to neutralize - negative charges in the reaction products. It then adds the appropriate number of - sodium ions to both the reactants and products. - - Parameters:: - - reaction_dict (Dict[str, any]): A dictionary representing a chemical reaction. - Must include keys for 'total_charge_in_products', 'reactants', 'products', 'R-id', - and 'label'. - - Returns: - - Dict[str, any]: A new reaction dictionary with adjusted reactants and products - to neutralize the negative charge. The 'total_charge_in_products' is set to 0, - assuming the charge has been neutralized. + ) -> Dict[str, Any]: + """Add [Na+] ions to neutralize negative product charge. + + :param reaction_dict: Dictionary with 'reactants', 'products', and charge info. + :type reaction_dict: Dict[str, Any] + :param charges_column: Key for product total charge. Defaults to 'total_charge_in_products'. + :type charges_column: str + :param id_column: Key for reaction identifier. Defaults to 'R-id'. + :type id_column: str + :param reaction_column: Key for reaction SMILES to update. Defaults to 'reactions'. + :type reaction_column: str + :returns: New dictionary with: + - updated `reaction_column` including added [Na+] ions + - 'reactants' and 'products' with ions appended + - charge column set to 0 + :rtype: Dict[str, Any] """ - - num_na_to_add = abs(reaction_dict[charges_column]) - sodium_ion = "[Na+]" - - # Generate the string to add, with the correct number of sodium ions - sodium_addition = ( - "." + ".".join([sodium_ion] * num_na_to_add) if num_na_to_add > 0 else "" - ) - - # Add the sodium ions to reactants and products - new_reactants = reaction_dict["reactants"] + sodium_addition - new_products = reaction_dict["products"] + sodium_addition - - # Generate the new reaction string - new_reactions = new_reactants + ">>" + new_products - - # Create the new reaction dictionary - new_reaction_dict = { - id_column: reaction_dict["R-id"], - reaction_column: new_reactions, - "reactants": new_reactants, - "products": new_products, - charges_column: 0, # Assuming the charge is neutralized + num_to_add = abs(reaction_dict.get(charges_column, 0)) + sodium = "[Na+]" + addition = ("." + ".".join([sodium] * num_to_add)) if num_to_add else "" + new_react = reaction_dict["reactants"] + addition + new_prod = reaction_dict["products"] + addition + new_reaction = f"{new_react}>>{new_prod}" + + return { + id_column: reaction_dict.get("R-id"), + reaction_column: new_reaction, + "reactants": new_react, + "products": new_prod, + charges_column: 0, } - return new_reaction_dict - @staticmethod def fix_positive_charge( - reaction_dict: Dict[str, any], + reaction_dict: Dict[str, Any], charges_column: str = "total_charge_in_products", id_column: str = "R-id", reaction_column: str = "reactions", - ) -> Dict[str, any]: - """ - Adjusts a reaction dictionary to compensate for a positive charge - in the products by adding [Cl-] ions. The function - takes into account the total positive charge indicated - in the reaction dictionary and adds an equivalent number of - chloride ions ([Cl-]) to both reactants and products to neutralize the charge. - - Parameters:: - - reaction_dict (Dict[str, any]): A dictionary representing a chemical reaction. - This dictionary must include keys for reactants, products, and a specified charge - column (default is 'total_charge_in_products') which contains the total charge of - the products. - - charges_column (str, optional): The key in `reaction_dict` that contains the - total charge of the products. Defaults to 'total_charge_in_products'. - - Returns: - - Dict[str, any]: A modified reaction dictionary with added [Cl-] ions to - neutralize the positive charge. The 'total_charge_in_products' is updated to 0, - indicating that the reaction's charge has been neutralized. The dictionary - includes updated 'reactants', 'products', and a new reaction string. + ) -> Dict[str, Any]: + """Add [Cl‑] ions to neutralize positive product charge. + + :param reaction_dict: Dictionary with 'reactants', 'products', and charge info. + :type reaction_dict: Dict[str, Any] + :param charges_column: Key for product total charge. Defaults to 'total_charge_in_products'. + :type charges_column: str + :param id_column: Key for reaction identifier. Defaults to 'R-id'. + :type id_column: str + :param reaction_column: Key for reaction SMILES to update. Defaults to 'reactions'. + :type reaction_column: str + :returns: New dictionary with: + - updated `reaction_column` including added [Cl‑] ions + - 'reactants' and 'products' with ions appended + - charge column set to 0 + :rtype: Dict[str, Any] """ - - num_cl_to_add = abs(reaction_dict[charges_column]) - chloride_ion = "[Cl-]" - - # Generate the string to add, with the correct number of chloride ions - chloride_addition = ( - "." + ".".join([chloride_ion] * num_cl_to_add) if num_cl_to_add > 0 else "" - ) - - # Add the chloride ions to reactants and products - new_reactants = reaction_dict["reactants"] + chloride_addition - new_products = reaction_dict["products"] + chloride_addition - - # Generate the new reaction string - new_reactions = new_reactants + ">>" + new_products - - # Create and return the new reaction dictionary with the neutralized charge - new_reaction_dict = { - "R-id": reaction_dict[id_column], - reaction_column: new_reactions, - "reactants": new_reactants, - "products": new_products, + num_to_add = abs(reaction_dict.get(charges_column, 0)) + chloride = "[Cl-]" + addition = ("." + ".".join([chloride] * num_to_add)) if num_to_add else "" + new_react = reaction_dict["reactants"] + addition + new_prod = reaction_dict["products"] + addition + new_reaction = f"{new_react}>>{new_prod}" + + return { + id_column: reaction_dict.get("R-id"), + reaction_column: new_reaction, + "reactants": new_react, + "products": new_prod, charges_column: 0, } - return new_reaction_dict - @staticmethod def fix_unbalanced_charged( - reaction_dict: Dict[str, any], - reaction_column: str, - ) -> Dict[str, any]: + reaction_dict: Dict[str, Any], reaction_column: str + ) -> Dict[str, Any]: + """Detect and neutralize unbalanced product charge by adding + counter‑ions. + + :param reaction_dict: Dictionary with raw reaction SMILES under `reaction_column`. + :type reaction_dict: Dict[str, Any] + :param reaction_column: Key for reaction SMILES in the input dict. + :type reaction_column: str + :returns: Dictionary with balanced charges and updated SMILES. + :rtype: Dict[str, Any] """ - Adjusts a reaction dictionary to compensate for an unbalanced charge in the - products by adding either [Cl-] ions for a positive charge or [Na+] ions for a - negative charge. The function determines the direction of the charge imbalance - using the specified charges column and applies the appropriate correction. - - Parameters:: - - reaction_dict (Dict[str, any]): A dictionary representing a chemical reaction. - This dictionary must include keys for reactants, products, and a specified charge - column which contains the total charge of the products. - - charges_column (str, optional): The key in `reaction_dict` that contains the - total charge of the products. Defaults to 'total_charge_in_products'. - - Returns: - - Dict[str, any]: A modified reaction dictionary with added ions to neutralize the - charge imbalance. The returned dictionary will have its charge neutralized and - include updated 'reactants', 'products', and a new reaction string. The specific - ions added ([Cl-] for positive charges or [Na+] for negative charges) depend on - the initial charge imbalance. - """ - reaction_dict = Neutralize.calculate_charge_dict(reaction_dict, reaction_column) - if reaction_dict["total_charge_in_products"] > 0: - return Neutralize.fix_positive_charge( - reaction_dict, "total_charge_in_products" - ) - elif reaction_dict["total_charge_in_products"] < 0: - return Neutralize.fix_negative_charge( - reaction_dict, "total_charge_in_products" - ) - else: - return reaction_dict + rd = Neutralize.calculate_charge_dict(reaction_dict, reaction_column) + total = rd.get("total_charge_in_products", 0) + if total > 0: + return Neutralize.fix_positive_charge(rd) + if total < 0: + return Neutralize.fix_negative_charge(rd) + return rd @classmethod def parallel_fix_unbalanced_charge( - cls, - reaction_dicts: List[Dict[str, Any]], - reaction_column: str, - n_jobs: int = 4, + cls, reaction_dicts: List[Dict[str, Any]], reaction_column: str, n_jobs: int = 4 ) -> List[Dict[str, Any]]: + """Neutralize charges in multiple reaction dictionaries in parallel. + + :param reaction_dicts: List of reaction dictionaries to process. + :type reaction_dicts: List[Dict[str, Any]] + :param reaction_column: Key for reaction SMILES in each dict. + :type reaction_column: str + :param n_jobs: Number of parallel jobs (use -1 for all cores). + Defaults to 4. + :type n_jobs: int + :returns: List of dictionaries with balanced charges and updated + SMILES. + :rtype: List[Dict[str, Any]] """ - Processes a list of reaction dictionaries in parallel to compensate - for unbalanced charges in the products, adding either [Cl-] ions - for positive charges or [Na+] ions for negative charges. - - Parameters:: - - reaction_dicts (List[Dict[str, Any]]): A list of dictionaries, each representing - a chemical reaction that may have an unbalanced charge. - - charges_column (str): The key in each reaction dictionary that contains the - total charge of the products. Defaults to 'total_charge_in_products'. - - n_jobs (int): The number of CPU cores to use for parallel processing. - -1 means using all available cores. - - Returns: - - List[Dict[str, Any]]: A list of modified reaction dictionaries - with charges neutralized, reflecting the addition of necessary ions. - - Note: - - This function requires the joblib library for parallel execution. - Ensure joblib is installed and available for import. - """ - # Use joblib.Parallel and joblib.delayed to parallelize the charge fixing - fixed_reactions = Parallel(n_jobs=n_jobs)( - delayed(cls.fix_unbalanced_charged)(reaction_dict, reaction_column) - for reaction_dict in reaction_dicts + return Parallel(n_jobs=n_jobs)( + delayed(cls.fix_unbalanced_charged)(d, reaction_column) + for d in reaction_dicts ) - return fixed_reactions diff --git a/synkit/Chem/Reaction/radical_wildcard.py b/synkit/Chem/Reaction/radical_wildcard.py index f431b44..166d981 100644 --- a/synkit/Chem/Reaction/radical_wildcard.py +++ b/synkit/Chem/Reaction/radical_wildcard.py @@ -5,9 +5,9 @@ class RadicalWildcardAdder: - """ - A utility for adding wildcard dummy atoms ([*]) to radical centers in reaction SMILES, - with unique incremental atom-map indices and correct propagation into products. + """A utility for adding wildcard dummy atoms ([*]) to radical centers in + reaction SMILES, with unique incremental atom-map indices and correct + propagation into products. Each reactive radical atom in the reactant block is identified by its unpaired electron count, assigned one or more wildcard map indices, and recorded. The same wildcard(s) are then appended @@ -27,37 +27,36 @@ class RadicalWildcardAdder: """ def __init__(self, start_map: Optional[int] = None) -> None: - """ - Initialize the adder with an optional starting map index. + """Initialize the adder with an optional starting map index. - :param start_map: Starting atom-map index for wildcards or None to auto-pick. + :param start_map: Starting atom-map index for wildcards or None + to auto-pick. :type start_map: Optional[int] """ self.start_map = start_map def __repr__(self) -> str: - """ - Official representation. - """ + """Official representation.""" return f"" def __str__(self) -> str: - """ - User-friendly description. - """ + """User-friendly description.""" m = self.start_map if self.start_map is not None else "auto" return f"RadicalWildcardAdder(start_map={m})" def transform(self, rxn_smiles: str) -> str: - """ - Append wildcard dummy atoms to each radical center in the reactant block - and propagate the same wildcards to the matching atoms in the product block. + """Append wildcard dummy atoms to each radical center in the reactant + block and propagate the same wildcards to the matching atoms in the + product block. - :param rxn_smiles: Reaction SMILES string, two-component or three-component. + :param rxn_smiles: Reaction SMILES string, two-component or + three-component. :type rxn_smiles: str - :returns: Modified reaction SMILES with consistent wildcard attachments. + :returns: Modified reaction SMILES with consistent wildcard + attachments. :rtype: str - :raises ValueError: If the SMILES is not valid or fragments fail to parse. + :raises ValueError: If the SMILES is not valid or fragments fail + to parse. """ # Split into reactants > agents? > products react_blk, agents_blk, prod_blk = self._split_reaction(rxn_smiles) @@ -148,14 +147,16 @@ def _process(frags: List[str], propagate: bool) -> List[str]: @staticmethod def _split_reaction(rxn: str) -> Tuple[str, Optional[str], str]: - """ - Split a reaction SMILES into reactants, agents (optional), and products. + """Split a reaction SMILES into reactants, agents (optional), and + products. :param rxn: The reaction SMILES string. :type rxn: str - :returns: Tuple of (reactants_block, agents_block or None, products_block). + :returns: Tuple of (reactants_block, agents_block or None, + products_block). :rtype: Tuple[str, Optional[str], str] - :raises ValueError: If the SMILES does not contain 2 or 3 '>' symbols. + :raises ValueError: If the SMILES does not contain 2 or 3 '>' + symbols. """ parts = rxn.split(">") if len(parts) == 2: diff --git a/synkit/Chem/Reaction/rsmi_utils.py b/synkit/Chem/Reaction/rsmi_utils.py deleted file mode 100644 index e567195..0000000 --- a/synkit/Chem/Reaction/rsmi_utils.py +++ /dev/null @@ -1,126 +0,0 @@ -from rdkit import Chem -from rdkit.Chem import rdChemReactions -from typing import List, Tuple, Optional - - -def remove_explicit_H_from_rsmi(rsmi: str) -> str: - """ - Remove explicit [H:...] atoms from a reaction SMILES with atom-atom mapping. - Keeps atom mapping intact for non-hydrogen atoms and returns a simplified reaction SMILES. - - Args: - rsmi (str): Atom-mapped reaction SMILES with explicit hydrogens. - - Returns: - str: Reaction SMILES with implicit hydrogens and AAM preserved. - """ - rxn = rdChemReactions.ReactionFromSmarts(rsmi, useSmiles=True) - - def cleaned_smiles(mols): - return ".".join( - Chem.MolToSmiles(Chem.RemoveHs(mol), isomericSmiles=True) for mol in mols - ) - - reactant_smiles = cleaned_smiles(rxn.GetReactants()) - product_smiles = cleaned_smiles(rxn.GetProducts()) - - return f"{reactant_smiles}>>{product_smiles}" - - -def remove_common_reagents(reaction_smiles: str) -> Tuple[Optional[str], Optional[str]]: - """ - Removes reagents that appear on both sides of a reaction SMILES string. - - Parameters: - - reaction_smiles (str): The reaction in SMILES format. - - Returns: - - Tuple[Optional[str], Optional[str]]: A tuple containing the cleaned reaction SMILES - and a list of common reagents removed. If no common reagents are found, the reaction - is returned unchanged and the second element of the tuple is `None`. - """ - reactants, products = reaction_smiles.split(">>") - reactant_list = reactants.split(".") - product_list = products.split(".") - common_reagents = set(reactant_list) & set(product_list) - - filtered_reactants = [r for r in reactant_list if r not in common_reagents] - filtered_products = [p for p in product_list if p not in common_reagents] - cleaned_reaction_smiles = ( - ".".join(filtered_reactants) + ">>" + ".".join(filtered_products) - ) - - return cleaned_reaction_smiles - - -def remove_duplicates(input_list: List[str]) -> List[str]: - """ - Removes duplicate strings from a list, retaining only the first occurrence of each string. - - Parameters: - - input_list (List[str]): A list of strings potentially containing duplicates. - - Returns: - - List[str]: A list of strings with duplicates removed. - """ - seen = set() - result = [] - for item in input_list: - if item not in seen: - result.append(item) - seen.add(item) - return result - - -def reverse_reaction(rsmi: str) -> str: - """ - Reverses the direction of a reaction SMILES string. - - Parameters: - - rsmi (str): The reaction SMILES string to reverse. - - Returns: - - str: The reversed reaction SMILES string. - """ - reactants, products = rsmi.split(">>") - return f"{products}>>{reactants}" - - -def merge_reaction(rsmi_1: str, rsmi_2: str) -> str: - """ - Merges two reaction SMILES strings into a single reaction. - - Parameters: - - rsmi_1 (str): The first reaction SMILES string. - - rsmi_2 (str): The second reaction SMILES string. - - Returns: - - str: A new reaction SMILES string combining both input reactions. - """ - try: - r1, p1 = rsmi_1.split(">>") - r2, p2 = rsmi_2.split(">>") - except ValueError: - return None # Returns None if there's a problem with splitting (e.g., no '>>') - - # Check if any part of the reaction is empty, which could be problematic for a meaningful merge. - if any(len(part.strip()) == 0 for part in (r1, p1, r2, p2)): - return None - - return f"{r1}.{r2}>>{p1}.{p2}" - - -def find_longest_fragment(input_list: List[str]) -> str: - """ - Finds the longest string in a list of strings. - - Parameters: - - input_list (List[str]): A list of strings from which the longest string is to be found. - - Returns: - - str: The longest string found in the input list. - """ - if len(input_list) == 0: - return None - longest_fragment = max(input_list, key=len) - return longest_fragment diff --git a/synkit/Chem/Reaction/standardize.py b/synkit/Chem/Reaction/standardize.py index def39ba..917a47f 100644 --- a/synkit/Chem/Reaction/standardize.py +++ b/synkit/Chem/Reaction/standardize.py @@ -1,31 +1,37 @@ -from rdkit import Chem from typing import List, Optional, Tuple +from rdkit import Chem class Standardize: - """ - A collection of utilities to normalize and filter reaction and molecule SMILES. + """Utilities to normalize and filter reaction and molecule SMILES. + + This class provides methods to remove atom‑mapping, filter invalid molecules, + canonicalize reaction SMILES, and a full pipeline via `fit`. + + :ivar None: Stateless helper class. """ def __init__(self) -> None: - """ - Initialize the Standardize helper. + """Initialize the Standardize helper. + + No instance attributes are set. """ pass @staticmethod def remove_atom_mapping(reaction_smiles: str, symbol: str = ">>") -> str: - """ - Remove atom-map numbers from both sides of a reaction SMILES. + """Remove atom‑map numbers from a reaction SMILES string. - :param reaction_smiles: A reaction SMILES string with atom mappings. + :param reaction_smiles: Reaction SMILES with atom maps, e.g. + 'C[CH3:1]>>C'. :type reaction_smiles: str - :param symbol: The separator between reactants and products. Defaults to ">>". + :param symbol: Separator between reactants and products. + Defaults to '>>'. :type symbol: str - :returns: The reaction SMILES with all atom-map annotations stripped. + :returns: Reaction SMILES without atom‑mapping annotations. :rtype: str - :raises ValueError: If the input is not in "reactants>>products" format - or contains invalid SMILES. + :raises ValueError: If the input format is invalid or contains + invalid SMILES. """ parts = reaction_smiles.split(symbol) if len(parts) != 2: @@ -46,15 +52,15 @@ def clean_smiles(smi: str) -> str: @staticmethod def filter_valid_molecules(smiles_list: List[str]) -> List[Chem.Mol]: - """ - Convert a list of SMILES to RDKit Mol objects, keeping only valid molecules. + """Filter and sanitize a list of SMILES, returning only valid Mol + objects. - :param smiles_list: A list of SMILES strings. - :type smiles_list: list of str - :returns: A list of sanitized RDKit Mol objects. - :rtype: list of rdkit.Chem.Mol + :param smiles_list: List of SMILES strings to validate. + :type smiles_list: List[str] + :returns: List of sanitized RDKit Mol objects. + :rtype: List[rdkit.Chem.Mol] """ - valid = [] + valid: List[Chem.Mol] = [] for smi in smiles_list: mol = Chem.MolFromSmiles(smi, sanitize=False) if mol: @@ -62,21 +68,21 @@ def filter_valid_molecules(smiles_list: List[str]) -> List[Chem.Mol]: Chem.SanitizeMol(mol) valid.append(mol) except Exception: - pass + continue return valid @staticmethod def standardize_rsmi(rsmi: str, stereo: bool = False) -> Optional[str]: """ - Normalize a reaction SMILES by validating, sorting, and optional stereochemistry. + Normalize a reaction SMILES: validate molecules, sort fragments, optionally keep stereo. - :param rsmi: The reaction SMILES to standardize. + :param rsmi: Reaction SMILES in 'reactants>>products' format. :type rsmi: str - :param stereo: If True, include stereochemical information. Defaults to False. + :param stereo: If True, include stereochemistry in the output. Defaults to False. :type stereo: bool - :returns: The standardized reaction SMILES or None if no valid molecules remain. - :rtype: str or None - :raises ValueError: If the input is not in "reactants>>products" format. + :returns: Standardized reaction SMILES or None if no valid molecules remain. + :rtype: Optional[str] + :raises ValueError: If the input format is invalid. """ try: react_str, prod_str = rsmi.split(">>") @@ -104,16 +110,16 @@ def fit( self, rsmi: str, remove_aam: bool = True, ignore_stereo: bool = True ) -> Optional[str]: """ - Full standardization pipeline: remove atom-maps, normalize SMILES, fix H notation. + Full standardization pipeline: strip atom‑mapping, normalize SMILES, fix hydrogen notation. - :param rsmi: The reaction SMILES to process. + :param rsmi: Reaction SMILES to process. :type rsmi: str - :param remove_aam: If True, strip atom-mapping numbers. Defaults to True. + :param remove_aam: If True, remove atom‑mapping annotations. Defaults to True. :type remove_aam: bool :param ignore_stereo: If True, drop stereochemistry. Defaults to True. :type ignore_stereo: bool - :returns: The processed reaction SMILES or None if standardization fails. - :rtype: str or None + :returns: The standardized reaction SMILES, or None if standardization fails. + :rtype: Optional[str] """ if remove_aam: rsmi = self.remove_atom_mapping(rsmi) @@ -122,27 +128,27 @@ def fit( if std is None: return None - # Explicitly format double hydrogens + # Format any double‑hydrogen notation return std.replace("[HH]", "[H][H]") @staticmethod def categorize_reactions( reactions: List[str], target_reaction: str ) -> Tuple[List[str], List[str]]: - """ - Partition a list of reaction SMILES into those matching a target and those not. + """Partition reactions into those matching a target and those not. - :param reactions: List of reaction SMILES strings to categorize. - :type reactions: list of str - :param target_reaction: The benchmark reaction SMILES for matching. + :param reactions: List of reaction SMILES to categorize. + :type reactions: List[str] + :param target_reaction: Benchmark reaction SMILES for comparison. :type target_reaction: str - :returns: A pair `(matches, non_matches)`: - - `matches`: reactions equal to the standardized target. - - `non_matches`: all others. - :rtype: tuple (list of str, list of str) + :returns: Tuple of (matches, non_matches): + - matches: reactions equal to standardized target + - non_matches: all others + :rtype: Tuple[List[str], List[str]] """ tgt = Standardize.standardize_rsmi(target_reaction, stereo=False) - matches, non_matches = [], [] + matches: List[str] = [] + non_matches: List[str] = [] for rxn in reactions: if rxn == tgt: matches.append(rxn) diff --git a/synkit/Chem/Reaction/tautomerize.py b/synkit/Chem/Reaction/tautomerize.py index 2ac3e51..2da5b2d 100644 --- a/synkit/Chem/Reaction/tautomerize.py +++ b/synkit/Chem/Reaction/tautomerize.py @@ -5,24 +5,22 @@ class Tautomerize: - """ - A class to standardize molecules by converting specific functional groups to their - more common forms using RDKit for molecule manipulation. - """ + """Standardize molecules by converting enol and hemiketal tautomers into + their more stable carbonyl forms, and apply these corrections to individual + SMILES or collections of reaction data.""" @staticmethod def standardize_enol(smiles: str, atom_indices: Optional[List[int]] = None) -> str: - """ - Converts an enol form to a carbonyl form based on specified atom indices. - - Parameters: - - smiles (str): The SMILES string. - - atom_indices (List[int], optional): List containing indices of two carbons and - one oxygen involved in the enol formation. Defaults to [0, 1, 2]. - - Returns: - - str: The SMILES string of the molecule after conversion. - Returns an error message if indices are invalid. + """Convert an enol tautomer into its corresponding carbonyl form. + + :param smiles: SMILES string of the enol-containing molecule. + :type smiles: str + :param atom_indices: List of three atom indices [C1, C2, O] + defining the enol. If None, defaults to [0, 1, 2]. + :type atom_indices: List[int] or None + :returns: SMILES of the molecule after enol→carbonyl conversion, + or an error message if the input is invalid or indices fail. + :rtype: str """ if atom_indices is None: atom_indices = [0, 1, 2] @@ -33,39 +31,40 @@ def standardize_enol(smiles: str, atom_indices: Optional[List[int]] = None) -> s emol = Chem.EditableMol(mol) try: - c1_idx, c2_idx = ( + c_idxs = [ i for i in atom_indices if mol.GetAtomWithIdx(i).GetSymbol() == "C" - ) + ] + c1_idx, c2_idx = c_idxs[:2] o_idx = next( i for i in atom_indices if mol.GetAtomWithIdx(i).GetSymbol() == "O" ) except Exception as e: - return f"Error processing indices: {str(e)}" + return f"Error processing indices: {e}" try: emol.RemoveBond(c1_idx, c2_idx) emol.RemoveBond(c2_idx, o_idx) - emol.AddBond(c1_idx, c2_idx, order=Chem.rdchem.BondType.SINGLE) - emol.AddBond(c2_idx, o_idx, order=Chem.rdchem.BondType.DOUBLE) + emol.AddBond(c1_idx, c2_idx, Chem.rdchem.BondType.SINGLE) + emol.AddBond(c2_idx, o_idx, Chem.rdchem.BondType.DOUBLE) new_mol = emol.GetMol() Chem.SanitizeMol(new_mol) return Chem.MolToSmiles(new_mol) except Exception as e: - return f"Error in modifying molecule: {str(e)}" + return f"Error in modifying molecule: {e}" @staticmethod def standardize_hemiketal(smiles: str, atom_indices: List[int]) -> str: - """ - Converts a hemiketal form to a carbonyl form based on specified atom indices. - - Parameters: - - smiles (str): SMILES representation of the original molecule. - - atom_indices (List[int]): Indices of the carbon and two oxygen atoms - involved in the transformation. - - Returns: - - str: SMILES string of the modified molecule if successful, - otherwise returns an error message. + """Convert a hemiketal tautomer into its corresponding carbonyl form. + + :param smiles: SMILES string of the hemiketal-containing + molecule. + :type smiles: str + :param atom_indices: List of atom indices [C, O1, O2] defining + the hemiketal. + :type atom_indices: List[int] + :returns: SMILES of the molecule after hemiketal→carbonyl + conversion, or an error message if the input is invalid. + :rtype: str """ mol = Chem.MolFromSmiles(smiles) if mol is None: @@ -76,67 +75,65 @@ def standardize_hemiketal(smiles: str, atom_indices: List[int]) -> str: c_idx = next( i for i in atom_indices if mol.GetAtomWithIdx(i).GetSymbol() == "C" ) - o1_idx, o2_idx = ( + o_idxs = [ i for i in atom_indices if mol.GetAtomWithIdx(i).GetSymbol() == "O" - ) + ] + o1_idx = o_idxs[0] + except Exception as e: + return f"Error processing indices: {e}" + + try: emol.RemoveBond(c_idx, o1_idx) - emol.RemoveBond(c_idx, o2_idx) - emol.AddBond(c_idx, o1_idx, order=Chem.rdchem.BondType.DOUBLE) + if len(o_idxs) > 1: + emol.RemoveBond(c_idx, o_idxs[1]) + emol.AddBond(c_idx, o1_idx, Chem.rdchem.BondType.DOUBLE) new_mol = emol.GetMol() Chem.SanitizeMol(new_mol) return Chem.MolToSmiles(new_mol) except Exception as e: - return f"Error in modifying molecule: {str(e)}" + return f"Error in modifying molecule: {e}" @staticmethod def fix_smiles(smiles: str) -> str: - """ - Performs the standardization process by identifying and converting all relevant - functional groups to their target forms based on predefined rules and updates the - SMILES string accordingly. + """Iteratively apply enol and hemiketal standardizations until no + further changes, then return the canonical SMILES. - Parameters: - - smiles (str): SMILES string of the original molecule. - - Returns: - - str: Canonical SMILES string of the standardized molecule. + :param smiles: SMILES string to standardize. + :type smiles: str + :returns: Canonical SMILES of the standardized molecule. + :rtype: str """ query = FGQuery() fg = query.get(smiles) for item in fg: - if "hemiketal" in item: - atom_indices = item[1] - smiles = Tautomerize.standardize_hemiketal(smiles, atom_indices) + label, indices = item + if label == "hemiketal": + smiles = Tautomerize.standardize_hemiketal(smiles, indices) fg = query.get(smiles) - elif "enol" in item: - atom_indices = item[1] - smiles = Tautomerize.standardize_enol(smiles, atom_indices) + elif label == "enol": + smiles = Tautomerize.standardize_enol(smiles, indices) fg = query.get(smiles) return Chem.CanonSmiles(smiles) @staticmethod def fix_dict(data: Dict[str, str], reaction_column: str) -> Dict[str, str]: - """ - Updates a dictionary containing reaction data by - standardizing the SMILES strings of reactants and products. - - Parameters: - - data (Dict[str, str]): Dictionary containing the reaction data. - - reaction_column (str): The key in the dictionary where the reaction SMILES - string is stored. - - Returns: - - Dict[str, str]: The updated dictionary with standardized SMILES strings. + """Standardize the reactant and product SMILES in a reaction + dictionary. + + :param data: Dictionary containing a reaction SMILES under `reaction_column`. + :type data: Dict[str, str] + :param reaction_column: Key in `data` where the reaction SMILES is stored. + :type reaction_column: str + :returns: The same dictionary with standardized reaction SMILES. + :rtype: Dict[str, str] """ try: - reactants, products = data[reaction_column].split(">>") - reactants = Tautomerize.fix_smiles(reactants) - products = Tautomerize.fix_smiles(products) - data[reaction_column] = f"{reactants}>>{products}" + react, prod = data[reaction_column].split(">>") + data[reaction_column] = ( + f"{Tautomerize.fix_smiles(react)}>>{Tautomerize.fix_smiles(prod)}" + ) except ValueError: - smiles = data[reaction_column] - smiles = Tautomerize.fix_smiles(smiles) - data[reaction_column] = smiles + data[reaction_column] = Tautomerize.fix_smiles(data[reaction_column]) return data @staticmethod @@ -146,21 +143,18 @@ def fix_dicts( n_jobs: int = 4, verbose: int = 0, ) -> List[Dict[str, str]]: - """ - Standardizes multiple dictionaries containing - reaction data in parallel. - - Parameters: - - data (List[Dict[str, str]]): List of dictionaries, each containing reaction - data. - - reaction_column (str): The key where the reaction SMILES strings are - stored in each dictionary. - - n_jobs (int, optional): Number of jobs to run in parallel. Defaults to 4. - - verbose (int, optional): The verbosity level. Defaults to 0. - - Returns: - - List[Dict[str, str]]: A list of updated dictionaries - with standardized SMILES strings. + """Standardize multiple reaction dictionaries in parallel. + + :param data: List of dictionaries containing reaction SMILES under `reaction_column`. + :type data: List[Dict[str, str]] + :param reaction_column: Key in each dictionary for the reaction SMILES. + :type reaction_column: str + :param n_jobs: Number of parallel jobs to run. Defaults to 4. + :type n_jobs: int + :param verbose: Verbosity level for the joblib Parallel call. Defaults to 0. + :type verbose: int + :returns: List of dictionaries with standardized SMILES. + :rtype: List[Dict[str, str]] """ results = Parallel(n_jobs=n_jobs, verbose=verbose)( delayed(Tautomerize.fix_dict)(d, reaction_column) for d in data diff --git a/synkit/Chem/utils.py b/synkit/Chem/utils.py index afb05c6..763902e 100644 --- a/synkit/Chem/utils.py +++ b/synkit/Chem/utils.py @@ -1,105 +1,123 @@ from rdkit import Chem -from typing import List, Union -from synkit.IO.debug import setup_logging - -logger = setup_logging() +from rdkit.Chem.MolStandardize import rdMolStandardize +from rdkit.Chem import rdChemReactions +import re +from typing import List, Optional, Tuple, Union + + +def enumerate_tautomers(reaction_smiles: str) -> Optional[List[str]]: + """Enumerate possible tautomers of reactants while canonicalizing products. + + :param reaction_smiles: Reaction SMILES in 'reactants>>products' + format. + :type reaction_smiles: str + :returns: List of reaction SMILES for each reactant tautomer + (including the original), or None on error. + :rtype: Optional[List[str]] + :raises ValueError: If reactant or product SMILES are invalid. + """ + try: + reactants_smiles, products_smiles = reaction_smiles.split(">>") + reactants_mol = Chem.MolFromSmiles(reactants_smiles) + products_mol = Chem.MolFromSmiles(products_smiles) + if reactants_mol is None or products_mol is None: + raise ValueError("Invalid reactant or product SMILES.") + enumerator = rdMolStandardize.TautomerEnumerator() + reactants_tautos = enumerator.Enumerate(reactants_mol) or [reactants_mol] + prod_can = Chem.MolToSmiles(products_mol, canonical=True) + rsmi_list = [Chem.MolToSmiles(m) + ">>" + prod_can for m in reactants_tautos] + rsmi_list.insert(0, reaction_smiles) + return rsmi_list + except ValueError: + raise + except Exception: + return None + + +def mapping_success_rate(list_mapping_data: List[str]) -> float: + """Calculate percentage of entries containing atom‑mapping annotations. + + :param list_mapping_data: List of strings to search for mappings. + :type list_mapping_data: List[str] + :returns: Percentage of entries containing `:` patterns, + rounded to two decimals. + :rtype: float + :raises ValueError: If input list is empty. + """ + if not list_mapping_data: + raise ValueError("The input list is empty, cannot calculate success rate.") + pattern = re.compile(r":\d+") + success = sum(1 for entry in list_mapping_data if pattern.search(entry)) + return round(100 * success / len(list_mapping_data), 2) def count_carbons(smiles: str) -> int: - """ " - Counts the number of carbon atoms in a molecule given a SMILES string. - - Parameters: - - smiles (str): SMILES representation of the molecule. - - Returns: - - int: Number of carbon atoms in the molecule if the SMILES string is valid. - - str: Error message indicating an invalid SMILES string. + """Count the number of carbon atoms in a molecule. + + :param smiles: SMILES string of the molecule. + :type smiles: str + :returns: Number of carbon atoms, or raises ValueError if SMILES + invalid. + :rtype: int + :raises ValueError: If the SMILES string is invalid. """ mol = Chem.MolFromSmiles(smiles) - - if mol: - carbon_count = sum(1 for atom in mol.GetAtoms() if atom.GetSymbol() == "C") - return carbon_count - else: - return "Invalid SMILES string" + if mol is None: + raise ValueError(f"Invalid SMILES string: {smiles}") + return sum(1 for atom in mol.GetAtoms() if atom.GetSymbol() == "C") def get_max_fragment(smiles: Union[str, List[str]]) -> str: - """ - Extracts and returns the SMILES string of the largest fragment from a SMILES string or a list of SMILES strings - of a compound that may contain multiple fragments. This function determines the largest fragment based on the - number of atoms. - - Parameters: - - smiles (Union[str, List[str]]): A single SMILES string or a list of SMILES strings containing potentially - multiple fragments. + """Return the largest fragment by atom count from SMILES. - Returns: - - str: SMILES string of the largest fragment based on the number of atoms. Returns an empty string if no valid - fragments can be processed. - - Examples: - - get_max_fragment("C.CC.CCC") returns "CCC" - - get_max_fragment(["C.CC", "CCC.C"]) returns "CCC" + :param smiles: SMILES string(s), possibly with '.' separators. + :type smiles: str or List[str] + :returns: SMILES of the fragment with the most atoms, or empty + string if none valid. + :rtype: str """ if isinstance(smiles, str): fragments = smiles.split(".") - elif isinstance(smiles, list): - fragments = [frag for s in smiles for frag in s.split(".")] else: + fragments = [frag for s in smiles for frag in s.split(".")] + mols = [Chem.MolFromSmiles(f) for f in fragments if f] + mols = [m for m in mols if m] + if not mols: return "" - - molecules = [Chem.MolFromSmiles(fragment) for fragment in fragments if fragment] - if not molecules: - return "" # Return empty string if no valid molecules are found - - max_mol = max( - molecules, key=lambda mol: mol.GetNumAtoms() if mol else 0, default=None - ) - return Chem.MolToSmiles(max_mol) if max_mol else "" + max_mol = max(mols, key=lambda m: m.GetNumAtoms()) + return Chem.MolToSmiles(max_mol) def filter_smiles(smiles_list: List[str], target_smiles: str) -> List[str]: + """Filter SMILES list to those containing carbon and not equal to a target. + + :param smiles_list: List of SMILES strings to filter. + :type smiles_list: List[str] + :param target_smiles: SMILES string to exclude. + :type target_smiles: str + :returns: Filtered list containing SMILES with at least one carbon atom + and not matching `target_smiles`. + :rtype: List[str] """ - Filters a list of SMILES strings to include only those that contain carbon atoms and are not identical - to a given target SMILES string. - - Parameters: - - smiles_list (List[str]): A list of SMILES strings to be filtered. - - target_smiles (str): The target SMILES string to exclude from the output. - - Returns: - - List[str]: A list of SMILES strings that contain carbon and are not the same as the target SMILES. - """ - filtered_smiles = [] - # Convert target SMILES to a molecule and standardize it for comparison target_mol = Chem.MolFromSmiles(target_smiles) - target_canonical = Chem.MolToSmiles(target_mol) if target_mol else None - - for smiles in smiles_list: - mol = Chem.MolFromSmiles(smiles) - if mol: - # Check if the molecule contains carbon - if any(atom.GetSymbol() == "C" for atom in mol.GetAtoms()): - # Standardize the SMILES for comparison - canonical_smiles = Chem.MolToSmiles(mol) - # Check that the SMILES is not the same as the target SMILES - if canonical_smiles != target_canonical: - filtered_smiles.append(smiles) - - return filtered_smiles + target_can = Chem.MolToSmiles(target_mol) if target_mol else "" + result: List[str] = [] + for smi in smiles_list: + mol = Chem.MolFromSmiles(smi) + if mol and any(atom.GetSymbol() == "C" for atom in mol.GetAtoms()): + can = Chem.MolToSmiles(mol) + if can != target_can: + result.append(smi) + return result def remove_atom_mappings(mol: Chem.Mol) -> Chem.Mol: - """ - Removes atom mapping numbers from a molecule by setting each atom's mapping number to zero. - - Parameters: - - mol (Chem.Mol): The RDKit molecule object from which to remove atom mappings. + """Strip atom‑mapping numbers from a molecule. - Returns: - - Chem.Mol: The same RDKit molecule object with atom mappings removed. + :param mol: RDKit Mol object. + :type mol: Chem.Mol + :returns: The same Mol with all atom‑map numbers set to zero. + :rtype: Chem.Mol """ for atom in mol.GetAtoms(): atom.SetAtomMapNum(0) @@ -107,81 +125,152 @@ def remove_atom_mappings(mol: Chem.Mol) -> Chem.Mol: def get_sanitized_smiles(smiles_list: List[str]) -> List[str]: - """ - Filters and returns a list of sanitizable SMILES strings from the provided list, with atom mappings removed - and excluding any SMILES containing reaction indicators ('->'). - - Parameters: - - smiles_list (List[str]): A list of SMILES strings to be sanitized. + """Sanitize SMILES list by removing mappings and invalid entries. - Returns: - - List[str]: A list of SMILES strings that can be successfully sanitized. + :param smiles_list: List of SMILES strings to sanitize. + :type smiles_list: List[str] + :returns: List of sanitized, isomeric SMILES of the largest + fragments only. + :rtype: List[str] """ - sanitized_smiles = [] + sanitized: List[str] = [] for smiles in smiles_list: - if "->" in smiles: # Skip SMILES with reaction indicators + if "->" in smiles: + continue + mol = Chem.MolFromSmiles(smiles) + if not mol: continue + mol = remove_atom_mappings(mol) try: - # Attempt to create a molecule from the SMILES string - mol = Chem.MolFromSmiles(smiles) - if mol: - # Remove atom mappings before sanitization - mol = remove_atom_mappings(mol) - - # Attempt to sanitize the molecule - Chem.SanitizeMol(mol) - - # If sanitization is successful, append the sanitized SMILES to the result list - sanitized_smiles.append(Chem.MolToSmiles(mol, isomericSmiles=True)) - sanitized_smiles = [get_max_fragment(sanitized_smiles)] - except (Chem.rdchem.ChemicalReactionException, ValueError) as e: - logger.error(e) + Chem.SanitizeMol(mol) + sanitized.append(Chem.MolToSmiles(mol, isomericSmiles=True)) + except Exception: continue - - return sanitized_smiles + # keep only the largest fragment across all + if sanitized: + sanitized = [get_max_fragment(sanitized)] + return sanitized def remove_duplicates(smiles_list: List[str]) -> List[str]: - """ - Removes duplicate SMILES strings from a list, maintaining the order of - first occurrences. Uses a set to track seen SMILES for efficiency. - - Parameters: - - smiles_list (List[str]): A list of SMILES strings representing - chemical reactions. + """Remove duplicate strings from a list, preserving first occurrence. - Returns: - - List[str]: A list with unique SMILES strings, preserving the original order. + :param smiles_list: List of strings (e.g., SMILES) possibly with + duplicates. + :type smiles_list: List[str] + :returns: List with duplicates removed in original order. + :rtype: List[str] """ seen = set() - unique_smiles = [ - smiles for smiles in smiles_list if not (smiles in seen or seen.add(smiles)) - ] - return unique_smiles + unique: List[str] = [] + for s in smiles_list: + if s not in seen: + unique.append(s) + seen.add(s) + return unique def process_smiles_list(smiles_list: List[str]) -> List[str]: + """Split dot‑connected SMILES into individual components. + + :param smiles_list: List of SMILES strings, some containing '.' + separators. + :type smiles_list: List[str] + :returns: Flattened list of component SMILES strings. + :rtype: List[str] """ - Processes a list of SMILES (Simplified Molecular Input Line Entry System) strings, - splitting any entries that contain disconnected molecular components - (indicated by a '.'), and returns a new list with each component as a separate entry. - - Parameters: - - smiles_list (List[str]): A list of SMILES strings, where some entries may contain - disconnected components separated by dots. - - Returns: - - List[str]: A new list of SMILES strings with all components separated. This list - does not include any original strings that contained dots; instead, it - includes their split components. - """ - new_smiles_list = [] # Create a new list to store processed SMILES strings + new_list: List[str] = [] for smiles in smiles_list: if "." in smiles: - # Split the SMILES string into components and extend the new list - components = smiles.split(".") - new_smiles_list.extend(components) + new_list.extend(smiles.split(".")) else: - # Add the unchanged SMILES string to the new list - new_smiles_list.append(smiles) - return new_smiles_list + new_list.append(smiles) + return new_list + + +def remove_explicit_H_from_rsmi(rsmi: str) -> str: + """Remove explicit H atoms from a reaction SMILES, preserving AAM. + + :param rsmi: Atom‑mapped reaction SMILES with explicit hydrogens. + :type rsmi: str + :returns: Simplified reaction SMILES with implicit hydrogens. + :rtype: str + """ + rxn = rdChemReactions.ReactionFromSmarts(rsmi, useSmiles=True) + + def cleaned(mols): + return ".".join( + Chem.MolToSmiles(Chem.RemoveHs(m), isomericSmiles=True) for m in mols + ) + + react = cleaned(rxn.GetReactants()) + prod = cleaned(rxn.GetProducts()) + return f"{react}>>{prod}" + + +def remove_common_reagents(reaction_smiles: str) -> Tuple[Optional[str], Optional[str]]: + """Remove reagents present on both sides of a reaction SMILES. + + :param reaction_smiles: Reaction SMILES 'reactants>>products'. + :type reaction_smiles: str + :returns: Tuple(cleaned_reaction, list_of_removed_reagents or None + if none found). + :rtype: Tuple[str, Optional[List[str]]] + """ + reactants, products = reaction_smiles.split(">>") + reactant_list = reactants.split(".") + product_list = products.split(".") + common_reagents = set(reactant_list) & set(product_list) + + filtered_reactants = [r for r in reactant_list if r not in common_reagents] + filtered_products = [p for p in product_list if p not in common_reagents] + cleaned_reaction_smiles = ( + ".".join(filtered_reactants) + ">>" + ".".join(filtered_products) + ) + + return cleaned_reaction_smiles + + +def reverse_reaction(rsmi: str) -> str: + """Reverse a reaction SMILES. + + :param rsmi: Reaction SMILES 'reactants>>products'. + :type rsmi: str + :returns: Reaction SMILES 'products>>reactants'. + :rtype: str + """ + parts = rsmi.split(">>") + return f"{parts[1]}>>{parts[0]}" if len(parts) == 2 else rsmi + + +def merge_reaction(rsmi_1: str, rsmi_2: str) -> Optional[str]: + """Merge two reaction SMILES into a single combined reaction. + + :param rsmi_1: First reaction SMILES. + :type rsmi_1: str + :param rsmi_2: Second reaction SMILES. + :type rsmi_2: str + :returns: Merged reaction SMILES or None if inputs invalid. + :rtype: Optional[str] + """ + try: + r1, p1 = rsmi_1.split(">>") + r2, p2 = rsmi_2.split(">>") + except ValueError: + return None + if not all([r1, p1, r2, p2]): + return None + return f"{r1}.{r2}>>{p1}.{p2}" + + +def find_longest_fragment(input_list: List[str]) -> Optional[str]: + """Find the longest string in a list. + + :param input_list: List of strings to search. + :type input_list: List[str] + :returns: Longest string or None if list empty. + :rtype: Optional[str] + """ + if not input_list: + return None + return max(input_list, key=len) diff --git a/synkit/Data/gen_partial_aam.py b/synkit/Data/gen_partial_aam.py index 8be300a..f9d5214 100644 --- a/synkit/Data/gen_partial_aam.py +++ b/synkit/Data/gen_partial_aam.py @@ -8,8 +8,8 @@ def _get_partial_aam(smart: str) -> str: - """ - Generate a partial atom‐atom mapping (AAM) SMILES string from a reactant SMARTS. + """Generate a partial atom‐atom mapping (AAM) SMILES string from a reactant + SMARTS. This function: 1. Parses the forward (“reactant”) and backward (“product”) halves of `smart`. @@ -63,8 +63,8 @@ def _get_partial_aam(smart: str) -> str: def _remove_small_smiles(smiles: str) -> str: - """ - Return the canonical SMILES of the largest fragment from an input SMILES. + """Return the canonical SMILES of the largest fragment from an input + SMILES. This function: 1. Parses `smiles` to an RDKit Mol without sanitization. @@ -105,9 +105,8 @@ def _remove_small_smiles(smiles: str) -> str: def _create_unbalanced_aam(rsmi: str, side: str = "right") -> str: - """ - Produce an unbalanced AAM reaction SMILES by keeping only the largest fragment - on the specified side(s) of the reaction. + """Produce an unbalanced AAM reaction SMILES by keeping only the largest + fragment on the specified side(s) of the reaction. :param rsmi: A reaction SMILES "reactant>>product". :type rsmi: str diff --git a/synkit/Graph/Canon/canon_algs.py b/synkit/Graph/Canon/canon_algs.py index b6dd9e8..e36959b 100644 --- a/synkit/Graph/Canon/canon_algs.py +++ b/synkit/Graph/Canon/canon_algs.py @@ -26,8 +26,7 @@ def _digest(text: str) -> Digest: - """ - Compute a 32-character hexadecimal SHA-256 digest of the input string. + """Compute a 32-character hexadecimal SHA-256 digest of the input string. Parameters ---------- @@ -43,8 +42,7 @@ def _digest(text: str) -> Digest: def ring_canonical_graph(g: nx.Graph) -> Tuple[nx.Graph, Digest]: - """ - Generate a relabelled graph based on SSSR membership hierarchy and + """Generate a relabelled graph based on SSSR membership hierarchy and compute its canonical signature. Nodes are ordered by: @@ -91,8 +89,8 @@ def ring_canonical_graph(g: nx.Graph) -> Tuple[nx.Graph, Digest]: def eigen_canonical_signature(g: nx.Graph) -> Digest: - """ - Compute a graph signature from sorted eigenvalues of its weighted adjacency matrix. + """Compute a graph signature from sorted eigenvalues of its weighted + adjacency matrix. Edge weights are taken from the 'order' attribute (default=1). The adjacency matrix is symmetric for undirected graphs. @@ -126,8 +124,7 @@ def eigen_canonical_signature(g: nx.Graph) -> Digest: def pgraph_signature(g: nx.Graph, p: int = 4) -> Digest: - """ - Generate a signature by hashing all simple paths up to length p. + """Generate a signature by hashing all simple paths up to length p. Each path is represented as a hyphen-separated sequence of node 'element' attributes (or '?' if missing), and the sorted list of these sequences @@ -160,8 +157,7 @@ def pgraph_signature(g: nx.Graph, p: int = 4) -> Digest: def canon_morgan( g: nx.Graph, morgan_radius: int = 2, node_attributes: List[str] = None ) -> Tuple[nx.Graph, Digest]: - """ - Prime-based neighbourhood refinement analogous to Morgan fingerprinting. + """Prime-based neighbourhood refinement analogous to Morgan fingerprinting. Each node is initially assigned a unique prime number; optionally, specified node attributes are incorporated into the seed label. @@ -236,8 +232,9 @@ def canon_morgan( # Utility to normalize and hash a node label with its neighbors and edge orders # Utility to normalize and hash a node label with its neighbors and edge orders def _hash_labels(node_label: int, neigh_info: List[Tuple[int, Any]]) -> int: - """ - Combine a node's label with sorted neighbor labels and edge orders into a new hash. + """Combine a node's label with sorted neighbor labels and edge orders into + a new hash. + neigh_info is a list of tuples (neighbor_label, edge_order). """ data = [str(node_label)] diff --git a/synkit/Graph/Canon/canon_graph.py b/synkit/Graph/Canon/canon_graph.py index bcd84dc..53e19a7 100644 --- a/synkit/Graph/Canon/canon_graph.py +++ b/synkit/Graph/Canon/canon_graph.py @@ -100,7 +100,8 @@ def _default_edge_key(u: NodeId, v: NodeId, data: EdgeData) -> Tuple[Any, ...]: def _digest(text: str) -> Digest: - """First 32 hex chars of SHA‑256 – short *but* collision‑safe for up to 2¹²⁸ graphs.""" + """First 32 hex chars of SHA‑256 – short *but* collision‑safe for up to + 2¹²⁸ graphs.""" return hashlib.sha256(text.encode()).hexdigest()[:32] @@ -110,8 +111,7 @@ def _digest(text: str) -> Digest: class GraphCanonicaliser: - """ - Factory that turns arbitrary ``networkx.Graph`` objects into their + """Factory that turns arbitrary ``networkx.Graph`` objects into their *canonical* twin plus a **stable 32‑hex digest**. Parameters @@ -169,8 +169,7 @@ def __init__( # High‑level helpers # # ------------------------------------------------------------------ # def canonicalise_graph(self, graph: nx.Graph) -> "CanonicalGraph": - """ - Return a :class:`CanonicalGraph` wrapper around *graph*. + """Return a :class:`CanonicalGraph` wrapper around *graph*. The wrapper exposes: @@ -183,8 +182,7 @@ def canonicalise_graphs( self, graphs: Iterable[nx.Graph], ) -> Tuple["CanonicalGraph", ...]: - """ - Bulk helper that returns *all* wrappers **sorted by hash**. + """Bulk helper that returns *all* wrappers **sorted by hash**. Useful when you want fast set comparison but need the canonical graphs as well: @@ -203,8 +201,7 @@ def canonicalise_graphs( # Digest / core methods # # ------------------------------------------------------------------ # def canonical_signature(self, graph: nx.Graph) -> Digest: - """ - Return the *hash of the canonical form* of *graph*. + """Return the *hash of the canonical form* of *graph*. Equal digests ⇒ graphs are guaranteed isomorphic **under the chosen back‑end and keys**. @@ -254,8 +251,7 @@ def _canon_generic(self, g: nx.Graph) -> nx.Graph: return G2 def _canon_wl(self, g: nx.Graph) -> nx.Graph: - """ - Weisfeiler–Lehman colour-refinement back-end (pure Python). + """Weisfeiler–Lehman colour-refinement back-end (pure Python). Seeds each node’s initial colour by the tuple of attributes in `self._wl_node_attrs` (e.g. ["element","charge","hcount"]), @@ -352,8 +348,7 @@ def __repr__(self) -> str: # pragma: no cover # Value wrapper (unchanged surface – richer docs) # ============================================================================= class CanonicalGraph: - """ - *Value object* tying together: + """*Value object* tying together: * the **original** NetworkX graph (mutable, user‑supplied); * its **canonical twin** (immutable copy, nodes relabelled 1…N); @@ -428,9 +423,9 @@ def help(self) -> None: # pragma: no cover class CanonicalRule: - """ - Value object that wraps a graph transformation rule in GML string form, - providing a canonicalised GML output and a stable 32-character SHA-256 hash. + """Value object that wraps a graph transformation rule in GML string form, + providing a canonicalised GML output and a stable 32-character SHA-256 + hash. Internally, the GML rule is parsed into a NetworkX graph via `gml_to_its`, canonicalised using a `GraphCanonicaliser`, and re-serialized back to GML @@ -458,8 +453,7 @@ def __init__( rule: str, canon: GraphCanonicaliser = GraphCanonicaliser(), ) -> None: - """ - Instantiate a CanonicalRule. + """Instantiate a CanonicalRule. Parameters ---------- @@ -523,9 +517,7 @@ def canonical_hash(self) -> Digest: return self._canonical_hash def help(self) -> None: - """ - Print original and canonical rule texts and underlying graphs. - """ + """Print original and canonical rule texts and underlying graphs.""" print("Original GML rule:") print(self._original_rule) print("\nCanonical GML rule:") diff --git a/synkit/Graph/Canon/nauty.py b/synkit/Graph/Canon/nauty.py index 2a89193..6a0333c 100644 --- a/synkit/Graph/Canon/nauty.py +++ b/synkit/Graph/Canon/nauty.py @@ -7,18 +7,17 @@ class NautyCanonicalizer: - """ - Perform Nauty‑style canonicalization of a NetworkX graph, optionally - refining and distinguishing nodes and edges by specified attributes, - and extracting automorphisms, orbits, and canonical permutations. + """Perform Nauty‑style canonicalization of a NetworkX graph, optionally + refining and distinguishing nodes and edges by specified attributes, and + extracting automorphisms, orbits, and canonical permutations. - :param node_attrs: List of node attribute keys to include in the initial - partition refinement. Nodes sharing the same tuple of - values under these keys will start in the same cell. + :param node_attrs: List of node attribute keys to include in the + initial partition refinement. Nodes sharing the same tuple of + values under these keys will start in the same cell. :type node_attrs: list[str] | None - :param edge_attrs: List of edge attribute keys to include when distinguishing - edges in the canonical label. If an edge has none of these - keys, its contribution will be empty. + :param edge_attrs: List of edge attribute keys to include when + distinguishing edges in the canonical label. If an edge has + none of these keys, its contribution will be empty. :type edge_attrs: list[str] | None """ @@ -29,12 +28,13 @@ def __init__( node_attrs: Optional[list[str]] = None, edge_attrs: Optional[list[str]] = None, ) -> None: - """ - Initialize the NautyCanonicalizer. + """Initialize the NautyCanonicalizer. - :param node_attrs: Node attribute names to use for initial partitioning. + :param node_attrs: Node attribute names to use for initial + partitioning. :type node_attrs: list[str] | None - :param edge_attrs: Edge attribute names to include in the canonical label. + :param edge_attrs: Edge attribute names to include in the + canonical label. :type edge_attrs: list[str] | None """ self.node_attrs = list(node_attrs) if node_attrs else [] @@ -59,8 +59,8 @@ def canonical_form( return_perm: bool = False, max_depth: Optional[int] = None, ): - """ - Compute canonical form of graph G with optional automorphisms, orbits, and early stopping. + """Compute canonical form of graph G with optional automorphisms, + orbits, and early stopping. :param G: NetworkX graph to canonicalize. :param return_aut: bool, whether to return list of automorphism permutations. @@ -317,4 +317,4 @@ def union_orbits(i, j): def graph_signature(self, G): G_canon = self.canonical_form(G) label = self._build_label(G_canon, sorted(G_canon.nodes())) - return hashlib.sha256(label.encode("utf-8")).hexdigest() + return hashlib.sha256(label.encode("utf-8")).hexdigest() \ No newline at end of file diff --git a/synkit/Graph/Context/hier_context.py b/synkit/Graph/Context/hier_context.py index 4714b34..e99849b 100644 --- a/synkit/Graph/Context/hier_context.py +++ b/synkit/Graph/Context/hier_context.py @@ -11,10 +11,11 @@ class HierContext(RadiusExpand): - """ - Hierarchical clustering class for reaction context graphs. Extends RadiusExpand to build - multi-level graph representations and clusters them based on structural features such as - Weisfeiler-Lehman hashing. + """Hierarchical clustering class for reaction context graphs. + + Extends RadiusExpand to build multi-level graph representations and + clusters them based on structural features such as Weisfeiler-Lehman + hashing. """ def __init__( @@ -24,8 +25,8 @@ def __init__( edge_attribute: str = "order", max_radius: int = 3, ) -> None: - """ - Initializes the HierContext class for hierarchical clustering of reaction context graphs. + """Initializes the HierContext class for hierarchical clustering of + reaction context graphs. Parameters: - node_label_names (List[str]): A list of node attribute names used for matching. @@ -46,8 +47,8 @@ def __init__( def _group_class( data: List[Dict[str, Any]], key: str ) -> Dict[Any, List[Dict[str, Any]]]: - """ - Groups a list of dictionaries into subgroups based on the specified key. + """Groups a list of dictionaries into subgroups based on the specified + key. Parameters: - data (List[Dict[str, Any]]): A list of dictionaries to be grouped. @@ -66,8 +67,8 @@ def _group_class( def _update_child_idx( data: List[List[Dict[str, Any]]], cls_id: str = "class" ) -> List[List[Dict[str, Any]]]: - """ - Updates hierarchical templates by assigning child IDs based on parent–cluster relationships. + """Updates hierarchical templates by assigning child IDs based on + parent–cluster relationships. Parameters: - data (List[List[Dict[str, Any]]]): A list of layers, where each layer is a list of dictionaries @@ -106,9 +107,9 @@ def _process( context_key: str, cls_func: Callable, ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: - """ - Processes a list of graph data entries by extracting context subgraphs and computing - their hashes, then classifies the data using the provided clustering function. + """Processes a list of graph data entries by extracting context + subgraphs and computing their hashes, then classifies the data using + the provided clustering function. Parameters: - data (List[Dict[str, Any]]): A list of dictionaries, each representing a graph or data entry. @@ -147,9 +148,9 @@ def _process_level( cls_func: Callable, radius: int = 1, ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: - """ - Processes a specific hierarchical level by grouping data based on parent cluster IDs, - extracting context for child levels, and clustering the data. + """Processes a specific hierarchical level by grouping data based on + parent cluster IDs, extracting context for child levels, and clustering + the data. Parameters: - data (List[Dict[str, Any]]): A list of dictionaries representing graph data entries. @@ -194,10 +195,11 @@ def fit( its_key: str = "ITS", context_key: str = "K", ) -> Tuple[List[Dict[str, Any]], List[List[Dict[str, Any]]]]: - """ - Processes a list of graph data entries, classifying each based on hierarchical clustering. - The method extracts context subgraphs, computes graph hashes, and clusters the data at multiple - hierarchical levels. Finally, child node indices are updated based on parent–cluster relationships. + """Processes a list of graph data entries, classifying each based on + hierarchical clustering. The method extracts context subgraphs, + computes graph hashes, and clusters the data at multiple hierarchical + levels. Finally, child node indices are updated based on parent–cluster + relationships. Parameters: - original_data (List[Dict[str, Any]]): A list of dictionaries, each representing a graph data entry diff --git a/synkit/Graph/Context/radius_expand.py b/synkit/Graph/Context/radius_expand.py index 22baf09..4006068 100644 --- a/synkit/Graph/Context/radius_expand.py +++ b/synkit/Graph/Context/radius_expand.py @@ -8,9 +8,8 @@ class RadiusExpand: - """ - A utility class for extracting and expanding reaction contexts - from chemical reaction graphs. + """A utility class for extracting and expanding reaction contexts from + chemical reaction graphs. This class provides methods to: - Identify reaction center nodes based on unequal edge orders. @@ -23,17 +22,17 @@ class RadiusExpand: """ def __init__(self) -> None: - """ - Initializes an instance of the RadiusExpand class. + """Initializes an instance of the RadiusExpand class. - This class does not maintain any instance-specific state and uses only static and class methods. + This class does not maintain any instance-specific state and + uses only static and class methods. """ pass @staticmethod def find_unequal_order_edges(G: nx.Graph) -> List[int]: - """ - Identifies reaction center nodes in a graph based on the presence of unequal order edges. + """Identifies reaction center nodes in a graph based on the presence of + unequal order edges. Parameters: - G (nx.Graph): Graph to analyze for reaction centers. @@ -56,8 +55,8 @@ def find_unequal_order_edges(G: nx.Graph) -> List[int]: def find_nearest_neighbors( G: nx.Graph, center_nodes: List[int], n_knn: int = 1 ) -> Set[int]: - """ - Finds the n-level nearest neighbors around the specified center nodes in a graph. + """Finds the n-level nearest neighbors around the specified center + nodes in a graph. Parameters: - G (nx.Graph): The graph in which to search for neighboring nodes. @@ -78,8 +77,8 @@ def find_nearest_neighbors( @staticmethod def extract_subgraph(G: nx.Graph, node_indices: List[int]) -> nx.Graph: - """ - Extracts a subgraph from the original graph containing the specified node indices. + """Extracts a subgraph from the original graph containing the specified + node indices. Parameters: - G (nx.Graph): The original graph. @@ -93,10 +92,9 @@ def extract_subgraph(G: nx.Graph, node_indices: List[int]) -> nx.Graph: @staticmethod def extract_k(its: nx.Graph, n_knn: int = 0) -> Tuple[nx.Graph, Any]: - """ - Constructs the context subgraph (K graph) from an ITS graph - based on reaction centers, and computes the longest extension path - from these centers constrained by 'standard_order' edges. + """Constructs the context subgraph (K graph) from an ITS graph based on + reaction centers, and computes the longest extension path from these + centers constrained by 'standard_order' edges. Parameters: - its (nx.Graph): The ITS graph representing the reaction network. @@ -127,9 +125,8 @@ def context_extraction( context_key: str = "K", n_knn: int = 0, ) -> Dict[str, Any]: - """ - Extracts the reaction context for a single reaction dictionary by computing - both the context subgraph and the longest extension path. + """Extracts the reaction context for a single reaction dictionary by + computing both the context subgraph and the longest extension path. Parameters: - data (Dict[str, Any]): Reaction data containing at least an ITS graph. @@ -143,7 +140,6 @@ def context_extraction( Returns: - Dict[str, Any]: The updated reaction data dictionary including the extracted context subgraph under the key specified by context_key. - """ context_data: Dict[str, Any] = copy.copy(data) its = context_data[its_key] @@ -161,8 +157,8 @@ def paralle_context_extraction( verbose: int = 0, n_knn: int = 0, ) -> List[Dict[str, Any]]: - """ - Performs parallel extraction of reaction contexts for multiple reaction dictionaries. + """Performs parallel extraction of reaction contexts for multiple + reaction dictionaries. Parameters: - data (List[Dict[str, Any]]): A list of reaction data dictionaries, each containing an ITS graph. @@ -186,8 +182,8 @@ def paralle_context_extraction( @staticmethod def remove_normal_edges(graph: nx.Graph, property_key: str) -> nx.Graph: - """ - Removes edges from a graph where the specified edge attribute has a value of 0. + """Removes edges from a graph where the specified edge attribute has a + value of 0. Parameters: - graph (nx.Graph): The input graph to modify. @@ -208,9 +204,9 @@ def remove_normal_edges(graph: nx.Graph, property_key: str) -> nx.Graph: @staticmethod def longest_radius_extension(G: nx.Graph, rc_nodes: List[int]) -> List[int]: - """ - Computes the longest unique extension path in the graph starting from the given reaction center nodes, - constrained by traversing only those edges where the 'standard_order' attribute equals 0. + """Computes the longest unique extension path in the graph starting + from the given reaction center nodes, constrained by traversing only + those edges where the 'standard_order' attribute equals 0. This method uses a depth-first search (DFS) strategy to explore all possible unique paths and returns the longest one. diff --git a/synkit/Graph/Feature/graph_descriptors.py b/synkit/Graph/Feature/graph_descriptors.py index c9b025e..04391e0 100644 --- a/synkit/Graph/Feature/graph_descriptors.py +++ b/synkit/Graph/Feature/graph_descriptors.py @@ -14,8 +14,7 @@ def __init__(self) -> None: @staticmethod def is_graph_empty(graph: Union[nx.Graph, dict, list, Any]) -> bool: - """ - Determine if a graph representation is empty. + """Determine if a graph representation is empty. Parameters: - graph (Union[nx.Graph, dict, list, Any]): A graph representation which can be @@ -40,8 +39,7 @@ def is_graph_empty(graph: Union[nx.Graph, dict, list, Any]) -> bool: @staticmethod def is_acyclic_graph(G: nx.Graph) -> bool: - """ - Determines if the given graph is acyclic. + """Determines if the given graph is acyclic. Parameters: - G (nx.Graph): The graph to be checked. @@ -54,8 +52,7 @@ def is_acyclic_graph(G: nx.Graph) -> bool: @staticmethod def is_single_cyclic_graph(G: nx.Graph) -> bool: - """ - Determines if the given graph has exactly one cycle. + """Determines if the given graph has exactly one cycle. Parameters: - G (nx.Graph): The graph to be checked. @@ -74,8 +71,7 @@ def is_single_cyclic_graph(G: nx.Graph) -> bool: @staticmethod def is_complex_cyclic_graph(G: nx.Graph) -> bool: - """ - Determines if the graph is complex cyclic with multiple cycles. + """Determines if the graph is complex cyclic with multiple cycles. Parameters: - G (nx.Graph): The graph to be checked. @@ -93,8 +89,7 @@ def is_complex_cyclic_graph(G: nx.Graph) -> bool: @staticmethod def check_graph_type(G: nx.Graph) -> str: - """ - Classifies the graph as acyclic, single cyclic, or complex cyclic. + """Classifies the graph as acyclic, single cyclic, or complex cyclic. Parameters: - G (nx.Graph): The graph to be checked. @@ -116,10 +111,9 @@ def check_graph_type(G: nx.Graph) -> str: @staticmethod def get_cycle_member_rings(G: nx.Graph, type="minimal") -> List[int]: - """ - Identifies all cycles in the given graph using cycle bases to ensure no overlap - and returns a list of the sizes of these cycles (member rings), - sorted in ascending order. + """Identifies all cycles in the given graph using cycle bases to ensure + no overlap and returns a list of the sizes of these cycles (member + rings), sorted in ascending order. Parameters: - G (nx.Graph): The NetworkX graph to be analyzed. @@ -142,8 +136,7 @@ def get_cycle_member_rings(G: nx.Graph, type="minimal") -> List[int]: @staticmethod def get_element_count(graph: nx.Graph) -> Dict[str, int]: - """ - Counts occurrences of each element in the graph nodes. + """Counts occurrences of each element in the graph nodes. Parameters: - graph (nx.Graph): A NetworkX graph with 'element' attribute in nodes. @@ -161,8 +154,8 @@ def get_descriptors( its: str = "ITS", condensed: bool = True, ) -> Dict: - """ - Enhance an entry dictionary with topology type and reaction type descriptors. + """Enhance an entry dictionary with topology type and reaction type + descriptors. Parameters: - entry (Dict): A dictionary with reaction data. @@ -208,8 +201,8 @@ def get_descriptors( @staticmethod def _extract_graph(entry: Dict, key: str) -> Union[nx.Graph, None]: - """ - Extracts a graph from an entry dictionary based on the specified key. + """Extracts a graph from an entry dictionary based on the specified + key. Parameters: - entry (Dict): The dictionary containing graph data. @@ -234,8 +227,7 @@ def _extract_graph(entry: Dict, key: str) -> Union[nx.Graph, None]: def _adjust_cycle_and_step( entry: Dict, cycle_key: str, topo_type: str, its_prefix: str = "" ) -> None: - """ - Adjusts cycle and step descriptors based on the graph topology type. + """Adjusts cycle and step descriptors based on the graph topology type. Parameters: - entry (Dict): The entry dictionary to update. @@ -258,8 +250,7 @@ def _adjust_cycle_and_step( @staticmethod def _validate_graph_input(G: nx.Graph) -> None: - """ - Validates that the input is a NetworkX graph. + """Validates that the input is a NetworkX graph. Parameters: - G (nx.Graph): The graph to validate. @@ -279,8 +270,8 @@ def process_entries_in_parallel( n_jobs: int = 4, verbose: int = 0, ) -> List[Dict]: - """ - Processes a list of entries in parallel to enhance each entry with descriptors. + """Processes a list of entries in parallel to enhance each entry with + descriptors. Parameters: - entries (List[Dict]): List of dictionaries containing reaction data to enhance. @@ -304,8 +295,7 @@ def process_entries_in_parallel( def check_graph_connectivity(graph: nx.Graph) -> str: - """ - Check the connectivity of a NetworkX graph. + """Check the connectivity of a NetworkX graph. This function assesses whether all nodes in the graph are connected by some path, applicable to undirected graphs. diff --git a/synkit/Graph/Feature/graph_fps.py b/synkit/Graph/Feature/graph_fps.py index 58b1214..b1ef798 100644 --- a/synkit/Graph/Feature/graph_fps.py +++ b/synkit/Graph/Feature/graph_fps.py @@ -7,9 +7,8 @@ class GraphFP: def __init__( self, graph: nx.Graph, nBits: int = 1024, hash_alg: str = "sha256" ) -> None: - """ - Initialize the GraphFP class to create binary fingerprints based on various graph - characteristics. + """Initialize the GraphFP class to create binary fingerprints based on + various graph characteristics. Parameters: - graph (nx.Graph): Graph on which to perform analysis. @@ -22,8 +21,8 @@ def __init__( self.hash_function = getattr(hashlib, self.hash_alg) def fingerprint(self, method: str) -> str: - """ - Generate a binary string fingerprint of the graph using the specified method. + """Generate a binary string fingerprint of the graph using the + specified method. Parameters: - method (str): The method to use for fingerprinting @@ -78,9 +77,8 @@ def _motif_count_fp(self) -> str: return triangle_str[: self.nBits] def iterative_deepening(self, remaining_bits: int) -> str: - """ - Extend the hash length using iterative hashing until the desired bit length is - achieved. + """Extend the hash length using iterative hashing until the desired bit + length is achieved. Parameters: - remaining_bits (int): Number of bits needed to complete the fingerprint diff --git a/synkit/Graph/Feature/graph_signature.py b/synkit/Graph/Feature/graph_signature.py index 0f558c2..99c9ce3 100644 --- a/synkit/Graph/Feature/graph_signature.py +++ b/synkit/Graph/Feature/graph_signature.py @@ -3,15 +3,17 @@ class GraphSignature: - """ - Provides methods to generate canonical signatures for graph edges (with flexible 'order' and 'state' attributes, - and node degrees/neighbor information), various spectral invariants, adjacency matrix, and complete graphs. - Aims for high uniqueness without relying solely on isomorphism checks. + """Provides methods to generate canonical signatures for graph edges (with + flexible 'order' and 'state' attributes, and node degrees/neighbor + information), various spectral invariants, adjacency matrix, and complete + graphs. + + Aims for high uniqueness without relying solely on isomorphism + checks. """ def __init__(self, graph: nx.Graph): - """ - Initializes the GraphSignature class with a specified graph. + """Initializes the GraphSignature class with a specified graph. Parameters: - graph (nx.Graph): A NetworkX graph instance. @@ -20,10 +22,9 @@ def __init__(self, graph: nx.Graph): self._validate_graph() def _validate_graph(self): - """ - Validates that all nodes have the required attributes ('element' and 'charge'), - and all edges have the required 'order' attribute as int, float, or tuple of two floats, - and optionally the 'state' attribute. + """Validates that all nodes have the required attributes ('element' and + 'charge'), and all edges have the required 'order' attribute as int, + float, or tuple of two floats, and optionally the 'state' attribute. Raises: - ValueError: If any node is missing the 'element' or 'charge' attribute, @@ -61,9 +62,10 @@ def _validate_graph(self): def create_edge_signature( self, include_neighbors: bool = False, max_hop: int = 2 ) -> str: - """ - Generates a canonical edge signature by formatting each edge with sorted node elements (including charge), - node degrees, bond order, bond state, and optionally including neighbor information and topological context. + """Generates a canonical edge signature by formatting each edge with + sorted node elements (including charge), node degrees, bond order, bond + state, and optionally including neighbor information and topological + context. Parameters: - include_neighbors (bool): Whether to include neighbors' details in the edge signature. @@ -139,8 +141,7 @@ def create_edge_signature( return "/".join(sorted(edge_signature_parts)) def _get_khop_neighbors(self, node, max_hop): - """ - Retrieves the k-hop neighborhood information for a given node. + """Retrieves the k-hop neighborhood information for a given node. Parameters: - node (int): The node for which to get neighborhood information. @@ -171,8 +172,8 @@ def _get_khop_neighbors(self, node, max_hop): ) def create_wl_hash(self, iterations: int = 3) -> str: - """ - Generates a Weisfeiler-Lehman (WL) hash for the graph to capture its structural features. + """Generates a Weisfeiler-Lehman (WL) hash for the graph to capture its + structural features. Parameters: - iterations (int): Number of WL iterations to perform. @@ -210,8 +211,8 @@ def create_graph_signature( include_neighbors: bool = True, max_hop: int = 1, ) -> str: - """ - Combines edge, various spectral invariants, and WL hash into a single comprehensive graph signature. + """Combines edge, various spectral invariants, and WL hash into a + single comprehensive graph signature. Parameters: - include_wl_hash (bool): Whether to include the Weisfeiler-Lehman hash. diff --git a/synkit/Graph/Feature/hash_fps.py b/synkit/Graph/Feature/hash_fps.py index 3febabb..8adbebd 100644 --- a/synkit/Graph/Feature/hash_fps.py +++ b/synkit/Graph/Feature/hash_fps.py @@ -7,8 +7,8 @@ class HashFPs: def __init__( self, graph: nx.Graph, numBits: int = 256, hash_alg: str = "sha256" ) -> None: - """ - Initialize the HashFPs class with a graph and configuration settings. + """Initialize the HashFPs class with a graph and configuration + settings. Parameters: - graph (nx.Graph): The graph to be fingerprinted. @@ -37,8 +37,8 @@ def hash_fps( end_node: Optional[int] = None, max_path_length: Optional[int] = None, ) -> str: - """ - Generate a binary hash fingerprint of the graph based on its paths and cycles. + """Generate a binary hash fingerprint of the graph based on its paths + and cycles. Parameters: - start_node (Optional[int]): The starting node index for path detection. @@ -55,7 +55,8 @@ def hash_fps( return full_hash_binary def initialize_hash(self) -> Any: - """Initialize and return the hash object based on the specified algorithm.""" + """Initialize and return the hash object based on the specified + algorithm.""" return getattr(hashlib, self.hash_alg)() def extract_features( @@ -64,8 +65,7 @@ def extract_features( end_node: Optional[int], max_path_length: Optional[int], ) -> str: - """ - Extract features from the graph based on paths and cycles. + """Extract features from the graph based on paths and cycles. Parameters: - start_node (Optional[int]): The starting node for path detection. @@ -90,9 +90,8 @@ def extract_features( return "".join(map(str, features)) def finalize_hash(self, hash_object: Any, features: str) -> str: - """ - Finalize the hash using the features extracted and return the hash as a binary - string. + """Finalize the hash using the features extracted and return the hash + as a binary string. Parameters: - hash_object (Any): The hash object. @@ -110,9 +109,8 @@ def finalize_hash(self, hash_object: Any, features: str) -> str: return full_hash_binary[: self.numBits] def iterative_deepening(self, hash_object: Any, remaining_bits: int) -> str: - """ - Extend hash length using iterative hashing until the desired bit length is - achieved. + """Extend hash length using iterative hashing until the desired bit + length is achieved. Parameters: - hash_object (hashlib._Hash): The hash object for iterative deepening. diff --git a/synkit/Graph/Feature/morgan_fps.py b/synkit/Graph/Feature/morgan_fps.py index 4bef1fa..b04f389 100644 --- a/synkit/Graph/Feature/morgan_fps.py +++ b/synkit/Graph/Feature/morgan_fps.py @@ -11,9 +11,9 @@ def __init__( nBits: int = 1024, hash_alg: str = "sha256", ): - """ - Initialize the MorganFPs class to generate fingerprints based on the Morgan - algorithm, approximating Extended Connectivity Fingerprints (ECFPs). + """Initialize the MorganFPs class to generate fingerprints based on the + Morgan algorithm, approximating Extended Connectivity Fingerprints + (ECFPs). Parameters: - graph (nx.Graph): The graph to analyze. @@ -29,10 +29,9 @@ def __init__( self.hash_function = getattr(hashlib, self.hash_alg) def generate_fingerprint(self) -> str: - """ - Generate a binary string fingerprint of the graph based on the local environments - of nodes. Ensures the output is exactly `nBits` in length using iterative - deepening if necessary. + """Generate a binary string fingerprint of the graph based on the local + environments of nodes. Ensures the output is exactly `nBits` in length + using iterative deepening if necessary. Returns: - str: A binary string of length `nBits` representing the fingerprint of the @@ -68,9 +67,8 @@ def generate_fingerprint(self) -> str: return fingerprint def iterative_deepening(self, hash_object: Any, remaining_bits: int) -> str: - """ - Extend the hash length using iterative hashing until the desired bit length is - achieved. + """Extend the hash length using iterative hashing until the desired bit + length is achieved. Parameters: - hash_object (hashlib._Hash): The hash object used for iterative deepening. diff --git a/synkit/Graph/Feature/path_fps.py b/synkit/Graph/Feature/path_fps.py index b5e4e92..6d9c488 100644 --- a/synkit/Graph/Feature/path_fps.py +++ b/synkit/Graph/Feature/path_fps.py @@ -11,9 +11,8 @@ def __init__( nBits: int = 1024, hash_alg: str = "sha256", ) -> None: - """ - Initialize the PathFPs class to create a binary fingerprint based on paths in a - graph. + """Initialize the PathFPs class to create a binary fingerprint based on + paths in a graph. Parameters: - graph (nx.Graph): Graph on which to perform analysis. @@ -29,9 +28,8 @@ def __init__( self.hash_function = getattr(hashlib, self.hash_alg) def generate_fingerprint(self) -> str: - """ - Generate a binary string fingerprint of the graph by hashing paths up to a certain - length and combining them. + """Generate a binary string fingerprint of the graph by hashing paths + up to a certain length and combining them. Returns: - str: A binary string of length `nBits` that represents the fingerprint of the @@ -63,9 +61,8 @@ def generate_fingerprint(self) -> str: return fingerprint def iterative_deepening(self, hash_object: Any, remaining_bits: int) -> str: - """ - Extend the hash length using iterative hashing until the desired bit length is - achieved. + """Extend the hash length using iterative hashing until the desired bit + length is achieved. Parameters: - hash_object (hashlib._Hash): The hash object used for iterative deepening. diff --git a/synkit/Graph/Feature/wl_hash.py b/synkit/Graph/Feature/wl_hash.py index 5f889dd..89317e3 100644 --- a/synkit/Graph/Feature/wl_hash.py +++ b/synkit/Graph/Feature/wl_hash.py @@ -4,8 +4,7 @@ class WLHash: - """ - A class that implements the Weisfeiler-Lehman graph hashing algorithm, + """A class that implements the Weisfeiler-Lehman graph hashing algorithm, supporting multiple node/edge attributes for hashing. Attributes: @@ -22,8 +21,7 @@ def __init__( iterations: int = 5, digest_size: int = 16, ): - """ - Initializes the WLHash class with configuration for hashing. + """Initializes the WLHash class with configuration for hashing. Parameters: - node: A node attribute name or list of node attribute names. @@ -39,8 +37,8 @@ def __init__( def _prepare_graph( self, graph: nx.Graph ) -> Tuple[nx.Graph, Union[str, None], Union[str, None]]: - """ - Prepare a deep copy of the graph with combined/missing node and edge attributes. + """Prepare a deep copy of the graph with combined/missing node and edge + attributes. Returns (H, node_attr_name, edge_attr_name). """ @@ -86,9 +84,7 @@ def _prepare_graph( return H, node_attr_name, edge_attr_name def weisfeiler_lehman_graph_hash(self, graph: nx.Graph) -> str: - """ - Computes the WL graph hash for the entire graph. - """ + """Computes the WL graph hash for the entire graph.""" G, node_attr, edge_attr = self._prepare_graph(graph) return nx.weisfeiler_lehman_graph_hash( G, @@ -101,9 +97,7 @@ def weisfeiler_lehman_graph_hash(self, graph: nx.Graph) -> str: def weisfeiler_lehman_subgraph_hashes( self, graph: nx.Graph ) -> Dict[Union[int, str], List[str]]: - """ - Computes the WL subgraph hashes for each node in the graph. - """ + """Computes the WL subgraph hashes for each node in the graph.""" G, node_attr, edge_attr = self._prepare_graph(graph) return nx.weisfeiler_lehman_subgraph_hashes( G, @@ -119,8 +113,8 @@ def process_data( graph_key: str = "ITS", subgraph: bool = False, ) -> List[Dict[str, Union[str, None]]]: - """ - Applies WL hashing (or subgraph hashing) to a list of data entries. + """Applies WL hashing (or subgraph hashing) to a list of data entries. + Each entry must contain a graph under 'graph_key'. """ for entry in data: diff --git a/synkit/Graph/Hyrogen/_misc.py b/synkit/Graph/Hyrogen/_misc.py index 98bfea8..866b4b9 100644 --- a/synkit/Graph/Hyrogen/_misc.py +++ b/synkit/Graph/Hyrogen/_misc.py @@ -8,8 +8,7 @@ def has_XH(G: nx.Graph) -> bool: - """ - Check whether the graph contains any heavy atom–hydrogen bond. + """Check whether the graph contains any heavy atom–hydrogen bond. A heavy atom is any atom whose 'element' attribute is not 'H'. This function searches for any edge that connects a heavy atom to a hydrogen atom. @@ -34,8 +33,7 @@ def has_XH(G: nx.Graph) -> bool: def has_HH(G: nx.Graph) -> bool: - """ - Check whether the graph contains any heavy atom–hydrogen bond. + """Check whether the graph contains any heavy atom–hydrogen bond. A heavy atom is any atom whose 'element' attribute is not 'H'. This function searches for any edge that connects a heavy atom to a hydrogen atom. @@ -60,8 +58,7 @@ def has_HH(G: nx.Graph) -> bool: def h_to_implicit(G: nx.Graph) -> nx.Graph: - """ - Convert explicit hydrogen atoms to implicit counts on heavy atoms. + """Convert explicit hydrogen atoms to implicit counts on heavy atoms. For each hydrogen atom ('element' == 'H'), its neighbor (assumed to be a heavy atom) will have its 'hcount' attribute incremented. The hydrogen nodes are then removed. @@ -107,8 +104,8 @@ def normalize_edge_orders(G: nx.Graph) -> None: def h_to_explicit(G: nx.Graph, nodes: List[int] = None, its: bool = False) -> nx.Graph: - """ - Convert implicit hydrogen counts on heavy atoms into explicit hydrogen nodes. + """Convert implicit hydrogen counts on heavy atoms into explicit hydrogen + nodes. For each node ID in `nodes`, this function reads the node's 'hcount', adds that many new hydrogen nodes, connects them to the node with a single bond (order=1.0), and @@ -170,11 +167,11 @@ def h_to_explicit(G: nx.Graph, nodes: List[int] = None, its: bool = False) -> nx def implicit_hydrogen( graph: nx.Graph, preserve_atom_maps: Set[int], reindex: bool = False ) -> nx.Graph: - """ - Adds implicit hydrogens to a molecular graph and removes non-preserved hydrogens. - This function operates on a deep copy of the input graph to avoid in-place modifications. - It counts hydrogen neighbors for each non-hydrogen node and adjusts based on - hydrogens that need to be preserved. Non-preserved hydrogen nodes are removed from the graph. + """Adds implicit hydrogens to a molecular graph and removes non-preserved + hydrogens. This function operates on a deep copy of the input graph to + avoid in-place modifications. It counts hydrogen neighbors for each non- + hydrogen node and adjusts based on hydrogens that need to be preserved. + Non-preserved hydrogen nodes are removed from the graph. Parameters: - graph (nx.Graph): A NetworkX graph representing the molecule, where each node has an 'element' @@ -330,8 +327,7 @@ def implicit_hydrogen( def check_equivariant_graph( its_graphs: List[nx.Graph], ) -> Tuple[List[Tuple[int, int]], int]: - """ - Checks for isomorphism among a list of ITS graphs. + """Checks for isomorphism among a list of ITS graphs. Parameters: - its_graphs (List[nx.Graph]): A list of ITS graphs. @@ -358,8 +354,8 @@ def check_equivariant_graph( def check_explicit_hydrogen(graph: nx.Graph) -> tuple: - """ - Counts the explicit hydrogen nodes in the given graph and collects their IDs. + """Counts the explicit hydrogen nodes in the given graph and collects their + IDs. Parameters: - graph (nx.Graph): The graph to inspect. @@ -376,9 +372,9 @@ def check_explicit_hydrogen(graph: nx.Graph) -> tuple: def check_hcount_change(react_graph: nx.Graph, prod_graph: nx.Graph) -> int: - """ - Computes the maximum change in hydrogen count ('hcount') between corresponding nodes - in the reactant and product graphs. It considers both hydrogen formation and breakage. + """Computes the maximum change in hydrogen count ('hcount') between + corresponding nodes in the reactant and product graphs. It considers both + hydrogen formation and breakage. Parameters: - react_graph (nx.Graph): The graph representing reactants. @@ -409,9 +405,8 @@ def check_hcount_change(react_graph: nx.Graph, prod_graph: nx.Graph) -> int: def get_cycle_member_rings(G: nx.Graph, type="minimal") -> List[int]: - """ - Identifies all cycles in the given graph using cycle bases to ensure no overlap - and returns a list of the sizes of these cycles (member rings), + """Identifies all cycles in the given graph using cycle bases to ensure no + overlap and returns a list of the sizes of these cycles (member rings), sorted in ascending order. Parameters: @@ -435,10 +430,9 @@ def get_cycle_member_rings(G: nx.Graph, type="minimal") -> List[int]: def get_priority(reaction_centers: List[Any]) -> List[int]: - """ - Evaluate reaction centers for specific graph characteristics, selecting indices based - on the shortest reaction paths and maximum ring sizes, and adjusting for certain - graph types by modifying the ring information. + """Evaluate reaction centers for specific graph characteristics, selecting + indices based on the shortest reaction paths and maximum ring sizes, and + adjusting for certain graph types by modifying the ring information. Parameters: - reaction_centers: List[Any], a list of reaction centers where each center should be diff --git a/synkit/Graph/Hyrogen/hcomplete.py b/synkit/Graph/Hyrogen/hcomplete.py index 9ab3839..32e6c1b 100644 --- a/synkit/Graph/Hyrogen/hcomplete.py +++ b/synkit/Graph/Hyrogen/hcomplete.py @@ -20,9 +20,8 @@ class HComplete: - """ - A class for infering hydrogen to complete reaction center or ITS graph. - """ + """A class for infering hydrogen to complete reaction center or ITS + graph.""" @staticmethod def process_single_graph_data( @@ -34,9 +33,8 @@ def process_single_graph_data( get_priority_graph: bool = False, max_hydrogen: int = 7, ) -> Dict[str, Optional[nx.Graph]]: - """ - Processes a single graph data dictionary by modifying hydrogen counts - and other features based on configuration settings. + """Processes a single graph data dictionary by modifying hydrogen + counts and other features based on configuration settings. Parameters: - graph_data (Dict[str, nx.Graph]): Dictionary containing the graph data. @@ -95,8 +93,7 @@ def process_graph_data_parallel( get_priority_graph: bool = False, max_hydrogen: int = 7, ) -> List[Dict[str, Optional[nx.Graph]]]: - """ - Processes a list of graph data dictionaries in parallel to optimize + """Processes a list of graph data dictionaries in parallel to optimize the hydrogen completion and other graph modifications. Parameters: @@ -140,9 +137,9 @@ def process_multiple_hydrogens( balance_its: bool, get_priority_graph: bool = False, ) -> Dict[str, Optional[nx.Graph]]: - """ - Handles significant hydrogen count changes between reactant and product graphs, - adjusting hydrogen nodes accordingly and assessing graph equivalence. + """Handles significant hydrogen count changes between reactant and + product graphs, adjusting hydrogen nodes accordingly and assessing + graph equivalence. Parameters: - graph_data (Dict[str, nx.Graph]): Dictionary containing the graph data. @@ -218,9 +215,9 @@ def add_hydrogen_nodes_multiple( balance_its: bool, get_priority_graph: bool = False, ) -> List[Tuple[nx.Graph, nx.Graph]]: - """ - Generates multiple permutations of reactant and product graphs by adjusting hydrogen counts, - exploring all possible configurations of hydrogen node additions or removals. + """Generates multiple permutations of reactant and product graphs by + adjusting hydrogen counts, exploring all possible configurations of + hydrogen node additions or removals. Parameters: - react_graph (nx.Graph): The reactant graph. @@ -310,9 +307,8 @@ def add_hydrogen_nodes_multiple_utils( node_id_pairs: Iterable[Tuple[int, int]], atom_map_update: bool = True, ) -> nx.Graph: - """ - Creates and returns a new graph with added hydrogen nodes based on the input graph - and node ID pairs. + """Creates and returns a new graph with added hydrogen nodes based on + the input graph and node ID pairs. Parameters: - graph (nx.Graph): The base graph to which the nodes will be added. diff --git a/synkit/Graph/Hyrogen/hextend.py b/synkit/Graph/Hyrogen/hextend.py index 43ddd55..c3e6ad8 100644 --- a/synkit/Graph/Hyrogen/hextend.py +++ b/synkit/Graph/Hyrogen/hextend.py @@ -19,8 +19,8 @@ class HExtend(HComplete): def get_unique_graphs_for_clusters( graphs: List[nx.Graph], cluster_indices: List[set] ) -> List[nx.Graph]: - """ - Retrieve a unique graph for each cluster from a list of graphs based on cluster indices. + """Retrieve a unique graph for each cluster from a list of graphs based + on cluster indices. This method selects one graph per cluster based on the first index found in each cluster set. Note: Clusters are expected to be represented @@ -60,8 +60,7 @@ def _extend( ignore_aromaticity: bool, balance_its: bool, ) -> Tuple[List[nx.Graph], List[nx.Graph], List[str]]: - """ - Process equivalent maps by adding hydrogen nodes and constructing + """Process equivalent maps by adding hydrogen nodes and constructing ITS graphs based on the balance and aromaticity settings. Parameters: @@ -107,9 +106,8 @@ def _process( ignore_aromaticity: bool, balance_its: bool, ) -> Dict: - """ - Processes a dictionary of graphs using specific graph processing functions - and updates the dictionary with new graph data. + """Processes a dictionary of graphs using specific graph processing + functions and updates the dictionary with new graph data. Parameters: - data_dict (Dict): Dictionary containing the graphs and their keys. @@ -143,9 +141,8 @@ def fit( n_jobs: int = 1, verbose: int = 0, ) -> List: - """ - Fit the model to the data in parallel, processing each entry to generate - new graph data based on the ITS and reaction graph keys. + """Fit the model to the data in parallel, processing each entry to + generate new graph data based on the ITS and reaction graph keys. Parameters: - data (iterable): Data to be processed. diff --git a/synkit/Graph/ITS/its_builder.py b/synkit/Graph/ITS/its_builder.py index 8869886..2ab5591 100644 --- a/synkit/Graph/ITS/its_builder.py +++ b/synkit/Graph/ITS/its_builder.py @@ -3,17 +3,17 @@ class ITSBuilder: - """ - Build and annotate an Imaginary Transition State (ITS) graph from a base graph - and a reaction-center (RC) graph. + """Build and annotate an Imaginary Transition State (ITS) graph from a base + graph and a reaction-center (RC) graph. - :cvar None: This class only provides static methods and does not maintain state. + :cvar None: This class only provides static methods and does not + maintain state. """ @staticmethod def update_atom_map(graph: nx.Graph) -> None: - """ - Reset and renumber the 'atom_map' attribute of every node to match its node index. + """Reset and renumber the 'atom_map' attribute of every node to match + its node index. :param graph: The graph whose nodes will be renumbered. :type graph: nx.Graph @@ -31,9 +31,9 @@ def update_atom_map(graph: nx.Graph) -> None: @staticmethod def ITSGraph(G: nx.Graph, RC: nx.Graph) -> nx.Graph: - """ - Create an ITS graph by merging attributes from a reaction-center graph (RC) - into a copy of the base graph G and initializing transition-state metadata. + """Create an ITS graph by merging attributes from a reaction-center + graph (RC) into a copy of the base graph G and initializing transition- + state metadata. The returned ITS graph will have: 1. A deep copy of G’s nodes and edges. @@ -111,4 +111,4 @@ def ITSGraph(G: nx.Graph, RC: nx.Graph) -> nx.Graph: # 7) Renumber atom_map to node indices ITSBuilder.update_atom_map(its) - return its + return its \ No newline at end of file diff --git a/synkit/Graph/ITS/its_construction.py b/synkit/Graph/ITS/its_construction.py index d56bd15..bfa51f7 100644 --- a/synkit/Graph/ITS/its_construction.py +++ b/synkit/Graph/ITS/its_construction.py @@ -12,8 +12,8 @@ def ITSGraph( attributes_defaults: Optional[Dict[str, Any]] = None, balance_its: bool = True, ) -> nx.Graph: - """ - Create a Combined Graph Representation (CGR) by merging nodes and edges of G and H. + """Create a Combined Graph Representation (CGR) by merging nodes and + edges of G and H. The resulting ITS graph: - Uses a deep copy of the smaller (or larger, if balance_its is False) input graph. @@ -81,8 +81,7 @@ def ITSGraph( def get_node_attribute( graph: nx.Graph, node: Hashable, attribute: str, default: Any ) -> Any: - """ - Retrieve a node attribute or return a default if missing. + """Retrieve a node attribute or return a default if missing. :param graph: The graph containing the node. :type graph: nx.Graph @@ -90,7 +89,8 @@ def get_node_attribute( :type node: hashable :param attribute: The name of the attribute to retrieve. :type attribute: str - :param default: The value to return if the attribute is not present. + :param default: The value to return if the attribute is not + present. :type default: Any :returns: The attribute value or the default. :rtype: Any @@ -104,8 +104,7 @@ def get_node_attribute( def get_node_attributes_with_defaults( graph: nx.Graph, node: int, attributes_defaults: Dict[str, Any] = None ) -> Tuple: - """ - Retrieve multiple node attributes, applying defaults where missing. + """Retrieve multiple node attributes, applying defaults where missing. :param graph: The graph containing the node. :type graph: nx.Graph @@ -134,8 +133,7 @@ def get_node_attributes_with_defaults( def add_edges_to_ITS( ITS: nx.Graph, G: nx.Graph, H: nx.Graph, ignore_aromaticity: bool = False ) -> nx.Graph: - """ - Add and label edges in the ITS graph based on presence in G and H. + """Add and label edges in the ITS graph based on presence in G and H. For each edge (u,v) in G or H: - If present in both, label ``order=(order_G, order_H)``. @@ -190,8 +188,8 @@ def add_edges_to_ITS( def add_standard_order_attribute( graph: nx.Graph, ignore_aromaticity: bool = False ) -> nx.Graph: - """ - Compute and attach 'standard_order' to each edge as difference of orders. + """Compute and attach 'standard_order' to each edge as difference of + orders. :param graph: Graph whose edges have ``order=(o_G, o_H)``. :type graph: nx.Graph @@ -286,12 +284,13 @@ def construct( return its def typesGH(self) -> Dict[str, Dict[str, Tuple[Any, Any]]]: - """ - Returns the types and default values for selected node and edge attributes, useful for - interpreting the 'typesGH' annotation on ITS graphs. + """Returns the types and default values for selected node and edge + attributes, useful for interpreting the 'typesGH' annotation on ITS + graphs. - :returns: Dictionary with node and edge attribute types and defaults, e.g. - {"node": {attr: (type, 0)}, "edge": {attr: (type, 0)}} + :returns: Dictionary with node and edge attribute types and + defaults, e.g. {"node": {attr: (type, 0)}, "edge": {attr: + (type, 0)}} :rtype: dict[str, dict[str, tuple[type, Any]]] """ node_prop_types: Dict[str, Any] = { @@ -323,4 +322,4 @@ def typesGH(self) -> Dict[str, Dict[str, Tuple[Any, Any]]]: } node_defaults = {k: (tp, 0) for k, tp in sel_nodes.items()} edge_defaults = {k: (tp, 0) for k, tp in sel_edges.items()} - return {"node": node_defaults, "edge": edge_defaults} + return {"node": node_defaults, "edge": edge_defaults} \ No newline at end of file diff --git a/synkit/Graph/ITS/its_decompose.py b/synkit/Graph/ITS/its_decompose.py index a021c08..c608acb 100644 --- a/synkit/Graph/ITS/its_decompose.py +++ b/synkit/Graph/ITS/its_decompose.py @@ -15,8 +15,7 @@ def get_rc( standard_key: str = "standard_order", disconnected: bool = False, ) -> nx.Graph: - """ - Extract the reaction-center (RC) subgraph from an ITS graph. + """Extract the reaction-center (RC) subgraph from an ITS graph. This function identifies: 1. All bonds whose standard order (difference between ITS orders) is non-zero. @@ -307,8 +306,8 @@ def _add_bond_order_changes( def its_decompose(its_graph: nx.Graph, nodes_share="typesGH", edges_share="order"): - """ - Decompose an ITS graph into two separate reactant (G) and product (H) graphs. + """Decompose an ITS graph into two separate reactant (G) and product (H) + graphs. Nodes and edges in `its_graph` carry composite attributes: - Each node has `its_graph.nodes[nodes_share] = (node_attrs_G, node_attrs_H)`. @@ -379,8 +378,7 @@ def compare_graphs( node_attrs: list = ["element", "aromatic", "hcount", "charge", "neighbors"], edge_attrs: list = ["order"], ) -> bool: - """ - Compare two graphs based on specified node and edge attributes. + """Compare two graphs based on specified node and edge attributes. Parameters: - graph1 (nx.Graph): The first graph to compare. @@ -428,12 +426,12 @@ def compare_graphs( def enumerate_tautomers(reaction_smiles: str) -> Optional[List[str]]: - """ - Enumerates possible tautomers for reactants while canonicalizing the products in a - reaction SMILES string. This function first splits the reaction SMILES string into - reactants and products. It then generates all possible tautomers for the reactants and - canonicalizes the product molecule. The function returns a list of reaction SMILES - strings for each tautomer of the reactants combined with the canonical product. + """Enumerates possible tautomers for reactants while canonicalizing the + products in a reaction SMILES string. This function first splits the + reaction SMILES string into reactants and products. It then generates all + possible tautomers for the reactants and canonicalizes the product + molecule. The function returns a list of reaction SMILES strings for each + tautomer of the reactants combined with the canonical product. Parameters: - reaction_smiles (str): A SMILES string of the reaction formatted as @@ -493,9 +491,8 @@ def enumerate_tautomers(reaction_smiles: str) -> Optional[List[str]]: def mapping_success_rate(list_mapping_data): - """ - Calculate the success rate of entries containing atom mappings in a list of data - strings. + """Calculate the success rate of entries containing atom mappings in a list + of data strings. Parameters: - list_mapping_in_data (list of str): List containing strings to be searched for atom @@ -516,4 +513,4 @@ def mapping_success_rate(list_mapping_data): ) rate = 100 * (success / len(list_mapping_data)) - return round(rate, 2) + return round(rate, 2) \ No newline at end of file diff --git a/synkit/Graph/ITS/its_expand.py b/synkit/Graph/ITS/its_expand.py index 260786a..bb3cf2f 100644 --- a/synkit/Graph/ITS/its_expand.py +++ b/synkit/Graph/ITS/its_expand.py @@ -11,21 +11,23 @@ class ITSExpand: - """ - Partially expand a reaction SMILES (RSMI) by reconstructing intermediate transition states - (ITS) and applying transformation rules based on the reaction center graph. + """Partially expand a reaction SMILES (RSMI) by reconstructing intermediate + transition states (ITS) and applying transformation rules based on the + reaction center graph. - This class identifies the reaction center from an RSMI, builds and reconstructs the ITS graph, - decomposes it back into reactants and products, and standardizes atom mappings to produce - a fully mapped AAM RSMI. + This class identifies the reaction center from an RSMI, builds and + reconstructs the ITS graph, decomposes it back into reactants and + products, and standardizes atom mappings to produce a fully mapped + AAM RSMI. :cvar std: Standardize instance for reaction SMILES standardization. :type std: Standardize """ def __init__(self) -> None: - """ - Initialize ITSExpand. No instance-specific attributes are required. + """Initialize ITSExpand. + + No instance-specific attributes are required. """ pass @@ -35,8 +37,8 @@ def expand_aam_with_its( relabel: bool = False, use_G: bool = True, ) -> str: - """ - Expand a partial reaction SMILES to a full AAM RSMI using ITS reconstruction. + """Expand a partial reaction SMILES to a full AAM RSMI using ITS + reconstruction. :param rsmi: Reaction SMILES string in the format 'reactant>>product'. :type rsmi: str @@ -81,4 +83,4 @@ def expand_aam_with_its( # Convert graphs back to RSMI and standardize atom mappings expanded_rsmi = graph_to_rsmi(new_react, new_prod, its_graph, True, False) - return std.fit(expanded_rsmi, remove_aam=False) + return std.fit(expanded_rsmi, remove_aam=False) \ No newline at end of file diff --git a/synkit/Graph/ITS/its_relabel.py b/synkit/Graph/ITS/its_relabel.py index d3cca6c..be71882 100644 --- a/synkit/Graph/ITS/its_relabel.py +++ b/synkit/Graph/ITS/its_relabel.py @@ -12,26 +12,22 @@ class ITSRelabel: - """ - Extend reaction SMILES through atom-map alignment between reactant and product SynGraphs. + """Extend reaction SMILES through atom-map alignment between reactant and + product SynGraphs. :cvar logger: Logger instance for debug and info messages. :type logger: logging.Logger - :ivar graph_to_mol: Converter from SynGraph to RDKit Mol. :type graph_to_mol: GraphToMol """ def __init__(self) -> None: - """ - Initialize ITSRelabel with default GraphToMol converter. - """ + """Initialize ITSRelabel with default GraphToMol converter.""" self.graph_to_mol = GraphToMol() @staticmethod def _get_nodes_with_atom_map(graph: SynGraph) -> List[Any]: - """ - Extract node IDs with a non-zero atom_map attribute from a SynGraph. + """Extract node IDs with a non-zero atom_map attribute from a SynGraph. :param graph: Input SynGraph with 'atom_map' on nodes. :type graph: SynGraph @@ -46,8 +42,7 @@ def _get_nodes_with_atom_map(graph: SynGraph) -> List[Any]: @staticmethod def _remove_internal_edges(graph: SynGraph, nodes: List[Any]) -> SynGraph: - """ - Remove edges connecting nodes in the given list from a SynGraph. + """Remove edges connecting nodes in the given list from a SynGraph. :param graph: Input SynGraph to prune. :type graph: SynGraph @@ -68,8 +63,7 @@ def _remove_internal_edges(graph: SynGraph, nodes: List[Any]) -> SynGraph: def _dict_to_tuple_list( mapping: Dict[Any, Any], sort_by_key: bool = False, sort_by_value: bool = False ) -> List[Tuple[Any, Any]]: - """ - Convert a mapping dict into a sorted list of tuples. + """Convert a mapping dict into a sorted list of tuples. :param mapping: Dictionary to convert. :type mapping: Dict[Any, Any] @@ -94,19 +88,20 @@ def _update_mapping( mapping: Iterable[Tuple[Any, Any]], aam_key: str = "atom_map", ) -> Tuple[SynGraph, SynGraph]: - """ - Update node attributes in two SynGraphs based on a sequential mapping. + """Update node attributes in two SynGraphs based on a sequential + mapping. - This method resets the specified atom-map attribute for all nodes in both - graphs to 0, then assigns a new atom-map value (i+1) for each mapped pair: - G.nodes[g_node][aam_key] = i + 1 - H.nodes[h_node][aam_key] = i + 1 + This method resets the specified atom-map attribute for all + nodes in both graphs to 0, then assigns a new atom-map value + (i+1) for each mapped pair: G.nodes[g_node][aam_key] = i + 1 + H.nodes[h_node][aam_key] = i + 1 :param G: First SynGraph to update (reactant). :type G: SynGraph :param H: Second SynGraph to update (product). :type H: SynGraph - :param mapping: Iterable of (g_node, h_node) tuples defining node correspondence. + :param mapping: Iterable of (g_node, h_node) tuples defining + node correspondence. :type mapping: Iterable[Tuple[Any, Any]] :param aam_key: Name of the atom-map attribute on each node. :type aam_key: str @@ -138,8 +133,8 @@ def _update_mapping( return G_copy, H_copy def fit(self, rsmi: str) -> str: - """ - Generate an extended reaction SMILES by aligning atom maps of reactant and product. + """Generate an extended reaction SMILES by aligning atom maps of + reactant and product. :param rsmi: Reaction SMILES string formatted as 'reactant>>product'. :type rsmi: str diff --git a/synkit/Graph/ITS/normalize_aam.py b/synkit/Graph/ITS/normalize_aam.py index 4c5fe8f..238fa28 100644 --- a/synkit/Graph/ITS/normalize_aam.py +++ b/synkit/Graph/ITS/normalize_aam.py @@ -11,22 +11,17 @@ class NormalizeAAM: - """ - Provides functionalities to normalize atom mappings in SMILES representations, - extract and process reaction centers from ITS graphs, and convert between - graph representations and molecular models. - """ + """Provides functionalities to normalize atom mappings in SMILES + representations, extract and process reaction centers from ITS graphs, and + convert between graph representations and molecular models.""" def __init__(self) -> None: - """ - Initializes the NormalizeAAM class. - """ + """Initializes the NormalizeAAM class.""" pass @staticmethod def fix_rsmi_kekulize(rsmi: str) -> str: - """ - Filters the reactants and products of a reaction SMILES string. + """Filters the reactants and products of a reaction SMILES string. Parameters: - rsmi (str): A string representing the reaction SMILES in the form of "reactants >> products". @@ -46,8 +41,8 @@ def fix_rsmi_kekulize(rsmi: str) -> str: @staticmethod def fix_kekulize(smiles: str) -> str: - """ - Filters and returns valid SMILES strings from a string of SMILES, joined by '.'. + """Filters and returns valid SMILES strings from a string of SMILES, + joined by '.'. This function processes a string of SMILES separated by periods (e.g., "CCO.CC=O"), filters out invalid SMILES, and returns a string of valid SMILES joined by periods. @@ -73,8 +68,8 @@ def fix_kekulize(smiles: str) -> str: @staticmethod def extract_subgraph(graph: nx.Graph, indices: List[int]) -> nx.Graph: - """ - Extracts a subgraph from a given graph based on a list of node indices. + """Extracts a subgraph from a given graph based on a list of node + indices. Parameters: graph (nx.Graph): The original graph from which to extract the subgraph. @@ -88,8 +83,8 @@ def extract_subgraph(graph: nx.Graph, indices: List[int]) -> nx.Graph: def reset_indices_and_atom_map( self, subgraph: nx.Graph, aam_key: str = "atom_map" ) -> nx.Graph: - """ - Resets the node indices and the atom_map of the subgraph to be continuous from 1 onwards. + """Resets the node indices and the atom_map of the subgraph to be + continuous from 1 onwards. Parameters: subgraph (nx.Graph): The subgraph with possibly non-continuous indices. @@ -111,9 +106,9 @@ def reset_indices_and_atom_map( return new_graph def fit(self, rsmi: str, fix_aam_indice: bool = True) -> str: - """ - Processes a reaction SMILES (RSMI) to adjust atom mappings, extract reaction centers, - decompose into separate reactant and product graphs, and generate the corresponding SMILES. + """Processes a reaction SMILES (RSMI) to adjust atom mappings, extract + reaction centers, decompose into separate reactant and product graphs, + and generate the corresponding SMILES. Parameters: - rsmi (str): The reaction SMILES string to be processed. diff --git a/synkit/Graph/MTG/group_comp.py b/synkit/Graph/MTG/group_comp.py index 3fb23f6..dc83c53 100644 --- a/synkit/Graph/MTG/group_comp.py +++ b/synkit/Graph/MTG/group_comp.py @@ -58,8 +58,7 @@ def get_mapping_from_nodes( edges2: Iterable[Edge], ) -> MappingList: """Return *single‑node* mappings ``[{v₁: v₂}, …]`` that obey the - groupoid order rule w.r.t **all** incident edges on each side. - """ + groupoid order rule w.r.t **all** incident edges on each side.""" # Index incident edges once – O(|E|) inc1: Dict[NodeId, List[Edge]] = defaultdict(list) inc2: Dict[NodeId, List[Edge]] = defaultdict(list) diff --git a/synkit/Graph/MTG/groupoid.py b/synkit/Graph/MTG/groupoid.py index ce21740..b5d473b 100644 --- a/synkit/Graph/MTG/groupoid.py +++ b/synkit/Graph/MTG/groupoid.py @@ -50,7 +50,8 @@ def node_constraint( nodes1: Iterable[Node], nodes2: Iterable[Node], ) -> Dict[NodeId, List[NodeId]]: - """Compute candidate node mappings based on element and groupoid charge rule. + """Compute candidate node mappings based on element and groupoid charge + rule. For each node v1 in nodes1 and v2 in nodes2, v2 is a candidate if: 1. v1.attrs['element'] == v2.attrs['element'], and @@ -169,9 +170,9 @@ def _edge_constraint_vf2( edges2: Iterable[Edge], node_mapping: Optional[Mapping[NodeId, List[NodeId]]] = None, ) -> MappingList: - """ - VF2‐style routine, fully in Python (no NetworkX), seeded like VF3 but + """VF2‐style routine, fully in Python (no NetworkX), seeded like VF3 but relaxed so it returns the same maximal‐common‐subgraph mappings. + The returned dicts will always have their keys sorted ascending. """ # --- build adjacency lists with valid 'order' tuples --- diff --git a/synkit/Graph/MTG/mcs_matcher.py b/synkit/Graph/MTG/mcs_matcher.py index fe5cfdd..e8a6228 100644 --- a/synkit/Graph/MTG/mcs_matcher.py +++ b/synkit/Graph/MTG/mcs_matcher.py @@ -183,7 +183,8 @@ def find_rc_mapping(self, rc1, rc2, *, mcs: bool = False) -> None: # type: igno # Properties and dunders # ------------------------------------------------------------------ def get_mappings(self) -> List[Dict[int, int]]: - """Return the cached mapping list (empty if `find_*` not yet called).""" + """Return the cached mapping list (empty if `find_*` not yet + called).""" return self._mappings.copy() @property diff --git a/synkit/Graph/MTG/mtg.py b/synkit/Graph/MTG/mtg.py index 31b9fab..613ce68 100644 --- a/synkit/Graph/MTG/mtg.py +++ b/synkit/Graph/MTG/mtg.py @@ -117,7 +117,8 @@ def _fuse_nodes(self): def _insert_edges_from( self, edge_iter, node_map: Dict[NodeID, NodeID], existing: List[Edge] = None ) -> List[Edge]: - """Insert edges into *existing* applying the groupoid rule when possible.""" + """Insert edges into *existing* applying the groupoid rule when + possible.""" existing = [] if existing is None else existing.copy() # Remap and append new edges diff --git a/synkit/Graph/Matcher/batch_cluster.py b/synkit/Graph/Matcher/batch_cluster.py index 3657458..db58290 100644 --- a/synkit/Graph/Matcher/batch_cluster.py +++ b/synkit/Graph/Matcher/batch_cluster.py @@ -19,9 +19,8 @@ def __init__( edge_attribute: str = "order", backend: str = "nx", ): - """ - Initializes an AutoCat instance which uses isomorphism checks for categorizing - new graphs or rules. + """Initializes an AutoCat instance which uses isomorphism checks for + categorizing new graphs or rules. Parameters: - node_label_names (List[str]): Names of the node attributes to use in @@ -75,8 +74,8 @@ def lib_check( nodeMatch: Optional[Callable] = None, edgeMatch: Optional[Callable] = None, ) -> Dict: - """ - Checks and classifies a graph or rule based on existing templates using either graph or rule isomorphism. + """Checks and classifies a graph or rule based on existing templates + using either graph or rule isomorphism. Parameters: - data (Dict): A dictionary representing a graph or rule with its attributes and @@ -138,8 +137,7 @@ def lib_check( @staticmethod def batch_dicts(input_list, batch_size): - """ - Splits a list of dictionaries into batches of a specified size. + """Splits a list of dictionaries into batches of a specified size. Args: input_list (list of dict): The list of dictionaries to be batched. @@ -175,8 +173,8 @@ def cluster( rule_key: str = "gml", attribute_key: str = "WLHash", ) -> Tuple[List[Dict], List[Dict]]: - """ - Processes a list of graph data entries, classifying each based on existing templates. + """Processes a list of graph data entries, classifying each based on + existing templates. Parameters: - data (List[Dict]): A list of dictionaries, each representing a graph or rule @@ -199,10 +197,10 @@ def fit( attribute_key: str = "WLHash", batch_size: Optional[int] = None, ) -> Tuple[List[Dict], List[Dict]]: - """ - Processes and classifies data in batches. Uses GraphCluster for initial processing - and a stratified sampling technique to update templates if there is only one batch - and no initial templates are provided. + """Processes and classifies data in batches. Uses GraphCluster for + initial processing and a stratified sampling technique to update + templates if there is only one batch and no initial templates are + provided. Parameters: - data (List[Dict]): Data to process. diff --git a/synkit/Graph/Matcher/graph_cluster.py b/synkit/Graph/Matcher/graph_cluster.py index aa5c936..7777371 100644 --- a/synkit/Graph/Matcher/graph_cluster.py +++ b/synkit/Graph/Matcher/graph_cluster.py @@ -21,10 +21,10 @@ def __init__( edge_attribute: str = "order", backend: str = "nx", ): - """ - Initializes the GraphCluster with customization options for node and edge - matching functions. This class is designed to facilitate clustering of graph nodes - and edges based on specified attributes and their matching criteria. + """Initializes the GraphCluster with customization options for node and + edge matching functions. This class is designed to facilitate + clustering of graph nodes and edges based on specified attributes and + their matching criteria. Parameters: - node_label_names (List[str]): A list of node attribute names to be considered @@ -84,9 +84,9 @@ def iterative_cluster( nodeMatch: Optional[Callable] = None, edgeMatch: Optional[Callable] = None, ) -> Tuple[List[Set[int]], Dict[int, int]]: - """ - Clusters rules based on their similarities, which could include structural or - attribute-based similarities depending on the given attributes. + """Clusters rules based on their similarities, which could include + structural or attribute-based similarities depending on the given + attributes. Parameters: - rules (List[str]): List of rules, potentially serialized strings of rule @@ -159,10 +159,9 @@ def fit( attribute_key: str = "WLHash", strip: bool = False, ) -> List[Dict]: - """ - Automatically clusters the rules and assigns them cluster indices based on the - similarity, potentially using provided templates for clustering, or generating - new templates. + """Automatically clusters the rules and assigns them cluster indices + based on the similarity, potentially using provided templates for + clustering, or generating new templates. Parameters: - data (List[Dict]): A list containing dictionaries, each representing a diff --git a/synkit/Graph/Matcher/graph_morphism.py b/synkit/Graph/Matcher/graph_morphism.py index 137a295..c608acb 100644 --- a/synkit/Graph/Matcher/graph_morphism.py +++ b/synkit/Graph/Matcher/graph_morphism.py @@ -1,380 +1,516 @@ -import logging -import itertools -from operator import eq -from typing import Callable, Optional, Union, List, Any, Dict +import re import networkx as nx -from networkx.algorithms import isomorphism -from networkx.algorithms.isomorphism import GraphMatcher -from networkx.algorithms.isomorphism import generic_node_match, generic_edge_match +from typing import Optional, List +from rdkit import Chem +from rdkit.Chem.MolStandardize import rdMolStandardize -# Alias for any NetworkX graph type -graph_types = Union[nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph] +__all__ = ["get_rc", "its_decompose"] -def find_graph_isomorphism( - G1: graph_types, - G2: graph_types, - node_match: Optional[Callable[[Dict[str, Any], Dict[str, Any]], bool]] = None, - edge_match: Optional[Callable[[Dict[str, Any], Dict[str, Any]], bool]] = None, - use_defaults: bool = True, - fast_invariant_check: bool = True, - logger: Optional[logging.Logger] = None, -) -> Optional[Dict[Any, Any]]: - """ - Check whether two graphs are isomorphic and return the node-mapping. - - :param G1: The first NetworkX graph to compare. - :type G1: nx.Graph | nx.DiGraph | nx.MultiGraph | nx.MultiDiGraph - :param G2: The second NetworkX graph to compare. - :type G2: nx.Graph | nx.DiGraph | nx.MultiGraph | nx.MultiDiGraph - :param node_match: Optional function taking two node attribute dicts and - returning True if they match. - :type node_match: callable or None - :param edge_match: Optional function taking two edge attribute dicts and - returning True if they match. - :type edge_match: callable or None - :param use_defaults: Whether to use default matchers when None. - :type use_defaults: bool - :param fast_invariant_check: Perform quick node/edge count and degree - sequence checks prior to matcher. - :type fast_invariant_check: bool - :param logger: Logger for debug messages. Defaults to root logger. - :type logger: logging.Logger or None - - :returns: A dict mapping nodes in G1 to nodes in G2 if isomorphic; otherwise None. - :rtype: dict[Any, Any] or None +def get_rc( + ITS: nx.Graph, + element_key: List[str] = ["element", "charge", "typesGH", "atom_map"], + bond_key: str = "order", + standard_key: str = "standard_order", + disconnected: bool = False, +) -> nx.Graph: + """Extract the reaction-center (RC) subgraph from an ITS graph. + + This function identifies: + 1. All bonds whose standard order (difference between ITS orders) is non-zero. + 2. All H–H bonds, ensuring they are included even if no order change is detected. + 3. (Optional) Additional nodes with charge changes and reconnection of edges + if `disconnected=True`. + + :param ITS: The integrated transition-state graph with composite node/edge attributes. + :type ITS: nx.Graph + :param element_key: List of node‐attribute keys to copy into the RC graph. + :type element_key: List[str] + :param bond_key: Edge attribute key representing the tuple of bond orders. + :type bond_key: str + :param standard_key: Edge attribute key for the computed standard_order. + :type standard_key: str + :param disconnected: If True, also include nodes with charge changes and + reconnect any ITS edges between RC nodes. + :type disconnected: bool + :returns: A new graph containing only the reaction-center nodes and edges. + :rtype: nx.Graph + + :example: + >>> ITS = nx.Graph() + >>> # ... populate ITS with 'order', 'standard_order', 'typesGH', etc. ... + >>> RC = get_rc(ITS, disconnected=True) + >>> isinstance(RC, nx.Graph) + True """ - log = logger or logging.getLogger(__name__) - - # 1) Ensure same graph type - if type(G1) is not type(G2): - log.debug("Graph types differ: %r vs %r", type(G1), type(G2)) - return None - - # 2) Quick invariants - if fast_invariant_check: - if G1.number_of_nodes() != G2.number_of_nodes(): - log.debug( - "Node counts differ: %d vs %d", - G1.number_of_nodes(), - G2.number_of_nodes(), - ) - return None - if G1.number_of_edges() != G2.number_of_edges(): - log.debug( - "Edge counts differ: %d vs %d", - G1.number_of_edges(), - G2.number_of_edges(), + rc = nx.Graph() + _add_bond_order_changes(ITS, rc, element_key, bond_key, standard_key) + + # 1.5) H-H bonds (force inclusion, with fallback typesGH) + for u, v, data in ITS.edges(data=True): + elem_u = ITS.nodes[u].get("element") + elem_v = ITS.nodes[v].get("element") + if elem_u == "H" and elem_v == "H": + for n in (u, v): + node_data = dict(ITS.nodes[n]) + if "typesGH" not in node_data: + node_data["typesGH"] = ( + ("H", False, 0, 0, []), + ("*", False, 0, 0, []), + ) + # Ensure typesGH is available even if not in original element_key + final_attrs = {k: node_data[k] for k in element_key if k in node_data} + final_attrs["typesGH"] = node_data["typesGH"] + rc.add_node(n, **final_attrs) + + rc.add_edge( + u, + v, + **{ + bond_key: data.get(bond_key), + standard_key: data.get(standard_key), + }, ) - return None - degs1 = sorted(d for _, d in G1.degree()) - degs2 = sorted(d for _, d in G2.degree()) - if degs1 != degs2: - log.debug("Degree sequences differ") - return None - - # 3) Default matchers - if use_defaults: - if node_match is None: - node_match = isomorphism.categorical_node_match( - ["element", "atom_map", "hcount"], ["*", 0, 0] + if disconnected: + _add_charge_change_nodes(ITS, rc, element_key) + _reconnect_rc_edges(ITS, rc, bond_key, standard_key) + + return rc + + +def _carry_node_attrs(src: nx.Graph, dst: nx.Graph, n: int, keys: List[str]) -> None: + """Copy node *n* from *src* to *dst* with only *keys* attributes.""" + if dst.has_node(n): + return + attrs = {k: src.nodes[n][k] for k in keys if k in src.nodes[n]} + dst.add_node(n, **attrs) + + +def _add_charge_change_nodes( + ITS: nx.Graph, + rc: nx.Graph, + keys: List[str], +) -> None: + """Step 3a – add nodes whose *typesGH* shows a charge change.""" + for n, data in ITS.nodes(data=True): + gh = data.get("typesGH") + if ( + isinstance(gh, (list, tuple)) + and len(gh) >= 2 + and gh[0][3] != gh[1][3] + and not rc.has_node(n) + ): + _carry_node_attrs(ITS, rc, n, keys) + + +def _reconnect_rc_edges( + ITS: nx.Graph, + rc: nx.Graph, + bond_key: str, + standard_key: str, +) -> None: + """Step 3b – re-add any original ITS edge between nodes already in RC.""" + for u, v, data in ITS.edges(data=True): + if rc.has_node(u) and rc.has_node(v) and not rc.has_edge(u, v): + rc.add_edge( + u, + v, + **{bond_key: data.get(bond_key), standard_key: data.get(standard_key)}, ) - if edge_match is None: - edge_match = isomorphism.categorical_edge_match("order", 1) - # 4) Select the correct matcher - if isinstance(G1, (nx.MultiGraph, nx.MultiDiGraph)): - if isinstance(G1, nx.MultiGraph): - Matcher = nx.algorithms.isomorphism.MultiGraphMatcher - else: - Matcher = nx.algorithms.isomorphism.MultiDiGraphMatcher - else: - if isinstance(G1, nx.Graph): - Matcher = nx.algorithms.isomorphism.GraphMatcher - else: - Matcher = nx.algorithms.isomorphism.DiGraphMatcher - - matcher = Matcher(G1, G2, node_match=node_match, edge_match=edge_match) - if matcher.is_isomorphic(): - log.debug("Graphs are isomorphic; mapping found") - return matcher.mapping - else: - log.debug("Graphs are not isomorphic") - return None - - -def graph_isomorphism( - graph_1: nx.Graph, - graph_2: nx.Graph, - node_match: Optional[Callable] = None, - edge_match: Optional[Callable] = None, - use_defaults: bool = False, -) -> bool: - """ - Determines if two graphs are isomorphic, considering provided node and edge matching - functions. Uses default matching settings if none are provided. - Parameters: - - graph_1 (nx.Graph): The first graph to compare. - - graph_2 (nx.Graph): The second graph to compare. - - node_match (Optional[Callable]): The function used to match nodes. - Uses default if None. - - edge_match (Optional[Callable]): The function used to match edges. - Uses default if None. +def _add_bond_order_changes( + ITS: nx.Graph, + rc: nx.Graph, + keys: List[str], + bond_key: str, + standard_key: str, +) -> None: + """Step 1 – bond-order-change edges and their nodes.""" + for u, v, data in ITS.edges(data=True): + old, new = data.get(bond_key, (None, None)) + if old == new: + continue + for n in (u, v): + _carry_node_attrs(ITS, rc, n, keys) + rc.add_edge( + u, v, **{bond_key: data[bond_key], standard_key: data.get(standard_key)} + ) - Returns: - - bool: True if the graphs are isomorphic, False otherwise. + +# def get_rc( +# ITS: nx.Graph, +# element_key: List[str] = ["element", "charge", "typesGH", "atom_map"], +# bond_key: str = "order", +# standard_key: str = "standard_order", +# disconnected: bool = False, +# ) -> nx.Graph: +# """ +# Extract the reaction center (RC) from ITS graph. + +# Enhancements: +# - Adds nodes and edges where bond order changes (core logic). +# - If disconnected=True: +# - Adds nodes with charge change based on typesGH. +# - Reconnects any ITS edge between two RC nodes. +# - NEW: Always includes H-H bonds in RC. Adds default typesGH if missing. +# """ +# rc = nx.Graph() + +# # 1) edges with bond-order change +# for u, v, data in ITS.edges(data=True): +# old, new = data.get(bond_key, [None, None]) +# if old != new: +# for n in (u, v): +# if not rc.has_node(n): +# rc.add_node( +# n, +# **{ +# k: ITS.nodes[n][k] for k in element_key if k in ITS.nodes[n] +# }, +# ) +# rc.add_edge( +# u, +# v, +# **{bond_key: data.get(bond_key), standard_key: data.get(standard_key)}, +# ) + +# # 1.5) H-H bonds (force inclusion, with fallback typesGH) +# for u, v, data in ITS.edges(data=True): +# elem_u = ITS.nodes[u].get("element") +# elem_v = ITS.nodes[v].get("element") +# if elem_u == "H" and elem_v == "H": +# for n in (u, v): +# node_data = dict(ITS.nodes[n]) +# if "typesGH" not in node_data: +# node_data["typesGH"] = ( +# ("H", False, 0, 0, []), +# ("*", False, 0, 0, []), +# ) +# # Ensure typesGH is available even if not in original element_key +# final_attrs = {k: node_data[k] for k in element_key if k in node_data} +# final_attrs["typesGH"] = node_data["typesGH"] +# rc.add_node(n, **final_attrs) + +# rc.add_edge( +# u, +# v, +# **{ +# bond_key: data.get(bond_key), +# standard_key: data.get(standard_key), +# }, +# ) + +# if disconnected: +# # 2) nodes with typesGH-based charge change +# for n, data in ITS.nodes(data=True): +# gh = data.get("typesGH") +# if ( +# isinstance(gh, (list, tuple)) +# and len(gh) >= 2 +# and len(gh[0]) > 3 +# and len(gh[1]) > 3 +# and gh[0][3] != gh[1][3] +# ): +# if not rc.has_node(n): +# rc.add_node(n, **{k: data[k] for k in element_key if k in data}) + +# # 3) reconnect RC nodes +# for u, v, data in ITS.edges(data=True): +# if rc.has_node(u) and rc.has_node(v) and not rc.has_edge(u, v): +# rc.add_edge( +# u, +# v, +# **{ +# bond_key: data.get(bond_key), +# standard_key: data.get(standard_key), +# }, +# ) + +# return rc + + +# def get_rc( +# ITS: nx.Graph, +# element_key: List[str] = ["element", "charge", "typesGH", "atom_map"], +# bond_key: str = "order", +# standard_key: str = "standard_order", +# disconnected: bool = False, +# ) -> nx.Graph: +# """ +# Extract the reaction center (RC) from ITS by: + +# 1. Always adding any edge whose bond order changes +# (bond_key[0] != bond_key[1]), plus its two end-nodes. +# 2. [if disconnected=True] Adding any node whose 'typesGH' record shows a charge change +# (typesGH[0][3] != typesGH[1][3]), even if isolated. +# 3. [if disconnected=True] Re-adding any ITS edge between two nodes already in RC +# (to preserve connectivity), carrying over bond_key & standard_key. + +# Parameters: +# - ITS (nx.Graph): input ITS graph. +# - element_key (List[str]): node attrs to carry over. +# - bond_key (str): edge attr key for bond order. +# - standard_key (str): edge attr key for standard order. +# - disconnected (bool): if True, include “charge-change” nodes (step 2) and +# reconnect any edges among RC nodes (step 3). If False, only performs step 1. +# """ +# rc = nx.Graph() + +# # 1) edges with bond-order change +# for u, v, data in ITS.edges(data=True): +# old, new = data.get(bond_key, [None, None]) +# if old != new: +# for n in (u, v): +# if not rc.has_node(n): +# rc.add_node( +# n, +# **{ +# k: ITS.nodes[n][k] for k in element_key if k in ITS.nodes[n] +# }, +# ) +# rc.add_edge( +# u, +# v, +# **{bond_key: data.get(bond_key), standard_key: data.get(standard_key)}, +# ) + +# if disconnected: +# # 2) nodes with a typesGH-based charge change +# for n, data in ITS.nodes(data=True): +# gh = data.get("typesGH") +# if ( +# isinstance(gh, (list, tuple)) +# and len(gh) >= 2 +# and len(gh[0]) > 3 +# and len(gh[1]) > 3 +# and gh[0][3] != gh[1][3] +# ): +# if not rc.has_node(n): +# rc.add_node(n, **{k: data[k] for k in element_key if k in data}) + +# # 3) re-add any ITS edge between RC nodes to preserve connectivity +# for u, v, data in ITS.edges(data=True): +# if rc.has_node(u) and rc.has_node(v) and not rc.has_edge(u, v): +# rc.add_edge( +# u, +# v, +# **{ +# bond_key: data.get(bond_key), +# standard_key: data.get(standard_key), +# }, +# ) + +# return rc + + +def its_decompose(its_graph: nx.Graph, nodes_share="typesGH", edges_share="order"): + """Decompose an ITS graph into two separate reactant (G) and product (H) + graphs. + + Nodes and edges in `its_graph` carry composite attributes: + - Each node has `its_graph.nodes[nodes_share] = (node_attrs_G, node_attrs_H)`. + - Each edge has `its_graph.edges[edges_share] = (order_G, order_H)`. + + This function splits those tuples to reconstruct the original G and H graphs. + + :param its_graph: The ITS graph with composite node/edge attributes. + :type its_graph: nx.Graph + :param nodes_share: Node attribute key storing (G_attrs, H_attrs) tuples. + :type nodes_share: str + :param edges_share: Edge attribute key storing (order_G, order_H) tuples. + :type edges_share: str + :returns: A tuple of two graphs (G, H) reconstructed from the ITS. + :rtype: Tuple[nx.Graph, nx.Graph] + + :example: + >>> its = nx.Graph() + >>> # ... set its.nodes[n]['typesGH'] and its.edges[e]['order'] ... + >>> G, H = its_decompose(its) + >>> isinstance(G, nx.Graph) and isinstance(H, nx.Graph) + True """ - # Define default node and edge attributes and match settings - if use_defaults: - node_label_names = ["element", "charge"] - node_label_default = ["*", 0] - edge_attribute = "order" - - # Default node and edge match functions if not provided - if node_match is None: - node_match = generic_node_match( - node_label_names, node_label_default, [eq] * len(node_label_names) + G = nx.Graph() + H = nx.Graph() + + # Decompose nodes + for node, data in its_graph.nodes(data=True): + if nodes_share in data: + node_attr_g, node_attr_h = data[nodes_share] + # Unpack node attributes for G + G.add_node( + node, + element=node_attr_g[0], + aromatic=node_attr_g[1], + hcount=node_attr_g[2], + charge=node_attr_g[3], + neighbors=node_attr_g[4], + atom_map=node, ) - if edge_match is None: - edge_match = generic_edge_match(edge_attribute, 1, eq) - - # Perform the isomorphism check using NetworkX - return nx.is_isomorphic( - graph_1, graph_2, node_match=node_match, edge_match=edge_match - ) - - -def subgraph_isomorphism( - child_graph: nx.Graph, - parent_graph: nx.Graph, - node_label_names: List[str] = ["element", "charge"], - node_label_default: List[Any] = ["*", 0], - edge_attribute: str = "order", - use_filter: bool = False, - check_type: str = "induced", # "induced" or "monomorphism" - node_comparator: Optional[Callable[[Any, Any], bool]] = None, - edge_comparator: Optional[Callable[[Any, Any], bool]] = None, + if len(node_attr_h) > 0: + # Unpack node attributes for H + H.add_node( + node, + element=node_attr_h[0], + aromatic=node_attr_h[1], + hcount=node_attr_h[2], + charge=node_attr_h[3], + neighbors=node_attr_h[4], + atom_map=node, + ) + + # Decompose edges + for u, v, data in its_graph.edges(data=True): + if edges_share in data: + order_g, order_h = data[edges_share] + if order_g > 0: # Assuming 0 means no edge in G + G.add_edge(u, v, order=order_g) + if order_h > 0: # Assuming 0 means no edge in H + H.add_edge(u, v, order=order_h) + + return G, H + + +def compare_graphs( + graph1: nx.Graph, + graph2: nx.Graph, + node_attrs: list = ["element", "aromatic", "hcount", "charge", "neighbors"], + edge_attrs: list = ["order"], ) -> bool: - """ - Enhanced checks if the child graph is a subgraph isomorphic to the parent graph based on - customizable node and edge attributes. + """Compare two graphs based on specified node and edge attributes. Parameters: - - child_graph (nx.Graph): The child graph. - - parent_graph (nx.Graph): The parent graph. - - node_label_names (List[str]): Labels to compare. - - node_label_default (List[Any]): Defaults for missing node labels. - - edge_attribute (str): The edge attribute to compare. - - use_filter (bool): Whether to use pre-filters based on node and edge count. - - check_type (str): "induced" (default) or "monomorphism" for the type of subgraph matching. - - node_comparator (Callable[[Any, Any], bool]): Custom comparator for node attributes. - - edge_comparator (Callable[[Any, Any], bool]): Custom comparator for edge attributes. + - graph1 (nx.Graph): The first graph to compare. + - graph2 (nx.Graph): The second graph to compare. + - node_attrs (list): A list of node attribute names to include in the comparison. + - edge_attrs (list): A list of edge attribute names to include in the comparison. Returns: - - bool: True if subgraph isomorphism is found, False otherwise. + - bool: True if both graphs are identical with respect to the specified attributes, + otherwise False. """ - if use_filter: - # Initial quick filters based on node and edge counts - if len(child_graph) > len(parent_graph) or len(child_graph.edges) > len( - parent_graph.edges - ): - return False + # Compare node sets + if set(graph1.nodes()) != set(graph2.nodes()): + return False - # Step 2: Node label filter - Only consider 'element' and 'charge' attributes - for _, child_data in child_graph.nodes(data=True): - found_match = False - for _, parent_data in parent_graph.nodes(data=True): - match = True - # Compare only the 'element' and 'charge' attributes - for label, default in zip(node_label_names, node_label_default): - child_value = child_data.get(label, default) - parent_value = parent_data.get(label, default) - if child_value != parent_value: - match = False - break - if match: - found_match = True - break - if not found_match: - return False - - # Step 3: Edge label filter - Ensure that the edge attribute 'order' matches if provided - if edge_attribute: - for child_edge in child_graph.edges(data=True): - child_node1, child_node2, child_data = child_edge - if child_node1 in parent_graph and child_node2 in parent_graph: - # Ensure the edge exists in the parent graph - if not parent_graph.has_edge(child_node1, child_node2): - return False - # Check if the 'order' attribute matches - parent_edge_data = parent_graph[child_node1][child_node2] - child_order = child_data.get(edge_attribute) - parent_order = parent_edge_data.get(edge_attribute) - - # Handle comparison of tuple values for 'order' attribute - if isinstance(child_order, tuple) and isinstance( - parent_order, tuple - ): - if child_order != parent_order: - return False - elif child_order != parent_order: - return False - else: - return False - - # Setting up attribute comparison functions - node_comparator = node_comparator if node_comparator else eq - edge_comparator = edge_comparator if edge_comparator else eq - - # Creating match conditions for nodes and edges based on custom or default comparators - node_match = generic_node_match( - node_label_names, node_label_default, [node_comparator] * len(node_label_names) - ) - edge_match = ( - generic_edge_match(edge_attribute, None, edge_comparator) - if edge_attribute - else None - ) - - # Graph matching setup - matcher = GraphMatcher( - parent_graph, child_graph, node_match=node_match, edge_match=edge_match - ) + # Compare nodes based on attributes + for node in graph1.nodes(): + if node not in graph2: + return False + node_data1 = {attr: graph1.nodes[node].get(attr, None) for attr in node_attrs} + node_data2 = {attr: graph2.nodes[node].get(attr, None) for attr in node_attrs} + if node_data1 != node_data2: + return False - # Executing the matching based on specified type - if check_type == "induced": - return matcher.subgraph_is_isomorphic() - else: - return matcher.subgraph_is_monomorphic() + # Compare edge sets with sorted tuples + if set(tuple(sorted(edge)) for edge in graph1.edges()) != set( + tuple(sorted(edge)) for edge in graph2.edges() + ): + return False + + # Compare edges based on attributes + for edge in graph1.edges(): + # Sort the edge for consistent comparison + sorted_edge = tuple(sorted(edge)) + if sorted_edge not in graph2.edges(): + return False + edge_data1 = {attr: graph1.edges[edge].get(attr, None) for attr in edge_attrs} + edge_data2 = { + attr: graph2.edges[sorted_edge].get(attr, None) for attr in edge_attrs + } + if edge_data1 != edge_data2: + return False + return True -def maximum_connected_common_subgraph( - graph_1: nx.Graph, - graph_2: nx.Graph, - node_label_names: List[str] = ["element", "charge"], - node_label_default: List[Any] = ["*", 0], - edge_attribute: str = "standard_order", -) -> nx.Graph: - """ - Computes the largest connected common subgraph (MCS) between two graphs using - subgraph isomorphism based on customizable node and edge attributes. - The function iterates over subsets of nodes from the smaller graph—starting from the largest - possible subgraph size down to 1—and returns the first (largest) candidate that is connected - and is isomorphic to a subgraph of the larger graph. +def enumerate_tautomers(reaction_smiles: str) -> Optional[List[str]]: + """Enumerates possible tautomers for reactants while canonicalizing the + products in a reaction SMILES string. This function first splits the + reaction SMILES string into reactants and products. It then generates all + possible tautomers for the reactants and canonicalizes the product + molecule. The function returns a list of reaction SMILES strings for each + tautomer of the reactants combined with the canonical product. Parameters: - - graph_1 (nx.Graph): The first graph for comparison. - - graph_2 (nx.Graph): The second graph for comparison. - - node_label_names (List[str]): List of node attribute names used for matching. - - node_label_default (List[Any]): Default values for missing node attributes. - - edge_attribute (str): The edge attribute to compare. + - reaction_smiles (str): A SMILES string of the reaction formatted as + 'reactants>>products'. Returns: - - nx.Graph: A graph representing the largest connected common subgraph found; if none exists, - returns an empty graph. + - List[str] | None: A list of SMILES strings for the reaction, with each string + representing a different + - tautomer of the reactants combined with the canonicalized products. Returns None if + an error occurs or if invalid SMILES strings are provided. + + Raises: + - ValueError: If the provided SMILES strings cannot be converted to molecule objects, + indicating invalid input. """ - node_match = generic_node_match( - node_label_names, node_label_default, [eq] * len(node_label_names) - ) - edge_match = generic_edge_match(edge_attribute, 1, eq) - - # Determine which graph is smaller for efficiency. - if graph_1.number_of_nodes() <= graph_2.number_of_nodes(): - smaller_graph, larger_graph = graph_1, graph_2 - else: - smaller_graph, larger_graph = graph_2, graph_1 - - num_nodes_smaller = smaller_graph.number_of_nodes() - # Iterate over possible subgraph sizes from the largest to 1. - for subgraph_size in range(num_nodes_smaller, 0, -1): - for nodes_subset in itertools.combinations( - smaller_graph.nodes(), subgraph_size - ): - candidate_subgraph = smaller_graph.subgraph(nodes_subset) - # If the subgraph has more than one node, check it is connected. - if candidate_subgraph.number_of_nodes() > 1 and not nx.is_connected( - candidate_subgraph - ): - continue - - # Check for subgraph isomorphism in the larger graph. - matcher = GraphMatcher( - larger_graph, - candidate_subgraph, - node_match=node_match, - edge_match=edge_match, + try: + # Split the input reaction SMILES string into reactants and products + reactants_smiles, products_smiles = reaction_smiles.split(">>") + + # Convert SMILES strings to molecule objects + reactants_mol = Chem.MolFromSmiles(reactants_smiles) + products_mol = Chem.MolFromSmiles(products_smiles) + + if reactants_mol is None or products_mol is None: + raise ValueError( + "Invalid SMILES string provided for reactants or products." ) - if matcher.subgraph_is_isomorphic(): - return candidate_subgraph.copy() - return nx.Graph() + # Initialize tautomer enumerator + enumerator = rdMolStandardize.TautomerEnumerator() -def heuristics_MCCS( - graphs: List[nx.Graph], - node_label_names: List[str] = ["element", "charge"], - node_label_default: List[Any] = ["*", 0], - edge_attribute: str = "standard_order", -) -> nx.Graph: - """ - Computes the Maximum Connected Common Subgraph (MCCS) over a list of graphs using a heuristic approach. + # Enumerate tautomers for the reactants and canonicalize the products + try: + reactants_can = enumerator.Enumerate(reactants_mol) + except Exception as e: + print(f"An error occurred: {e}") + reactants_can = [reactants_mol] + products_can = products_mol + + # Convert molecule objects back to SMILES strings + reactants_can_smiles = [Chem.MolToSmiles(i) for i in reactants_can] + products_can_smiles = Chem.MolToSmiles(products_can) + + # Combine each reactant tautomer with the canonical product in SMILES format + rsmi_list = [i + ">>" + products_can_smiles for i in reactants_can_smiles] + if len(rsmi_list) == 0: + return [reaction_smiles] + else: + # rsmi_list.remove(reaction_smiles) + rsmi_list.insert(0, reaction_smiles) + return rsmi_list - This function computes the MCCS between the first two graphs using the - `maximum_connected_common_subgraph` function based on customizable node and edge attributes. - For more than two graphs, it iteratively updates the common subgraph by calculating the MCCS - between the current common subgraph and each subsequent graph. An early exit occurs if the - intermediate common subgraph becomes empty. + except Exception as e: + print(f"An error occurred: {e}") + return [reaction_smiles] + + +def mapping_success_rate(list_mapping_data): + """Calculate the success rate of entries containing atom mappings in a list + of data strings. Parameters: - - graphs (List[nx.Graph]): A list of networkx graphs for which the common subgraph is to be computed. - - node_label_names (List[str]): List of node attribute names used for matching. - - node_label_default (List[Any]): Default values for missing node attributes. - - edge_attribute (str): The edge attribute to compare. + - list_mapping_in_data (list of str): List containing strings to be searched for atom + mappings. Returns: - - nx.Graph: The maximum connected common subgraph common to all provided graphs. If no common - subgraph exists, an empty graph is returned. + - float: The success rate of finding atom mappings in the list as a percentage. Raises: - - ValueError: If the input list of graphs is empty. + - ValueError: If the input list is empty. """ - if not graphs: - raise ValueError("Input list of graphs is empty.") - - if len(graphs) == 1: - return graphs[0].copy() - - # Handle the two-graph case explicitly. - if len(graphs) == 2: - return maximum_connected_common_subgraph( - graphs[0], - graphs[1], - node_label_names=node_label_names, - node_label_default=node_label_default, - edge_attribute=edge_attribute, - ) + atom_map_pattern = re.compile(r":\d+") + if not list_mapping_data: + raise ValueError("The input list is empty, cannot calculate success rate.") - # Iteratively compute the MCCS for more than two graphs. - current_mcs = maximum_connected_common_subgraph( - graphs[0], - graphs[1], - node_label_names=node_label_names, - node_label_default=node_label_default, - edge_attribute=edge_attribute, + success = sum( + 1 for entry in list_mapping_data if re.search(atom_map_pattern, entry) ) + rate = 100 * (success / len(list_mapping_data)) - for graph in graphs[2:]: - if current_mcs.number_of_nodes() == 0: - break # Early exit if no common subgraph remains. - current_mcs = maximum_connected_common_subgraph( - current_mcs, - graph, - node_label_names=node_label_names, - node_label_default=node_label_default, - edge_attribute=edge_attribute, - ) - - return current_mcs + return round(rate, 2) \ No newline at end of file diff --git a/synkit/Graph/Matcher/multi_turbo_iso.py b/synkit/Graph/Matcher/multi_turbo_iso.py index bcc2a85..db1e37e 100644 --- a/synkit/Graph/Matcher/multi_turbo_iso.py +++ b/synkit/Graph/Matcher/multi_turbo_iso.py @@ -9,21 +9,22 @@ class MultiTurboISO: - """ - Accelerated sub-graph search across a batch of host graphs. + """Accelerated sub-graph search across a batch of host graphs. Builds a single global signature bucket over all hosts and reuses a - lightweight TurboISO matcher per host. For each query graph, hosts are - first pruned by a signature + degree filter, and then TurboISO’s + lightweight TurboISO matcher per host. For each query graph, hosts + are first pruned by a signature + degree filter, and then TurboISO’s backtracking is run only on the surviving hosts. :param hosts: List of host graphs to index. :type hosts: List[nx.Graph] :param node_label: Node attribute(s) used for signature matching. :type node_label: str or list[str] - :param edge_label: Edge attribute(s) to match; pass None to ignore edges. + :param edge_label: Edge attribute(s) to match; pass None to ignore + edges. :type edge_label: str or list[str] or None - :param distance_threshold: Skip distance filtering if candidate pool is smaller. + :param distance_threshold: Skip distance filtering if candidate pool + is smaller. :type distance_threshold: int :returns: An instance of MultiTurboISO with global index built. :rtype: MultiTurboISO @@ -99,7 +100,10 @@ def node_label(self) -> List[str]: @property def edge_label(self) -> List[str]: - """Edge‑attribute selector(s). Empty list means ‘ignore’.""" + """Edge‑attribute selector(s). + + Empty list means ‘ignore’. + """ return list(self._edge_attr) # -------------------------------------------------------------- helpers @@ -172,7 +176,7 @@ def search_many( ) -> List[Dict[int, Union[bool, List[Dict[Any, Any]]]]]: """Match a list of pattern graphs. - Returns a list of per‑pattern dictionaries in the same order as the - input list. + Returns a list of per‑pattern dictionaries in the same order as + the input list. """ return [self.search_one(p, prune=prune) for p in patterns] diff --git a/synkit/Graph/Matcher/sing.py b/synkit/Graph/Matcher/sing.py index 3e4f1b9..39c9a8b 100644 --- a/synkit/Graph/Matcher/sing.py +++ b/synkit/Graph/Matcher/sing.py @@ -5,9 +5,10 @@ class SING: """Subgraph search In Non-homogeneous Graphs (SING) - A lightweight Python implementation adopting a *filter-and-refine* strategy - with path-based features. This version supports **heterogeneous graphs** - through flexible **node and edge attribute selections**. + A lightweight Python implementation adopting a *filter-and-refine* + strategy with path-based features. This version supports + **heterogeneous graphs** through flexible **node and edge attribute + selections**. """ # --------------------------------------------------------------------- @@ -62,14 +63,16 @@ def __init__( # ------------------------------------------------------------------ def _node_signature(self, v: Any, G: nx.Graph) -> str: - """Return a string signature for *v* in *G* based on ``self.node_att``.""" + """Return a string signature for *v* in *G* based on + ``self.node_att``.""" vals = [str(G.nodes[v].get(a, "#")) for a in self.node_att] return "|".join(vals) def _edge_signature(self, u: Any, v: Any, G: nx.Graph) -> str: """Return a string signature for edge *(u,v)* in *G* based on - ``self.edge_att``. If no edge attributes were requested, returns an - empty string. + ``self.edge_att``. + + If no edge attributes were requested, returns an empty string. """ if not self.edge_att: return "" @@ -85,7 +88,9 @@ def _extract_path_features( ) -> Set[str]: """Enumerate *all* simple paths starting at *node* up to ``self.max_path_length`` edges (inclusive), represented as label - sequences. Works for both data and query graphs. + sequences. + + Works for both data and query graphs. """ features: Set[str] = set() max_len = self.max_path_length @@ -126,8 +131,7 @@ def _build_index(self) -> None: def _candidate_vertices(self, query_graph: nx.Graph) -> Dict[Any, Set[Any]]: """Return *per-query-vertex* candidate sets using posting-list - intersections. - """ + intersections.""" cand: Dict[Any, Set[Any]] = {} for qv in query_graph.nodes: q_feats = self._extract_path_features(qv, query_graph, is_query=True) diff --git a/synkit/Graph/Matcher/subgraph_matcher.py b/synkit/Graph/Matcher/subgraph_matcher.py index aa70236..1eb5678 100644 --- a/synkit/Graph/Matcher/subgraph_matcher.py +++ b/synkit/Graph/Matcher/subgraph_matcher.py @@ -109,16 +109,16 @@ # Core engine class # --------------------------------------------------------------------------- class SubgraphMatch: - """ - Boolean-only checks for graph isomorphism and subgraph (induced or monomorphic) matching. + """Boolean-only checks for graph isomorphism and subgraph (induced or + monomorphic) matching. - Provides static methods for NetworkX-based checks and optional GML "rule" backend. + Provides static methods for NetworkX-based checks and optional GML + "rule" backend. """ @staticmethod def _get_edge_labels(graph: Any) -> list: - """ - Extracts the bond types (edge labels) from a given graph. + """Extracts the bond types (edge labels) from a given graph. Parameters: - graph: The graph object containing the edges. @@ -130,8 +130,7 @@ def _get_edge_labels(graph: Any) -> list: @staticmethod def _get_node_labels(graph: Any) -> list: - """ - Extracts the atom IDs (node labels) from a given graph. + """Extracts the atom IDs (node labels) from a given graph. Parameters: - graph: The graph object containing the vertices. @@ -145,9 +144,8 @@ def _get_node_labels(graph: Any) -> list: def rule_subgraph_morphism( rule_1: str, rule_2: str, use_filter: bool = False ) -> bool: - """ - Evaluates if two GML-formatted rule representations are isomorphic or one is a - subgraph of the other (monomorphic). + """Evaluates if two GML-formatted rule representations are isomorphic + or one is a subgraph of the other (monomorphic). Parameters: - rule_1 (str): GML string of the first rule. @@ -191,10 +189,8 @@ def subgraph_isomorphism( node_comparator: Optional[Callable[[Any, Any], bool]] = None, edge_comparator: Optional[Callable[[Any, Any], bool]] = None, ) -> bool: - """ - Enhanced checks if the child graph is a subgraph isomorphic to the parent graph based on - customizable node and edge attributes. - """ + """Enhanced checks if the child graph is a subgraph isomorphic to the + parent graph based on customizable node and edge attributes.""" if use_filter: if ( child_graph.number_of_nodes() > parent_graph.number_of_nodes() @@ -263,9 +259,8 @@ def is_subgraph( check_type: str = "induced", backend: str = "nx", ) -> bool: - """ - Unified API for subgraph/isomorphism either via NX or GML backend. - """ + """Unified API for subgraph/isomorphism either via NX or GML + backend.""" if backend == "nx": return SubgraphMatch.subgraph_isomorphism( pattern, @@ -303,8 +298,8 @@ def find_subgraph_mappings( max_results: Optional[int] = None, strict_cc_count: bool = True, ) -> List[MappingDict]: - """ - Dispatch to a subgraph-matching strategy and return all pattern→host mappings. + """Dispatch to a subgraph-matching strategy and return all pattern→host + mappings. Depending on `strategy`, this will call: - ALL → `_all_monomorphisms` @@ -406,8 +401,7 @@ def _find_component_aware_subgraph_mappings( max_results: Optional[int] = None, strict_cc_count: bool = False, ) -> List[MappingDict]: - """ - Component‑aware VF2 without any attribute/degree/WL‑1 pre‑filters. + """Component‑aware VF2 without any attribute/degree/WL‑1 pre‑filters. The only constraints are: • each pattern‑CC must fit in a *distinct* host‑CC diff --git a/synkit/Graph/Matcher/turbo_iso.py b/synkit/Graph/Matcher/turbo_iso.py index 22dfa77..4a01601 100644 --- a/synkit/Graph/Matcher/turbo_iso.py +++ b/synkit/Graph/Matcher/turbo_iso.py @@ -81,7 +81,9 @@ def _init_candidates(self, Q: nx.Graph) -> Dict[Any, Set[Any]]: # ------------------------------------------------ distance consistency def _within_dist(self, src: Any, dsts: Set[Any], limit: int) -> bool: """Check whether *any* dst in *dsts* lies within *limit* hops of src. - Stops BFS early once found. Returns True/False.""" + + Stops BFS early once found. Returns True/False. + """ if not dsts: return False if limit == float("inf"): diff --git a/synkit/Graph/Wildcard/fuse_graph.py b/synkit/Graph/Wildcard/fuse_graph.py index dbc93d2..4007a0b 100644 --- a/synkit/Graph/Wildcard/fuse_graph.py +++ b/synkit/Graph/Wildcard/fuse_graph.py @@ -15,11 +15,10 @@ def find_wc_graph_isomorphism( edge_match: Optional[Callable[[Dict[str, Any], Dict[str, Any]], bool]] = None, logger: Optional[logging.Logger] = None, ) -> Optional[Dict[Any, Any]]: - """ - Wildcard‑aware sub‑graph isomorphism. Returns a mapping from every node in - the **smaller** graph to a node in the **larger** graph, allowing any node - whose ``element == "*"`` to match *any* concrete node (or group of nodes) - on the host side. + """Wildcard‑aware sub‑graph isomorphism. Returns a mapping from every node + in the **smaller** graph to a node in the **larger** graph, allowing any + node whose ``element == "*"`` to match *any* concrete node (or group of + nodes) on the host side. :param G1: First input graph. :type G1: nx.Graph | nx.DiGraph | nx.MultiGraph | nx.MultiDiGraph @@ -87,8 +86,7 @@ def fuse_wc_graphs( wildcard: str = "*", logger: Optional[logging.Logger] = None, ) -> GraphType: - """ - Fuse a wildcard‑pattern graph *G1* into the concrete host *G2*. + """Fuse a wildcard‑pattern graph *G1* into the concrete host *G2*. The result lives **entirely in G2’s node‑ID space** and contains: diff --git a/synkit/Graph/canon_graph.py b/synkit/Graph/canon_graph.py index b0c9043..f134d88 100644 --- a/synkit/Graph/canon_graph.py +++ b/synkit/Graph/canon_graph.py @@ -100,7 +100,8 @@ def _default_edge_key(u: NodeId, v: NodeId, data: EdgeData) -> Tuple[Any, ...]: def _digest(text: str) -> Digest: - """First 32 hex chars of SHA‑256 – short *but* collision‑safe for up to 2¹²⁸ graphs.""" + """First 32 hex chars of SHA‑256 – short *but* collision‑safe for up to + 2¹²⁸ graphs.""" return hashlib.sha256(text.encode()).hexdigest()[:32] @@ -110,8 +111,7 @@ def _digest(text: str) -> Digest: class GraphCanonicaliser: - """ - Factory that turns arbitrary ``networkx.Graph`` objects into their + """Factory that turns arbitrary ``networkx.Graph`` objects into their *canonical* twin plus a **stable 32‑hex digest**. Parameters @@ -169,8 +169,7 @@ def __init__( # High‑level helpers # # ------------------------------------------------------------------ # def canonicalise_graph(self, graph: nx.Graph) -> "CanonicalGraph": - """ - Return a :class:`CanonicalGraph` wrapper around *graph*. + """Return a :class:`CanonicalGraph` wrapper around *graph*. The wrapper exposes: @@ -183,8 +182,7 @@ def canonicalise_graphs( self, graphs: Iterable[nx.Graph], ) -> Tuple["CanonicalGraph", ...]: - """ - Bulk helper that returns *all* wrappers **sorted by hash**. + """Bulk helper that returns *all* wrappers **sorted by hash**. Useful when you want fast set comparison but need the canonical graphs as well: @@ -203,8 +201,7 @@ def canonicalise_graphs( # Digest / core methods # # ------------------------------------------------------------------ # def canonical_signature(self, graph: nx.Graph) -> Digest: - """ - Return the *hash of the canonical form* of *graph*. + """Return the *hash of the canonical form* of *graph*. Equal digests ⇒ graphs are guaranteed isomorphic **under the chosen back‑end and keys**. @@ -254,8 +251,7 @@ def _canon_generic(self, g: nx.Graph) -> nx.Graph: return G2 def _canon_wl(self, g: nx.Graph) -> nx.Graph: - """ - Weisfeiler–Lehman colour-refinement back-end (pure Python). + """Weisfeiler–Lehman colour-refinement back-end (pure Python). Seeds each node’s initial colour by the tuple of attributes in `self._wl_node_attrs` (e.g. ["element","charge","hcount"]), @@ -352,8 +348,7 @@ def __repr__(self) -> str: # pragma: no cover # Value wrapper (unchanged surface – richer docs) # ============================================================================= class CanonicalGraph: - """ - *Value object* tying together: + """*Value object* tying together: * the **original** NetworkX graph (mutable, user‑supplied); * its **canonical twin** (immutable copy, nodes relabelled 1…N); @@ -428,9 +423,9 @@ def help(self) -> None: # pragma: no cover class CanonicalRule: - """ - Value object that wraps a graph transformation rule in GML string form, - providing a canonicalised GML output and a stable 32-character SHA-256 hash. + """Value object that wraps a graph transformation rule in GML string form, + providing a canonicalised GML output and a stable 32-character SHA-256 + hash. Internally, the GML rule is parsed into a NetworkX graph via `gml_to_its`, canonicalised using a `GraphCanonicaliser`, and re-serialized back to GML @@ -458,8 +453,7 @@ def __init__( rule: str, canon: GraphCanonicaliser = GraphCanonicaliser(), ) -> None: - """ - Instantiate a CanonicalRule. + """Instantiate a CanonicalRule. Parameters ---------- @@ -523,9 +517,7 @@ def canonical_hash(self) -> Digest: return self._canonical_hash def help(self) -> None: - """ - Print original and canonical rule texts and underlying graphs. - """ + """Print original and canonical rule texts and underlying graphs.""" print("Original GML rule:") print(self._original_rule) print("\nCanonical GML rule:") diff --git a/synkit/Graph/syn_graph.py b/synkit/Graph/syn_graph.py index 685f264..3b8d5e0 100644 --- a/synkit/Graph/syn_graph.py +++ b/synkit/Graph/syn_graph.py @@ -34,9 +34,8 @@ class SynGraph: - """ - Wrapper around networkx.Graph providing both its original and (optionally) - canonicalized form, plus a SHA-256 signature. + """Wrapper around networkx.Graph providing both its original and + (optionally) canonicalized form, plus a SHA-256 signature. Parameters: - graph (nx.Graph): The NetworkX graph to wrap. @@ -62,8 +61,7 @@ def __init__( canonicaliser: Optional[GraphCanonicaliser] = None, canon: bool = True, ) -> None: - """ - Initialize a SynGraph wrapper. + """Initialize a SynGraph wrapper. Parameters: - graph (nx.Graph): Input graph. @@ -82,23 +80,18 @@ def __init__( self._canonical = None def __getattr__(self, name: str) -> Any: - """ - Delegate any unknown attribute lookup to the underlying ._raw graph. - """ + """Delegate any unknown attribute lookup to the underlying ._raw + graph.""" return getattr(self._raw, name) def __eq__(self, other: object) -> bool: - """ - Two SynGraph instances are equal iff their signatures match. - """ + """Two SynGraph instances are equal iff their signatures match.""" if not isinstance(other, SynGraph): return False return self.signature == other.signature def __hash__(self) -> int: - """ - Hash on the signature, allowing use in sets and as dict keys. - """ + """Hash on the signature, allowing use in sets and as dict keys.""" return hash(self.signature) @property @@ -119,8 +112,7 @@ def signature(self) -> Optional[str]: def get_nodes( self, data: bool = True ) -> Iterable[Union[Any, Tuple[Any, Dict[str, Any]]]]: - """ - Yield nodes from the original graph. + """Yield nodes from the original graph. Parameters ---------- @@ -132,8 +124,7 @@ def get_nodes( def get_edges( self, data: bool = True ) -> Iterable[Union[Tuple[Any, Any], Tuple[Any, Any, Dict[str, Any]]]]: - """ - Yield edges from the original graph. + """Yield edges from the original graph. Parameters ---------- @@ -151,9 +142,7 @@ def __repr__(self) -> str: return f"" def help(self) -> None: - """ - Print a summary of the SynGraph API. - """ + """Print a summary of the SynGraph API.""" print( "SynGraph Help\n" "----------\n" diff --git a/synkit/Graph/utils.py b/synkit/Graph/utils.py index 0e5fa5f..bca1aac 100644 --- a/synkit/Graph/utils.py +++ b/synkit/Graph/utils.py @@ -3,8 +3,7 @@ def print_graph_attributes(G: nx.Graph) -> None: - """ - Print all node and edge attributes from a NetworkX graph. + """Print all node and edge attributes from a NetworkX graph. Parameters: G (nx.Graph): A NetworkX graph (Graph, DiGraph, MultiGraph, etc.). @@ -23,8 +22,7 @@ def print_graph_attributes(G: nx.Graph) -> None: def remove_wildcard_nodes(G: nx.Graph, inplace: bool = True) -> nx.Graph: - """ - Remove all wildcard nodes from the graph. + """Remove all wildcard nodes from the graph. A wildcard node is identified by having its 'element' attribute equal to '*'. @@ -59,8 +57,7 @@ def add_wildcard_subgraph_for_unmapped( edge_keys: List[str] = ["order"], inplace: bool = False, ) -> Tuple[nx.Graph, Dict[Any, Any]]: - """ - Extend G with wildcard nodes/edges for every L-node not already mapped, + """Extend G with wildcard nodes/edges for every L-node not already mapped, preserving original L->G mapping and returning the full mapping. Parameters @@ -123,10 +120,9 @@ def add_wildcard_subgraph_for_unmapped( def clean_graph_keep_largest_component(graph: nx.Graph) -> nx.Graph: - """ - Return a shallow copy of the input graph with all edges removed - where the 'standard_order' attribute is exactly 0, then retain only - the largest connected component. + """Return a shallow copy of the input graph with all edges removed where + the 'standard_order' attribute is exactly 0, then retain only the largest + connected component. Parameters ---------- diff --git a/synkit/IO/chem_converter.py b/synkit/IO/chem_converter.py index 31feb61..db60bf1 100644 --- a/synkit/IO/chem_converter.py +++ b/synkit/IO/chem_converter.py @@ -31,20 +31,22 @@ def smiles_to_graph( ], edge_attrs: Optional[List[str]] = ["order"], ) -> Optional[nx.Graph]: - """ - Helper function to convert a SMILES string to a NetworkX graph. + """Helper function to convert a SMILES string to a NetworkX graph. :param smiles: SMILES representation of the molecule. :type smiles: str - :param drop_non_aam: Whether to drop nodes without atom mapping numbers. + :param drop_non_aam: Whether to drop nodes without atom mapping + numbers. :type drop_non_aam: bool :param light_weight: Whether to create a light-weight graph. :type light_weight: bool :param sanitize: Whether to sanitize the molecule during conversion. :type sanitize: bool - :param use_index_as_atom_map: Whether to use atom indices as atom-map numbers. + :param use_index_as_atom_map: Whether to use atom indices as atom- + map numbers. :type use_index_as_atom_map: bool - :returns: The NetworkX graph representation, or None if conversion fails. + :returns: The NetworkX graph representation, or None if conversion + fails. :rtype: networkx.Graph or None """ @@ -97,20 +99,22 @@ def rsmi_to_graph( ], edge_attrs: Optional[List[str]] = ["order"], ) -> Tuple[Optional[nx.Graph], Optional[nx.Graph]]: - """ - Convert a reaction SMILES (RSMI) into reactant and product graphs. + """Convert a reaction SMILES (RSMI) into reactant and product graphs. :param rsmi: Reaction SMILES string in “reactants>>products” format. :type rsmi: str - :param drop_non_aam: If True, drop nodes without atom mapping numbers. + :param drop_non_aam: If True, drop nodes without atom mapping + numbers. :type drop_non_aam: bool :param light_weight: If True, create a light-weight graph. :type light_weight: bool :param sanitize: If True, sanitize molecules during conversion. :type sanitize: bool - :param use_index_as_atom_map: Whether to use atom indices as atom-map numbers. + :param use_index_as_atom_map: Whether to use atom indices as atom- + map numbers. :type use_index_as_atom_map: bool - :returns: A tuple `(reactant_graph, product_graph)`, each a NetworkX graph or None. + :returns: A tuple `(reactant_graph, product_graph)`, each a NetworkX + graph or None. :rtype: tuple of (networkx.Graph or None, networkx.Graph or None) """ try: @@ -142,15 +146,16 @@ def graph_to_smi( sanitize: bool = True, preserve_atom_maps: Optional[List[int]] = None, ) -> Optional[str]: - """ - Convert a NetworkX molecular graph to a SMILES string. + """Convert a NetworkX molecular graph to a SMILES string. - :param graph: Graph representation of the molecule. - Nodes must carry chemical attributes (e.g. ‘element’, atom maps). + :param graph: Graph representation of the molecule. Nodes must carry + chemical attributes (e.g. ‘element’, atom maps). :type graph: networkx.Graph - :param sanitize: Whether to perform RDKit sanitization on the resulting molecule. + :param sanitize: Whether to perform RDKit sanitization on the + resulting molecule. :type sanitize: bool - :param preserve_atom_maps: List of atom-map numbers for which hydrogens remain explicit. + :param preserve_atom_maps: List of atom-map numbers for which + hydrogens remain explicit. :type preserve_atom_maps: list of int or None :returns: SMILES string, or None if conversion fails. :rtype: str or None @@ -177,20 +182,22 @@ def graph_to_rsmi( sanitize: bool = True, explicit_hydrogen: bool = False, ) -> Optional[str]: - """ - Convert reactant and product graphs into a reaction SMILES string. + """Convert reactant and product graphs into a reaction SMILES string. :param r: Graph representing the reactants. :type r: networkx.Graph :param p: Graph representing the products. :type p: networkx.Graph - :param its: Imaginary transition state graph. If None, it will be constructed. + :param its: Imaginary transition state graph. If None, it will be + constructed. :type its: networkx.Graph or None :param sanitize: Whether to sanitize molecules during conversion. :type sanitize: bool - :param explicit_hydrogen: Whether to preserve explicit hydrogens in the SMILES. + :param explicit_hydrogen: Whether to preserve explicit hydrogens in + the SMILES. :type explicit_hydrogen: bool - :returns: Reaction SMILES string in 'reactants>>products' format or None on failure. + :returns: Reaction SMILES string in 'reactants>>products' format or + None on failure. :rtype: str or None """ try: @@ -229,22 +236,28 @@ def smart_to_gml( explicit_hydrogen: bool = False, useSmiles: bool = True, ) -> str: - """ - Convert a reaction SMARTS (or SMILES) template into a GML‐encoded DPO rule. + """Convert a reaction SMARTS (or SMILES) template into a GML‐encoded DPO + rule. :param smart: The reaction SMARTS or SMILES string. :type smart: str - :param core: If True, include only the reaction core in the GML. Defaults to True. + :param core: If True, include only the reaction core in the GML. + Defaults to True. :type core: bool - :param sanitize: If True, sanitize molecules during conversion. Defaults to True. + :param sanitize: If True, sanitize molecules during conversion. + Defaults to True. :type sanitize: bool - :param rule_name: Identifier for the output rule. Defaults to "rule". + :param rule_name: Identifier for the output rule. Defaults to + "rule". :type rule_name: str - :param reindex: If True, reindex graph nodes before exporting. Defaults to False. + :param reindex: If True, reindex graph nodes before exporting. + Defaults to False. :type reindex: bool - :param explicit_hydrogen: If True, include explicit hydrogen atoms. Defaults to False. + :param explicit_hydrogen: If True, include explicit hydrogen atoms. + Defaults to False. :type explicit_hydrogen: bool - :param useSmiles: If True, treat input as SMILES; if False, as SMARTS. Defaults to True. + :param useSmiles: If True, treat input as SMILES; if False, as + SMARTS. Defaults to True. :type useSmiles: bool :returns: The GML representation of the reaction rule. :rtype: str @@ -271,14 +284,14 @@ def gml_to_smart( explicit_hydrogen: bool = False, useSmiles: bool = True, ) -> Tuple[str, nx.Graph]: - """ - Convert a GML string back to a SMARTS string and ITS graph. + """Convert a GML string back to a SMARTS string and ITS graph. :param gml: The GML string to convert. :type gml: str :param sanitize: Whether to sanitize molecules upon conversion. :type sanitize: bool - :param explicit_hydrogen: Whether hydrogens are explicitly represented. + :param explicit_hydrogen: Whether hydrogens are explicitly + represented. :type explicit_hydrogen: bool :param useSmiles: If True, output SMILES; otherwise SMARTS. :type useSmiles: bool @@ -303,18 +316,19 @@ def its_to_gml( reindex: bool = True, explicit_hydrogen: bool = False, ) -> str: - """ - Convert an ITS graph (reaction graph) to GML format. + """Convert an ITS graph (reaction graph) to GML format. :param its: The input ITS graph representing the reaction. :type its: networkx.Graph - :param core: If True, focus only on the reaction center. Defaults to True. + :param core: If True, focus only on the reaction center. Defaults to + True. :type core: bool :param rule_name: Name of the reaction rule. Defaults to "rule". :type rule_name: str :param reindex: If True, reindex graph nodes. Defaults to True. :type reindex: bool - :param explicit_hydrogen: If True, include explicit hydrogens. Defaults to False. + :param explicit_hydrogen: If True, include explicit hydrogens. + Defaults to False. :type explicit_hydrogen: bool :returns: The GML representation of the ITS graph. :rtype: str @@ -335,8 +349,8 @@ def its_to_gml( def gml_to_its(gml: str) -> nx.Graph: - """ - Convert a GML string representation of a reaction back into an ITS graph. + """Convert a GML string representation of a reaction back into an ITS + graph. :param gml: The GML string representing the reaction. :type gml: str @@ -367,28 +381,37 @@ def rsmi_to_its( edge_attrs: Optional[List[str]] = ["order"], explicit_hydrogen: bool = False, ) -> nx.Graph: - """ - Convert a reaction SMILES (rSMI) to an ITS (Imaginary Transition State) graph. + """Convert a reaction SMILES (rSMI) to an ITS (Imaginary Transition State) + graph. - :param rsmi: The reaction SMILES string, optionally containing atom-map labels. + :param rsmi: The reaction SMILES string, optionally containing atom- + map labels. :type rsmi: str - :param drop_non_aam: If True, discard any molecular fragments without atom-atom maps. + :param drop_non_aam: If True, discard any molecular fragments + without atom-atom maps. :type drop_non_aam: bool - :param sanitize: If True, perform molecule sanitization (valence checks, kekulization). + :param sanitize: If True, perform molecule sanitization (valence + checks, kekulization). :type sanitize: bool - :param use_index_as_atom_map: If True, override atom-map labels by atom indices. + :param use_index_as_atom_map: If True, override atom-map labels by + atom indices. :type use_index_as_atom_map: bool - :param core: If True, return only the reaction-center subgraph of the ITS. + :param core: If True, return only the reaction-center subgraph of + the ITS. :type core: bool - :param node_attrs: Node attributes to include in the ITS graph (e.g., element, charge). + :param node_attrs: Node attributes to include in the ITS graph + (e.g., element, charge). :type node_attrs: list[str] - :param edge_attrs: Edge attributes to include in the ITS graph (e.g., order). + :param edge_attrs: Edge attributes to include in the ITS graph + (e.g., order). :type edge_attrs: list[str] - :param explicit_hydrogen: If True, convert implicit hydrogens to explicit nodes. + :param explicit_hydrogen: If True, convert implicit hydrogens to + explicit nodes. :type explicit_hydrogen: bool :returns: A NetworkX graph representing the complete or core ITS. :rtype: networkx.Graph - :raises ValueError: If the SMILES string is invalid or graph construction fails. + :raises ValueError: If the SMILES string is invalid or graph + construction fails. """ r, p = rsmi_to_graph( rsmi, @@ -411,26 +434,27 @@ def its_to_rsmi( sanitize: bool = True, explicit_hydrogen: bool = False, ) -> str: - """ - Convert an ITS graph into a reaction SMILES (rSMI) string. + """Convert an ITS graph into a reaction SMILES (rSMI) string. - :param its: A fully annotated ITS graph (nodes with atom-map attributes). + :param its: A fully annotated ITS graph (nodes with atom-map + attributes). :type its: networkx.Graph :param sanitize: If True, sanitize prior to SMILES generation. :type sanitize: bool :param explicit_hydrogen: If True, include explicit hydrogens. :type explicit_hydrogen: bool - :returns: A canonical reaction-SMILES string ('reactants>agents>products'). + :returns: A canonical reaction-SMILES string + ('reactants>agents>products'). :rtype: str - :raises ValueError: If graph cannot be decomposed or sanitisation fails. + :raises ValueError: If graph cannot be decomposed or sanitisation + fails. """ r, p = its_decompose(its) return graph_to_rsmi(r, p, its, sanitize, explicit_hydrogen) def rsmi_to_rsmarts(rsmi: str) -> str: - """ - Convert a mapped reaction SMILES to a reaction SMARTS string. + """Convert a mapped reaction SMILES to a reaction SMARTS string. :param rsmi: Reaction SMILES input. :type rsmi: str @@ -446,8 +470,7 @@ def rsmi_to_rsmarts(rsmi: str) -> str: def rsmarts_to_rsmi(rsmarts: str) -> str: - """ - Convert a reaction SMARTS to a reaction SMILES string. + """Convert a reaction SMARTS to a reaction SMILES string. :param rsmarts: Reaction SMARTS input. :type rsmarts: str diff --git a/synkit/IO/data_io.py b/synkit/IO/data_io.py index a66bd7e..c817c60 100644 --- a/synkit/IO/data_io.py +++ b/synkit/IO/data_io.py @@ -11,14 +11,13 @@ def save_database(database: List[Dict], pathname: str = "./Data/database.json") -> None: - """ - Save a database (a list of dictionaries) to a JSON file. + """Save a database (a list of dictionaries) to a JSON file. :param database: The database to be saved. :type database: list[dict] - :param pathname: The path where the database will be saved. Defaults to './Data/database.json'. + :param pathname: The path where the database will be saved. Defaults + to './Data/database.json'. :type pathname: str - :raises TypeError: If the database is not a list of dictionaries. :raises ValueError: If there is an error writing the file. """ @@ -32,15 +31,13 @@ def save_database(database: List[Dict], pathname: str = "./Data/database.json") def load_database(pathname: str = "./Data/database.json") -> List[Dict]: - """ - Load a database (a list of dictionaries) from a JSON file. + """Load a database (a list of dictionaries) from a JSON file. - :param pathname: The path from where the database will be loaded. Defaults to './Data/database.json'. + :param pathname: The path from where the database will be loaded. + Defaults to './Data/database.json'. :type pathname: str - :returns: The loaded database. :rtype: list[dict] - :raises ValueError: If there is an error reading the file. """ try: @@ -52,8 +49,7 @@ def load_database(pathname: str = "./Data/database.json") -> List[Dict]: def save_to_pickle(data: List[Dict[str, Any]], filename: str) -> None: - """ - Save a list of dictionaries to a pickle file. + """Save a list of dictionaries to a pickle file. :param data: A list of dictionaries to be saved. :type data: list[dict] @@ -65,12 +61,10 @@ def save_to_pickle(data: List[Dict[str, Any]], filename: str) -> None: def load_from_pickle(filename: str) -> List[Any]: - """ - Load data from a pickle file. + """Load data from a pickle file. :param filename: The name of the pickle file to load data from. :type filename: str - :returns: The data loaded from the pickle file. :rtype: list """ @@ -79,13 +73,12 @@ def load_from_pickle(filename: str) -> List[Any]: def load_gml_as_text(gml_file_path: str) -> Optional[str]: - """ - Load the contents of a GML file as a text string. + """Load the contents of a GML file as a text string. :param gml_file_path: The file path to the GML file. :type gml_file_path: str - - :returns: The text content of the GML file, or None if the file does not exist or an error occurs. + :returns: The text content of the GML file, or None if the file does + not exist or an error occurs. :rtype: str or None """ try: @@ -100,14 +93,12 @@ def load_gml_as_text(gml_file_path: str) -> Optional[str]: def save_text_as_gml(gml_text: str, file_path: str) -> bool: - """ - Save a GML text string to a file. + """Save a GML text string to a file. :param gml_text: The GML content as a text string. :type gml_text: str :param file_path: The file path where the GML text will be saved. :type file_path: str - :returns: True if saving was successful, False otherwise. :rtype: bool """ @@ -122,28 +113,26 @@ def save_text_as_gml(gml_text: str, file_path: str) -> bool: def save_compressed(array: ndarray, filename: str) -> None: - """ - Saves a NumPy array in a compressed format using .npz extension. + """Saves a NumPy array in a compressed format using .npz extension. :param array: The NumPy array to be saved. :type array: numpy.ndarray - :param filename: The file path or name to save the array to, with a '.npz' extension. + :param filename: The file path or name to save the array to, with a + '.npz' extension. :type filename: str """ np.savez_compressed(filename, array=array) def load_compressed(filename: str) -> ndarray: - """ - Loads a NumPy array from a compressed .npz file. + """Loads a NumPy array from a compressed .npz file. :param filename: The path of the .npz file to load. :type filename: str - :returns: The loaded NumPy array. :rtype: numpy.ndarray - - :raises KeyError: If the .npz file does not contain an array with the key 'array'. + :raises KeyError: If the .npz file does not contain an array with + the key 'array'. """ with np.load(filename) as data: if "array" in data: @@ -155,8 +144,7 @@ def load_compressed(filename: str) -> ndarray: def save_model(model: Any, filename: str) -> None: - """ - Save a machine learning model to a file using joblib. + """Save a machine learning model to a file using joblib. :param model: The machine learning model to save. :type model: object @@ -168,12 +156,11 @@ def save_model(model: Any, filename: str) -> None: def load_model(filename: str) -> Any: - """ - Load a machine learning model from a file using joblib. + """Load a machine learning model from a file using joblib. - :param filename: The path to the file from which the model will be loaded. + :param filename: The path to the file from which the model will be + loaded. :type filename: str - :returns: The loaded machine learning model. :rtype: object """ @@ -183,12 +170,12 @@ def load_model(filename: str) -> Any: def save_dict_to_json(data: dict, file_path: str) -> None: - """ - Save a dictionary to a JSON file. + """Save a dictionary to a JSON file. :param data: The dictionary to be saved. :type data: dict - :param file_path: The path to the file where the dictionary should be saved. + :param file_path: The path to the file where the dictionary should + be saved. :type file_path: str """ with open(file_path, "w") as json_file: @@ -197,13 +184,13 @@ def save_dict_to_json(data: dict, file_path: str) -> None: def load_dict_from_json(file_path: str) -> Optional[dict]: - """ - Load a dictionary from a JSON file. + """Load a dictionary from a JSON file. - :param file_path: The path to the JSON file from which to load the dictionary. + :param file_path: The path to the JSON file from which to load the + dictionary. :type file_path: str - - :returns: The dictionary loaded from the JSON file, or None if an error occurs. + :returns: The dictionary loaded from the JSON file, or None if an + error occurs. :rtype: dict or None """ try: @@ -217,13 +204,13 @@ def load_dict_from_json(file_path: str) -> Optional[dict]: def load_from_pickle_generator(file_path: str) -> Generator[Any, None, None]: - """ - A generator that yields items from a pickle file where each pickle load returns a list of dictionaries. + """A generator that yields items from a pickle file where each pickle load + returns a list of dictionaries. :param file_path: The path to the pickle file to load. :type file_path: str - - :yields: A single item from the list of dictionaries stored in the pickle file. + :yields: A single item from the list of dictionaries stored in the + pickle file. :rtype: Any """ with open(file_path, "rb") as file: @@ -237,16 +224,16 @@ def load_from_pickle_generator(file_path: str) -> Generator[Any, None, None]: def collect_data(num_batches: int, temp_dir: str, file_template: str) -> List[Any]: - """ - Collects and aggregates data from multiple pickle files into a single list. + """Collects and aggregates data from multiple pickle files into a single + list. :param num_batches: The number of batch files to process. :type num_batches: int :param temp_dir: The directory where the batch files are stored. :type temp_dir: str - :param file_template: The template string for batch file names, expecting an integer formatter. + :param file_template: The template string for batch file names, + expecting an integer formatter. :type file_template: str - :returns: A list of aggregated data items from all batch files. :rtype: list """ @@ -259,8 +246,7 @@ def collect_data(num_batches: int, temp_dir: str, file_template: str) -> List[An def save_list_to_file(data_list: list, file_path: str) -> None: - """ - Save a list to a file in JSON format. + """Save a list to a file in JSON format. :param data_list: The list to save. :type data_list: list @@ -272,12 +258,10 @@ def save_list_to_file(data_list: list, file_path: str) -> None: def load_list_from_file(file_path: str) -> list: - """ - Load a list from a JSON-formatted file. + """Load a list from a JSON-formatted file. :param file_path: The path to the file to read the list from. :type file_path: str - :returns: The list loaded from the file. :rtype: list """ @@ -286,17 +270,14 @@ def load_list_from_file(file_path: str) -> list: def save_dg(dg, path: str) -> str: - """ - Save a DG instance to disk using MØD's dump method. + """Save a DG instance to disk using MØD's dump method. :param dg: The derivation graph to save. :type dg: DG :param path: The file path where the graph will be dumped. :type path: str - :returns: The path of the dumped file. :rtype: str - :raises Exception: If saving fails. """ try: @@ -309,19 +290,17 @@ def save_dg(dg, path: str) -> str: def load_dg(path: str, graph_db: list, rule_db: list): - """ - Load a DG instance from a dumped file. + """Load a DG instance from a dumped file. :param path: The file path of the dumped graph. :type path: str - :param graph_db: List of Graph objects representing the graph database. + :param graph_db: List of Graph objects representing the graph + database. :type graph_db: list :param rule_db: List of Rule objects required for loading the DG. :type rule_db: list - :returns: The loaded derivation graph instance. :rtype: DG - :raises Exception: If loading fails. """ from mod import DG diff --git a/synkit/IO/data_process.py b/synkit/IO/data_process.py index 3475674..ec88283 100644 --- a/synkit/IO/data_process.py +++ b/synkit/IO/data_process.py @@ -7,10 +7,9 @@ def merge_dicts( key: str, intersection: bool = True, ) -> List[Dict[str, Any]]: - """ - Merges two lists of dictionaries based on a specified key, with an option to - either merge only dictionaries with matching key values (intersection) or - all dictionaries (union). + """Merges two lists of dictionaries based on a specified key, with an + option to either merge only dictionaries with matching key values + (intersection) or all dictionaries (union). Parameters: - list1 (List[Dict[str, Any]]): The first list of dictionaries. diff --git a/synkit/IO/debug.py b/synkit/IO/debug.py index 08deb6d..fdba57c 100644 --- a/synkit/IO/debug.py +++ b/synkit/IO/debug.py @@ -7,8 +7,8 @@ def setup_logging( log_level: str = "INFO", log_filename: str = None, task_type: str = None ) -> logging.Logger: - """ - Configures logging to either the console or a file, based on provided parameters. + """Configures logging to either the console or a file, based on provided + parameters. :param log_level: Logging level to set. Defaults to 'INFO'. Options: 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'. @@ -47,19 +47,17 @@ def setup_logging( def configure_warnings_and_logs( ignore_warnings: bool = False, disable_rdkit_logs: bool = False ) -> None: - """ - Configures Python warnings and RDKit log behavior based on input flags. + """Configures Python warnings and RDKit log behavior based on input flags. - :param ignore_warnings: Whether to suppress all Python warnings. Default is False. + :param ignore_warnings: Whether to suppress all Python warnings. + Default is False. :type ignore_warnings: bool - :param disable_rdkit_logs: Whether to disable RDKit error and warning logs. Default is False. + :param disable_rdkit_logs: Whether to disable RDKit error and + warning logs. Default is False. :type disable_rdkit_logs: bool - - :returns: None - - :usage: - Use this function to control verbosity (e.g. in production or testing), but use with - caution during development to avoid missing critical issues. + :returns: None :usage: Use this function to control verbosity (e.g. + in production or testing), but use with caution during + development to avoid missing critical issues. """ if ignore_warnings: warnings.filterwarnings("ignore") diff --git a/synkit/IO/dg_to_gml.py b/synkit/IO/dg_to_gml.py index 00a3902..c290ec6 100644 --- a/synkit/IO/dg_to_gml.py +++ b/synkit/IO/dg_to_gml.py @@ -104,9 +104,8 @@ def printGraph(g): return s, Rule.fromGMLString(s, add=False) def fit(self, dg, origSmiles): - """ - Matches the original SMILES to a list of generated reaction SMILES and - returns the parsed reaction. + """Matches the original SMILES to a list of generated reaction SMILES + and returns the parsed reaction. Parameters: - dg (DataGenerator): The data generator instance containing the reactions. diff --git a/synkit/IO/gml_to_nx.py b/synkit/IO/gml_to_nx.py index 4c28587..a186068 100644 --- a/synkit/IO/gml_to_nx.py +++ b/synkit/IO/gml_to_nx.py @@ -5,21 +5,21 @@ class GMLToNX: - """ - Parses GML-like text and transforms it into three NetworkX graphs - representing the left, right, and context graphs of a chemical reaction step. + """Parses GML-like text and transforms it into three NetworkX graphs + representing the left, right, and context graphs of a chemical reaction + step. :param gml_text: The GML-like text to parse. :type gml_text: str - - :ivar graphs: A dictionary containing 'left', 'right', and 'context' NetworkX graphs. + :ivar graphs: A dictionary containing 'left', 'right', and 'context' + NetworkX graphs. :vartype graphs: dict[str, nx.Graph] """ def __init__(self, gml_text: str): - """ - Initializes a GMLToNX object that can parse GML-like text into separate - NetworkX graphs representing different stages or components of a chemical reaction. + """Initializes a GMLToNX object that can parse GML-like text into + separate NetworkX graphs representing different stages or components of + a chemical reaction. :param gml_text: The GML-like text to be parsed. :type gml_text: str @@ -28,13 +28,13 @@ def __init__(self, gml_text: str): self.graphs = {"left": nx.Graph(), "context": nx.Graph(), "right": nx.Graph()} def _parse_element(self, line: str, current_section: str): - """ - Parses a line of GML-like text to extract node or edge data and adds it to the - current section's graph. + """Parses a line of GML-like text to extract node or edge data and adds + it to the current section's graph. :param line: A single line of GML-like text. :type line: str - :param current_section: Which section ('left', 'right', 'context') to add the node/edge to. + :param current_section: Which section ('left', 'right', + 'context') to add the node/edge to. :type current_section: str """ label_to_order = {"-": 1, ":": 1.5, "=": 2, "#": 3} @@ -60,12 +60,11 @@ def _parse_element(self, line: str, current_section: str): self.graphs[current_section].add_edge(source, target, order=order) def _extract_element_and_charge(self, label: str) -> Tuple[str, int]: - """ - Extracts the chemical element and its charge from a node label. + """Extracts the chemical element and its charge from a node label. - :param label: The label string from a GML node (e.g., 'N+', 'O2-', etc.). + :param label: The label string from a GML node (e.g., 'N+', + 'O2-', etc.). :type label: str - :returns: A tuple of (element symbol, formal charge). :rtype: tuple[str, int] """ @@ -82,10 +81,10 @@ def _extract_element_and_charge(self, label: str) -> Tuple[str, int]: return element, charge def _synchronize_nodes_and_edges(self): - """ - Ensures that all nodes and edges in 'context' appear in both 'left' and 'right'. - We do not remove edges from left or right if they are not in context. - We only add missing context nodes and edges to left and right. + """Ensures that all nodes and edges in 'context' appear in both 'left' + and 'right'. We do not remove edges from left or right if they are not + in context. We only add missing context nodes and edges to left and + right. :returns: None """ diff --git a/synkit/IO/graph_to_mol.py b/synkit/IO/graph_to_mol.py index 3c8f012..8049f0d 100644 --- a/synkit/IO/graph_to_mol.py +++ b/synkit/IO/graph_to_mol.py @@ -4,18 +4,20 @@ class GraphToMol: - """ - Converts a NetworkX graph representation of a molecule into an RDKit molecule object. + """Converts a NetworkX graph representation of a molecule into an RDKit + molecule object. - This class reconstructs RDKit molecules from node and edge attributes in a graph, - correctly interpreting atom types, charges, mapping numbers, bond orders, and optionally - explicit hydrogen counts. + This class reconstructs RDKit molecules from node and edge + attributes in a graph, correctly interpreting atom types, charges, + mapping numbers, bond orders, and optionally explicit hydrogen + counts. - :param node_attributes: Mapping of expected attribute names to node keys in the graph. For example, - {"element": "element", "charge": "charge", "atom_map": "atom_map"}. + :param node_attributes: Mapping of expected attribute names to node + keys in the graph. For example, {"element": "element", "charge": + "charge", "atom_map": "atom_map"}. :type node_attributes: Dict[str, str] - :param edge_attributes: Mapping of expected attribute names to edge keys in the graph. - For example, {"order": "order"}. + :param edge_attributes: Mapping of expected attribute names to edge + keys in the graph. For example, {"order": "order"}. :type edge_attributes: Dict[str, str] """ @@ -28,14 +30,15 @@ def __init__( }, edge_attributes: Dict[str, str] = {"order": "order"}, ): - """ - Initializes the GraphToMol object with mappings for node and edge attributes. + """Initializes the GraphToMol object with mappings for node and edge + attributes. - :param node_attributes: Mapping from desired atom attribute names to graph node keys. - E.g. {"element": "element", "charge": "charge", "atom_map": "atom_map"} + :param node_attributes: Mapping from desired atom attribute + names to graph node keys. E.g. {"element": "element", + "charge": "charge", "atom_map": "atom_map"} :type node_attributes: Dict[str, str] - :param edge_attributes: Mapping from desired bond attribute names to graph edge keys. - E.g. {"order": "order"} + :param edge_attributes: Mapping from desired bond attribute + names to graph edge keys. E.g. {"order": "order"} :type edge_attributes: Dict[str, str] """ self.node_attributes = node_attributes @@ -48,22 +51,23 @@ def graph_to_mol( sanitize: bool = True, use_h_count: bool = False, ) -> Chem.Mol: - """ - Converts a NetworkX graph into an RDKit molecule. + """Converts a NetworkX graph into an RDKit molecule. :param graph: The NetworkX graph representing the molecule. :type graph: nx.Graph - :param ignore_bond_order: If True, all bonds are created as single bonds regardless of edge attributes. - Defaults to False. + :param ignore_bond_order: If True, all bonds are created as + single bonds regardless of edge attributes. Defaults to + False. :type ignore_bond_order: bool - :param sanitize: If True, the resulting RDKit molecule will be sanitized after construction. - Defaults to True. + :param sanitize: If True, the resulting RDKit molecule will be + sanitized after construction. Defaults to True. :type sanitize: bool - :param use_h_count: If True, the 'hcount' attribute (if present) will be used to set explicit hydrogen counts - on atoms. Defaults to False. + :param use_h_count: If True, the 'hcount' attribute (if present) + will be used to set explicit hydrogen counts on atoms. + Defaults to False. :type use_h_count: bool - - :returns: An RDKit molecule constructed from the graph's nodes and edges. + :returns: An RDKit molecule constructed from the graph's nodes + and edges. :rtype: Chem.Mol """ mol = Chem.RWMol() @@ -110,12 +114,13 @@ def graph_to_mol( @staticmethod def get_bond_type_from_order(order: float) -> Chem.BondType: - """ - Converts a numerical bond order into the corresponding RDKit BondType. + """Converts a numerical bond order into the corresponding RDKit + BondType. :param order: The numerical bond order (typically 1, 2, or 3). :type order: float - :returns: The corresponding RDKit bond type (single, double, triple, or aromatic). + :returns: The corresponding RDKit bond type (single, double, + triple, or aromatic). :rtype: Chem.BondType """ if order == 1: diff --git a/synkit/IO/mol_to_graph.py b/synkit/IO/mol_to_graph.py index 55524c0..312bdf0 100644 --- a/synkit/IO/mol_to_graph.py +++ b/synkit/IO/mol_to_graph.py @@ -35,12 +35,13 @@ def __init__( ], edge_attrs: Optional[List[str]] = ["order"], ) -> None: - """ - Initialize the MolToGraph helper. + """Initialize the MolToGraph helper. - :param node_attrs: Names of node attributes to keep when transforming. + :param node_attrs: Names of node attributes to keep when + transforming. :type node_attrs: List[str] - :param edge_attrs: Names of edge attributes to keep when transforming. + :param edge_attrs: Names of edge attributes to keep when + transforming. :type edge_attrs: List[str] """ self.node_attrs: List[str] = node_attrs or [] @@ -52,18 +53,21 @@ def transform( drop_non_aam: bool = False, use_index_as_atom_map: bool = False, ) -> nx.Graph: - """ - Build a graph directly from a molecule, including only selected attributes. + """Build a graph directly from a molecule, including only selected + attributes. :param mol: The RDKit molecule to convert. :type mol: Chem.Mol - :param drop_non_aam: If True, skips atoms without atom mapping numbers - (requires use_index_as_atom_map=True). Defaults to False. + :param drop_non_aam: If True, skips atoms without atom mapping + numbers (requires use_index_as_atom_map=True). Defaults to + False. :type drop_non_aam: bool - :param use_index_as_atom_map: If True, uses atom mapping numbers as node IDs when present; - otherwise uses atom index+1. Defaults to False. + :param use_index_as_atom_map: If True, uses atom mapping numbers + as node IDs when present; otherwise uses atom index+1. + Defaults to False. :type use_index_as_atom_map: bool - :returns: A NetworkX graph containing only the specified node and edge attributes. + :returns: A NetworkX graph containing only the specified node + and edge attributes. :rtype: nx.Graph """ if drop_non_aam and not use_index_as_atom_map: @@ -107,8 +111,7 @@ def transform( @staticmethod def _gather_atom_properties(atom: Chem.Atom) -> Dict[str, Any]: - """ - Collect the full set of atom attributes for graph nodes. + """Collect the full set of atom attributes for graph nodes. :param atom: The RDKit Atom object. :type atom: Chem.Atom @@ -137,8 +140,7 @@ def _gather_atom_properties(atom: Chem.Atom) -> Dict[str, Any]: @staticmethod def _gather_bond_properties(bond: Chem.Bond) -> Dict[str, Any]: - """ - Collect the full set of bond attributes for graph edges. + """Collect the full set of bond attributes for graph edges. :param bond: The RDKit Bond object. :type bond: Chem.Bond @@ -155,8 +157,7 @@ def _gather_bond_properties(bond: Chem.Bond) -> Dict[str, Any]: @staticmethod def get_stereochemistry(atom: Chem.Atom) -> str: - """ - Determine the stereochemistry (R/S) of a chiral atom. + """Determine the stereochemistry (R/S) of a chiral atom. :param atom: The RDKit Atom object. :type atom: Chem.Atom @@ -172,12 +173,12 @@ def get_stereochemistry(atom: Chem.Atom) -> str: @staticmethod def get_bond_stereochemistry(bond: Chem.Bond) -> str: - """ - Determine the stereochemistry (E/Z) of a double bond. + """Determine the stereochemistry (E/Z) of a double bond. :param bond: The RDKit Bond object. :type bond: Chem.Bond - :returns: 'E', 'Z', or 'N' for non-stereospecific or non-double bond. + :returns: 'E', 'Z', or 'N' for non-stereospecific or non-double + bond. :rtype: str """ if bond.GetBondType() != Chem.BondType.DOUBLE: @@ -191,8 +192,7 @@ def get_bond_stereochemistry(bond: Chem.Bond) -> str: @staticmethod def has_atom_mapping(mol: Chem.Mol) -> bool: - """ - Check if any atom in the molecule has an atom mapping number. + """Check if any atom in the molecule has an atom mapping number. :param mol: The RDKit molecule. :type mol: Chem.Mol @@ -203,8 +203,7 @@ def has_atom_mapping(mol: Chem.Mol) -> bool: @staticmethod def random_atom_mapping(mol: Chem.Mol) -> Chem.Mol: - """ - Assign random atom mapping numbers to all atoms in the molecule. + """Assign random atom mapping numbers to all atoms in the molecule. :param mol: The RDKit molecule. :type mol: Chem.Mol @@ -225,17 +224,18 @@ def mol_to_graph( light_weight: bool = False, use_index_as_atom_map: bool = False, ) -> nx.Graph: - """ - Convert a molecule to a full-featured NetworkX graph. + """Convert a molecule to a full-featured NetworkX graph. :param mol: The RDKit molecule to convert. :type mol: Chem.Mol :param drop_non_aam: If True, drop atoms without mapping numbers - (requires use_index_as_atom_map=True). Defaults to False. + (requires use_index_as_atom_map=True). Defaults to False. :type drop_non_aam: bool - :param light_weight: If True, create a lightweight graph with minimal attributes. Defaults to False. + :param light_weight: If True, create a lightweight graph with + minimal attributes. Defaults to False. :type light_weight: bool - :param use_index_as_atom_map: If True, prefer atom maps as node IDs. Defaults to False. + :param use_index_as_atom_map: If True, prefer atom maps as node + IDs. Defaults to False. :type use_index_as_atom_map: bool :returns: A NetworkX graph of the molecule with all attributes. :rtype: nx.Graph @@ -257,14 +257,15 @@ def _create_light_weight_graph( drop_non_aam: bool = False, use_index_as_atom_map: bool = False, ) -> nx.Graph: - """ - Create a lightweight graph with basic atom and bond info. + """Create a lightweight graph with basic atom and bond info. :param mol: The RDKit molecule. :type mol: Chem.Mol - :param drop_non_aam: If True, skip atoms without mapping numbers. Defaults to False. + :param drop_non_aam: If True, skip atoms without mapping + numbers. Defaults to False. :type drop_non_aam: bool - :param use_index_as_atom_map: If True, use atom maps as node IDs when present. Defaults to False. + :param use_index_as_atom_map: If True, use atom maps as node IDs + when present. Defaults to False. :type use_index_as_atom_map: bool :returns: A NetworkX graph with minimal node/edge attributes. :rtype: nx.Graph @@ -306,14 +307,15 @@ def _create_detailed_graph( drop_non_aam: bool = True, use_index_as_atom_map: bool = True, ) -> nx.Graph: - """ - Create a detailed graph with full atom and bond attributes. + """Create a detailed graph with full atom and bond attributes. :param mol: The RDKit molecule. :type mol: Chem.Mol - :param drop_non_aam: If True, skip atoms without mapping numbers. Defaults to True. + :param drop_non_aam: If True, skip atoms without mapping + numbers. Defaults to True. :type drop_non_aam: bool - :param use_index_as_atom_map: If True, use atom maps as node IDs when present. Defaults to True. + :param use_index_as_atom_map: If True, use atom maps as node IDs + when present. Defaults to True. :type use_index_as_atom_map: bool :returns: A NetworkX graph with full node/edge attributes. :rtype: nx.Graph @@ -341,8 +343,7 @@ def _create_detailed_graph( @staticmethod def add_partial_charges(mol: Chem.Mol) -> None: - """ - Compute and assign Gasteiger charges to all atoms in the molecule. + """Compute and assign Gasteiger charges to all atoms in the molecule. :param mol: The RDKit molecule. :type mol: Chem.Mol diff --git a/synkit/IO/nx_to_gml.py b/synkit/IO/nx_to_gml.py index 5f66ef6..ed63280 100644 --- a/synkit/IO/nx_to_gml.py +++ b/synkit/IO/nx_to_gml.py @@ -4,29 +4,28 @@ class NXToGML: - """ - Converts NetworkX graph representations of chemical reactions to GML (Graph Modelling Language) strings. - Useful for exporting reaction rules in a standard graph format. + """Converts NetworkX graph representations of chemical reactions to GML + (Graph Modelling Language) strings. Useful for exporting reaction rules in + a standard graph format. - This class provides static methods for converting individual graphs, sets of reaction graphs, and - managing charge/attribute changes in the export process. + This class provides static methods for converting individual graphs, + sets of reaction graphs, and managing charge/attribute changes in + the export process. """ def __init__(self) -> None: - """ - Initializes an NXToGML object. - """ + """Initializes an NXToGML object.""" pass @staticmethod def _charge_to_string(charge: int) -> str: - """ - Converts an integer charge into a string representation. + """Converts an integer charge into a string representation. - :param charge: The charge value, which can be positive, negative, or zero. + :param charge: The charge value, which can be positive, + negative, or zero. :type charge: int - - :returns: The string representation of the charge (e.g. '+', '2+', '-', '3-', ''). + :returns: The string representation of the charge (e.g. '+', + '2+', '-', '3-', ''). :rtype: str """ if charge > 0: @@ -40,8 +39,8 @@ def _charge_to_string(charge: int) -> str: def _find_changed_nodes( graph1: nx.Graph, graph2: nx.Graph, attributes: List[str] = ["charge"] ) -> List[int]: - """ - Identifies nodes with changes in specified attributes between two NetworkX graphs. + """Identifies nodes with changes in specified attributes between two + NetworkX graphs. :param graph1: The first NetworkX graph. :type graph1: nx.Graph @@ -49,8 +48,8 @@ def _find_changed_nodes( :type graph2: nx.Graph :param attributes: List of attribute names to check for changes. :type attributes: list[str] - - :returns: Node identifiers that have changes in the specified attributes. + :returns: Node identifiers that have changes in the specified + attributes. :rtype: list[int] """ changed_nodes = [] @@ -71,19 +70,21 @@ def _convert_graph_to_gml( changed_node_ids: List[int], explicit_hydrogen: bool = False, ) -> str: - """ - Converts a NetworkX graph to a GML string for a specific reaction section. + """Converts a NetworkX graph to a GML string for a specific reaction + section. :param graph: The NetworkX graph to be converted. :type graph: nx.Graph - :param section: The section name in the GML output ('left', 'right', or 'context'). + :param section: The section name in the GML output ('left', + 'right', or 'context'). :type section: str :param changed_node_ids: List of nodes with changed attributes. :type changed_node_ids: list[int] - :param explicit_hydrogen: Whether to explicitly include hydrogen atoms in the output. + :param explicit_hydrogen: Whether to explicitly include hydrogen + atoms in the output. :type explicit_hydrogen: bool - - :returns: The GML string representation of the graph for the specified section. + :returns: The GML string representation of the graph for the + specified section. :rtype: str """ order_to_label = {1: "-", 1.5: ":", 2: "=", 3: "#"} @@ -134,8 +135,8 @@ def _rule_grammar( changed_node_ids: List[int], explicit_hydrogen: bool, ) -> str: - """ - Generates a GML string for a chemical rule, including left, context, and right graphs. + """Generates a GML string for a chemical rule, including left, context, + and right graphs. :param L: The left graph. :type L: nx.Graph @@ -147,9 +148,9 @@ def _rule_grammar( :type rule_name: str :param changed_node_ids: List of nodes with changed attributes. :type changed_node_ids: list[int] - :param explicit_hydrogen: Whether to explicitly include hydrogen atoms in the output. + :param explicit_hydrogen: Whether to explicitly include hydrogen + atoms in the output. :type explicit_hydrogen: bool - :returns: The GML string representation of the rule. :rtype: str """ @@ -171,21 +172,22 @@ def transform( attributes: List[str] = ["charge"], explicit_hydrogen: bool = False, ) -> str: - """ - Processes a triple of reaction graphs to generate a GML string rule, with options for node - reindexing and explicit hydrogen expansion. + """Processes a triple of reaction graphs to generate a GML string rule, + with options for node reindexing and explicit hydrogen expansion. :param graph_rules: Tuple containing (L, R, K) reaction graphs. :type graph_rules: tuple[nx.Graph, nx.Graph, nx.Graph] :param rule_name: The rule name to use in the output. :type rule_name: str - :param reindex: Whether to reindex node IDs based on the L graph sequence. + :param reindex: Whether to reindex node IDs based on the L graph + sequence. :type reindex: bool - :param attributes: List of attribute names to check for node changes. + :param attributes: List of attribute names to check for node + changes. :type attributes: list[str] - :param explicit_hydrogen: Whether to explicitly include hydrogen atoms in the output. + :param explicit_hydrogen: Whether to explicitly include hydrogen + atoms in the output. :type explicit_hydrogen: bool - :returns: The GML string representing the chemical rule. :rtype: str """ diff --git a/synkit/IO/smiles_to_id.py b/synkit/IO/smiles_to_id.py index bd7bb71..26d6afa 100644 --- a/synkit/IO/smiles_to_id.py +++ b/synkit/IO/smiles_to_id.py @@ -6,8 +6,8 @@ def smiles_to_iupac(smiles_string: str, timeout: int = 1): - """ - Converts a SMILES string to its corresponding IUPAC name(s) using the PubChem PUG REST API. + """Converts a SMILES string to its corresponding IUPAC name(s) using the + PubChem PUG REST API. Parameters: - smiles_string (str): The SMILES string of the compound (e.g., "C=O" for formaldehyde). @@ -64,8 +64,7 @@ def smiles_to_iupac(smiles_string: str, timeout: int = 1): def batch_process_smiles(smiles_batch: List[str], timeout=1): - """ - Processes a batch of SMILES strings to get IUPAC names. + """Processes a batch of SMILES strings to get IUPAC names. Parameters: - smiles_batch (list): A list of SMILES strings to process. @@ -80,8 +79,8 @@ def batch_process_smiles(smiles_batch: List[str], timeout=1): def get_iupac_for_smiles_list( smiles_list: List[str], batch_size=10, n_jobs=4, timeout=1 ): - """ - Convert a list of SMILES strings to their corresponding IUPAC names using the PubChem API with batch processing. + """Convert a list of SMILES strings to their corresponding IUPAC names + using the PubChem API with batch processing. Parameters: smiles_list (list): A list of SMILES strings to be converted to IUPAC names. diff --git a/synkit/Rule/Apply/reactor_rule.py b/synkit/Rule/Apply/reactor_rule.py index ff49a4b..74b60c5 100644 --- a/synkit/Rule/Apply/reactor_rule.py +++ b/synkit/Rule/Apply/reactor_rule.py @@ -3,7 +3,7 @@ from synkit.IO.chem_converter import gml_to_smart from synkit.Chem.Reaction.standardize import Standardize -from synkit.Chem.Reaction.rsmi_utils import reverse_reaction +from synkit.Chem.utils import reverse_reaction from synkit.Graph.ITS.normalize_aam import NormalizeAAM from synkit.Graph.ITS.its_expand import ITSExpand @@ -27,16 +27,15 @@ class ReactorRule: - """ - Handles the transformation of SMILES strings to reaction SMILES (RSMI) by applying - chemical reaction rules defined in GML strings. It can optionally reverse the reaction, - exclude atom mappings, and include unchanged reagents in the output. + """Handles the transformation of SMILES strings to reaction SMILES (RSMI) + by applying chemical reaction rules defined in GML strings. + + It can optionally reverse the reaction, exclude atom mappings, and + include unchanged reagents in the output. """ def __init__(self) -> None: - """ - Initializes the ReactorRule object. - """ + """Initializes the ReactorRule object.""" pass def _process( @@ -47,9 +46,9 @@ def _process( exclude_aam: bool = False, include_reagents: bool = False, ) -> List[str]: - """ - Processes a reaction SMILES (RSMI) to adjust atom mappings, extract reaction centers, - decompose into separate reactant and product graphs, and generate the corresponding SMILES. + """Processes a reaction SMILES (RSMI) to adjust atom mappings, extract + reaction centers, decompose into separate reactant and product graphs, + and generate the corresponding SMILES. Parameters: - smiles (str): The SMILES string of the molecule to be transformed. diff --git a/synkit/Rule/Apply/retro_reactor.py b/synkit/Rule/Apply/retro_reactor.py index caba446..9a29cfc 100644 --- a/synkit/Rule/Apply/retro_reactor.py +++ b/synkit/Rule/Apply/retro_reactor.py @@ -23,8 +23,7 @@ class RetroReactor: def __init__(self) -> None: - """ - Initialize the RuleFrag class with caches and null initial values. + """Initialize the RuleFrag class with caches and null initial values. Attributes: - backward_cache: A dictionary cache (keyed by (smiles, rule)) to avoid redundant computations. @@ -32,9 +31,9 @@ def __init__(self) -> None: self.backward_cache: Dict[Tuple[str, str], List[str]] = {} def _apply_backward(self, smiles: str, rule: str) -> List[str]: - """ - Apply a transformation rule in backward mode to a SMILES string, returning possible precursors. - Uses caching to avoid redundant computations. + """Apply a transformation rule in backward mode to a SMILES string, + returning possible precursors. Uses caching to avoid redundant + computations. Parameters: - smiles (str): SMILES string to transform. @@ -71,10 +70,9 @@ def _apply_backward(self, smiles: str, rule: str) -> List[str]: return self.backward_cache[cache_key] def _heuristic(self, current_smiles: str, precursor_smiles: str) -> int: - """ - Heuristic function for A* search. Here, we define the "distance" as the - absolute difference in the carbon count between the current SMILES and - the known precursor SMILES. + """Heuristic function for A* search. Here, we define the "distance" as + the absolute difference in the carbon count between the current SMILES + and the known precursor SMILES. Parameters: - current_smiles (str): The SMILES of the node being expanded. @@ -93,8 +91,8 @@ def backward_synthesis_search( max_solutions: int = 1, fast_process: bool = True, ) -> List[Dict[str, List]]: - """ - Perform a backward synthesis search from a product to a known precursor using A* search. + """Perform a backward synthesis search from a product to a known + precursor using A* search. Constrains any intermediate X to satisfy: n_C(known_precursor_smiles) <= n_C(X) <= n_C(product_smiles). diff --git a/synkit/Rule/Apply/rule_matcher.py b/synkit/Rule/Apply/rule_matcher.py index a63fbe5..4e9b88f 100644 --- a/synkit/Rule/Apply/rule_matcher.py +++ b/synkit/Rule/Apply/rule_matcher.py @@ -29,8 +29,7 @@ class RuleMatcher: - """ - Match a reaction SMILES against a transformation‑rule graph and extract + """Match a reaction SMILES against a transformation‑rule graph and extract the SMARTS pattern that reproduces the reaction. On initialization, the matcher standardizes the RSMI, builds reactant/product @@ -56,15 +55,15 @@ class RuleMatcher: """ def __init__(self, rsmi: str, rule: nx.Graph) -> None: - """ - Initialize the matcher by standardizing the RSMI, building graphs, + """Initialize the matcher by standardizing the RSMI, building graphs, checking balance, and computing the match. :param rsmi: Reaction SMILES in 'reactant>>product' format. :type rsmi: str :param rule: Transformation‑rule graph. :type rule: nx.Graph - :raises ValueError: If no SMARTS reproduces the RSMI under the given rule. + :raises ValueError: If no SMARTS reproduces the RSMI under the + given rule. """ self.std = Standardize() self.rsmi = self.std.fit(rsmi) @@ -85,8 +84,7 @@ def __init__(self, rsmi: str, rule: nx.Graph) -> None: self.result = match def get_result(self) -> Tuple[str, nx.Graph]: - """ - Return the SMARTS and rule graph found during initialization. + """Return the SMARTS and rule graph found during initialization. :returns: A tuple (smarts, rule_graph). :rtype: tuple[str, nx.Graph] @@ -94,10 +92,10 @@ def get_result(self) -> Tuple[str, nx.Graph]: return self.result def _match_valid(self) -> Optional[Tuple[str, nx.Graph]]: - """ - Attempt a direct (balanced) match of the rule. + """Attempt a direct (balanced) match of the rule. - :returns: (smarts, rule) if direct match succeeds; otherwise None. + :returns: (smarts, rule) if direct match succeeds; otherwise + None. :rtype: Optional[tuple[str, nx.Graph]] """ reactor = SynReactor(substrate=self.r_graph, template=self.rule) @@ -107,13 +105,13 @@ def _match_valid(self) -> Optional[Tuple[str, nx.Graph]]: return None def _match_reverse(self) -> Optional[Tuple[str, nx.Graph]]: - """ - Attempt a reverse‑balance (partial) match for unbalanced reactions. + """Attempt a reverse‑balance (partial) match for unbalanced reactions. - First tries matching on product fragments, then on reactant fragments - with the template inverted. + First tries matching on product fragments, then on reactant + fragments with the template inverted. - :returns: (smarts, rule) if a partial match is found; otherwise None. + :returns: (smarts, rule) if a partial match is found; otherwise + None. :rtype: Optional[tuple[str, nx.Graph]] """ # Product‑side fragments @@ -138,8 +136,7 @@ def _match_reverse(self) -> Optional[Tuple[str, nx.Graph]]: @staticmethod def all_in(a: List[str], b: List[str]) -> bool: - """ - Check if every element of list `a` appears in list `b`. + """Check if every element of list `a` appears in list `b`. :param a: List of elements to test for membership. :type a: list[str] @@ -151,8 +148,7 @@ def all_in(a: List[str], b: List[str]) -> bool: return set(a).issubset(b) def help(self) -> None: - """ - Print internal state and candidate SMARTS patterns for debugging. + """Print internal state and candidate SMARTS patterns for debugging. :returns: None :rtype: NoneType @@ -165,8 +161,7 @@ def help(self) -> None: print(" ", smarts) def __str__(self) -> str: - """ - Short string showing the RSMI and balance status. + """Short string showing the RSMI and balance status. :returns: Human‑readable summary. :rtype: str @@ -175,8 +170,7 @@ def __str__(self) -> str: return f"" def __repr__(self) -> str: - """ - Detailed representation including rule size and balance. + """Detailed representation including rule size and balance. :returns: repr string. :rtype: str diff --git a/synkit/Rule/Apply/rule_rbl.py b/synkit/Rule/Apply/rule_rbl.py index f94d18d..613b3b3 100644 --- a/synkit/Rule/Apply/rule_rbl.py +++ b/synkit/Rule/Apply/rule_rbl.py @@ -1,7 +1,7 @@ import importlib.util from typing import List from synkit.Chem.Reaction.standardize import Standardize -from synkit.Chem.Reaction.rsmi_utils import ( +from synkit.Chem.utils import ( find_longest_fragment, merge_reaction, remove_common_reagents, @@ -26,8 +26,8 @@ def __init__(self) -> None: pass def rbl(self, rsmi: str, gml_rule: str, remove_aam: bool = True) -> List[str]: - """ - Applies transformation rules to a reaction SMILES string based on GML rules. + """Applies transformation rules to a reaction SMILES string based on + GML rules. Parameters: - rsmi (str): Reaction SMILES string to process. diff --git a/synkit/Rule/Compose/compose_rule.py b/synkit/Rule/Compose/compose_rule.py index 57b0ee0..cbcf82d 100644 --- a/synkit/Rule/Compose/compose_rule.py +++ b/synkit/Rule/Compose/compose_rule.py @@ -4,8 +4,8 @@ from synkit.IO.chem_converter import gml_to_smart, smart_to_gml from synkit.Rule.Modify.rule_utils import _increment_gml_ids from synkit.Chem.Reaction.standardize import Standardize -from synkit.Chem.Reaction.cleanning import Cleanning -from synkit.Chem.Reaction.rsmi_utils import find_longest_fragment +from synkit.Chem.Reaction.cleaning import Cleaning +from synkit.Chem.utils import find_longest_fragment logger = setup_logging() @@ -23,8 +23,7 @@ class ComposeRule: @staticmethod def filter_smallest_vertex(combo: List[object]) -> List[object]: - """ - Filters and returns the elements from a list that have the smallest + """Filters and returns the elements from a list that have the smallest number of vertices in their context. Parameters: @@ -50,9 +49,8 @@ def filter_smallest_vertex(combo: List[object]) -> List[object]: @staticmethod def rule_cluster(graphs: List[Any]) -> List[Any]: - """ - Cluster graphs based on their isomorphic relationships and - return a representative from each cluster. + """Cluster graphs based on their isomorphic relationships and return a + representative from each cluster. Parameters: - graphs (List[Any]): A list of graph objects. @@ -84,8 +82,8 @@ def rule_cluster(graphs: List[Any]) -> List[Any]: def _compose_mapping( rule_1: str, rule_2: str, mapping: Dict[int, int], return_string: bool = True ) -> Any: - """ - Compose two rule graphs from their GML representations using a mapping between external IDs. + """Compose two rule graphs from their GML representations using a + mapping between external IDs. Parameters: - rule_1 (str): The GML representation for the first rule. @@ -118,8 +116,8 @@ def _compose_mapping( @staticmethod def _compose(rule_1: str, rule_2: str, return_string: bool = True) -> List[Any]: - """ - Compose two rules and return a list of modifications that pass chemical valence checks. + """Compose two rules and return a list of modifications that pass + chemical valence checks. Parameters: - rule_1 (str): The first rule (in GML format) to compose. @@ -145,8 +143,8 @@ def _compose(rule_1: str, rule_2: str, return_string: bool = True) -> List[Any]: @staticmethod def _get_valid_rule(rules: List[str], format: str = "gml") -> List[str]: - """ - Validate and convert a list of rule GML strings to either SMARTS or GML format. + """Validate and convert a list of rule GML strings to either SMARTS or + GML format. Parameters: - rules (List[str]): A list of rule GML strings. @@ -171,8 +169,8 @@ def _get_valid_rule(rules: List[str], format: str = "gml") -> List[str]: @staticmethod def _get_comp_reaction(smart_1: str, smart_2: str) -> str: - """ - Compute a representative reaction SMILES for the composed rule from two SMARTS strings. + """Compute a representative reaction SMILES for the composed rule from + two SMARTS strings. Parameters: - smart_1 (str): The first reaction in SMARTS notation. @@ -190,8 +188,8 @@ def _get_comp_reaction(smart_1: str, smart_2: str) -> str: return new_rsmi def get_rule_comp(self, smart_1: str, smart_2: str) -> Optional[str]: - """ - Compose two reaction SMARTS strings into a rule (GML format) that reproduces a reference reaction. + """Compose two reaction SMARTS strings into a rule (GML format) that + reproduces a reference reaction. Parameters: - smart_1 (str): The first reaction in SMARTS notation. @@ -215,7 +213,7 @@ def get_rule_comp(self, smart_1: str, smart_2: str) -> Optional[str]: for candidate in candidate_rules: reactor = MODReactor(initial_smiles, candidate).run() inferred_rsmi = reactor.get_reaction_smiles() - inferred_rsmi = Cleanning.clean_smiles(inferred_rsmi) + inferred_rsmi = Cleaning.clean_smiles(inferred_rsmi) inferred_prod = [i.split(">>")[1].split(".") for i in inferred_rsmi] if any(largest_prod in smi for smi in inferred_prod): cds.append(candidate) diff --git a/synkit/Rule/Compose/rule_compose.py b/synkit/Rule/Compose/rule_compose.py index 2a401f2..9d9936b 100644 --- a/synkit/Rule/Compose/rule_compose.py +++ b/synkit/Rule/Compose/rule_compose.py @@ -25,8 +25,7 @@ def __init__(self) -> None: @staticmethod def filter_smallest_vertex(combo: List[object]) -> List[object]: - """ - Filters and returns the elements from a list that have the smallest + """Filters and returns the elements from a list that have the smallest number of vertices in their context. Parameters: @@ -52,9 +51,8 @@ def filter_smallest_vertex(combo: List[object]) -> List[object]: @staticmethod def rule_cluster(graphs: List) -> List: - """ - Clusters graphs based on their isomorphic relationship and returns - a list of graphs, each from a different cluster. + """Clusters graphs based on their isomorphic relationship and returns a + list of graphs, each from a different cluster. Parameters: - graphs: A list of graph objects. @@ -89,8 +87,8 @@ def rule_cluster(graphs: List) -> List: @staticmethod def _compose(rule_1, rule_2): - """ - Compose two rules and filter the results based on chemical valence constraints. + """Compose two rules and filter the results based on chemical valence + constraints. Parameters: - rule_1: First rule object to compose. @@ -115,8 +113,7 @@ def _compose(rule_1, rule_2): @staticmethod def _process_compose(rule_1_id, rule_2_id, rule_path, rule_path_compose): - """ - Process and compose two rules based on their GML files. + """Process and compose two rules based on their GML files. Parameters: - rule_1_id (str): Identifier for the first rule. @@ -144,8 +141,8 @@ def _process_compose(rule_1_id, rule_2_id, rule_path, rule_path_compose): @staticmethod def _auto_compose(rule_path, rule_path_compose): - """ - Automatically find all GML files in the given directory and compose them pairwise. + """Automatically find all GML files in the given directory and compose + them pairwise. Parameters: - rule_path (str): Directory path where the GML files are stored. @@ -182,11 +179,10 @@ def _auto_compose(rule_path, rule_path_compose): def save_gml_from_text( gml_content: str, gml_file_path: str, rule_id: str, parent_ids: List[str] ) -> bool: - """ - Save a text string to a GML file by modifying the 'ruleID' line to include parent - rule names. This function parses the given GML content, identifies any lines - starting with 'ruleID', and replaces these lines with a new ruleID that - incorporates identifiers from parent rules. + """Save a text string to a GML file by modifying the 'ruleID' line to + include parent rule names. This function parses the given GML content, + identifies any lines starting with 'ruleID', and replaces these lines + with a new ruleID that incorporates identifiers from parent rules. Parameters: - gml_content (str): The content to be saved to the GML file. This should be the diff --git a/synkit/Rule/Compose/rule_mapping.py b/synkit/Rule/Compose/rule_mapping.py index 6613c6c..8c031b1 100644 --- a/synkit/Rule/Compose/rule_mapping.py +++ b/synkit/Rule/Compose/rule_mapping.py @@ -11,8 +11,9 @@ class RuleMapping: def enumerate_all_unique_mappings( child: nx.Graph, parent: nx.Graph ) -> List[Dict[Any, Any]]: - """ - Generate all unique mappings (as dictionaries) from the child graph to the parent graph. + """Generate all unique mappings (as dictionaries) from the child graph + to the parent graph. + A mapping is valid if: - Every node from the child graph is assigned exactly one parent node. - The parent's node has the same 'element' attribute as the child node. @@ -66,9 +67,9 @@ def backtrack( def standardize_order( order_tuple: Tuple[float, ...], ) -> Optional[Tuple[float, ...]]: - """ - Standardizes an order tuple by adding 1 to every element repeatedly until no element is negative. - If the resulting tuple becomes all zeros, returns None, which indicates that the edge should be dropped. + """Standardizes an order tuple by adding 1 to every element repeatedly + until no element is negative. If the resulting tuple becomes all zeros, + returns None, which indicates that the edge should be dropped. For example: (-1.0, 0.0) --> add 1 gives (0.0, 1.0) @@ -90,8 +91,8 @@ def standardize_order( @staticmethod def keep_largest_component(graph: nx.Graph) -> nx.Graph: - """ - Given an undirected graph, returns the subgraph corresponding to the largest connected component. + """Given an undirected graph, returns the subgraph corresponding to the + largest connected component. Parameters: - graph (nx.Graph): The input graph from which the largest component is extracted. @@ -211,9 +212,9 @@ def graph_alignment( node_label_default: List[str] = ["*"], edge_attribute: str = "standard_order", ) -> Tuple[bool, Optional[Dict[Any, Any]]]: - """ - Check whether the child and parent graphs are isomorphic using specified node and edge match criteria. - If they are isomorphic, return the mapping from child to parent. + """Check whether the child and parent graphs are isomorphic using + specified node and edge match criteria. If they are isomorphic, return + the mapping from child to parent. Parameters: - child (nx.Graph): The child graph to align. @@ -244,8 +245,8 @@ def get_child1_to_child2_mapping( mapping_child1_to_parent: Dict[Any, Any], mapping_child2_to_parent: Dict[Any, Any], ) -> Dict[Any, Optional[Any]]: - """ - Build a mapping from Child1 to Child2 using each child's mapping to a common Parent. + """Build a mapping from Child1 to Child2 using each child's mapping to + a common Parent. If a Parent node in Child1's mapping is not in Child2's inverted mapping, that Child1 node will map to None. @@ -277,8 +278,8 @@ def get_child1_to_child2_mapping( return mapping_child1_to_child2 def fit(self, rule_1: str, rule_2: str, comp_rule: str) -> Optional[Dict[Any, Any]]: - """ - Demonstrate an alignment-based composition workflow using the class methods. + """Demonstrate an alignment-based composition workflow using the class + methods. 1. Convert each GML-based rule into an internal graph (via gml_to_its). 2. Enumerate all unique mappings from rule_2 to comp_rule. diff --git a/synkit/Rule/Compose/seq_comp.py b/synkit/Rule/Compose/seq_comp.py index 5eec9a5..d28363c 100644 --- a/synkit/Rule/Compose/seq_comp.py +++ b/synkit/Rule/Compose/seq_comp.py @@ -5,24 +5,23 @@ class SeqComp: - """ - A class for generating pairwise mappings between sequential chemical reaction rules. + """A class for generating pairwise mappings between sequential chemical + reaction rules. - This class takes a list of reaction SMARTS strings, converts them to their corresponding - GML representations, composes candidate reaction rules for each consecutive pair, and computes - a mapping between the rules using a rule mapping algorithm. + This class takes a list of reaction SMARTS strings, converts them to + their corresponding GML representations, composes candidate reaction + rules for each consecutive pair, and computes a mapping between the + rules using a rule mapping algorithm. """ def __init__(self) -> None: - """ - Initialize an instance of the SeqComp class. - """ + """Initialize an instance of the SeqComp class.""" pass @staticmethod def sequence_map(smarts: List[str]) -> Dict[str, Optional[dict]]: - """ - Generate pairwise mapping dictionaries between consecutive reaction SMARTS strings. + """Generate pairwise mapping dictionaries between consecutive reaction + SMARTS strings. This function processes a list of reaction SMARTS strings by: 1. Converting each SMARTS string to its GML representation. diff --git a/synkit/Rule/Compose/valence_constrain.py b/synkit/Rule/Compose/valence_constrain.py index 84a5f93..543430d 100644 --- a/synkit/Rule/Compose/valence_constrain.py +++ b/synkit/Rule/Compose/valence_constrain.py @@ -17,9 +17,8 @@ class ValenceConstrain: def __init__(self): - """ - Initialize the ValenceConstrain class by setting up bond type orders and loading - the maximum valence data. + """Initialize the ValenceConstrain class by setting up bond type orders + and loading the maximum valence data. Parameters: - None @@ -39,8 +38,7 @@ def __init__(self): self.maxValence = load_database(maxValence_path)[0] def valence(self, vertex) -> int: - """ - Calculate the valence of a vertex based on its incident edges. + """Calculate the valence of a vertex based on its incident edges. Parameters: - vertex (Vertex): The vertex for which to calculate the valence. @@ -51,8 +49,7 @@ def valence(self, vertex) -> int: return sum(self.btToOrder[edge.bondType] for edge in vertex.incidentEdges) def check_rule(self, rule, verbose: bool = False, log_error: bool = False) -> bool: - """ - Check if the rule is chemically valid according to valence rules. + """Check if the rule is chemically valid according to valence rules. Parameters: - rule (Rule): The rule to check for chemical validity. @@ -92,8 +89,7 @@ def check_rule(self, rule, verbose: bool = False, log_error: bool = False) -> bo return False def split(self, rules: List) -> Tuple[List, List]: - """ - Split rules into 'good' and 'bad' based on their chemical validity. + """Split rules into 'good' and 'bad' based on their chemical validity. Parameters: - rules (List[Rule]): A list of rules to be checked and split. diff --git a/synkit/Rule/Modify/implict_rule.py b/synkit/Rule/Modify/implict_rule.py index 44e83af..0ef9cca 100644 --- a/synkit/Rule/Modify/implict_rule.py +++ b/synkit/Rule/Modify/implict_rule.py @@ -9,9 +9,8 @@ def implicit_rule( rsmi: Union[str, List[str]], disconnected: bool = True, balance_its: bool = False ) -> Union[Any, List[Any]]: - """ - Construct reaction-center objects from reaction SMILES by applying implicit‐H rules - and ITS graph construction. + """Construct reaction-center objects from reaction SMILES by applying + implicit‐H rules and ITS graph construction. Parameters ---------- diff --git a/synkit/Rule/Modify/longest_path.py b/synkit/Rule/Modify/longest_path.py index e202ba0..af9b617 100644 --- a/synkit/Rule/Modify/longest_path.py +++ b/synkit/Rule/Modify/longest_path.py @@ -5,8 +5,7 @@ class LongestPath: def __init__(self, G: nx.Graph): - """ - Initializes the LongestPath object with a graph. + """Initializes the LongestPath object with a graph. Parameters: - G (nx.Graph): The networkx graph. @@ -15,9 +14,8 @@ def __init__(self, G: nx.Graph): self.vertices = len(G.nodes) def BFS(self, u: int) -> Tuple[int, int]: - """ - Performs a Breadth-First Search (BFS) from a given node `u` to - find the farthest node and its distance. + """Performs a Breadth-First Search (BFS) from a given node `u` to find + the farthest node and its distance. Parameters: - u (int): The starting node for the BFS. @@ -55,9 +53,8 @@ def BFS(self, u: int) -> Tuple[int, int]: return nodeIdx, maxDis def LongestPathInDisconnectedGraph(self) -> int: - """ - Finds the longest path in a potentially disconnected graph. - The graph can consist of multiple components. + """Finds the longest path in a potentially disconnected graph. The + graph can consist of multiple components. This method performs a BFS on every unvisited component to find the farthest node and computes the longest path across all components. diff --git a/synkit/Rule/Modify/molecule_rule.py b/synkit/Rule/Modify/molecule_rule.py index c6bd471..9d1027a 100644 --- a/synkit/Rule/Modify/molecule_rule.py +++ b/synkit/Rule/Modify/molecule_rule.py @@ -6,20 +6,17 @@ class MoleculeRule: - """ - A class for generating molecule rules, atom-mapped SMILES, and GML representations from SMILES strings. - """ + """A class for generating molecule rules, atom-mapped SMILES, and GML + representations from SMILES strings.""" def __init__(self) -> None: - """ - Initializes the MoleculeRule object. - """ + """Initializes the MoleculeRule object.""" pass @staticmethod def remove_edges_from_left_right(input_str: str) -> str: - """ - Remove all contents from the 'left' and 'right' sections of a chemical rule description. + """Remove all contents from the 'left' and 'right' sections of a + chemical rule description. Parameters: - input_str (str): The string representation of the rule. @@ -45,8 +42,8 @@ def remove_edges_from_left_right(input_str: str) -> str: @staticmethod def generate_atom_map(smiles: str) -> Optional[str]: - """ - Generate atom-mapped SMILES by assigning unique map numbers to each atom in the molecule. + """Generate atom-mapped SMILES by assigning unique map numbers to each + atom in the molecule. Parameters: - smiles (str): The SMILES string representing the molecule. @@ -67,8 +64,7 @@ def generate_atom_map(smiles: str) -> Optional[str]: @staticmethod def generate_molecule_smart(smiles: str) -> Optional[str]: - """ - Generate a SMARTS-like string from atom-mapped SMILES. + """Generate a SMARTS-like string from atom-mapped SMILES. Parameters: - smiles (str): The SMILES string representing the molecule. @@ -90,8 +86,7 @@ def generate_molecule_rule( explicit_hydrogen: bool = True, sanitize: bool = True, ) -> Optional[str]: - """ - Generate a GML representation of the molecule rule from SMILES. + """Generate a GML representation of the molecule rule from SMILES. Parameters: - smiles (str): The SMILES string representing the molecule. diff --git a/synkit/Rule/Modify/prune_templates.py b/synkit/Rule/Modify/prune_templates.py index 7ab6f97..f9ea216 100644 --- a/synkit/Rule/Modify/prune_templates.py +++ b/synkit/Rule/Modify/prune_templates.py @@ -6,8 +6,8 @@ class PruneTemplate: def __init__(self, templates: List[List[Dict[str, Any]]], graph_key: str) -> None: - """ - Initialize the PruneTemplate object with the provided templates and graph key. + """Initialize the PruneTemplate object with the provided templates and + graph key. Parameters: - templates (List[List[Dict[str, Any]]]): A list of lists containing dictionaries @@ -22,8 +22,7 @@ def __init__(self, templates: List[List[Dict[str, Any]]], graph_key: str) -> Non def remove_edges_by_attribute( input_graph: nx.Graph, attribute: str = "standard_order", value: Any = 0 ) -> nx.Graph: - """ - Remove edges from the input graph where a given attribute equals a + """Remove edges from the input graph where a given attribute equals a specified value. Parameters: @@ -49,9 +48,8 @@ def remove_edges_by_attribute( return graph def fit(self) -> List[List[Dict[str, Any]]]: - """ - Prune the templates by removing subgraphs where the longest path is shorter - than the radius. + """Prune the templates by removing subgraphs where the longest path is + shorter than the radius. Returns: List[List[Dict[str, Any]]]: The pruned list of templates. diff --git a/synkit/Rule/Modify/rule_utils.py b/synkit/Rule/Modify/rule_utils.py index 0e618f6..30e4e6e 100644 --- a/synkit/Rule/Modify/rule_utils.py +++ b/synkit/Rule/Modify/rule_utils.py @@ -6,9 +6,10 @@ def find_block(lines, keyword): - """ - Finds the start and end indices of a block (e.g., "left [", "context [", etc.) - in the given lines of GML. Returns (start_idx, end_idx) or (None, None) if not found. + """Finds the start and end indices of a block (e.g., "left [", "context [", + etc.) in the given lines of GML. + + Returns (start_idx, end_idx) or (None, None) if not found. """ start_idx = None depth = 0 @@ -29,8 +30,8 @@ def find_block(lines, keyword): def get_nodes_from_edges(block_lines): - """ - Extract node IDs from edges in the given block lines. + """Extract node IDs from edges in the given block lines. + Returns a set of node IDs found in the edges. """ node_set = set() @@ -43,8 +44,8 @@ def get_nodes_from_edges(block_lines): def parse_context(context_lines, node_regex=None, edge_regex=None): - """ - Parse the context lines to identify nodes and edges. + """Parse the context lines to identify nodes and edges. + Returns two structures: - context_nodes: {node_id: label} - context_edges: list of (source, target, label) @@ -67,9 +68,10 @@ def parse_context(context_lines, node_regex=None, edge_regex=None): def filter_context(context_lines, relevant_nodes): - """ - Given the context lines and a set of relevant nodes, remove hydrogen nodes - not in relevant_nodes and all edges connected to them. Returns filtered lines. + """Given the context lines and a set of relevant nodes, remove hydrogen + nodes not in relevant_nodes and all edges connected to them. + + Returns filtered lines. """ context_nodes, context_edges = parse_context(context_lines) @@ -105,11 +107,11 @@ def filter_context(context_lines, relevant_nodes): def strip_context(gml_text: str, remove_all: bool = True) -> str: - """ - Filters or clears the 'context' section of GML-like content based on the remove_all flag. - If remove_all is True, all edges in the 'context' section are removed. - If False, it removes hydrogen nodes that do not appear in both 'left' and 'right' sections, - along with their edges, while preserving the original structure and formatting of the GML. + """Filters or clears the 'context' section of GML-like content based on the + remove_all flag. If remove_all is True, all edges in the 'context' section + are removed. If False, it removes hydrogen nodes that do not appear in both + 'left' and 'right' sections, along with their edges, while preserving the + original structure and formatting of the GML. Parameters: - gml_text (str): GML-like content describing a chemical reaction rule. @@ -173,8 +175,8 @@ def strip_context(gml_text: str, remove_all: bool = True) -> str: def _increment_gml_ids(gml_content: str) -> str: - """ - Increment the numerical IDs within a GML content string if node id 0 exists. + """Increment the numerical IDs within a GML content string if node id 0 + exists. Parameters: - gml_content (str): The GML content as a string. diff --git a/synkit/Rule/Modify/strip_rule.py b/synkit/Rule/Modify/strip_rule.py index 2dfbaec..c236d7a 100644 --- a/synkit/Rule/Modify/strip_rule.py +++ b/synkit/Rule/Modify/strip_rule.py @@ -2,9 +2,10 @@ def filter_context(context_lines, left_edges): - """ - Given the context lines and a set of edges from the left graph, remove edges - from the context that are also present in the left graph (ignoring labels). + """Given the context lines and a set of edges from the left graph, remove + edges from the context that are also present in the left graph (ignoring + labels). + Returns filtered lines. """ # Create a set of edges from the left graph (ignoring labels) @@ -32,10 +33,12 @@ def filter_context(context_lines, left_edges): def strip_context(gml_text: str, remove_all: bool = False) -> str: - """ - Filters or clears the 'context' section of GML-like content based on the remove_all flag. - If remove_all is True, all edges in the 'context' section are removed. - If False, it removes edges in the 'context' that are also present in the 'left' section. + """Filters or clears the 'context' section of GML-like content based on the + remove_all flag. + + If remove_all is True, all edges in the 'context' section are + removed. If False, it removes edges in the 'context' that are also + present in the 'left' section. """ lines = gml_text.split("\n") diff --git a/synkit/Rule/syn_rule.py b/synkit/Rule/syn_rule.py index 6ec5625..5408625 100644 --- a/synkit/Rule/syn_rule.py +++ b/synkit/Rule/syn_rule.py @@ -160,10 +160,10 @@ def _strip_explicit_h( left: nx.Graph, right: nx.Graph, ) -> None: - """ - Remove explicit hydrogens from rc, left, right—but only when *both* - left & right agree the H should be implicit. Otherwise an H remains - explicit in all three graphs. + """Remove explicit hydrogens from rc, left, right—but only when *both* + left & right agree the H should be implicit. + + Otherwise an H remains explicit in all three graphs. """ def _removable_on(graph: nx.Graph, h: str) -> bool: diff --git a/synkit/Synthesis/CRN/crn.py b/synkit/Synthesis/CRN/crn.py index 0506533..646e63f 100644 --- a/synkit/Synthesis/CRN/crn.py +++ b/synkit/Synthesis/CRN/crn.py @@ -4,7 +4,7 @@ from copy import deepcopy from typing import Any, Dict, List, Sequence, Union -from synkit.Chem.Reaction.cleanning import Cleanning +from synkit.Chem.Reaction.cleaning import Cleaning from synkit.Chem.utils import ( count_carbons, get_max_fragment, @@ -18,8 +18,7 @@ class CRN: - """ - Expand an initial pool of molecules through several rounds of rule + """Expand an initial pool of molecules through several rounds of rule application using **MODReactor** under the hood. Public attributes @@ -91,8 +90,7 @@ def rule_count(self) -> int: @property def product_sets(self) -> Dict[str, List[str]]: - """ - Dict view of the per‑round reaction SMILES. + """Dict view of the per‑round reaction SMILES. Handles both shapes: @@ -136,7 +134,8 @@ def __repr__(self) -> str: # ============================================================ internals def _expand_once(self, smiles: List[str]) -> List[str]: - """Apply every rule once to the molecule pool and return reaction RSMI.""" + """Apply every rule once to the molecule pool and return reaction + RSMI.""" rxn_results: List[str] = [] smiles_for_mod = process_smiles_list(smiles) @@ -150,7 +149,7 @@ def _expand_once(self, smiles: List[str]) -> List[str]: ) reactor.run() rsmi = reactor.get_reaction_smiles() - rsmi = Cleanning().clean_smiles(rsmi) + rsmi = Cleaning().clean_smiles(rsmi) rsmi = [_remove_reagent(r) for r in rsmi] rxn_results.extend(rsmi) @@ -164,7 +163,8 @@ def _update_smiles_pool( starting: str, target: str, ) -> List[str]: - """Merge products from *reactions* into *current* with optional pruning.""" + """Merge products from *reactions* into *current* with optional + pruning.""" new: List[str] = [] for rsmi in reactions: diff --git a/synkit/Synthesis/CRN/dcrn.py b/synkit/Synthesis/CRN/dcrn.py index e036206..6613ad2 100644 --- a/synkit/Synthesis/CRN/dcrn.py +++ b/synkit/Synthesis/CRN/dcrn.py @@ -1,14 +1,14 @@ from collections import defaultdict import heapq from typing import List, Dict, Any -from synkit.Chem.Reaction.cleanning import Cleanning + from synkit.Chem.utils import ( count_carbons, process_smiles_list, get_max_fragment, ) from synkit.Synthesis.reactor_utils import _remove_reagent -from synkit.Synthesis.core_engine import CoreEngine +from synkit.Synthesis.Reactor.mod_reactor import MODReactor class DCRN: @@ -31,14 +31,13 @@ def __init__( @staticmethod def _get_valid_node(molecules, lower, upper): - """ - Filters molecules by their carbon count within the given range. - """ + """Filters molecules by their carbon count within the given range.""" return [mol for mol in molecules if lower <= count_carbons(mol) <= upper] def _expand(self, smiles_list: List[str]) -> List[str]: - """ - Expands molecules based on transformation rules. Uses caching to avoid redundant computation. + """Expands molecules based on transformation rules. + + Uses caching to avoid redundant computation. """ smiles_tuple = tuple(smiles_list) if smiles_tuple in self.expansion_cache: @@ -47,8 +46,8 @@ def _expand(self, smiles_list: List[str]) -> List[str]: results = [] processed_smiles = process_smiles_list(smiles_list) for rule_dict in self.rule_list: - expansions = CoreEngine()._inference(rule_dict["gml"], processed_smiles) - expansions = Cleanning().clean_smiles(expansions) + expansions = MODReactor()._inference(rule_dict["gml"], processed_smiles) + expansions = MODReactor().clean_smiles(expansions) expansions = [_remove_reagent(e) for e in expansions] for r in expansions: product = r.split(">>")[1] @@ -63,15 +62,13 @@ def _expand(self, smiles_list: List[str]) -> List[str]: return valid_nodes def _heuristic(self, a: str, b: str) -> int: - """ - Returns the heuristic estimate (absolute difference in carbon count) between two compounds. - """ + """Returns the heuristic estimate (absolute difference in carbon count) + between two compounds.""" return abs(count_carbons(a) - count_carbons(b)) def _dynamic_expand_node(self, node: str, smiles_list: list) -> None: - """ - Dynamically expands the given node to generate new possible compounds. - """ + """Dynamically expands the given node to generate new possible + compounds.""" if node not in self.visited: self.visited.add(node) expanded_nodes = self._expand( @@ -86,8 +83,9 @@ def build_and_search( max_solutions: int = 5, fast_process: bool = True, ) -> Dict[str, Any]: - """ - Builds the search graph and searches for paths from the starting compound to the target compound. + """Builds the search graph and searches for paths from the starting + compound to the target compound. + Ensures depth levels follow a sequential order starting from 0. """ # Initialize the heap with the starting compound at depth 0 diff --git a/synkit/Synthesis/CRN/mod_crn.py b/synkit/Synthesis/CRN/mod_crn.py index 51ad4d0..cc941dd 100644 --- a/synkit/Synthesis/CRN/mod_crn.py +++ b/synkit/Synthesis/CRN/mod_crn.py @@ -22,9 +22,7 @@ class MODCRN: - """ - MODCRN - ====== + """MODCRN ====== High-level class for constructing, inspecting, and reporting a chemical reaction network using the MØD derivation graph (DG) API. @@ -136,28 +134,21 @@ def build(self) -> None: builder.execute(strat) def print_summary(self) -> None: - """ - Print and save a concise summary of the derivation graph. - """ + """Print and save a concise summary of the derivation graph.""" out_dir = "out" os.makedirs(out_dir, exist_ok=True) self._dg.print() def export_report(self) -> None: - """ - Generate an external report via the `mod_post` CLI. - - """ + """Generate an external report via the `mod_post` CLI.""" try: subprocess.run(["mod_post"], check=True) except subprocess.CalledProcessError as e: logger.error(f"mod_post failed with exit code {e.returncode}") def help(self) -> None: - """ - Print usage examples and API summary for MODCRN. - """ + """Print usage examples and API summary for MODCRN.""" print( "MODCRN Usage:\n" " crn = MODCRN(rule_db_path, initial_smiles, repeats)\n" diff --git a/synkit/Synthesis/MSR/multi_steps.py b/synkit/Synthesis/MSR/multi_steps.py index 1c0ad98..2c144d9 100644 --- a/synkit/Synthesis/MSR/multi_steps.py +++ b/synkit/Synthesis/MSR/multi_steps.py @@ -9,17 +9,15 @@ class MultiSteps: def __init__(self) -> None: - """ - Initialize the MultiStep class with a Standardize instance. - """ + """Initialize the MultiStep class with a Standardize instance.""" self.std = Standardize() @staticmethod def _process( gml_list: List[str], order: List[int], rsmi: str, exclude_aam: bool = True ) -> Tuple[List[List[str]], Dict[str, List[str]]]: - """ - Process a series of chemical reactions according to given rules and order. + """Process a series of chemical reactions according to given rules and + order. Parameters: - gml_list (List[str]): List of GML format strings representing reaction rules. @@ -73,8 +71,8 @@ def _process( def _get_aam( rsmi_list: List[str], rule_list: List[str], order: List[int] ) -> List[str]: - """ - Apply atom-atom mapping to a series of reaction SMILES strings according to specified rules. + """Apply atom-atom mapping to a series of reaction SMILES strings + according to specified rules. Parameters: - rsmi_list (List[str]): List of reaction SMILES strings. @@ -111,8 +109,8 @@ def multi_step( order: List[int], cat: Union[str, List[str]], ) -> List[str]: - """ - Orchestrate a multi-step chemical reaction process using a set of rules and a starting reactant. + """Orchestrate a multi-step chemical reaction process using a set of + rules and a starting reactant. Parameters: - original_rsmi (str): Initial reactant SMILES string. diff --git a/synkit/Synthesis/MSR/path_finder.py b/synkit/Synthesis/MSR/path_finder.py index 9693d09..a610ab8 100644 --- a/synkit/Synthesis/MSR/path_finder.py +++ b/synkit/Synthesis/MSR/path_finder.py @@ -9,9 +9,9 @@ def __init__( self, reaction_rounds: List[Dict[str, List[str]]], ): - """ - Initialize with a list of dictionaries, each representing a reaction round, - plus an optional random state for reproducible Monte Carlo search. + """Initialize with a list of dictionaries, each representing a reaction + round, plus an optional random state for reproducible Monte Carlo + search. Parameters: - reaction_rounds (List[Dict[str, List[str]]]): A list where each dictionary @@ -56,9 +56,9 @@ def search_paths( max_solutions: Optional[int] = None, cheapest: bool = True, ) -> List[List[str]]: - """ - Search for reaction pathways from the input molecule to the target molecule - using a specified method, optionally limiting the number of solutions. + """Search for reaction pathways from the input molecule to the target + molecule using a specified method, optionally limiting the number of + solutions. Additionally, `cheapest` can be set to True or False: - If cheapest=True, BFS uses a visited set and A* prunes costlier routes (typical approach). @@ -92,12 +92,13 @@ def _bfs( max_solutions: Optional[int], cheapest: bool, ) -> List[List[str]]: - """ - Perform a BFS search. If cheapest=True, use a visited set to avoid re-processing - the same (molecule, round_index). If cheapest=False, skip that pruning and collect - *all* possible solutions (potentially large if cycles exist). + """Perform a BFS search. If cheapest=True, use a visited set to avoid + re-processing the same (molecule, round_index). If cheapest=False, skip + that pruning and collect *all* possible solutions (potentially large if + cycles exist). - Returns a list of successful reaction pathways, up to max_solutions if specified. + Returns a list of successful reaction pathways, up to + max_solutions if specified. """ queue = deque([(input_smiles, [], 0)]) @@ -134,9 +135,10 @@ def _bfs( return pathways def _heuristic(self, smiles: str, target_smiles: str) -> int: - """ - Heuristic function for A* search. - Returns difference in SMILES lengths as a stand-in for "distance." + """Heuristic function for A* search. + + Returns difference in SMILES lengths as a stand-in for + "distance." """ return abs(len(smiles) - len(target_smiles)) @@ -147,12 +149,12 @@ def _astar( max_solutions: Optional[int], cheapest: bool, ) -> List[List[str]]: - """ - A* search. If cheapest=True, we track the best cost visited for each state - and prune costlier paths. If cheapest=False, we do not prune, so we collect - all solutions (but it may be large). + """A* search. If cheapest=True, we track the best cost visited for each + state and prune costlier paths. If cheapest=False, we do not prune, so + we collect all solutions (but it may be large). - Returns a list of successful reaction pathways, up to max_solutions if specified. + Returns a list of successful reaction pathways, up to + max_solutions if specified. """ start_cost = self._heuristic(input_smiles, target_smiles) # Heap stores (cost, current_smiles, current_path, round_index) diff --git a/synkit/Synthesis/Metrics/_base.py b/synkit/Synthesis/Metrics/_base.py index 5ed47ab..09657c5 100644 --- a/synkit/Synthesis/Metrics/_base.py +++ b/synkit/Synthesis/Metrics/_base.py @@ -15,8 +15,7 @@ def _compute_metrics( k: int = 5, beta: float = 1, ) -> Dict[str, float]: - """ - Computes the metrics for a list of reactions data. + """Computes the metrics for a list of reactions data. Parameters: - reactions_data (List[Dict[str, any]]): List of dictionaries containing RSMI strings. diff --git a/synkit/Synthesis/Metrics/_plot.py b/synkit/Synthesis/Metrics/_plot.py index ab84809..0751580 100644 --- a/synkit/Synthesis/Metrics/_plot.py +++ b/synkit/Synthesis/Metrics/_plot.py @@ -17,9 +17,9 @@ def plot_recognition_coverage_curve( show_f2=True, show_legend=True, ): - """ - Plots a Recognition-Coverage curve using provided data, including optional - F2 scores annotated. Styled with Seaborn for enhanced visual appearance. + """Plots a Recognition-Coverage curve using provided data, including + optional F2 scores annotated. Styled with Seaborn for enhanced visual + appearance. Parameters: - data (dict): Nested dictionary containing the data for each radii, @@ -66,9 +66,8 @@ def plot_recognition_coverage_curve( def plot_f2_scores_line(data, figsize=(8, 6), show_f2=True, show_legend=True): - """ - Plots F2 scores across different radii using a line plot, showing - the trend of F2 score changes, and annotated with optional F2 scores. + """Plots F2 scores across different radii using a line plot, showing the + trend of F2 score changes, and annotated with optional F2 scores. Parameters: - data (dict): Dictionary containing nested dictionaries with 'F2_score' diff --git a/synkit/Synthesis/Metrics/_ranking.py b/synkit/Synthesis/Metrics/_ranking.py index b11fd88..50b773c 100644 --- a/synkit/Synthesis/Metrics/_ranking.py +++ b/synkit/Synthesis/Metrics/_ranking.py @@ -5,9 +5,9 @@ def _coverage( reactions_data: List[Dict[str, str]], key_ground_truth: str, key_prediction: str ) -> float: - """ - Calculates the coverage percentage, which measures how many of the predicted reactions - exactly match the ground truth reactions given in a list of dictionaries. + """Calculates the coverage percentage, which measures how many of the + predicted reactions exactly match the ground truth reactions given in a + list of dictionaries. Parameters: - reactions_data (List[Dict[str, str]]): List of dictionaries containing @@ -29,11 +29,10 @@ def _coverage( def _novelty_rate( reactions_data: List[Dict[str, any]], key_ground_truth: str, key_prediction: str ) -> float: - """ - Calculates the False Positive Rate (FPR) for each observation and then averages - these values across all observations. The FPR represents the proportion of - predictions that do not match - the ground truth for each individual entry in the dataset. + """Calculates the False Positive Rate (FPR) for each observation and then + averages these values across all observations. The FPR represents the + proportion of predictions that do not match the ground truth for each + individual entry in the dataset. Parameters: - reactions_data (List[Dict[str, any]]): List of dictionaries containing @@ -68,10 +67,10 @@ def _novelty_rate( def _recognition_rate( reactions_data: List[Dict[str, any]], key_ground_truth: str, key_prediction: str ) -> float: - """ - Calculates the recognition rate for each observation and averages these rates - across all observations. The recognition rate measures the proportion of - the prediction list that matches the single ground truth reaction for each entry. + """Calculates the recognition rate for each observation and averages these + rates across all observations. The recognition rate measures the proportion + of the prediction list that matches the single ground truth reaction for + each entry. Parameters: - reactions_data (List[Dict[str, any]]): List of dictionaries containing @@ -110,9 +109,9 @@ def _top_k_accuracy( key_prediction: str, k: int, ) -> float: - """ - Calculates the Top-K accuracy by using the coverage function on the top K predictions. - This measures the probability that the true reaction is within the top K predictions. + """Calculates the Top-K accuracy by using the coverage function on the top + K predictions. This measures the probability that the true reaction is + within the top K predictions. Parameters: - reactions_data (List[Dict[str, any]]): List of dictionaries containing @@ -137,12 +136,12 @@ def _calculate_f_beta_score( coverage_rate: float, # This serves as the recall beta: float = 1.0, # Beta factor, default is 1.0 for F1 score ) -> float: - """ - Computes the F-beta Score, which is a weighted harmonic mean of recognition rate - and coverage rate. The recognition rate (precision) and coverage rate (recall) - must be expressed as percentages. A beta value of 1.0 means equal importance to - precision and recall (F1 Score), greater than 1.0 gives more importance to recall - (e.g., F2 Score), and less than 1.0 prioritizes precision (e.g., F0.5 Score). + """Computes the F-beta Score, which is a weighted harmonic mean of + recognition rate and coverage rate. The recognition rate (precision) and + coverage rate (recall) must be expressed as percentages. A beta value of + 1.0 means equal importance to precision and recall (F1 Score), greater than + 1.0 gives more importance to recall (e.g., F2 Score), and less than 1.0 + prioritizes precision (e.g., F0.5 Score). Parameters: - recognition_rate (float): The recognition rate of the predictions, diff --git a/synkit/Synthesis/Reactor/batch_reactor.py b/synkit/Synthesis/Reactor/batch_reactor.py index a87cd87..987d797 100644 --- a/synkit/Synthesis/Reactor/batch_reactor.py +++ b/synkit/Synthesis/Reactor/batch_reactor.py @@ -8,8 +8,7 @@ class BatchReactor: - """ - Apply a collection of pattern-graphs (rules) to a batch of substrates. + """Apply a collection of pattern-graphs (rules) to a batch of substrates. Each data entry can be: - a dict (expects substrate under `host_key`) @@ -53,25 +52,26 @@ def __init__( implicit_temp: bool = False, strategy: str = "bt", ) -> None: - """ - Initialize batch reactor configuration. - - :param data: Batch of substrates to process. - :type data: list - :param host_key: Key to extract graph/SMILES from dict entries. - :type host_key: str or None - :param react_engine: Which reactor engine to use ('syn' or 'mod'). - :type react_engine: str - :param filter_engine: RuleFilter engine (or None to skip filtering). - :type filter_engine: str or None - :param invert: Use inverted rule patterns if True. - :type invert: bool - :param explicit_h: Use explicit hydrogens in SynReactor. - :type explicit_h: bool - :param implicit_temp: Use implicit templates in SynReactor. - :type implicit_temp: bool - :param strategy: Matching strategy identifier. - :type strategy: str + """Initialize batch reactor configuration. + + :param data: Batch of substrates to process. + :type data: list + :param host_key: Key to extract graph/SMILES from dict entries. + :type host_key: str or None + :param react_engine: Which reactor engine to use ('syn' or + 'mod'). + :type react_engine: str + :param filter_engine: RuleFilter engine (or None to skip + filtering). + :type filter_engine: str or None + :param invert: Use inverted rule patterns if True. + :type invert: bool + :param explicit_h: Use explicit hydrogens in SynReactor. + :type explicit_h: bool + :param implicit_temp: Use implicit templates in SynReactor. + :type implicit_temp: bool + :param strategy: Matching strategy identifier. + :type strategy: str """ self._data = data self._host_key = host_key @@ -89,13 +89,13 @@ def __init__( def _get_substrate( self, entry: Union[Dict[str, Any], str, nx.Graph] ) -> Union[nx.Graph, str]: - """ - Normalize and validate an entry based on react_engine. + """Normalize and validate an entry based on react_engine. - :param entry: The substrate entry (dict, SMILES, or Graph). - :type entry: dict or str or nx.Graph - :returns: networkx.Graph (for 'syn') or SMILES string (for 'mod'). - :rtype: nx.Graph or str + :param entry: The substrate entry (dict, SMILES, or Graph). + :type entry: dict or str or nx.Graph + :returns: networkx.Graph (for 'syn') or SMILES string (for + 'mod'). + :rtype: nx.Graph or str """ # extract from dict if needed if isinstance(entry, dict): @@ -122,15 +122,14 @@ def _get_substrate( def _filter_rules( self, substrate: Union[nx.Graph, str], rules_list: List[Any] ) -> List[Any]: - """ - Apply rule filtering if configured. - - :param substrate: Host graph or SMILES to filter against. - :type substrate: nx.Graph or str - :param rules_list: List of rule patterns. - :type rules_list: list - :returns: Filtered list of rules. - :rtype: list + """Apply rule filtering if configured. + + :param substrate: Host graph or SMILES to filter against. + :type substrate: nx.Graph or str + :param rules_list: List of rule patterns. + :type rules_list: list + :returns: Filtered list of rules. + :rtype: list """ if self._filter_engine and self._react_engine == "syn": rf = RuleFilter( @@ -143,13 +142,13 @@ def _filter_rules( return rules_list def fit(self, rules_list: List[Any]) -> List[List[str]]: - """ - Apply each rule to every substrate, returning product SMARTS or reaction SMILES. + """Apply each rule to every substrate, returning product SMARTS or + reaction SMILES. - :param rules_list: List of rules (pattern-graphs or objects). - :type rules_list: list - :returns: Nested list: outputs[i] for substrate i. - :rtype: List[List[str]] + :param rules_list: List of rules (pattern-graphs or objects). + :type rules_list: list + :returns: Nested list: outputs[i] for substrate i. + :rtype: List[List[str]] """ results: List[List[str]] = [] for entry in self._data: @@ -181,8 +180,7 @@ def fit(self, rules_list: List[Any]) -> List[List[str]]: @property def data(self) -> List[Union[Dict[str, Any], str, nx.Graph]]: - """ - Original batch input data. + """Original batch input data. :returns: The list of data entries. :rtype: list @@ -191,8 +189,7 @@ def data(self) -> List[Union[Dict[str, Any], str, nx.Graph]]: @property def filter_engine(self) -> Optional[str]: - """ - The engine used for rule filtering. + """The engine used for rule filtering. :returns: Name of filter engine or None. :rtype: str or None @@ -201,8 +198,7 @@ def filter_engine(self) -> Optional[str]: @property def react_engine(self) -> str: - """ - The engine used for reaction application. + """The engine used for reaction application. :returns: Name of react engine ('syn' or 'mod'). :rtype: str @@ -210,8 +206,7 @@ def react_engine(self) -> str: return self._react_engine def __repr__(self) -> str: - """ - Concise summary of BatchReactor configuration. + """Concise summary of BatchReactor configuration. :returns: Representation string. :rtype: str @@ -222,8 +217,7 @@ def __repr__(self) -> str: ) def __help__(self) -> str: - """ - Return class documentation for interactive help. + """Return class documentation for interactive help. :returns: The class docstring. :rtype: str diff --git a/synkit/Synthesis/Reactor/core_engine.py b/synkit/Synthesis/Reactor/core_engine.py deleted file mode 100644 index 2624e0e..0000000 --- a/synkit/Synthesis/Reactor/core_engine.py +++ /dev/null @@ -1,212 +0,0 @@ -# import warnings -# from rdkit import Chem -# from pathlib import Path -# from typing import List, Union -# from collections import Counter -# from synkit.IO.data_io import load_gml_as_text -# from synkit.Synthesis.reactor_utils import _deduplicateGraphs, _get_connected_subgraphs - -# import mod -# from mod import smiles, config, ruleGMLString, DG - - -# class CoreEngine: -# """ -# The MØDModeling class encapsulates functionalities for reaction modeling using the MØD -# toolkit. It provides methods for forward and backward prediction based on templates -# library. -# """ - -# def __init__(self) -> None: -# warnings.warn("deprecated", DeprecationWarning) -# pass - -# @staticmethod -# def generate_reaction_smiles( -# temp_results: List[str], base_smiles: str, is_forward: bool = True -# ) -> List[str]: -# """ -# Constructs reaction SMILES strings from intermediate results using a base SMILES -# string, indicating whether the process is a forward or backward reaction. This -# function iterates over a list of intermediate SMILES strings, combines them with -# the base SMILES, and formats them into complete reaction SMILES strings. - -# Parameters: -# - temp_results (List[str]): Intermediate SMILES strings resulting from partial -# reactions or combinations. -# - base_smiles (str): The SMILES string representing the starting point of the -# reaction, either as reactants or products, depending on the reaction direction. -# - is_forward (bool, optional): Flag to determine the direction of the reaction; -# 'True' for forward reactions where 'base_smiles' are reactants, and 'False' for -# backward reactions where 'base_smiles' are products. Defaults to True. - -# Returns: -# - List[str]: A list of complete reaction SMILES strings, formatted according to -# the specified reaction direction. -# """ -# results = [] -# for comb in temp_results: -# joined_smiles = ".".join(comb) -# reaction_smiles = ( -# f"{base_smiles}>>{joined_smiles}" -# if is_forward -# else f"{joined_smiles}>>{base_smiles}" -# ) -# results.append(reaction_smiles) -# return results - -# @staticmethod -# def _prediction_wo_reagent( -# initial_molecules: List[Union[str, object]], -# rule: mod.libpymod.Rule, -# print_results: bool = False, -# verbosity: int = 0, -# ) -> List[List[str]]: -# """ -# Applies the reaction rule to the given molecules without considering reagents. - -# Parameters: -# - initial_molecules (List[Union[str, object]]): List of initial molecules represented by SMILES or objects. -# - rule (mod.libpymod.Rule): The reaction rule to apply. -# - print_results (bool): Whether to print the results. -# - verbosity (int): Verbosity level for output. - -# Returns: -# - List[List[str]]: A list of intermediate SMILES strings for the reaction products. -# """ -# # Initialize the derivation graph and execute the strategy -# dg = DG(graphDatabase=initial_molecules) -# config.dg.doRuleIsomorphismDuringBinding = False -# dg.build().apply(initial_molecules, rule, verbosity=verbosity) -# if print_results: -# dg.print() - -# temp_results = [] -# for e in dg.edges: -# productSmiles = [v.graph.smiles for v in e.targets] -# temp_results.append(productSmiles) -# del dg -# return temp_results - -# @staticmethod -# def _prediction_with_reagent( -# initial_smiles: List[str], -# initial_molecules: List[Union[str, object]], -# rule: mod.libpymod.Rule, -# print_results: bool = False, -# verbosity: int = 0, -# ) -> List[List[str]]: -# """ -# Applies the reaction rule to the given molecules considering the reagents. - -# Parameters: -# - initial_smiles (List[str]): Initial molecules represented as SMILES strings. -# - initial_molecules (List[Union[str, object]]): List of initial molecules. -# - rule (mod.libpymod.Rule): The reaction rule to apply. -# - print_results (bool): Whether to print the results. -# - verbosity (int): Verbosity level for output. - -# Returns: -# - List[List[str]]: A list of intermediate SMILES strings with reagents included. -# """ -# dg = DG(graphDatabase=initial_molecules) -# config.dg.doRuleIsomorphismDuringBinding = False -# dg.build().apply(initial_molecules, rule, verbosity=verbosity, onlyProper=False) -# if print_results: -# dg.print() -# temp_results, small_educt = [], [] -# for edge in dg.edges: -# temp_results.append([vertex.graph.smiles for vertex in edge.targets]) -# small_educt.append([vertex.graph.smiles for vertex in edge.sources]) - -# for key, solution in enumerate(temp_results): -# educt = small_educt[key] -# small_educt_counts = Counter( -# Chem.CanonSmiles(smile) for smile in educt if smile is not None -# ) -# reagent_counts = Counter([Chem.CanonSmiles(s) for s in initial_smiles]) -# reagent_counts.subtract(small_educt_counts) -# reagent = [ -# smile -# for smile, count in reagent_counts.items() -# for _ in range(count) -# if count > 0 -# ] -# solution.extend(reagent) -# del dg -# return temp_results - -# @staticmethod -# def _inference( -# rule_file_path: Union[str, Path], -# initial_smiles: List[str], -# prediction_type: str = "forward", -# print_results: bool = False, -# verbosity: int = 0, -# ) -> List[str]: -# """ -# Applies a specified reaction rule to a set of initial molecules represented by SMILES strings. -# The reaction can be simulated in forward or backward direction. - -# Parameters: -# - rule_file_path (Union[str, Path]): Path to the GML file containing the reaction rule. -# - initial_smiles (List[str]): Initial molecules as SMILES strings. -# - prediction_type (str): Direction of the reaction ('forward' or 'backward'). -# - print_results (bool): Whether to print the results. -# - verbosity (int): Verbosity level for output. - -# Returns: -# - List[str]: SMILES strings of the resulting molecules or reactions. -# """ - -# # Determine the rule inversion based on reaction type -# invert_rule = prediction_type == "backward" -# # Convert SMILES strings to molecule objects, avoiding duplicate conversions -# initial_molecules = [smiles(smile, add=False) for smile in (initial_smiles)] - -# initial_molecules = _deduplicateGraphs(initial_molecules) - -# initial_molecules = sorted( -# initial_molecules, key=lambda molecule: molecule.numVertices, reverse=False -# ) -# # Load the reaction rule from the GML file -# rule_path = Path(rule_file_path) - -# try: -# if rule_path.is_file(): -# gml_content = load_gml_as_text(rule_file_path) -# else: -# gml_content = rule_file_path -# except Exception as e: -# # print(f"An error occurred while loading the GML file: {e}") -# gml_content = rule_file_path -# reaction_rule = ruleGMLString(gml_content, invert=invert_rule, add=False) - -# _number_subgraphs = _get_connected_subgraphs(gml_content, invert=invert_rule) -# if len(initial_molecules) <= _number_subgraphs: -# temp_results = CoreEngine._prediction_wo_reagent( -# initial_molecules, reaction_rule, print_results, verbosity -# ) -# else: -# temp_results = CoreEngine._prediction_with_reagent( -# initial_smiles, -# initial_molecules, -# reaction_rule, -# print_results, -# verbosity, -# ) - -# reaction_processing_map = { -# "forward": lambda smiles: CoreEngine.generate_reaction_smiles( -# temp_results, ".".join(initial_smiles), is_forward=True -# ), -# "backward": lambda smiles: CoreEngine.generate_reaction_smiles( -# temp_results, ".".join(initial_smiles), is_forward=False -# ), -# } - -# # Use the reaction type to select the appropriate processing function and apply it -# if prediction_type in reaction_processing_map: -# return reaction_processing_map[prediction_type](initial_smiles) -# else: -# return "" diff --git a/synkit/Synthesis/Reactor/mod_aam.py b/synkit/Synthesis/Reactor/mod_aam.py index d047cf0..951f3bb 100644 --- a/synkit/Synthesis/Reactor/mod_aam.py +++ b/synkit/Synthesis/Reactor/mod_aam.py @@ -29,7 +29,7 @@ from synkit.Graph.ITS.its_expand import ITSExpand from synkit.Graph.ITS.normalize_aam import NormalizeAAM from synkit.Chem.Reaction.standardize import Standardize -from synkit.Chem.Reaction.rsmi_utils import reverse_reaction +from synkit.Chem.utils import reverse_reaction from synkit.Synthesis.reactor_utils import _get_unique_aam, _get_reagent, _add_reagent from synkit.Synthesis.Reactor.strategy import Strategy @@ -39,8 +39,7 @@ class MODAAM: - """ - Runs MØD (via MODReactor) then a full AAM/ITS post-processing pipeline. + """Runs MØD (via MODReactor) then a full AAM/ITS post-processing pipeline. Parameters ---------- @@ -106,9 +105,7 @@ def _run_pipeline(self) -> None: self._aam_smiles = self._process_aam(self._dg) def run(self) -> List[str]: - """ - Re-run the entire pipeline (MØD + AAM) and return fresh results. - """ + """Re-run the entire pipeline (MØD + AAM) and return fresh results.""" self._run_pipeline() return self._aam_smiles @@ -231,8 +228,7 @@ def _deduplicate(self, smiles: List[str]) -> List[str]: def expand_aam(rsmi: str, rule: str) -> List[str]: - """ - Expand Atom–Atom Mapping (AAM) for a given reaction SMARTS/SMILES (rsmi) + """Expand Atom–Atom Mapping (AAM) for a given reaction SMARTS/SMILES (rsmi) using a pre‐sanitized GML rule string. Parameters diff --git a/synkit/Synthesis/Reactor/mod_reactor.py b/synkit/Synthesis/Reactor/mod_reactor.py index a1042c3..cc15304 100644 --- a/synkit/Synthesis/Reactor/mod_reactor.py +++ b/synkit/Synthesis/Reactor/mod_reactor.py @@ -49,8 +49,7 @@ # MODReactor # ────────────────────────────────────────────────────────────────────────────── class MODReactor: - """ - Lazy, ergonomic wrapper around the MØD toolkit’s derivation pipeline. + """Lazy, ergonomic wrapper around the MØD toolkit’s derivation pipeline. Workflow -------- @@ -108,8 +107,8 @@ def __init__( # Public high‑level API # ------------------------------------------------------------------ def run(self) -> "MODReactor": - """ - Execute the chosen strategy **once** and return *self* so you can chain: + """Execute the chosen strategy **once** and return *self* so you can + chain: ```python r = MODReactor(...).run() @@ -122,8 +121,7 @@ def run(self) -> "MODReactor": # helpers for outside world ------------------------------------------------ def get_reaction_smiles(self) -> List[str]: - """ - Retrieve the reaction SMILES strings (lazy). + """Retrieve the reaction SMILES strings (lazy). Returns ------- @@ -133,8 +131,7 @@ def get_reaction_smiles(self) -> List[str]: return self.reaction_smiles def get_dg(self) -> DG: - """ - Access the underlying derivation graph. + """Access the underlying derivation graph. Returns ------- @@ -163,9 +160,7 @@ def __str__(self) -> str: __repr__ = __str__ def help(self) -> None: - """ - Print a one-page summary of reactor configuration and results. - """ + """Print a one-page summary of reactor configuration and results.""" print("MODReactor".ljust(60, "─")) print(f"Rule file : {self.rule_file}") print(f"Substrate : {'.'.join(self.initial_smiles)}") @@ -182,8 +177,7 @@ def help(self) -> None: # ------------------------------------------------------------------ @property def dg(self) -> Optional[DG]: - """ - DG or None – cached derivation graph. + """DG or None – cached derivation graph. See also -------- @@ -193,23 +187,18 @@ def dg(self) -> Optional[DG]: @property def product_sets(self) -> List[List[str]]: - """ - Raw product sets (lists of SMILES) before joining into full reactions. - """ + """Raw product sets (lists of SMILES) before joining into full + reactions.""" return self.temp_results @property def product_smiles(self) -> List[str]: - """ - Flattened list of all product SMILES (may contain duplicates). - """ + """Flattened list of all product SMILES (may contain duplicates).""" return [s for batch in self.temp_results for s in batch] @property def prediction_count(self) -> int: - """ - Number of distinct prediction batches generated. - """ + """Number of distinct prediction batches generated.""" return len(self._temp_results or []) # ------------------------------------------------------------------ @@ -217,8 +206,7 @@ def prediction_count(self) -> int: # ------------------------------------------------------------------ @property def temp_results(self) -> List[List[str]]: - """ - Lazy-loaded raw product lists. + """Lazy-loaded raw product lists. Returns ------- @@ -230,8 +218,7 @@ def temp_results(self) -> List[List[str]]: @property def reaction_smiles(self) -> List[str]: - """ - Lazy-loaded reaction SMILES strings of form “A>>B”. + """Lazy-loaded reaction SMILES strings of form “A>>B”. Returns ------- @@ -248,8 +235,7 @@ def reaction_smiles(self) -> List[str]: # Internals – setup # ------------------------------------------------------------------ def _prepare_initial_molecules(self) -> List[Any]: - """ - Convert SMILES → MØD molecule objects, dedupe, and sort. + """Convert SMILES → MØD molecule objects, dedupe, and sort. Returns ------- @@ -262,8 +248,7 @@ def _prepare_initial_molecules(self) -> List[Any]: return mols def _parse_reaction_rule(self) -> Any: - """ - Load or parse the reaction rule from raw GML or file. + """Load or parse the reaction rule from raw GML or file. Returns ------- @@ -304,8 +289,7 @@ def _parse_reaction_rule(self) -> Any: # Internals – strategy dispatch # ------------------------------------------------------------------ def _predict(self) -> List[List[str]]: - """ - Dispatch to the appropriate application strategy. + """Dispatch to the appropriate application strategy. Returns ------- @@ -350,8 +334,7 @@ def _apply_components(self) -> List[List[str]]: return products def _apply_all(self) -> List[List[str]]: - """ - Classic “ALL” strategy: VF2 with reagents included. + """Classic “ALL” strategy: VF2 with reagents included. Returns ------- @@ -413,9 +396,8 @@ def generate_reaction_smiles( arrow: str = ">>", separator: str = ".", ) -> List[str]: - """ - Build reaction SMILES of the form “A>>B”, where A and B swap - roles if invert=True. + """Build reaction SMILES of the form “A>>B”, where A and B swap roles + if invert=True. Parameters ---------- diff --git a/synkit/Synthesis/Reactor/partial_engine.py b/synkit/Synthesis/Reactor/partial_engine.py index 368ed11..d1330e0 100644 --- a/synkit/Synthesis/Reactor/partial_engine.py +++ b/synkit/Synthesis/Reactor/partial_engine.py @@ -1,25 +1,25 @@ from synkit.IO import rsmi_to_its, smiles_to_graph from synkit.Chem.Reaction.radical_wildcard import RadicalWildcardAdder from synkit.Synthesis.Reactor.syn_reactor import SynReactor -from synkit.Chem.Reaction.rsmi_utils import remove_explicit_H_from_rsmi +from synkit.Chem.utils import remove_explicit_H_from_rsmi class PartialEngine: - """ - Partial Reaction Learning Engine that applies a single‐direction + """Partial Reaction Learning Engine that applies a single‐direction (forward or backward) template transformation, injects radical wildcards, and returns a list of intermediate ITS strings. - :param smi: A reaction SMARTS (rsmi) string in the form "Reactants>>Products" or - a simple SMILES string when used for one‐sided synthesis. + :param smi: A reaction SMARTS (rsmi) string in the form + "Reactants>>Products" or a simple SMILES string when used for + one‐sided synthesis. :type smi: str - :param template: A reaction template SMARTS string, which may include explicit H. + :param template: A reaction template SMARTS string, which may + include explicit H. :type template: str """ def __init__(self, smi: str, template: str) -> None: - """ - Initialize the PartialEngine. + """Initialize the PartialEngine. - Removes explicit hydrogens from the given template SMARTS. - Parses the cleaned template into an internal template structure (ITS). @@ -41,8 +41,7 @@ def __init__(self, smi: str, template: str) -> None: self.host = smiles_to_graph(smi) def fit(self, invert: bool = False) -> list[str]: - """ - Apply the template in one direction to generate radical‐wildcarded + """Apply the template in one direction to generate radical‐wildcarded reaction SMARTS (ITS). - Instantiates a SynReactor on the host graph and ITS. diff --git a/synkit/Synthesis/Reactor/rbl_engine.py b/synkit/Synthesis/Reactor/rbl_engine.py index 19f7ce0..f3d790a 100644 --- a/synkit/Synthesis/Reactor/rbl_engine.py +++ b/synkit/Synthesis/Reactor/rbl_engine.py @@ -1,37 +1,38 @@ from rdkit import Chem from synkit.IO import its_to_rsmi, rsmi_to_its, smiles_to_graph -from synkit.Chem.Reaction.rsmi_utils import remove_explicit_H_from_rsmi +from synkit.Chem.utils import remove_explicit_H_from_rsmi from synkit.Chem.Reaction.radical_wildcard import RadicalWildcardAdder from synkit.Synthesis.Reactor.syn_reactor import SynReactor from synkit.Graph.Wildcard.fuse_graph import fuse_wc_graphs, find_wc_graph_isomorphism class RBLEngine: - """ - Reaction-based Learning Engine that takes a reaction SMARTS (rsmi) and a + """Reaction-based Learning Engine that takes a reaction SMARTS (rsmi) and a transformation template, applies forward and backward synthesis via SynReactor, augments with radical wildcards, identifies wildcard-aware graph isomorphisms between forward and backward intermediates, and fuses matching graphs into new intermediates. - :param rsmi: Reaction SMARTS string in the form "Reactants>>Products". + :param rsmi: Reaction SMARTS string in the form + "Reactants>>Products". :type rsmi: str - :param template: A reaction template SMARTS string, may include explicit H. + :param template: A reaction template SMARTS string, may include + explicit H. :type template: str """ def __init__(self, rsmi: str, template: str) -> None: - """ - Initialize the RBLEngine with a reaction SMARTS and a template. + """Initialize the RBLEngine with a reaction SMARTS and a template. - Cleans explicit hydrogens in the template, parses the template into - an ITS (internal template structure), and converts the reactant and - product SMARTS into graph representations for host forward and - backward graphs. + Cleans explicit hydrogens in the template, parses the template + into an ITS (internal template structure), and converts the + reactant and product SMARTS into graph representations for host + forward and backward graphs. :param rsmi: Reaction SMARTS string "Reactants>>Products". :type rsmi: str - :param template: Reaction template SMARTS, possibly with explicit H. + :param template: Reaction template SMARTS, possibly with + explicit H. :type template: str """ self.rsmi = rsmi @@ -43,8 +44,7 @@ def __init__(self, rsmi: str, template: str) -> None: self.host_bw = smiles_to_graph(p) def _fw(self): - """ - Generate forward reaction intermediates using SynReactor, then apply + """Generate forward reaction intermediates using SynReactor, then apply RadicalWildcardAdder to each reaction SMARTS, and return their ITS representations. @@ -59,9 +59,9 @@ def _fw(self): return [rsmi_to_its(rxn) for rxn in fw] def _bw(self): - """ - Generate backward reaction intermediates by inverting the template in - SynReactor, apply RadicalWildcardAdder, and return ITS representations. + """Generate backward reaction intermediates by inverting the template + in SynReactor, apply RadicalWildcardAdder, and return ITS + representations. :returns: List of ITS objects for backward intermediates. :rtype: List @@ -79,8 +79,8 @@ def _bw(self): return [rsmi_to_its(rxn) for rxn in bw] def fit(self): - """ - Attempt to fuse forward and backward ITS graphs into new intermediates. + """Attempt to fuse forward and backward ITS graphs into new + intermediates. For each forward ITS and backward ITS pair: 1. Find a wildcard-aware graph isomorphism. @@ -109,8 +109,7 @@ def fit(self): @staticmethod def remove_explicith_rsmi(rsmi: str) -> str: - """ - Strip any explicit hydrogens from a reaction SMARTS string. + """Strip any explicit hydrogens from a reaction SMARTS string. :param rsmi: Reaction SMARTS "Reactants>>Products". :type rsmi: str diff --git a/synkit/Synthesis/Reactor/rule_filter.py b/synkit/Synthesis/Reactor/rule_filter.py index 2064e3d..6e174ad 100644 --- a/synkit/Synthesis/Reactor/rule_filter.py +++ b/synkit/Synthesis/Reactor/rule_filter.py @@ -8,28 +8,30 @@ class RuleFilter: - """ - Filter a host graph by a list of transformation rules (patterns), - keeping only those rules whose (decomposed) pattern appears as a - subgraph in the host. + """Filter a host graph by a list of transformation rules (patterns), + keeping only those rules whose (decomposed) pattern appears as a subgraph + in the host. - :param host_graph: The host graph to search within (will be converted to explicit H). + :param host_graph: The host graph to search within (will be + converted to explicit H). :type host_graph: nx.Graph :param rules_list: A list of rule objects to filter against. :type rules_list: list - :param invert: If True, use the "modifier" component of each decomposition; otherwise use the normal part. + :param invert: If True, use the "modifier" component of each + decomposition; otherwise use the normal part. :type invert: bool - :param engine: Matching engine to use: "turbo", "sing", "nx", or "mod". + :param engine: Matching engine to use: "turbo", "sing", "nx", or + "mod". :type engine: str :param node_label: Node attribute(s) for TurboISO to match on. :type node_label: str or list :param edge_label: Edge attribute(s) for TurboISO to match on. :type edge_label: str or list - :param distance_threshold: Threshold to skip distance filtering in TurboISO. + :param distance_threshold: Threshold to skip distance filtering in + TurboISO. :type distance_threshold: int :param sing_max_path: Maximum path length for SING engine. :type sing_max_path: int - :returns: An instance with only the rules that matched. :rtype: RuleFilter """ @@ -45,14 +47,14 @@ def __init__( distance_threshold: int = 5000, sing_max_path: int = 3, ) -> None: - """ - Initialize the RuleFilter and perform the filtering pass. + """Initialize the RuleFilter and perform the filtering pass. :param host_graph: The host graph to search within. :type host_graph: nx.Graph :param rules_list: A list of rule objects to filter against. :type rules_list: list - :param invert: If True, use the "modifier" component of each decomposition. + :param invert: If True, use the "modifier" component of each + decomposition. :type invert: bool :param engine: Matching engine to use. :type engine: str @@ -60,7 +62,8 @@ def __init__( :type node_label: str or list :param edge_label: Edge attribute(s) for TurboISO to match on. :type edge_label: str or list - :param distance_threshold: Threshold to skip distance filtering in TurboISO. + :param distance_threshold: Threshold to skip distance filtering + in TurboISO. :type distance_threshold: int :param sing_max_path: Maximum path length for SING engine. :type sing_max_path: int @@ -97,8 +100,7 @@ def __init__( self._new_rules = [r for r, m in zip(self._rules, self._matches) if m] def _match(self, pattern: nx.Graph) -> bool: - """ - Test whether the given pattern occurs as a subgraph in the host. + """Test whether the given pattern occurs as a subgraph in the host. :param pattern: The query graph pattern to match. :type pattern: nx.Graph @@ -120,8 +122,7 @@ def _match(self, pattern: nx.Graph) -> bool: @property def host(self) -> nx.Graph: - """ - The explicit host graph. + """The explicit host graph. :returns: The host graph used for matching. :rtype: nx.Graph @@ -130,8 +131,7 @@ def host(self) -> nx.Graph: @property def rules(self) -> List[Any]: - """ - Original list of rules provided. + """Original list of rules provided. :returns: The list of rules. :rtype: list @@ -140,8 +140,7 @@ def rules(self) -> List[Any]: @property def patterns(self) -> List[nx.Graph]: - """ - Decomposed subgraph queries used internally. + """Decomposed subgraph queries used internally. :returns: List of ITS-decomposed query graphs. :rtype: list of nx.Graph @@ -150,8 +149,7 @@ def patterns(self) -> List[nx.Graph]: @property def matches(self) -> List[bool]: - """ - Boolean list indicating which patterns were found. + """Boolean list indicating which patterns were found. :returns: List of booleans aligned with `patterns`. :rtype: list of bool @@ -160,8 +158,7 @@ def matches(self) -> List[bool]: @property def new_rules(self) -> List[Any]: - """ - Subset of rules for which `matches[i]` is True. + """Subset of rules for which `matches[i]` is True. :returns: Filtered list of matching rules. :rtype: list @@ -170,8 +167,7 @@ def new_rules(self) -> List[Any]: @property def engine(self) -> str: - """ - Matching engine in use. + """Matching engine in use. :returns: The name of the engine. :rtype: str @@ -179,8 +175,7 @@ def engine(self) -> str: return self._engine def __repr__(self) -> str: - """ - Concise representation of the filter. + """Concise representation of the filter. :returns: Representation string. :rtype: str @@ -192,8 +187,7 @@ def __repr__(self) -> str: ) def __help__(self) -> str: - """ - Return the class docstring for interactive help. + """Return the class docstring for interactive help. :returns: The class documentation. :rtype: str diff --git a/synkit/Synthesis/Reactor/single_predictor.py b/synkit/Synthesis/Reactor/single_predictor.py index 4d51fb0..b9db0e4 100644 --- a/synkit/Synthesis/Reactor/single_predictor.py +++ b/synkit/Synthesis/Reactor/single_predictor.py @@ -1,28 +1,23 @@ from typing import List, Any, Dict - -# from synkit.Synthesis.Reactor.core_engine import CoreEngine from synkit.Synthesis.Reactor.mod_reactor import MODReactor class SinglePredictor: - """ - A class designed for one-step chemical reaction predictions using transformation rules. + """A class designed for one-step chemical reaction predictions using + transformation rules. - This class utilizes transformation rules to predict the outcomes of chemical reactions based - on provided SMILES strings. + This class utilizes transformation rules to predict the outcomes of + chemical reactions based on provided SMILES strings. """ def __init__(self) -> None: - """ - Initializes the StepPredictor instance. - """ + """Initializes the StepPredictor instance.""" pass def _single_rule( self, smiles_list: List[str], rule: str, invert: bool = False ) -> List[Any]: - """ - Applies a single transformation rule to a list of SMILES strings. + """Applies a single transformation rule to a list of SMILES strings. This function applies the transformation rule to generate potential reaction outcomes from given SMILES strings. The results are returned and the memory is cleaned up immediately @@ -47,8 +42,7 @@ def _single_rule( def _multiple_rules( self, smiles_list: List[str], rules: List[str], invert: bool = False ) -> List[Any]: - """ - Applies multiple transformation rules to a list of SMILES strings. + """Applies multiple transformation rules to a list of SMILES strings. Parameters: - smiles_list (List[str]): The list of SMILES strings to process. @@ -72,8 +66,8 @@ def _perform( rule_key: str = "gml", invert: bool = False, ) -> List[Dict[str, Any]]: - """ - Performs prediction for each entry in the data using the specified rules. + """Performs prediction for each entry in the data using the specified + rules. Parameters: - data (List[Dict[str, Any]]): The dataset containing chemical reactions. diff --git a/synkit/Synthesis/Reactor/strategy.py b/synkit/Synthesis/Reactor/strategy.py index 79d8f00..edb0c7b 100644 --- a/synkit/Synthesis/Reactor/strategy.py +++ b/synkit/Synthesis/Reactor/strategy.py @@ -3,13 +3,12 @@ class Strategy(str, Enum): - """ - Strategy for sub-graph matching/application: + """Strategy for sub-graph matching/application: - - ALL: classic VF2 on the whole graph - - COMPONENT: component-aware only (no cross-CC backtracking) - - BACKTRACK: component-aware with backtracking across CCs - - PARTIAL: partial matching (mcs) + - ALL: classic VF2 on the whole graph + - COMPONENT: component-aware only (no cross-CC backtracking) + - BACKTRACK: component-aware with backtracking across CCs + - PARTIAL: partial matching (mcs) """ ALL = "all" @@ -19,8 +18,7 @@ class Strategy(str, Enum): @classmethod def from_string(cls, value: Union[str, "Strategy"]) -> "Strategy": - """ - Convert a string or Strategy to a Strategy enum. + """Convert a string or Strategy to a Strategy enum. Parameters ---------- diff --git a/synkit/Synthesis/Reactor/syn_reactor.py b/synkit/Synthesis/Reactor/syn_reactor.py index b096d1a..fb00feb 100644 --- a/synkit/Synthesis/Reactor/syn_reactor.py +++ b/synkit/Synthesis/Reactor/syn_reactor.py @@ -14,7 +14,7 @@ graph_to_smi, ) from synkit.IO import setup_logging -from synkit.Chem.Reaction.rsmi_utils import reverse_reaction +from synkit.Chem.utils import reverse_reaction from synkit.Rule import SynRule from synkit.Graph.syn_graph import SynGraph @@ -49,33 +49,33 @@ @dataclass class SynReactor: - """ - A hardened and typed re-write of the original SynReactor, preserving API compatibility - while offering safer, faster, and cleaner behavior. + """A hardened and typed re-write of the original SynReactor, preserving API + compatibility while offering safer, faster, and cleaner behavior. - :param substrate: The input reaction substrate, as a SMILES string, a raw NetworkX graph, - or a SynGraph. + :param substrate: The input reaction substrate, as a SMILES string, + a raw NetworkX graph, or a SynGraph. :type substrate: Union[str, nx.Graph, SynGraph] - :param template: Reaction template, provided as SMILES/SMARTS, a raw NetworkX graph, - or a SynRule. + :param template: Reaction template, provided as SMILES/SMARTS, a raw + NetworkX graph, or a SynRule. :type template: Union[str, nx.Graph, SynRule] - :param invert: Whether to invert the reaction (predict precursors). Defaults to False. + :param invert: Whether to invert the reaction (predict precursors). + Defaults to False. :type invert: bool - :param canonicaliser: Optional canonicaliser for intermediate graphs. If None, a default - GraphCanonicaliser is used. + :param canonicaliser: Optional canonicaliser for intermediate + graphs. If None, a default GraphCanonicaliser is used. :type canonicaliser: Optional[GraphCanonicaliser] - :param explicit_h: If True, render all hydrogens explicitly in the reaction‑center SMARTS. - Defaults to True. + :param explicit_h: If True, render all hydrogens explicitly in the + reaction‑center SMARTS. Defaults to True. :type explicit_h: bool - :param implicit_temp: If True, treat the input template as implicit-H (forces explicit_h=False). - Defaults to False. + :param implicit_temp: If True, treat the input template as + implicit-H (forces explicit_h=False). Defaults to False. :type implicit_temp: bool - :param strategy: Matching strategy, one of Strategy.ALL, 'comp', or 'bt'. - Defaults to Strategy.ALL. + :param strategy: Matching strategy, one of Strategy.ALL, 'comp', or + 'bt'. Defaults to Strategy.ALL. :type strategy: Strategy or str - :param partial: If True, use a partial matching fallback. Defaults to False. + :param partial: If True, use a partial matching fallback. Defaults + to False. :type partial: bool - :ivar _graph: Cached SynGraph for the substrate. :vartype _graph: Optional[SynGraph] :ivar _rule: Cached SynRule for the template. @@ -86,7 +86,8 @@ class SynReactor: :vartype _its: Optional[List[nx.Graph]] :ivar _smarts: Cached list of SMARTS strings. :vartype _smarts: Optional[List[str]] - :ivar _flag_pattern_has_explicit_H: Internal flag indicating explicit‑H constraints. + :ivar _flag_pattern_has_explicit_H: Internal flag indicating + explicit‑H constraints. :vartype _flag_pattern_has_explicit_H: bool """ @@ -108,8 +109,8 @@ class SynReactor: _flag_pattern_has_explicit_H: bool = field(init=False, default=False, repr=False) def __post_init__(self) -> None: - """ - Validate and enforce consistency of `explicit_h` and `implicit_temp`. + """Validate and enforce consistency of `explicit_h` and + `implicit_temp`. :raises ValueError: If `explicit_h` is True while `implicit_temp` is False. """ @@ -169,8 +170,7 @@ def from_smiles( # ------------------------------------------------------------------ @property def graph(self) -> SynGraph: # noqa: D401 – read‑only property - """ - Lazily wrap the substrate into a SynGraph. + """Lazily wrap the substrate into a SynGraph. :returns: The reaction substrate as a `SynGraph`. :rtype: SynGraph @@ -181,8 +181,7 @@ def graph(self) -> SynGraph: # noqa: D401 – read‑only property @property def rule(self) -> SynRule: # noqa: D401 - """ - Lazily wrap the template into a SynRule. + """Lazily wrap the template into a SynRule. :returns: The reaction template as a `SynRule`. :rtype: SynRule @@ -196,8 +195,7 @@ def rule(self) -> SynRule: # noqa: D401 # ------------------------------------------------------------------ @property def mappings(self) -> List[MappingDict]: - """ - Find subgraph mappings between substrate and template. + """Find subgraph mappings between substrate and template. :returns: A list of node-mapping dictionaries. :rtype: list of dict @@ -235,8 +233,7 @@ def mappings(self) -> List[MappingDict]: @property def its_list(self) -> List[nx.Graph]: - """ - Build ITS graphs for each subgraph mapping. + """Build ITS graphs for each subgraph mapping. :returns: A list of ITS (Internal Transition State) graphs. :rtype: list of networkx.Graph @@ -264,8 +261,7 @@ def its_list(self) -> List[nx.Graph]: @property def smarts_list(self) -> List[str]: - """ - Serialise each ITS graph to a reaction-SMARTS string. + """Serialise each ITS graph to a reaction-SMARTS string. :returns: A list of SMARTS strings (inverted if `invert=True`). :rtype: list of str @@ -576,4 +572,4 @@ def _to_smarts(its: nx.Graph) -> str: p_smi = graph_to_smi(right) if r_smi is None or p_smi is None: return None - return f"{r_smi}>>{p_smi}" + return f"{r_smi}>>{p_smi}" \ No newline at end of file diff --git a/synkit/Synthesis/reactor_utils.py b/synkit/Synthesis/reactor_utils.py index 6902f54..67cf1e2 100644 --- a/synkit/Synthesis/reactor_utils.py +++ b/synkit/Synthesis/reactor_utils.py @@ -6,8 +6,8 @@ def _get_unique_aam(list_aam: list) -> list: - """ - Retrieves the unique atom-atom mappings (AAM) by clustering a list of ITS graphs. + """Retrieves the unique atom-atom mappings (AAM) by clustering a list of + ITS graphs. This function first converts each item in the provided list of AAM strings to an ITS graph using the `rsmi_to_its` function. Then, it performs iterative clustering of the ITS graphs @@ -39,8 +39,7 @@ def _get_unique_aam(list_aam: list) -> list: def _deduplicateGraphs(initial) -> list: - """ - Deduplicates a list of molecular graphs by checking for isomorphisms. + """Deduplicates a list of molecular graphs by checking for isomorphisms. This method checks each graph in the `initial` list against the others for isomorphism, and removes duplicates by keeping only one representative for each unique graph. @@ -67,9 +66,9 @@ def _deduplicateGraphs(initial) -> list: def _get_connected_subgraphs(gml: str, invert: bool = False): - """ - Given a GML string, this function returns the number of connected subgraphs based - on the 'smart' representation split or a list of subgraphs, depending on the invert flag. + """Given a GML string, this function returns the number of connected + subgraphs based on the 'smart' representation split or a list of subgraphs, + depending on the invert flag. Parameters: - gml: str, the GML string to be converted into a 'smart' format. @@ -103,8 +102,8 @@ def _get_connected_subgraphs(gml: str, invert: bool = False): def _get_reagent(original_smiles: list, output_rsmi: str, invert: bool = False): - """ - Identifies reagents present in the original SMILES list that are absent in the processed output SMILES string. + """Identifies reagents present in the original SMILES list that are absent + in the processed output SMILES string. Parameters: - original_smiles: list of SMILES strings representing the original reagents. @@ -132,10 +131,9 @@ def _get_reagent(original_smiles: list, output_rsmi: str, invert: bool = False): def _get_reagent_rsmi(rsmi: str) -> List[str]: - """ - Identifies reagents that appear in both the reactant and product sides of - a reaction SMILES string, suggesting these elements are unchanged - by the chemical reaction. + """Identifies reagents that appear in both the reactant and product sides + of a reaction SMILES string, suggesting these elements are unchanged by the + chemical reaction. Parameters: - rsmi (str): A reaction SMILES string formatted as "reactants>>products". @@ -165,8 +163,8 @@ def _get_reagent_rsmi(rsmi: str) -> List[str]: def _remove_reagent(rsmi: str) -> str: - """ - Removes common molecules from the reactants and products in a SMILES reaction string. + """Removes common molecules from the reactants and products in a SMILES + reaction string. This function identifies the molecules that appear on both sides of the reaction (reactants and products) and removes one occurrence of each common molecule from @@ -224,8 +222,8 @@ def _remove_reagent(rsmi: str) -> str: def _add_reagent(rsmi: str, reagents: list): - """ - Modifies the SMILES representation of a reaction by adding additional reagents. + """Modifies the SMILES representation of a reaction by adding additional + reagents. Parameters: - rsmi: str, the SMILES reaction string, expected to contain '>>' separating reactants and products. @@ -255,8 +253,7 @@ def _add_reagent(rsmi: str, reagents: list): def _calculate_max_depth(reaction_tree, current_node=None, depth=0): - """ - Calculate the maximum depth of a reaction tree. + """Calculate the maximum depth of a reaction tree. Parameters: - reaction_tree (dict): A dictionary where keys are reaction SMILES (RSMI) @@ -293,8 +290,8 @@ def _find_all_paths( current_depth=0, path=None, ): - """ - Recursively find all paths from the root to the maximum depth in the reaction tree. + """Recursively find all paths from the root to the maximum depth in the + reaction tree. Parameters: - reaction_tree (dict): A dictionary of reaction SMILES with products. diff --git a/synkit/Utils/utils.py b/synkit/Utils/utils.py index 4a89977..10ee153 100644 --- a/synkit/Utils/utils.py +++ b/synkit/Utils/utils.py @@ -12,8 +12,7 @@ def stratified_random_sample( seed: Optional[int] = 42, bypass: bool = False, ) -> List[Dict[str, any]]: - """ - Stratifies and samples data from a list of dictionaries based on a + """Stratifies and samples data from a list of dictionaries based on a specified property key. Parameters: @@ -66,8 +65,7 @@ def stratified_random_sample( def calculate_processing_time(start_time_str: str, end_time_str: str) -> float: - """ - Calculates the processing time in seconds between two timestamps. + """Calculates the processing time in seconds between two timestamps. Parameters: - start_time_str (str): A string representing the start time in the format @@ -94,9 +92,9 @@ def calculate_processing_time(start_time_str: str, end_time_str: str) -> float: def remove_explicit_hydrogen( Graph: nx.Graph, excluded_indices: Iterable[int] ) -> nx.Graph: - """ - Processes a molecular graph by calculating hydrogen count ('h_count') for each node and - removing hydrogen nodes that are not specified in the excluded indices. + """Processes a molecular graph by calculating hydrogen count ('h_count') + for each node and removing hydrogen nodes that are not specified in the + excluded indices. Parameters ---------- @@ -141,11 +139,10 @@ def remove_explicit_hydrogen( def fix_implicit_hydrogen(Graph: nx.Graph, indices: Iterable[int]) -> nx.Graph: - """ - Adjusts the 'h_count' attribute of specific nodes in a molecular graph, - decreasing it based on the presence of neighboring hydrogen atoms that are also - included in the specified indices. This function works on a copy - of the provided graph and returns the modified copy. + """Adjusts the 'h_count' attribute of specific nodes in a molecular graph, + decreasing it based on the presence of neighboring hydrogen atoms that are + also included in the specified indices. This function works on a copy of + the provided graph and returns the modified copy. Parameters ---------- diff --git a/synkit/Vis/embedding.py b/synkit/Vis/embedding.py index 29514a8..3121b6a 100644 --- a/synkit/Vis/embedding.py +++ b/synkit/Vis/embedding.py @@ -11,8 +11,8 @@ def __init__( verbose: int = 0, custom_tsne_params: Optional[Dict] = None, ) -> None: - """ - Initialize the Embedding class with options for caching directory, verbosity, and custom t-SNE parameters. + """Initialize the Embedding class with options for caching directory, + verbosity, and custom t-SNE parameters. Parameters: cache_dir (str): Directory where cached results are stored. @@ -32,8 +32,7 @@ def __init__( self.tsne_params = self.default_tsne_params.copy() def set_tsne_params(self, **params) -> None: - """ - Sets parameters for t-SNE computations. + """Sets parameters for t-SNE computations. Parameters: **params: Arbitrary number of parameters for t-SNE. @@ -41,14 +40,12 @@ def set_tsne_params(self, **params) -> None: self.tsne_params.update(params) def reset_tsne_params(self) -> None: - """ - Resets t-SNE parameters to default values. - """ + """Resets t-SNE parameters to default values.""" self.tsne_params = self.default_tsne_params.copy() def _compute_tsne(self, X: np.ndarray) -> np.ndarray: - """ - Direct computation of the t-SNE embedding with the current parameters. + """Direct computation of the t-SNE embedding with the current + parameters. Parameters: X (np.ndarray): High-dimensional data points. @@ -60,8 +57,7 @@ def _compute_tsne(self, X: np.ndarray) -> np.ndarray: return tsne.fit_transform(X) def compute_tsne(self, X: np.ndarray, cache: bool = True) -> np.ndarray: - """ - Computes or retrieves the t-SNE embedding from cache. + """Computes or retrieves the t-SNE embedding from cache. Parameters: X (np.ndarray): High-dimensional data points. @@ -77,8 +73,7 @@ def compute_tsne(self, X: np.ndarray, cache: bool = True) -> np.ndarray: @property def cache(self) -> Any: - """ - Decorator for caching the compute_tsne function. + """Decorator for caching the compute_tsne function. Returns: Callable: Cached function. @@ -86,7 +81,5 @@ def cache(self) -> Any: return self.memory.cache(self._compute_tsne) def clear_cache(self) -> None: - """ - Clears the cache directory. - """ + """Clears the cache directory.""" self.memory.clear() diff --git a/synkit/Vis/graph_visualizer.py b/synkit/Vis/graph_visualizer.py index de64811..f197c49 100644 --- a/synkit/Vis/graph_visualizer.py +++ b/synkit/Vis/graph_visualizer.py @@ -239,7 +239,8 @@ def plot_as_mol( ) def visualize_its(self, its: nx.Graph, **kwargs) -> plt.Figure: - """Return a Matplotlib Figure plotting the ITS graph without duplicate display.""" + """Return a Matplotlib Figure plotting the ITS graph without duplicate + display.""" # Temporarily disable interactive mode to prevent auto-display was_interactive = plt.isinteractive() plt.ioff() @@ -277,10 +278,8 @@ def help(self) -> None: ) def __repr__(self) -> str: - """ - Return a detailed representation of the GraphVisualizer, showing configured - node and edge attribute keys. - """ + """Return a detailed representation of the GraphVisualizer, showing + configured node and edge attribute keys.""" na = list(self._node_attributes.keys()) ea = list(self._edge_attributes.keys()) return f"GraphVisualizer(node_attributes={na!r}, " f"edge_attributes={ea!r})" @@ -294,8 +293,7 @@ def visualize_its_grid( figsize: tuple[float, float] = (12, 6), **kwargs, ) -> tuple[plt.Figure, list[list[plt.Axes]]]: - """ - Plot multiple ITS graphs in a grid layout. + """Plot multiple ITS graphs in a grid layout. Parameters ---------- diff --git a/synkit/Vis/pdf_writer.py b/synkit/Vis/pdf_writer.py index 6dfc4bc..3943c96 100644 --- a/synkit/Vis/pdf_writer.py +++ b/synkit/Vis/pdf_writer.py @@ -1,5 +1,6 @@ -""" -This module comprises several functions adapted from the work of Klaus Weinbauer. +"""This module comprises several functions adapted from the work of Klaus +Weinbauer. + The original code can be found at his GitHub repository: https://github.com/klausweinbauer/FGUtils. Adaptations were made to enhance functionality and integrate with other system components. """ @@ -11,8 +12,8 @@ class PdfWriter: - """ - A utility class to create PDF reports with plots from a list of figures or dynamically generated plots. + """A utility class to create PDF reports with plots from a list of figures + or dynamically generated plots. Parameters: - file (str): The file name of the output PDF. @@ -51,8 +52,7 @@ def __init__( self.show_progress = show_progress def plot(self, data: Union[List[plt.Figure], List], **kwargs): - """ - Generate plots from data or save pre-generated figures to the PDF. + """Generate plots from data or save pre-generated figures to the PDF. Parameters: - data (Union[List[matplotlib.figure.Figure], List]): Input data or list of figures. @@ -120,8 +120,7 @@ def plot(self, data: Union[List[plt.Figure], List], **kwargs): break def save_figure(self, figure: plt.Figure): - """ - Save a pre-generated matplotlib figure directly to the PDF. + """Save a pre-generated matplotlib figure directly to the PDF. Parameters: - figure (matplotlib.figure.Figure): The figure to save. @@ -134,8 +133,7 @@ def save_figure(self, figure: plt.Figure): self.pdf_pages.savefig(figure, bbox_inches="tight", pad_inches=1) def close(self): - """ - Close the PDF file, ensuring all pages are written. + """Close the PDF file, ensuring all pages are written. Returns: - None diff --git a/synkit/Vis/rule_vis.py b/synkit/Vis/rule_vis.py index 16c6c19..8a630f5 100644 --- a/synkit/Vis/rule_vis.py +++ b/synkit/Vis/rule_vis.py @@ -17,8 +17,9 @@ def __init__(self, backend: str = "nx") -> None: self.vis_graph = GraphVisualizer() def vis(self, input: Union[str, Tuple[nx.Graph, nx.Graph, nx.Graph]], **kwargs): - """ - Wrapper to select between nx_vis and mod_vis based on backend and input type. + """Wrapper to select between nx_vis and mod_vis based on backend and + input type. + Converts input as needed. """ if self.backend == "nx": @@ -60,11 +61,9 @@ def nx_vis( add_gridbox: bool = False, rule: bool = False, ) -> plt.Figure: - """ - Visualize reactants, ITS, and products side-by-side or vertically, - with interactive plotting turned off to prevent double-display, - and correct handling of matplotlib axes arrays. - """ + """Visualize reactants, ITS, and products side-by-side or vertically, + with interactive plotting turned off to prevent double-display, and + correct handling of matplotlib axes arrays.""" # Disable interactive mode & clear any leftover figures was_interactive = plt.isinteractive() plt.ioff() @@ -153,9 +152,7 @@ def nx_vis( plt.ion() def mod_vis(self, gml: str, path: str = "./") -> None: - """ - Simple MOD visualization via mod_post CLI. - """ + """Simple MOD visualization via mod_post CLI.""" from mod import ruleGMLString rule = ruleGMLString(gml, add=False) @@ -165,10 +162,7 @@ def mod_vis(self, gml: str, path: str = "./") -> None: self.post() def post(self) -> None: - """ - Generate an external report via the `mod_post` CLI. - - """ + """Generate an external report via the `mod_post` CLI.""" try: subprocess.run(["mod_post"], check=True) except subprocess.CalledProcessError as e: diff --git a/synkit/Vis/rxn_vis.py b/synkit/Vis/rxn_vis.py index 9e1cf74..2f0357d 100644 --- a/synkit/Vis/rxn_vis.py +++ b/synkit/Vis/rxn_vis.py @@ -17,8 +17,7 @@ def __init__( atom_label_font_size: int = 12, show_atom_map: bool = False, ): - """ - Initialize the reaction/molecule visualizer. + """Initialize the reaction/molecule visualizer. Parameters ---------- @@ -49,8 +48,7 @@ def __init__( def render( self, smiles: str, return_bytes: bool = False ) -> Union[Image.Image, bytes]: - """ - Render a molecule or reaction SMILES to a cropped PNG. + """Render a molecule or reaction SMILES to a cropped PNG. Parameters ---------- @@ -116,8 +114,7 @@ def render( return png if return_bytes else img def save_png(self, smiles: str, path: str) -> None: - """ - Render and save as a PNG file. + """Render and save as a PNG file. Parameters ---------- @@ -130,8 +127,7 @@ def save_png(self, smiles: str, path: str) -> None: img.save(path, format="PNG") def save_pdf(self, smiles: str, path: str, resolution: float = 300.0) -> None: - """ - Render and save as a single‐page PDF. + """Render and save as a single‐page PDF. Parameters ---------- From 0eb22366726c9c3d9c78dcad86163b32c0cada83 Mon Sep 17 00:00:00 2001 From: Tieu Long Phan <125431507+TieuLongPhan@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:20:16 +0200 Subject: [PATCH 2/6] Prepare release bump version (#27) * update refractor * fix reactor * fix lint * prepare bechmark * refractor MODReactor and SynReactor * update crn * refractor cluster, change to matcher, fix code synreactor, now resnow comparable to modreactor * test 3 os * test * test * fix workflow * fix lint * fix win * fix win * fix win again * update smart * add synreactor implicit hydrogen * fix mcsmatcher * refractor visualization * fix conflict rdkit, upgrade to 2025.3.1 * fix lint * move aam_validator to Chem submodule * fix lint * prepare benchmark matcher * change backend rule to mod * prepare doc * add doc * update fih * update graph module doc * update doc * prepare release * . * fix doc * clean doc * fix docstring * fix tutorial * update fig * update explicit_hydrogen for its * prepare release * build doc * fix lint * fix bug in explicit hydrogen for ITS * fix build * fix * fix * fix * fix doc * update nauty canon, rule filters, change benchmark * prepare release * fix bug in nauty alg * update doc * add features for expanding its * add rule_matcher.py * add testcase rule matcher * add wildcard for smiles * add partial engine * update new features * update document * add data * update Chem features * format docstring and refractor Chem module * add auto-test pypi * create dependabot * test run yml * test * test docker * add docker * add docker * . * add readme * rename * release docker * remove redundant file * fix lint * fix bug * fix lint * add partial its beta * test publising conda * test publising conda * fix workflow * fix workflow * fix workflow * fix workflow again * tes pre-release * tes pre-release * tes pre-release * tes pre-release * tes pre-release * tes pre-release * fix meta.yaml * fix meta.yaml * fix meta.yaml * fix meta.yaml * fix meta.yaml * fix meta.yaml * publish beta * publish beta * debug * debug * debug * debug * debug * debug * debug * debug * prepare release --- .github/workflows/conda-forge-publish.yml | 109 +++ pyproject.toml | 4 +- recipe/meta.yaml | 37 + synkit/Graph/Canon/nauty.py | 4 +- synkit/Graph/ITS/partial_its.py | 238 +++++++ synkit/Graph/Matcher/graph_morphism.py | 813 +++++++++------------- synkit/IO/graph_to_mol.py | 4 +- 7 files changed, 728 insertions(+), 481 deletions(-) create mode 100644 .github/workflows/conda-forge-publish.yml create mode 100644 recipe/meta.yaml create mode 100644 synkit/Graph/ITS/partial_its.py diff --git a/.github/workflows/conda-forge-publish.yml b/.github/workflows/conda-forge-publish.yml new file mode 100644 index 0000000..12fd6a2 --- /dev/null +++ b/.github/workflows/conda-forge-publish.yml @@ -0,0 +1,109 @@ +name: Publish to conda-forge + +on: + release: + types: [published] + push: + branches: [refactor] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + outputs: + pkg_paths: ${{ steps.build.outputs.paths }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Miniconda + uses: conda-incubator/setup-miniconda@v2 + with: + channels: conda-forge + auto-update-conda: true + auto-activate-base: true + + - name: Create & activate build env + shell: bash -l {0} + run: | + conda create -n build python=3.11 'conda-build>=3.21' -c conda-forge -y + conda activate build + + - id: build + name: Build conda package + shell: bash -l {0} + env: + GITHUB_RUN_NUMBER: ${{ github.run_number }} + run: | + conda activate build + rm -rf conda-bld && mkdir conda-bld + + conda-build recipe --output-folder ./conda-bld + + echo "DEBUG: Built files:" + find conda-bld -type f \( -name "*.conda" -o -name "*.tar.bz2" \) -print + + files=$(find conda-bld -type f \( -name "*.conda" -o -name "*.tar.bz2" \) -print | tr '\n' ' ') + echo "paths=$files" >> $GITHUB_OUTPUT + + - name: Upload built packages as artifact + uses: actions/upload-artifact@v4 + with: + name: conda-packages + path: conda-bld/ + + publish_release: + needs: build + if: github.event_name == 'release' + runs-on: ubuntu-latest + steps: + - name: Download built packages + uses: actions/download-artifact@v4 + with: + name: conda-packages + path: conda-bld + + - name: Install Anaconda Client + run: python3 -m pip install --upgrade anaconda-client + + - name: Upload to conda-forge / main + env: + ANACONDA_TOKEN: ${{ secrets.ANACONDA_TOKEN }} + run: | + for pkg in ${{ needs.build.outputs.pkg_paths }}; do + anaconda -t "$ANACONDA_TOKEN" upload \ + --user tieulongphan \ + --label main \ + --no-progress \ + "$pkg" + done + + publish_beta: + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/refactor' + runs-on: ubuntu-latest + steps: + - name: Download built packages + uses: actions/download-artifact@v4 + with: + name: conda-packages + path: conda-bld + + - name: Install Anaconda Client + run: python3 -m pip install --upgrade anaconda-client + + - name: Upload to conda-forge / beta + env: + ANACONDA_TOKEN: ${{ secrets.ANACONDA_TOKEN }} + run: | + for pkg in ${{ needs.build.outputs.pkg_paths }}; do + anaconda -t "$ANACONDA_TOKEN" upload \ + --user tieulongphan \ + --label beta \ + --no-progress \ + "$pkg" + done diff --git a/pyproject.toml b/pyproject.toml index 688cc3d..38f4bfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,9 @@ build-backend = "hatchling.build" [project] name = "synkit" -version = "0.0.11" +version = "0.0.12" +license = { text = "MIT" } +license-files = ["LICENSE"] authors = [ {name="Tieu Long Phan", email="tieu@bioinf.uni-leipzig.de"} ] diff --git a/recipe/meta.yaml b/recipe/meta.yaml new file mode 100644 index 0000000..5b6d4fb --- /dev/null +++ b/recipe/meta.yaml @@ -0,0 +1,37 @@ +package: + name: synkit + version: 0.0.12 + +source: + path: .. + +build: + noarch: python + number: 0 + script: "{{ PYTHON }} -m pip install . --no-deps -vv" + +requirements: + host: + - python >=3.11,<3.12 # force a 3.11 build env + - pip + - hatchling # your PEP 517 build backend + run: + - python >=3.11,<3.12 + - scikit-learn >=1.4.0 + - pandas >=1.5.3 + - rdkit >=2025.3.1 + - networkx >=3.3 + - seaborn >=0.13.2 + - requests >=2.32.3 + - regex >=2024.11.6 + - numpy >=2.2.0 + +about: + home: https://github.com/TieuLongPhan/SynKit + license: MIT + license_file: LICENSE + summary: Utility for reaction modeling using graph grammar + +extra: + recipe-maintainers: + - TieuLongPhan diff --git a/synkit/Graph/Canon/nauty.py b/synkit/Graph/Canon/nauty.py index 6a0333c..f2d3405 100644 --- a/synkit/Graph/Canon/nauty.py +++ b/synkit/Graph/Canon/nauty.py @@ -7,7 +7,7 @@ class NautyCanonicalizer: - """Perform Nauty‑style canonicalization of a NetworkX graph, optionally + """Perform Nauty-style canonicalization of a NetworkX graph, optionally refining and distinguishing nodes and edges by specified attributes, and extracting automorphisms, orbits, and canonical permutations. @@ -317,4 +317,4 @@ def union_orbits(i, j): def graph_signature(self, G): G_canon = self.canonical_form(G) label = self._build_label(G_canon, sorted(G_canon.nodes())) - return hashlib.sha256(label.encode("utf-8")).hexdigest() \ No newline at end of file + return hashlib.sha256(label.encode("utf-8")).hexdigest() diff --git a/synkit/Graph/ITS/partial_its.py b/synkit/Graph/ITS/partial_its.py new file mode 100644 index 0000000..ee73d5a --- /dev/null +++ b/synkit/Graph/ITS/partial_its.py @@ -0,0 +1,238 @@ +import networkx as nx +from typing import Dict, Any, Optional, Tuple, Hashable + + +class PartialITS: + """Utility class for building **partial** Imaginary‑Transition‑State (ITS) + graphs from a pair of reactant/product `networkx` graphs. + + The resulting ITS graph contains + + * a **union** of nodes from *G* (reactant) and *H* (product), + * a per‑node attribute ``typesGH`` – a 2‑tuple ``(attrs_from_G, attrs_from_H)`` – + where missing sides are filled by the present one, + * edges categorised as **unchanged**, **broken** or **formed** and stored as + an ``order`` tuple ``(o_G, o_H)``, and + * a convenience edge attribute ``standard_order = o_G - o_H`` (optionally + zeroed when |Δ| < 1 to ignore aromaticity changes). + """ + + # --------------------------------------------------------------------- + # Helper – node‐attribute retrieval + # --------------------------------------------------------------------- + @staticmethod + def _get_node_attr_tuple( + graph: nx.Graph, + node: Hashable, + defaults: Dict[str, Any], + ) -> Tuple[Any, ...]: + """Return a tuple containing *all* attributes in *defaults* order. + + :param graph: graph to query. + :param node: node identifier. + :param defaults: mapping of attribute → default value. + :returns: tuple in the order of *defaults.keys()*. + """ + return tuple( + graph.nodes[node].get(attr, default) for attr, default in defaults.items() + ) + + # ------------------------------------------------------------------ + # Helper – standard_order + # ------------------------------------------------------------------ + @staticmethod + def _attach_standard_order( + graph: nx.Graph, + ignore_aromaticity: bool = False, + ) -> nx.Graph: + """Attach ``standard_order`` edge attribute in‑place. + + :param graph: ITS graph with ``order`` tuples. + :param ignore_aromaticity: if *True*, set Δ=0 when |Δ|<1. + :returns: *graph* (for chaining). + """ + for u, v, data in graph.edges(data=True): + o_g, o_h = data.get("order", (0, 0)) + delta = o_g - o_h + if ignore_aromaticity and abs(delta) < 1: + delta = 0 + graph[u][v]["standard_order"] = delta + return graph + + # ------------------------------------------------------------------ + # Helper – edge insertion logic + # ------------------------------------------------------------------ + @staticmethod + def _populate_edges( + its: nx.Graph, + G: nx.Graph, + H: nx.Graph, + ) -> None: + """Populate *its* with ``order`` tuples following the rules. + + Unchanged (present in both): ``(o, o)`` + Broken (present only in G): ``(o, 0)`` *when one end survives* + Formed (present only in H): ``(0, o)`` *when one end survives* + Unchanged-external (only in one, *both* ends external): ``(o, o)`` + """ + common = set(G.nodes()) & set(H.nodes()) + seen: set[Tuple[Hashable, Hashable]] = set() + + def add(u: Hashable, v: Hashable, order: Tuple[float, float]): + if (u, v) in seen or (v, u) in seen: + return + its.add_edge(u, v, order=order) + seen.add((u, v)) + + # Pass 1 – edges from G + for u, v, d in G.edges(data=True): + o_g = d.get("order", 0) + if H.has_edge(u, v): # unchanged (core) + add(u, v, (o_g, o_g)) + else: + if (u in common) ^ (v in common): # broken + add(u, v, (o_g, 0)) + else: # unchanged non‑core (G only) + add(u, v, (o_g, o_g)) + + # Pass 2 – edges unique to H + for u, v, d in H.edges(data=True): + if G.has_edge(u, v): + continue # already handled + o_h = d.get("order", 0) + if (u in common) ^ (v in common): # formed + add(u, v, (0, o_h)) + else: # unchanged non‑core (H only) + add(u, v, (o_h, o_h)) + + @staticmethod + def balance_valences(graph: nx.Graph) -> nx.Graph: + """ + Balances valences in a NetworkX graph by adding wildcard '*' nodes for atoms + that have missing bonds according to their broken bonds and hydrogen counts. + + :param graph: NetworkX Graph with node attributes: + - element: str, chemical symbol + - charge: int, formal charge + - typesGH: tuple of descriptors (element, aromatic, hcount, h_change, connections) + - atom_map: int, unique identifier (node key) + :type graph: nx.Graph + :return: Modified graph with wildcard nodes added + :rtype: nx.Graph + """ + # Copy to avoid modifying the original + G = graph.copy() + # Determine next wildcard index (integer keys only) + existing_ids = [n for n in G.nodes if isinstance(n, int)] + next_id = max(existing_ids, default=0) + 1 + + # Iterate over original nodes + for atom in list(G.nodes): + data = G.nodes[atom] + # Skip wildcards + if data.get("element") == "*": + continue + # Sum of positive standard_order values (broken bonds) + broken = sum( + d.get("standard_order", 0) + for _, _, d in G.edges(atom, data=True) + if d.get("standard_order", 0) > 0 + ) + if broken <= 0: + continue + # Available hydrogen counts from typesGH descriptors (index 2) + h_counts = [desc[2] for desc in data["typesGH"]] + # If any descriptor has hydrogen >= broken, no wildcard needed + if max(h_counts, default=0) >= broken: + continue + # Need wildcard for remaining broken bonds + wc_id = next_id + next_id += 1 + # Add wildcard node with two GH types: one providing valence and one default + G.add_node( + wc_id, + element="*", + charge=0, + typesGH=(("*", False, broken, 0, []), ("*", False, 0, 0, [])), + atom_map=wc_id, + ) + # Forming bond with wildcard: dynamic order=broken, negative standard_order + G.add_edge( + atom, wc_id, order=(0.0, float(broken)), standard_order=-float(broken) + ) + return G + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + @staticmethod + def construct( + G: nx.Graph, + H: nx.Graph, + *, + ignore_aromaticity: bool = False, + attributes_defaults: Optional[Dict[str, Any]] = None, + balance: bool = True, + ) -> nx.Graph: + """Return a partial ITS graph for *G* → *H*. + + :param G: reactant graph. + :param H: product graph. + :keyword ignore_aromaticity: if *True*, set ``standard_order`` to 0 when + |Δ|<1. + :keyword attributes_defaults: mapping of attribute → default value used + for the ``typesGH`` tuples. If *None*, a + small sensible default set is used. + :returns: an ITS graph with nodes, ``typesGH`` tuples and annotated + edges. + """ + # ------------------------------------------------------------------ + # Set defaults + # ------------------------------------------------------------------ + if attributes_defaults is None: + attributes_defaults = { + "element": "*", + "aromatic": False, + "hcount": 0, + "charge": 0, + "neighbors": [], + } + + # ------------------------------------------------------------------ + # Build node union + # ------------------------------------------------------------------ + its = nx.Graph() + its.add_nodes_from(G.nodes(data=True)) + its.add_nodes_from((n, d) for n, d in H.nodes(data=True) if n not in its) + + # ------------------------------------------------------------------ + # typesGH per node + # ------------------------------------------------------------------ + types: Dict[Hashable, Tuple[Tuple[Any, ...], Tuple[Any, ...]]] = {} + for n in its.nodes(): + in_g, in_h = n in G.nodes(), n in H.nodes() + attrs_g = PartialITS._get_node_attr_tuple( + G if in_g else H, n, attributes_defaults + ) + attrs_h = ( + PartialITS._get_node_attr_tuple(H, n, attributes_defaults) + if in_h + else attrs_g + ) + if not in_h: + attrs_h = attrs_g + types[n] = (attrs_g, attrs_h) + nx.set_node_attributes(its, types, "typesGH") + + # ------------------------------------------------------------------ + # Edges with order tuples + # ------------------------------------------------------------------ + PartialITS._populate_edges(its, G, H) + + # ------------------------------------------------------------------ + # Attach standard_order and return + # ------------------------------------------------------------------ + its = PartialITS._attach_standard_order(its, ignore_aromaticity) + if balance: + its = PartialITS.balance_valences(its) + return its diff --git a/synkit/Graph/Matcher/graph_morphism.py b/synkit/Graph/Matcher/graph_morphism.py index c608acb..4a76ddf 100644 --- a/synkit/Graph/Matcher/graph_morphism.py +++ b/synkit/Graph/Matcher/graph_morphism.py @@ -1,516 +1,377 @@ -import re +import logging +import itertools +from operator import eq +from typing import Callable, Optional, Union, List, Any, Dict import networkx as nx -from typing import Optional, List - -from rdkit import Chem -from rdkit.Chem.MolStandardize import rdMolStandardize - -__all__ = ["get_rc", "its_decompose"] - - -def get_rc( - ITS: nx.Graph, - element_key: List[str] = ["element", "charge", "typesGH", "atom_map"], - bond_key: str = "order", - standard_key: str = "standard_order", - disconnected: bool = False, -) -> nx.Graph: - """Extract the reaction-center (RC) subgraph from an ITS graph. - - This function identifies: - 1. All bonds whose standard order (difference between ITS orders) is non-zero. - 2. All H–H bonds, ensuring they are included even if no order change is detected. - 3. (Optional) Additional nodes with charge changes and reconnection of edges - if `disconnected=True`. - - :param ITS: The integrated transition-state graph with composite node/edge attributes. - :type ITS: nx.Graph - :param element_key: List of node‐attribute keys to copy into the RC graph. - :type element_key: List[str] - :param bond_key: Edge attribute key representing the tuple of bond orders. - :type bond_key: str - :param standard_key: Edge attribute key for the computed standard_order. - :type standard_key: str - :param disconnected: If True, also include nodes with charge changes and - reconnect any ITS edges between RC nodes. - :type disconnected: bool - :returns: A new graph containing only the reaction-center nodes and edges. - :rtype: nx.Graph - - :example: - >>> ITS = nx.Graph() - >>> # ... populate ITS with 'order', 'standard_order', 'typesGH', etc. ... - >>> RC = get_rc(ITS, disconnected=True) - >>> isinstance(RC, nx.Graph) - True +from networkx.algorithms import isomorphism +from networkx.algorithms.isomorphism import GraphMatcher +from networkx.algorithms.isomorphism import generic_node_match, generic_edge_match + + +# Alias for any NetworkX graph type +graph_types = Union[nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph] + + +def find_graph_isomorphism( + G1: graph_types, + G2: graph_types, + node_match: Optional[Callable[[Dict[str, Any], Dict[str, Any]], bool]] = None, + edge_match: Optional[Callable[[Dict[str, Any], Dict[str, Any]], bool]] = None, + use_defaults: bool = True, + fast_invariant_check: bool = True, + logger: Optional[logging.Logger] = None, +) -> Optional[Dict[Any, Any]]: + """Check whether two graphs are isomorphic and return the node-mapping. + + :param G1: The first NetworkX graph to compare. + :type G1: nx.Graph | nx.DiGraph | nx.MultiGraph | nx.MultiDiGraph + :param G2: The second NetworkX graph to compare. + :type G2: nx.Graph | nx.DiGraph | nx.MultiGraph | nx.MultiDiGraph + :param node_match: Optional function taking two node attribute dicts + and returning True if they match. + :type node_match: callable or None + :param edge_match: Optional function taking two edge attribute dicts + and returning True if they match. + :type edge_match: callable or None + :param use_defaults: Whether to use default matchers when None. + :type use_defaults: bool + :param fast_invariant_check: Perform quick node/edge count and + degree sequence checks prior to matcher. + :type fast_invariant_check: bool + :param logger: Logger for debug messages. Defaults to root logger. + :type logger: logging.Logger or None + :returns: A dict mapping nodes in G1 to nodes in G2 if isomorphic; + otherwise None. + :rtype: dict[Any, Any] or None """ - rc = nx.Graph() - _add_bond_order_changes(ITS, rc, element_key, bond_key, standard_key) - - # 1.5) H-H bonds (force inclusion, with fallback typesGH) - for u, v, data in ITS.edges(data=True): - elem_u = ITS.nodes[u].get("element") - elem_v = ITS.nodes[v].get("element") - if elem_u == "H" and elem_v == "H": - for n in (u, v): - node_data = dict(ITS.nodes[n]) - if "typesGH" not in node_data: - node_data["typesGH"] = ( - ("H", False, 0, 0, []), - ("*", False, 0, 0, []), - ) - # Ensure typesGH is available even if not in original element_key - final_attrs = {k: node_data[k] for k in element_key if k in node_data} - final_attrs["typesGH"] = node_data["typesGH"] - rc.add_node(n, **final_attrs) - - rc.add_edge( - u, - v, - **{ - bond_key: data.get(bond_key), - standard_key: data.get(standard_key), - }, + log = logger or logging.getLogger(__name__) + + # 1) Ensure same graph type + if type(G1) is not type(G2): + log.debug("Graph types differ: %r vs %r", type(G1), type(G2)) + return None + + # 2) Quick invariants + if fast_invariant_check: + if G1.number_of_nodes() != G2.number_of_nodes(): + log.debug( + "Node counts differ: %d vs %d", + G1.number_of_nodes(), + G2.number_of_nodes(), ) - if disconnected: - _add_charge_change_nodes(ITS, rc, element_key) - _reconnect_rc_edges(ITS, rc, bond_key, standard_key) - - return rc - - -def _carry_node_attrs(src: nx.Graph, dst: nx.Graph, n: int, keys: List[str]) -> None: - """Copy node *n* from *src* to *dst* with only *keys* attributes.""" - if dst.has_node(n): - return - attrs = {k: src.nodes[n][k] for k in keys if k in src.nodes[n]} - dst.add_node(n, **attrs) - - -def _add_charge_change_nodes( - ITS: nx.Graph, - rc: nx.Graph, - keys: List[str], -) -> None: - """Step 3a – add nodes whose *typesGH* shows a charge change.""" - for n, data in ITS.nodes(data=True): - gh = data.get("typesGH") - if ( - isinstance(gh, (list, tuple)) - and len(gh) >= 2 - and gh[0][3] != gh[1][3] - and not rc.has_node(n) - ): - _carry_node_attrs(ITS, rc, n, keys) - - -def _reconnect_rc_edges( - ITS: nx.Graph, - rc: nx.Graph, - bond_key: str, - standard_key: str, -) -> None: - """Step 3b – re-add any original ITS edge between nodes already in RC.""" - for u, v, data in ITS.edges(data=True): - if rc.has_node(u) and rc.has_node(v) and not rc.has_edge(u, v): - rc.add_edge( - u, - v, - **{bond_key: data.get(bond_key), standard_key: data.get(standard_key)}, + return None + if G1.number_of_edges() != G2.number_of_edges(): + log.debug( + "Edge counts differ: %d vs %d", + G1.number_of_edges(), + G2.number_of_edges(), ) + return None + degs1 = sorted(d for _, d in G1.degree()) + degs2 = sorted(d for _, d in G2.degree()) + if degs1 != degs2: + log.debug("Degree sequences differ") + return None + + # 3) Default matchers + if use_defaults: + if node_match is None: + node_match = isomorphism.categorical_node_match( + ["element", "atom_map", "hcount"], ["*", 0, 0] + ) + if edge_match is None: + edge_match = isomorphism.categorical_edge_match("order", 1) + # 4) Select the correct matcher + if isinstance(G1, (nx.MultiGraph, nx.MultiDiGraph)): + if isinstance(G1, nx.MultiGraph): + Matcher = nx.algorithms.isomorphism.MultiGraphMatcher + else: + Matcher = nx.algorithms.isomorphism.MultiDiGraphMatcher + else: + if isinstance(G1, nx.Graph): + Matcher = nx.algorithms.isomorphism.GraphMatcher + else: + Matcher = nx.algorithms.isomorphism.DiGraphMatcher + + matcher = Matcher(G1, G2, node_match=node_match, edge_match=edge_match) + if matcher.is_isomorphic(): + log.debug("Graphs are isomorphic; mapping found") + return matcher.mapping + else: + log.debug("Graphs are not isomorphic") + return None + + +def graph_isomorphism( + graph_1: nx.Graph, + graph_2: nx.Graph, + node_match: Optional[Callable] = None, + edge_match: Optional[Callable] = None, + use_defaults: bool = False, +) -> bool: + """Determines if two graphs are isomorphic, considering provided node and + edge matching functions. Uses default matching settings if none are + provided. -def _add_bond_order_changes( - ITS: nx.Graph, - rc: nx.Graph, - keys: List[str], - bond_key: str, - standard_key: str, -) -> None: - """Step 1 – bond-order-change edges and their nodes.""" - for u, v, data in ITS.edges(data=True): - old, new = data.get(bond_key, (None, None)) - if old == new: - continue - for n in (u, v): - _carry_node_attrs(ITS, rc, n, keys) - rc.add_edge( - u, v, **{bond_key: data[bond_key], standard_key: data.get(standard_key)} - ) - + Parameters: + - graph_1 (nx.Graph): The first graph to compare. + - graph_2 (nx.Graph): The second graph to compare. + - node_match (Optional[Callable]): The function used to match nodes. + Uses default if None. + - edge_match (Optional[Callable]): The function used to match edges. + Uses default if None. -# def get_rc( -# ITS: nx.Graph, -# element_key: List[str] = ["element", "charge", "typesGH", "atom_map"], -# bond_key: str = "order", -# standard_key: str = "standard_order", -# disconnected: bool = False, -# ) -> nx.Graph: -# """ -# Extract the reaction center (RC) from ITS graph. - -# Enhancements: -# - Adds nodes and edges where bond order changes (core logic). -# - If disconnected=True: -# - Adds nodes with charge change based on typesGH. -# - Reconnects any ITS edge between two RC nodes. -# - NEW: Always includes H-H bonds in RC. Adds default typesGH if missing. -# """ -# rc = nx.Graph() - -# # 1) edges with bond-order change -# for u, v, data in ITS.edges(data=True): -# old, new = data.get(bond_key, [None, None]) -# if old != new: -# for n in (u, v): -# if not rc.has_node(n): -# rc.add_node( -# n, -# **{ -# k: ITS.nodes[n][k] for k in element_key if k in ITS.nodes[n] -# }, -# ) -# rc.add_edge( -# u, -# v, -# **{bond_key: data.get(bond_key), standard_key: data.get(standard_key)}, -# ) - -# # 1.5) H-H bonds (force inclusion, with fallback typesGH) -# for u, v, data in ITS.edges(data=True): -# elem_u = ITS.nodes[u].get("element") -# elem_v = ITS.nodes[v].get("element") -# if elem_u == "H" and elem_v == "H": -# for n in (u, v): -# node_data = dict(ITS.nodes[n]) -# if "typesGH" not in node_data: -# node_data["typesGH"] = ( -# ("H", False, 0, 0, []), -# ("*", False, 0, 0, []), -# ) -# # Ensure typesGH is available even if not in original element_key -# final_attrs = {k: node_data[k] for k in element_key if k in node_data} -# final_attrs["typesGH"] = node_data["typesGH"] -# rc.add_node(n, **final_attrs) - -# rc.add_edge( -# u, -# v, -# **{ -# bond_key: data.get(bond_key), -# standard_key: data.get(standard_key), -# }, -# ) - -# if disconnected: -# # 2) nodes with typesGH-based charge change -# for n, data in ITS.nodes(data=True): -# gh = data.get("typesGH") -# if ( -# isinstance(gh, (list, tuple)) -# and len(gh) >= 2 -# and len(gh[0]) > 3 -# and len(gh[1]) > 3 -# and gh[0][3] != gh[1][3] -# ): -# if not rc.has_node(n): -# rc.add_node(n, **{k: data[k] for k in element_key if k in data}) - -# # 3) reconnect RC nodes -# for u, v, data in ITS.edges(data=True): -# if rc.has_node(u) and rc.has_node(v) and not rc.has_edge(u, v): -# rc.add_edge( -# u, -# v, -# **{ -# bond_key: data.get(bond_key), -# standard_key: data.get(standard_key), -# }, -# ) - -# return rc - - -# def get_rc( -# ITS: nx.Graph, -# element_key: List[str] = ["element", "charge", "typesGH", "atom_map"], -# bond_key: str = "order", -# standard_key: str = "standard_order", -# disconnected: bool = False, -# ) -> nx.Graph: -# """ -# Extract the reaction center (RC) from ITS by: - -# 1. Always adding any edge whose bond order changes -# (bond_key[0] != bond_key[1]), plus its two end-nodes. -# 2. [if disconnected=True] Adding any node whose 'typesGH' record shows a charge change -# (typesGH[0][3] != typesGH[1][3]), even if isolated. -# 3. [if disconnected=True] Re-adding any ITS edge between two nodes already in RC -# (to preserve connectivity), carrying over bond_key & standard_key. - -# Parameters: -# - ITS (nx.Graph): input ITS graph. -# - element_key (List[str]): node attrs to carry over. -# - bond_key (str): edge attr key for bond order. -# - standard_key (str): edge attr key for standard order. -# - disconnected (bool): if True, include “charge-change” nodes (step 2) and -# reconnect any edges among RC nodes (step 3). If False, only performs step 1. -# """ -# rc = nx.Graph() - -# # 1) edges with bond-order change -# for u, v, data in ITS.edges(data=True): -# old, new = data.get(bond_key, [None, None]) -# if old != new: -# for n in (u, v): -# if not rc.has_node(n): -# rc.add_node( -# n, -# **{ -# k: ITS.nodes[n][k] for k in element_key if k in ITS.nodes[n] -# }, -# ) -# rc.add_edge( -# u, -# v, -# **{bond_key: data.get(bond_key), standard_key: data.get(standard_key)}, -# ) - -# if disconnected: -# # 2) nodes with a typesGH-based charge change -# for n, data in ITS.nodes(data=True): -# gh = data.get("typesGH") -# if ( -# isinstance(gh, (list, tuple)) -# and len(gh) >= 2 -# and len(gh[0]) > 3 -# and len(gh[1]) > 3 -# and gh[0][3] != gh[1][3] -# ): -# if not rc.has_node(n): -# rc.add_node(n, **{k: data[k] for k in element_key if k in data}) - -# # 3) re-add any ITS edge between RC nodes to preserve connectivity -# for u, v, data in ITS.edges(data=True): -# if rc.has_node(u) and rc.has_node(v) and not rc.has_edge(u, v): -# rc.add_edge( -# u, -# v, -# **{ -# bond_key: data.get(bond_key), -# standard_key: data.get(standard_key), -# }, -# ) - -# return rc - - -def its_decompose(its_graph: nx.Graph, nodes_share="typesGH", edges_share="order"): - """Decompose an ITS graph into two separate reactant (G) and product (H) - graphs. - - Nodes and edges in `its_graph` carry composite attributes: - - Each node has `its_graph.nodes[nodes_share] = (node_attrs_G, node_attrs_H)`. - - Each edge has `its_graph.edges[edges_share] = (order_G, order_H)`. - - This function splits those tuples to reconstruct the original G and H graphs. - - :param its_graph: The ITS graph with composite node/edge attributes. - :type its_graph: nx.Graph - :param nodes_share: Node attribute key storing (G_attrs, H_attrs) tuples. - :type nodes_share: str - :param edges_share: Edge attribute key storing (order_G, order_H) tuples. - :type edges_share: str - :returns: A tuple of two graphs (G, H) reconstructed from the ITS. - :rtype: Tuple[nx.Graph, nx.Graph] - - :example: - >>> its = nx.Graph() - >>> # ... set its.nodes[n]['typesGH'] and its.edges[e]['order'] ... - >>> G, H = its_decompose(its) - >>> isinstance(G, nx.Graph) and isinstance(H, nx.Graph) - True + Returns: + - bool: True if the graphs are isomorphic, False otherwise. """ - G = nx.Graph() - H = nx.Graph() - - # Decompose nodes - for node, data in its_graph.nodes(data=True): - if nodes_share in data: - node_attr_g, node_attr_h = data[nodes_share] - # Unpack node attributes for G - G.add_node( - node, - element=node_attr_g[0], - aromatic=node_attr_g[1], - hcount=node_attr_g[2], - charge=node_attr_g[3], - neighbors=node_attr_g[4], - atom_map=node, + # Define default node and edge attributes and match settings + if use_defaults: + node_label_names = ["element", "charge"] + node_label_default = ["*", 0] + edge_attribute = "order" + + # Default node and edge match functions if not provided + if node_match is None: + node_match = generic_node_match( + node_label_names, node_label_default, [eq] * len(node_label_names) ) - if len(node_attr_h) > 0: - # Unpack node attributes for H - H.add_node( - node, - element=node_attr_h[0], - aromatic=node_attr_h[1], - hcount=node_attr_h[2], - charge=node_attr_h[3], - neighbors=node_attr_h[4], - atom_map=node, - ) - - # Decompose edges - for u, v, data in its_graph.edges(data=True): - if edges_share in data: - order_g, order_h = data[edges_share] - if order_g > 0: # Assuming 0 means no edge in G - G.add_edge(u, v, order=order_g) - if order_h > 0: # Assuming 0 means no edge in H - H.add_edge(u, v, order=order_h) - - return G, H - - -def compare_graphs( - graph1: nx.Graph, - graph2: nx.Graph, - node_attrs: list = ["element", "aromatic", "hcount", "charge", "neighbors"], - edge_attrs: list = ["order"], + if edge_match is None: + edge_match = generic_edge_match(edge_attribute, 1, eq) + + # Perform the isomorphism check using NetworkX + return nx.is_isomorphic( + graph_1, graph_2, node_match=node_match, edge_match=edge_match + ) + + +def subgraph_isomorphism( + child_graph: nx.Graph, + parent_graph: nx.Graph, + node_label_names: List[str] = ["element", "charge"], + node_label_default: List[Any] = ["*", 0], + edge_attribute: str = "order", + use_filter: bool = False, + check_type: str = "induced", # "induced" or "monomorphism" + node_comparator: Optional[Callable[[Any, Any], bool]] = None, + edge_comparator: Optional[Callable[[Any, Any], bool]] = None, ) -> bool: - """Compare two graphs based on specified node and edge attributes. + """Enhanced checks if the child graph is a subgraph isomorphic to the + parent graph based on customizable node and edge attributes. Parameters: - - graph1 (nx.Graph): The first graph to compare. - - graph2 (nx.Graph): The second graph to compare. - - node_attrs (list): A list of node attribute names to include in the comparison. - - edge_attrs (list): A list of edge attribute names to include in the comparison. + - child_graph (nx.Graph): The child graph. + - parent_graph (nx.Graph): The parent graph. + - node_label_names (List[str]): Labels to compare. + - node_label_default (List[Any]): Defaults for missing node labels. + - edge_attribute (str): The edge attribute to compare. + - use_filter (bool): Whether to use pre-filters based on node and edge count. + - check_type (str): "induced" (default) or "monomorphism" for the type of subgraph matching. + - node_comparator (Callable[[Any, Any], bool]): Custom comparator for node attributes. + - edge_comparator (Callable[[Any, Any], bool]): Custom comparator for edge attributes. Returns: - - bool: True if both graphs are identical with respect to the specified attributes, - otherwise False. + - bool: True if subgraph isomorphism is found, False otherwise. """ - # Compare node sets - if set(graph1.nodes()) != set(graph2.nodes()): - return False - - # Compare nodes based on attributes - for node in graph1.nodes(): - if node not in graph2: - return False - node_data1 = {attr: graph1.nodes[node].get(attr, None) for attr in node_attrs} - node_data2 = {attr: graph2.nodes[node].get(attr, None) for attr in node_attrs} - if node_data1 != node_data2: + if use_filter: + # Initial quick filters based on node and edge counts + if len(child_graph) > len(parent_graph) or len(child_graph.edges) > len( + parent_graph.edges + ): return False - # Compare edge sets with sorted tuples - if set(tuple(sorted(edge)) for edge in graph1.edges()) != set( - tuple(sorted(edge)) for edge in graph2.edges() - ): - return False - - # Compare edges based on attributes - for edge in graph1.edges(): - # Sort the edge for consistent comparison - sorted_edge = tuple(sorted(edge)) - if sorted_edge not in graph2.edges(): - return False - edge_data1 = {attr: graph1.edges[edge].get(attr, None) for attr in edge_attrs} - edge_data2 = { - attr: graph2.edges[sorted_edge].get(attr, None) for attr in edge_attrs - } - if edge_data1 != edge_data2: - return False + # Step 2: Node label filter - Only consider 'element' and 'charge' attributes + for _, child_data in child_graph.nodes(data=True): + found_match = False + for _, parent_data in parent_graph.nodes(data=True): + match = True + # Compare only the 'element' and 'charge' attributes + for label, default in zip(node_label_names, node_label_default): + child_value = child_data.get(label, default) + parent_value = parent_data.get(label, default) + if child_value != parent_value: + match = False + break + if match: + found_match = True + break + if not found_match: + return False + + # Step 3: Edge label filter - Ensure that the edge attribute 'order' matches if provided + if edge_attribute: + for child_edge in child_graph.edges(data=True): + child_node1, child_node2, child_data = child_edge + if child_node1 in parent_graph and child_node2 in parent_graph: + # Ensure the edge exists in the parent graph + if not parent_graph.has_edge(child_node1, child_node2): + return False + # Check if the 'order' attribute matches + parent_edge_data = parent_graph[child_node1][child_node2] + child_order = child_data.get(edge_attribute) + parent_order = parent_edge_data.get(edge_attribute) + + # Handle comparison of tuple values for 'order' attribute + if isinstance(child_order, tuple) and isinstance( + parent_order, tuple + ): + if child_order != parent_order: + return False + elif child_order != parent_order: + return False + else: + return False + + # Setting up attribute comparison functions + node_comparator = node_comparator if node_comparator else eq + edge_comparator = edge_comparator if edge_comparator else eq + + # Creating match conditions for nodes and edges based on custom or default comparators + node_match = generic_node_match( + node_label_names, node_label_default, [node_comparator] * len(node_label_names) + ) + edge_match = ( + generic_edge_match(edge_attribute, None, edge_comparator) + if edge_attribute + else None + ) - return True + # Graph matching setup + matcher = GraphMatcher( + parent_graph, child_graph, node_match=node_match, edge_match=edge_match + ) + + # Executing the matching based on specified type + if check_type == "induced": + return matcher.subgraph_is_isomorphic() + else: + return matcher.subgraph_is_monomorphic() -def enumerate_tautomers(reaction_smiles: str) -> Optional[List[str]]: - """Enumerates possible tautomers for reactants while canonicalizing the - products in a reaction SMILES string. This function first splits the - reaction SMILES string into reactants and products. It then generates all - possible tautomers for the reactants and canonicalizes the product - molecule. The function returns a list of reaction SMILES strings for each - tautomer of the reactants combined with the canonical product. +def maximum_connected_common_subgraph( + graph_1: nx.Graph, + graph_2: nx.Graph, + node_label_names: List[str] = ["element", "charge"], + node_label_default: List[Any] = ["*", 0], + edge_attribute: str = "standard_order", +) -> nx.Graph: + """Computes the largest connected common subgraph (MCS) between two graphs + using subgraph isomorphism based on customizable node and edge attributes. + + The function iterates over subsets of nodes from the smaller graph—starting from the largest + possible subgraph size down to 1—and returns the first (largest) candidate that is connected + and is isomorphic to a subgraph of the larger graph. Parameters: - - reaction_smiles (str): A SMILES string of the reaction formatted as - 'reactants>>products'. + - graph_1 (nx.Graph): The first graph for comparison. + - graph_2 (nx.Graph): The second graph for comparison. + - node_label_names (List[str]): List of node attribute names used for matching. + - node_label_default (List[Any]): Default values for missing node attributes. + - edge_attribute (str): The edge attribute to compare. Returns: - - List[str] | None: A list of SMILES strings for the reaction, with each string - representing a different - - tautomer of the reactants combined with the canonicalized products. Returns None if - an error occurs or if invalid SMILES strings are provided. - - Raises: - - ValueError: If the provided SMILES strings cannot be converted to molecule objects, - indicating invalid input. + - nx.Graph: A graph representing the largest connected common subgraph found; if none exists, + returns an empty graph. """ - try: - # Split the input reaction SMILES string into reactants and products - reactants_smiles, products_smiles = reaction_smiles.split(">>") - - # Convert SMILES strings to molecule objects - reactants_mol = Chem.MolFromSmiles(reactants_smiles) - products_mol = Chem.MolFromSmiles(products_smiles) - - if reactants_mol is None or products_mol is None: - raise ValueError( - "Invalid SMILES string provided for reactants or products." + node_match = generic_node_match( + node_label_names, node_label_default, [eq] * len(node_label_names) + ) + edge_match = generic_edge_match(edge_attribute, 1, eq) + + # Determine which graph is smaller for efficiency. + if graph_1.number_of_nodes() <= graph_2.number_of_nodes(): + smaller_graph, larger_graph = graph_1, graph_2 + else: + smaller_graph, larger_graph = graph_2, graph_1 + + num_nodes_smaller = smaller_graph.number_of_nodes() + # Iterate over possible subgraph sizes from the largest to 1. + for subgraph_size in range(num_nodes_smaller, 0, -1): + for nodes_subset in itertools.combinations( + smaller_graph.nodes(), subgraph_size + ): + candidate_subgraph = smaller_graph.subgraph(nodes_subset) + # If the subgraph has more than one node, check it is connected. + if candidate_subgraph.number_of_nodes() > 1 and not nx.is_connected( + candidate_subgraph + ): + continue + + # Check for subgraph isomorphism in the larger graph. + matcher = GraphMatcher( + larger_graph, + candidate_subgraph, + node_match=node_match, + edge_match=edge_match, ) + if matcher.subgraph_is_isomorphic(): + return candidate_subgraph.copy() - # Initialize tautomer enumerator - - enumerator = rdMolStandardize.TautomerEnumerator() + return nx.Graph() - # Enumerate tautomers for the reactants and canonicalize the products - try: - reactants_can = enumerator.Enumerate(reactants_mol) - except Exception as e: - print(f"An error occurred: {e}") - reactants_can = [reactants_mol] - products_can = products_mol - - # Convert molecule objects back to SMILES strings - reactants_can_smiles = [Chem.MolToSmiles(i) for i in reactants_can] - products_can_smiles = Chem.MolToSmiles(products_can) - - # Combine each reactant tautomer with the canonical product in SMILES format - rsmi_list = [i + ">>" + products_can_smiles for i in reactants_can_smiles] - if len(rsmi_list) == 0: - return [reaction_smiles] - else: - # rsmi_list.remove(reaction_smiles) - rsmi_list.insert(0, reaction_smiles) - return rsmi_list - - except Exception as e: - print(f"An error occurred: {e}") - return [reaction_smiles] +def heuristics_MCCS( + graphs: List[nx.Graph], + node_label_names: List[str] = ["element", "charge"], + node_label_default: List[Any] = ["*", 0], + edge_attribute: str = "standard_order", +) -> nx.Graph: + """Computes the Maximum Connected Common Subgraph (MCCS) over a list of + graphs using a heuristic approach. -def mapping_success_rate(list_mapping_data): - """Calculate the success rate of entries containing atom mappings in a list - of data strings. + This function computes the MCCS between the first two graphs using the + `maximum_connected_common_subgraph` function based on customizable node and edge attributes. + For more than two graphs, it iteratively updates the common subgraph by calculating the MCCS + between the current common subgraph and each subsequent graph. An early exit occurs if the + intermediate common subgraph becomes empty. Parameters: - - list_mapping_in_data (list of str): List containing strings to be searched for atom - mappings. + - graphs (List[nx.Graph]): A list of networkx graphs for which the common subgraph is to be computed. + - node_label_names (List[str]): List of node attribute names used for matching. + - node_label_default (List[Any]): Default values for missing node attributes. + - edge_attribute (str): The edge attribute to compare. Returns: - - float: The success rate of finding atom mappings in the list as a percentage. + - nx.Graph: The maximum connected common subgraph common to all provided graphs. If no common + subgraph exists, an empty graph is returned. Raises: - - ValueError: If the input list is empty. + - ValueError: If the input list of graphs is empty. """ - atom_map_pattern = re.compile(r":\d+") - if not list_mapping_data: - raise ValueError("The input list is empty, cannot calculate success rate.") + if not graphs: + raise ValueError("Input list of graphs is empty.") + + if len(graphs) == 1: + return graphs[0].copy() + + # Handle the two-graph case explicitly. + if len(graphs) == 2: + return maximum_connected_common_subgraph( + graphs[0], + graphs[1], + node_label_names=node_label_names, + node_label_default=node_label_default, + edge_attribute=edge_attribute, + ) - success = sum( - 1 for entry in list_mapping_data if re.search(atom_map_pattern, entry) + # Iteratively compute the MCCS for more than two graphs. + current_mcs = maximum_connected_common_subgraph( + graphs[0], + graphs[1], + node_label_names=node_label_names, + node_label_default=node_label_default, + edge_attribute=edge_attribute, ) - rate = 100 * (success / len(list_mapping_data)) - return round(rate, 2) \ No newline at end of file + for graph in graphs[2:]: + if current_mcs.number_of_nodes() == 0: + break # Early exit if no common subgraph remains. + current_mcs = maximum_connected_common_subgraph( + current_mcs, + graph, + node_label_names=node_label_names, + node_label_default=node_label_default, + edge_attribute=edge_attribute, + ) + + return current_mcs diff --git a/synkit/IO/graph_to_mol.py b/synkit/IO/graph_to_mol.py index 8049f0d..ddcf048 100644 --- a/synkit/IO/graph_to_mol.py +++ b/synkit/IO/graph_to_mol.py @@ -74,7 +74,7 @@ def graph_to_mol( node_to_idx: Dict[int, int] = {} for node, data in graph.nodes(data=True): - element = data.get(self.node_attributes["element"], "C") + element = data.get(self.node_attributes["element"], "*") charge = data.get(self.node_attributes["charge"], 0) atom_map = ( data.get(self.node_attributes["atom_map"], 0) @@ -93,7 +93,7 @@ def graph_to_mol( atom.SetAtomMapNum(atom_map) if hcount is not None: atom.SetNoImplicit(True) - atom.SetNumExplicitHs(hcount) + atom.SetNumExplicitHs(int(hcount)) idx = mol.AddAtom(atom) node_to_idx[node] = idx From 691f9c08b6a3bfc706f099e774491f31f39feab5 Mon Sep 17 00:00:00 2001 From: TieuLongPhan Date: Tue, 22 Jul 2025 11:27:50 +0200 Subject: [PATCH 3/6] pass test staging, prepare release --- .github/workflows/test-and-lint.yml | 2 +- doc/conf.py | 2 +- synkit/Graph/ITS/its_builder.py | 2 +- synkit/Graph/ITS/its_construction.py | 2 +- synkit/Graph/ITS/its_decompose.py | 2 +- synkit/Graph/ITS/its_expand.py | 2 +- synkit/Synthesis/Reactor/syn_reactor.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-and-lint.yml b/.github/workflows/test-and-lint.yml index 8222b16..f62fa21 100644 --- a/.github/workflows/test-and-lint.yml +++ b/.github/workflows/test-and-lint.yml @@ -2,7 +2,7 @@ name: Test & Lint on: push: - branches: [ "main", "dev", "maintain", "refractor" ] + branches: [ "main", "dev", "staging", "refractor" ] pull_request: branches: [ "main" ] diff --git a/doc/conf.py b/doc/conf.py index 509d687..16d2f98 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -45,4 +45,4 @@ # -- Options for HTML output ------------------------------------------------- html_theme = "sphinx_rtd_theme" -html_static_path = ["_static"] \ No newline at end of file +html_static_path = ["_static"] diff --git a/synkit/Graph/ITS/its_builder.py b/synkit/Graph/ITS/its_builder.py index 2ab5591..e6999b2 100644 --- a/synkit/Graph/ITS/its_builder.py +++ b/synkit/Graph/ITS/its_builder.py @@ -111,4 +111,4 @@ def ITSGraph(G: nx.Graph, RC: nx.Graph) -> nx.Graph: # 7) Renumber atom_map to node indices ITSBuilder.update_atom_map(its) - return its \ No newline at end of file + return its diff --git a/synkit/Graph/ITS/its_construction.py b/synkit/Graph/ITS/its_construction.py index bfa51f7..c5bd596 100644 --- a/synkit/Graph/ITS/its_construction.py +++ b/synkit/Graph/ITS/its_construction.py @@ -322,4 +322,4 @@ def typesGH(self) -> Dict[str, Dict[str, Tuple[Any, Any]]]: } node_defaults = {k: (tp, 0) for k, tp in sel_nodes.items()} edge_defaults = {k: (tp, 0) for k, tp in sel_edges.items()} - return {"node": node_defaults, "edge": edge_defaults} \ No newline at end of file + return {"node": node_defaults, "edge": edge_defaults} diff --git a/synkit/Graph/ITS/its_decompose.py b/synkit/Graph/ITS/its_decompose.py index c608acb..df9a1fd 100644 --- a/synkit/Graph/ITS/its_decompose.py +++ b/synkit/Graph/ITS/its_decompose.py @@ -513,4 +513,4 @@ def mapping_success_rate(list_mapping_data): ) rate = 100 * (success / len(list_mapping_data)) - return round(rate, 2) \ No newline at end of file + return round(rate, 2) diff --git a/synkit/Graph/ITS/its_expand.py b/synkit/Graph/ITS/its_expand.py index bb3cf2f..0a23438 100644 --- a/synkit/Graph/ITS/its_expand.py +++ b/synkit/Graph/ITS/its_expand.py @@ -83,4 +83,4 @@ def expand_aam_with_its( # Convert graphs back to RSMI and standardize atom mappings expanded_rsmi = graph_to_rsmi(new_react, new_prod, its_graph, True, False) - return std.fit(expanded_rsmi, remove_aam=False) \ No newline at end of file + return std.fit(expanded_rsmi, remove_aam=False) diff --git a/synkit/Synthesis/Reactor/syn_reactor.py b/synkit/Synthesis/Reactor/syn_reactor.py index fb00feb..7ba82f8 100644 --- a/synkit/Synthesis/Reactor/syn_reactor.py +++ b/synkit/Synthesis/Reactor/syn_reactor.py @@ -572,4 +572,4 @@ def _to_smarts(its: nx.Graph) -> str: p_smi = graph_to_smi(right) if r_smi is None or p_smi is None: return None - return f"{r_smi}>>{p_smi}" \ No newline at end of file + return f"{r_smi}>>{p_smi}" From 72461ceee717749b9747bc8d8d33750031982f5a Mon Sep 17 00:00:00 2001 From: TieuLongPhan Date: Fri, 25 Jul 2025 15:20:30 +0200 Subject: [PATCH 4/6] quick fix hydrogen --- synkit/Graph/Hyrogen/hcomplete.py | 1 + 1 file changed, 1 insertion(+) diff --git a/synkit/Graph/Hyrogen/hcomplete.py b/synkit/Graph/Hyrogen/hcomplete.py index 32e6c1b..c85216d 100644 --- a/synkit/Graph/Hyrogen/hcomplete.py +++ b/synkit/Graph/Hyrogen/hcomplete.py @@ -350,4 +350,5 @@ def add_hydrogen_nodes_multiple_utils( # conjugated=False, # in_ring=False, ) + new_graph.nodes[node_id]["hcount"] -= 1 return new_graph From 54ddf1cb41f519e95aa68db6dee8e71bd0b495bf Mon Sep 17 00:00:00 2001 From: TieuLongPhan Date: Fri, 25 Jul 2025 15:21:17 +0200 Subject: [PATCH 5/6] prepare release --- pyproject.toml | 2 +- recipe/meta.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 38f4bfe..657e6cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "synkit" -version = "0.0.12" +version = "0.0.13" license = { text = "MIT" } license-files = ["LICENSE"] authors = [ diff --git a/recipe/meta.yaml b/recipe/meta.yaml index 5b6d4fb..4705fba 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -1,6 +1,6 @@ package: name: synkit - version: 0.0.12 + version: 0.0.13 source: path: .. From 5f841089d86c62a85f83886d0c820359b8d889be Mon Sep 17 00:00:00 2001 From: Tieu Long Phan <125431507+TieuLongPhan@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:44:35 +0200 Subject: [PATCH 6/6] Prepare release (#30) * add partial its construction and imbaengine * add wildcard and placeholder * add testcase * add mtg * fix bug mtg * prepare release --- .github/workflows/test-and-lint.yml | 2 +- .gitignore | 2 + README.md | 11 + Test/Graph/MTG/test_mtg.py | 16 +- Test/Graph/Wildcard/__init__.py | 0 Test/Graph/Wildcard/test_radwc.py | 55 + Test/Graph/Wildcard/test_wildcard.py | 81 ++ Test/IO/combinatorial/__init__.py | 0 Test/IO/combinatorial/test_smarts_expander.py | 38 + .../combinatorial/test_smarts_generalizer.py | 62 + Test/IO/combinatorial/test_smarts_to_graph.py | 85 ++ Test/Synthesis/Reactor/test_core_engine.py | 112 -- Test/Synthesis/Reactor/test_imba_engine.py | 94 ++ doc/figures/mtg_mechanism.png | Bin 0 -> 1247567 bytes doc/graph.rst | 94 +- lint.sh | 4 +- pyproject.toml | 2 +- recipe/meta.yaml | 2 +- synkit/Chem/Reaction/radical_wildcard.py | 57 + synkit/Graph/Feature/Fingerprint/__init__.py | 0 .../Graph/Feature/Fingerprint/wl_rxn_fps.py | 231 ++++ synkit/Graph/ITS/its_decompose.py | 503 ++++---- synkit/Graph/MTG/mcs_matcher.py | 92 +- synkit/Graph/MTG/mtg.py | 1054 ++++++++++++++--- synkit/Graph/MTG/mtg_explore.py | 74 ++ synkit/Graph/MTG/utils.py | 425 +++++++ synkit/Graph/Matcher/subgraph_matcher.py | 327 +++++ synkit/Graph/Wildcard/radwc.py | 117 ++ synkit/Graph/Wildcard/wildcard.py | 230 ++++ synkit/IO/combinatorial/__init__.py | 8 + synkit/IO/combinatorial/gml_to_graph.py | 254 ++++ synkit/IO/combinatorial/graph_to_gml.py | 291 +++++ synkit/IO/combinatorial/graph_to_smarts.py | 189 +++ synkit/IO/combinatorial/smarts_expander.py | 152 +++ synkit/IO/combinatorial/smarts_generalizer.py | 134 +++ synkit/IO/combinatorial/smarts_to_graph.py | 183 +++ synkit/Rule/Apply/rule_matcher.py | 20 +- synkit/Synthesis/Reactor/imba_engine.py | 165 +++ synkit/Synthesis/Reactor/syn_reactor.py | 4 +- synkit/Vis/graph_visualizer.py | 10 +- synkit/Vis/rule_vis.py | 4 +- synkit/examples.py | 50 + 42 files changed, 4633 insertions(+), 601 deletions(-) create mode 100644 Test/Graph/Wildcard/__init__.py create mode 100644 Test/Graph/Wildcard/test_radwc.py create mode 100644 Test/Graph/Wildcard/test_wildcard.py create mode 100644 Test/IO/combinatorial/__init__.py create mode 100644 Test/IO/combinatorial/test_smarts_expander.py create mode 100644 Test/IO/combinatorial/test_smarts_generalizer.py create mode 100644 Test/IO/combinatorial/test_smarts_to_graph.py delete mode 100644 Test/Synthesis/Reactor/test_core_engine.py create mode 100644 Test/Synthesis/Reactor/test_imba_engine.py create mode 100644 doc/figures/mtg_mechanism.png create mode 100644 synkit/Graph/Feature/Fingerprint/__init__.py create mode 100644 synkit/Graph/Feature/Fingerprint/wl_rxn_fps.py create mode 100644 synkit/Graph/MTG/mtg_explore.py create mode 100644 synkit/Graph/MTG/utils.py create mode 100644 synkit/Graph/Wildcard/radwc.py create mode 100644 synkit/Graph/Wildcard/wildcard.py create mode 100644 synkit/IO/combinatorial/__init__.py create mode 100644 synkit/IO/combinatorial/gml_to_graph.py create mode 100644 synkit/IO/combinatorial/graph_to_gml.py create mode 100644 synkit/IO/combinatorial/graph_to_smarts.py create mode 100644 synkit/IO/combinatorial/smarts_expander.py create mode 100644 synkit/IO/combinatorial/smarts_generalizer.py create mode 100644 synkit/IO/combinatorial/smarts_to_graph.py create mode 100644 synkit/Synthesis/Reactor/imba_engine.py create mode 100644 synkit/examples.py diff --git a/.github/workflows/test-and-lint.yml b/.github/workflows/test-and-lint.yml index f62fa21..58a646c 100644 --- a/.github/workflows/test-and-lint.yml +++ b/.github/workflows/test-and-lint.yml @@ -2,7 +2,7 @@ name: Test & Lint on: push: - branches: [ "main", "dev", "staging", "refractor" ] + branches: [ "main", "partialits", "staging", "refractor" ] pull_request: branches: [ "main" ] diff --git a/.gitignore b/.gitignore index 07ad7b7..40588fd 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ run.sh docs/* run_rdcanon.py Data/Fragment/* +test_partial.py +Data/Benchmark/synthesis/* diff --git a/README.md b/README.md index 47d57fe..c64736d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,15 @@ # SynKit +[![PyPI version](https://img.shields.io/pypi/v/synkit.svg)](https://pypi.org/project/synkit/) +[![Conda version](https://img.shields.io/conda/vn/tieulongphan/synkit.svg)](https://anaconda.org/tieulongphan/synkit) +[![Docker Pulls](https://img.shields.io/docker/pulls/tieulongphan/synkit.svg)](https://hub.docker.com/r/tieulongphan/synkit) +[![Docker Image Version](https://img.shields.io/docker/v/tieulongphan/synkit/latest?label=container)](https://hub.docker.com/r/tieulongphan/synkit) +[![License](https://img.shields.io/github/license/tieulongphan/synkit.svg)](https://github.com/tieulongphan/synkit/blob/main/LICENSE) +[![Release](https://img.shields.io/github/v/release/tieulongphan/synkit.svg)](https://github.com/tieulongphan/synkit/releases) +[![Last Commit](https://img.shields.io/github/last-commit/tieulongphan/synkit.svg)](https://github.com/tieulongphan/synkit/commits) +[![Zenodo](https://zenodo.org/badge/DOI/10.5281/zenodo.15269901.svg)](https://doi.org/10.5281/zenodo.15269901) +[![CI](https://github.com/tieulongphan/synkit/actions/workflows/test-and-lint.yml/badge.svg?branch=main)](https://github.com/tieulongphan/synkit/actions/workflows/test-and-lint.yml) +[![Dependency PRs](https://img.shields.io/github/issues-pr-raw/tieulongphan/synkit?label=dependency%20PRs)](https://github.com/tieulongphan/synkit/pulls?q=is%3Apr+label%3Adependencies) +[![Stars](https://img.shields.io/github/stars/tieulongphan/synkit.svg?style=social&label=Star)](https://github.com/tieulongphan/synkit/stargazers) **Toolkit for Synthesis Planning** diff --git a/Test/Graph/MTG/test_mtg.py b/Test/Graph/MTG/test_mtg.py index 2750293..0f8827d 100644 --- a/Test/Graph/MTG/test_mtg.py +++ b/Test/Graph/MTG/test_mtg.py @@ -20,19 +20,17 @@ def setUp(self) -> None: self.test_graph_2 = [get_rc(rsmi_to_its(var)) for var in test_2] def test_MTG_1(self): - grp = GroupComp(self.test_graph_1[0], self.test_graph_1[1]) - candidates = grp.get_mapping() - print(candidates) - mtg = MTG(self.test_graph_1[0], self.test_graph_1[1], candidates[0]) - self.assertEqual(len(mtg.get_nodes()), 6) - self.assertEqual(len(mtg.get_edges()), 7) + mtg = MTG(self.test_graph_1[0:2], mcs_mol=True) + self.assertEqual(mtg._graph.number_of_nodes(), 6) + self.assertEqual(mtg._graph.number_of_edges(), 7) def test_MTG_2(self): grp = GroupComp(self.test_graph_2[0], self.test_graph_2[1]) candidates = grp.get_mapping() - mtg = MTG(self.test_graph_2[0], self.test_graph_2[1], candidates[0]) - self.assertEqual(len(mtg.get_nodes()), 5) - self.assertEqual(len(mtg.get_edges()), 4) + # print(candidates) + mtg = MTG(self.test_graph_2[0:], candidates) + self.assertEqual(mtg._graph.number_of_nodes(), 5) + self.assertEqual(mtg._graph.number_of_edges(), 4) if __name__ == "__main__": diff --git a/Test/Graph/Wildcard/__init__.py b/Test/Graph/Wildcard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Test/Graph/Wildcard/test_radwc.py b/Test/Graph/Wildcard/test_radwc.py new file mode 100644 index 0000000..9df29bb --- /dev/null +++ b/Test/Graph/Wildcard/test_radwc.py @@ -0,0 +1,55 @@ +import unittest +from synkit.Graph.Wildcard.radwc import RadWC + + +class TestRadWC(unittest.TestCase): + def test_no_product_radicals(self): + """If product has no radicals, output should be unchanged.""" + rxn = "[CH3:1][OH:2]>>[CH3:1][OH:2]" + self.assertEqual(RadWC.transform(rxn), rxn) + + def test_single_radical_in_product(self): + """A single radical in product gets a wildcard.""" + rxn = "[CH3:1][OH:2]>>[CH2:1].[OH:2]" + out = RadWC.transform(rxn) + # Check [*:3] is attached to [CH2:1] + self.assertIn("[CH2:1]([*:3])", out) # Atom-maps: 1,2 exist, so 3 is next + + def test_multiple_radicals_in_product(self): + """Multiple radicals in product get multiple wildcards.""" + rxn = "[CH3:1][OH:2]>>[CH2:1].[O:2]" + out = RadWC.transform(rxn) + # [CH2:1] has *:3 and *:4, [O:2] has *:5 + self.assertIn("[CH2:1]([*:3])", out) + self.assertIn("[O:2]([*:5])", out) + + def test_radical_and_nonradical_mixture(self): + """Mixed radical/non-radical product fragments, only radicals get wildcard.""" + rxn = "[CH3:1][OH:2]>>[CH2:1].[OH:2]" + out = RadWC.transform(rxn) + # [CH2:1] gets *:3, [OH:2] is unchanged + self.assertIn("[CH2:1]([*:3])", out) + self.assertIn("[OH:2]", out) + + def test_user_start_map(self): + """User-supplied map index is used for wildcards.""" + rxn = "[CH3:7][OH:8]>>[CH2:7].[OH:8]" + out = RadWC.transform(rxn, start_map=50) + self.assertIn("[CH2:7]([*:50])", out) + + def test_empty_reaction(self): + """Empty input should raise ValueError.""" + with self.assertRaises(ValueError): + RadWC.transform("") + + def test_three_component(self): + """Agent block is preserved.""" + rxn = "[CH3:1][OH:2]>[Na+]>[CH2:1].[OH:2]" + out = RadWC.transform(rxn) + self.assertTrue(out.startswith("[CH3:1][OH:2]>[Na+]>")) + self.assertIn("[CH2:1]([*:3])", out) + self.assertIn("[OH:2]", out) + + +if __name__ == "__main__": + unittest.main() diff --git a/Test/Graph/Wildcard/test_wildcard.py b/Test/Graph/Wildcard/test_wildcard.py new file mode 100644 index 0000000..d403f8d --- /dev/null +++ b/Test/Graph/Wildcard/test_wildcard.py @@ -0,0 +1,81 @@ +import unittest +from synkit.IO import rsmi_to_graph +from synkit.Graph.Wildcard.wildcard import WildCard + + +class TestWildCard(unittest.TestCase): + def setUp(self): + # The main, complex test case with atom mapping + self.rsmi_main = ( + "[cH:1]1[cH:14][c:10]2[c:23]([cH:11][n:25]1)[cH:17][cH:12][cH:4][c:31]2[NH2:28]." + "[cH:2]1[c:20]([C:22]([OH:7])=[O:21])[s:18][c:24]([S:6][c:29]2[c:15]([Cl:26])[cH:8]" + "[n:19][cH:9][c:16]2[Cl:27])[c:30]1[N+:5]([O-:3])=[O:13]>>" + "[cH:1]1[cH:14][c:10]2[c:23]([cH:11][n:25]1)[cH:17][cH:12][cH:4][c:31]2[NH:28]" + "[C:22]([c:20]1[cH:2][c:30]([N+:5]([O-:3])=[O:13])[c:24]([S:6][c:29]2[c:15]([Cl:26])" + "[cH:8][n:19][cH:9][c:16]2[Cl:27])[s:18]1)=[O:21]" + ) + # No atoms lost: R == P, should not add wildcards + self.rsmi_no_loss = "CCO>>CCO" + # All atoms lost: RSMI that loses everything (nonsense, but good test) + self.rsmi_all_lost = "CCO>>" + # Empty + self.rsmi_empty = "" + # Wildcard already present + self.rsmi_existing_wildcard = "[CH3:1][CH2:2][OH:3]>>[CH2:1][CH2:2].[*:4][OH:3]" + # No atom map (should raise error) + self.rsmi_no_atom_map = "C(C)Cl>>CC" + + def test_main_case_wildcard_added(self): + """Complex case: output product contains wildcard and roundtrip is valid.""" + out_rsmi = WildCard.rsmi_with_wildcards(self.rsmi_main) + _, product = out_rsmi.split(">>") + self.assertIsInstance(out_rsmi, str) + self.assertIn( + "*", product, "Wildcard '*' should be present in the product side." + ) + # Roundtrip: should parse back without error + r, p = rsmi_to_graph(out_rsmi) + self.assertTrue(r.number_of_nodes() > 0) + self.assertTrue(p.number_of_nodes() > 0) + + def test_no_atoms_lost(self): + """No atoms lost: should raise ValueError if input is not atom-mapped.""" + with self.assertRaises(ValueError): + WildCard.rsmi_with_wildcards(self.rsmi_no_loss) + + def test_all_atoms_lost(self): + """All atoms lost: should raise ValueError if input is not atom-mapped.""" + with self.assertRaises(ValueError): + WildCard.rsmi_with_wildcards(self.rsmi_all_lost) + + def test_empty_input(self): + """Empty input: should raise ValueError.""" + with self.assertRaises(ValueError): + WildCard.rsmi_with_wildcards(self.rsmi_empty) + + def test_wildcard_not_duplicated(self): + """Existing wildcards: should not create duplicate wildcards for same lost bond.""" + out_rsmi = WildCard.rsmi_with_wildcards(self.rsmi_existing_wildcard) + _, product = out_rsmi.split(">>") + # At least one '*' in the product SMILES string + self.assertIn("*", product) + + def test_no_false_positive_wildcards(self): + """Wildcards are only added if there are truly lost subgraphs; non-atom-mapped input raises.""" + rsmi = "C>>C" + with self.assertRaises(ValueError): + WildCard.rsmi_with_wildcards(rsmi) + + def test_output_is_str_and_split(self): + """Should raise ValueError if input is not atom-mapped.""" + with self.assertRaises(ValueError): + WildCard.rsmi_with_wildcards(self.rsmi_no_loss) + + def test_missing_atom_map_raises(self): + """Should raise ValueError if atom_map attributes are missing.""" + with self.assertRaises(ValueError): + WildCard.rsmi_with_wildcards(self.rsmi_no_atom_map) + + +if __name__ == "__main__": + unittest.main() diff --git a/Test/IO/combinatorial/__init__.py b/Test/IO/combinatorial/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Test/IO/combinatorial/test_smarts_expander.py b/Test/IO/combinatorial/test_smarts_expander.py new file mode 100644 index 0000000..61d6990 --- /dev/null +++ b/Test/IO/combinatorial/test_smarts_expander.py @@ -0,0 +1,38 @@ +import unittest +from synkit.IO.combinatorial.smarts_expander import SMARTSExpander + + +class TestSMARTSExpander(unittest.TestCase): + + def test_no_placeholders(self): + s = "CCO" + self.assertEqual(list(SMARTSExpander.expand_iter(s)), ["CCO"]) + self.assertEqual(SMARTSExpander.expand(s), ["CCO"]) + + def test_simple_expansion(self): + s = "[C,N:1][O,P:2]" + result = SMARTSExpander.expand(s) + self.assertEqual( + set(result), {"[C:1][O:2]", "[C:1][P:2]", "[N:1][O:2]", "[N:1][P:2]"} + ) + + def test_disjoint_constraint(self): + s = "[C,N:1][O:1]" + with self.assertRaises(ValueError): + list(SMARTSExpander.expand_iter(s)) + + def test_realistic_reaction(self): + rxn = ( + "[H+:6].[C:7](-[O:8](-[H:12]))(-[C,N,O,P,S:9])(-[C,N,O,P,S:10])(-[H:11])." + "[C:2](-[S:4](-[C,N,O,P,S:5]))(-[C,N,O,P,S:1])(=[O:3])>>" + "[S:4](-[H:6])(-[C,N,O,P,S:5]).[H+:12]." + "[C:7](-[O:8](-[C:2](-[C,N,O,P,S:1])(=[O:3])))(-[C,N,O,P,S:9])(-[C,N,O,P,S:10])(-[H:11])" + ) + ex_list = list(SMARTSExpander.expand_iter(rxn)) + self.assertEqual(len(ex_list), 625) + # Optional: Just check format or count, not endswith + self.assertTrue(ex_list[0].startswith("[H+:6]")) + + +if __name__ == "__main__": + unittest.main() diff --git a/Test/IO/combinatorial/test_smarts_generalizer.py b/Test/IO/combinatorial/test_smarts_generalizer.py new file mode 100644 index 0000000..0f2e907 --- /dev/null +++ b/Test/IO/combinatorial/test_smarts_generalizer.py @@ -0,0 +1,62 @@ +import unittest +from rdkit import Chem +from synkit.IO.combinatorial.smarts_generalizer import SMARTSGeneralizer + + +class TestSMARTSGeneralizer(unittest.TestCase): + + def setUp(self): + self.gen = SMARTSGeneralizer(sanity_check=True) + + def test_basic_generalization(self): + inputs = [ + "[C:1]-[N:2]>>[N:1]-[C:2]", + "[N:1]-[N:2]>>[N:1]-[N:2]", + "[O:1]-[N:2]>>[N:1]-[N:2]", + ] + output = self.gen.generalize(inputs) + # Instead of strict string match, check correct mapped elements + self.assertIn("[C,N,O:1]", output) + self.assertIn("[N:2]", output) + self.assertIn(">>", output) + + def test_single_smarts(self): + inputs = ["[C:1]-[N:2]>>[N:1]-[C:2]"] + output = self.gen.generalize(inputs) + # Should match input exactly + self.assertEqual(output, "[C:1]-[N:2]>>[N:1]-[C:2]") + + def test_different_topology_raises(self): + inputs = ["[C:1]-[N:2]>>[N:1]-[C:2]", "[N:1]-[N:2]-[C:3]>>[N:1]-[N:2]-[C:3]"] + with self.assertRaises(ValueError): + self.gen.generalize(inputs) + + def test_empty_input_raises(self): + with self.assertRaises(ValueError): + self.gen.generalize([]) + + def test_molecule_smarts(self): + gen = SMARTSGeneralizer(sanity_check=True) + inputs = ["[C:1]-[N:2]", "[N:1]-[N:2]", "[O:1]-[N:2]"] + out = gen.generalize(inputs) + self.assertEqual(out, "[C,N,O:1]-[N:2]") + + mol = Chem.MolFromSmarts(out) + self.assertIsNotNone(mol) + + def test_invalid_sanity_check(self): + gen = SMARTSGeneralizer(sanity_check=True) + # Using an obviously broken SMARTS (bad bracket placement) + _ = [ + "[C:1]-[N:2]>>[N:1]-[X:2]" + ] # 'X' is a valid SMARTS wildcard! Use a real error + with self.assertRaises(ValueError): + gen.generalize(["[C:1][C:2]>>[N:1][C:2]["]) # broken SMARTS + + def test_repr(self): + gen = SMARTSGeneralizer() + self.assertIn("sanity_check", repr(gen)) + + +if __name__ == "__main__": + unittest.main() diff --git a/Test/IO/combinatorial/test_smarts_to_graph.py b/Test/IO/combinatorial/test_smarts_to_graph.py new file mode 100644 index 0000000..999066a --- /dev/null +++ b/Test/IO/combinatorial/test_smarts_to_graph.py @@ -0,0 +1,85 @@ +import unittest +import networkx as nx + +from synkit.IO.combinatorial.smarts_to_graph import SMARTSToGraph + + +class TestSMARTSToGraph(unittest.TestCase): + + def setUp(self): + self.stg = SMARTSToGraph() + + def test_smarts_to_graph_simple(self): + g = self.stg.smarts_to_graph("[C:1]-[O:2]") + self.assertIsInstance(g, nx.Graph) + self.assertEqual(set(g.nodes), {1, 2}) + self.assertEqual(g.nodes[1]["element"], "C") + self.assertEqual(g.nodes[2]["element"], "O") + self.assertIsNone(g.nodes[1]["constraint"]) + + def test_smarts_to_graph_constraint(self): + g = self.stg.smarts_to_graph("[C,N,O:1]-[N:2]") + # Node 1 should be placeholder + self.assertEqual(g.nodes[1]["element"], "*") + self.assertIsInstance(g.nodes[1]["constraint"], list) + self.assertIn("C", g.nodes[1]["constraint"]) + self.assertEqual(g.nodes[2]["element"], "N") + self.assertIsNone(g.nodes[2]["constraint"]) + + def test_smarts_to_graph_hcount(self): + g = self.stg.smarts_to_graph("[CH3:1]-[O:2]") + # For SMARTS as written, RDKit returns 0 hydrogens for both + self.assertEqual(g.nodes[1]["hcount"], 0) + self.assertEqual(g.nodes[2]["hcount"], 0) + + def test_invalid_smarts(self): + with self.assertRaises(ValueError): + self.stg.smarts_to_graph("[C:1]-[N") + + def test_missing_atom_map(self): + with self.assertRaises(ValueError): + self.stg.smarts_to_graph("[C]-[O:2]") + + def test_rxn_smarts_to_graphs(self): + rxn = ( + "[H+:6].[C:7](-[O:8](-[H:12]))(-[C,N,O,P,S:9])(-[C,N,O,P,S:10])(-[H:11])." + "[C:2](-[S:4](-[C,N,O,P,S:5]))(-[C,N,O,P,S:1])(=[O:3])>>" + "[S:4](-[H:6])(-[C,N,O,P,S:5]).[H+:12]." + "[C:7](-[O:8](-[C:2](-[C,N,O,P,S:1])(=[O:3])))(-[C,N,O,P,S:9])(-[C,N,O,P,S:10])(-[H:11])" + ) + g_react, _ = self.stg.rxn_smarts_to_graphs(rxn) + + # These are the atom_map indices that should have constraint (from SMARTS [C,N,O,P,S:idx]) + expected_constraint_nodes = {1, 5, 9, 10} + for idx in expected_constraint_nodes: + self.assertIn(idx, g_react.nodes) + self.assertIsNotNone( + g_react.nodes[idx]["constraint"], + f"Node {idx} should have a constraint list but does not", + ) + self.assertEqual( + set(g_react.nodes[idx]["constraint"]), + {"C", "N", "O", "P", "S"}, + f"Node {idx} has incorrect constraint list", + ) + # All other nodes should NOT have a constraint + for idx in set(g_react.nodes) - expected_constraint_nodes: + self.assertIsNone( + g_react.nodes[idx]["constraint"], + f"Node {idx} should NOT have a constraint list", + ) + + def test_rxn_separator(self): + with self.assertRaises(ValueError): + self.stg.rxn_smarts_to_graphs("[C:1]-[O:2]") # no '>>' + + def test_repr_and_describe(self): + r = repr(self.stg) + self.assertIn("placeholders", r) + desc = self.stg.describe() + self.assertIn("smarts_to_graph", desc) + self.assertIn("rxn_smarts_to_graphs", desc) + + +if __name__ == "__main__": + unittest.main() diff --git a/Test/Synthesis/Reactor/test_core_engine.py b/Test/Synthesis/Reactor/test_core_engine.py deleted file mode 100644 index d30cb9d..0000000 --- a/Test/Synthesis/Reactor/test_core_engine.py +++ /dev/null @@ -1,112 +0,0 @@ -# import os -# import unittest -# import tempfile -# from synkit.Synthesis.Reactor.core_engine import CoreEngine - - -# class TestCoreEngine(unittest.TestCase): -# def setUp(self): -# # Create a temporary directory -# self.temp_dir = tempfile.TemporaryDirectory() - -# # Path for the rule file -# self.rule_file_path = os.path.join(self.temp_dir.name, "test_rule.gml") - -# # Define rule content -# self.rule_content = """ -# rule [ -# ruleID "1" -# left [ -# edge [ source 1 target 2 label "=" ] -# edge [ source 3 target 4 label "-" ] -# ] -# context [ -# node [ id 1 label "C" ] -# node [ id 2 label "C" ] -# node [ id 3 label "H" ] -# node [ id 4 label "H" ] -# ] -# right [ -# edge [ source 1 target 2 label "-" ] -# edge [ source 1 target 3 label "-" ] -# edge [ source 2 target 4 label "-" ] -# ] -# ] -# """ - -# # Write rule content to the temporary file -# with open(self.rule_file_path, "w") as rule_file: -# rule_file.write(self.rule_content) - -# # Initialize SMILES strings for testing -# self.initial_smiles_fw = ["CC=CC", "[HH]"] -# self.initial_smiles_bw = ["CCCC"] - -# def tearDown(self): -# # Clean up temporary directory -# self.temp_dir.cleanup() - -# def test_perform_reaction_forward(self): -# # Test the perform_reaction method with forward reaction type -# result = CoreEngine._inference( -# rule_file_path=self.rule_file_path, -# initial_smiles=self.initial_smiles_fw, -# prediction_type="forward", -# print_results=False, -# verbosity=0, -# ) -# print(result) -# # Check if result is a list of strings and has content -# self.assertIsInstance( -# result, list, "Expected a list of reaction SMILES strings." -# ) -# self.assertTrue( -# len(result) > 0, "Result should contain reaction SMILES strings." -# ) - -# self.assertEqual(result[0], "CC=CC.[HH]>>CCCC") - -# # Check if the result SMILES format matches expected output format -# for reaction_smiles in result: -# self.assertIn(">>", reaction_smiles, "Reaction SMILES format is incorrect.") -# parts = reaction_smiles.split(">>") -# self.assertEqual( -# parts[0], -# ".".join(self.initial_smiles_fw), -# "Base SMILES are not correctly formatted.", -# ) -# self.assertTrue(len(parts[1]) > 0, "Product SMILES should be non-empty.") - -# def test_perform_reaction_backward(self): -# # Test the perform_reaction method with backward reaction type -# result = CoreEngine._inference( -# rule_file_path=self.rule_file_path, -# initial_smiles=self.initial_smiles_bw, -# prediction_type="backward", -# print_results=False, -# verbosity=0, -# ) -# # Check if result is a list of strings and has content -# self.assertIsInstance( -# result, list, "Expected a list of reaction SMILES strings." -# ) -# self.assertTrue( -# len(result) > 0, "Result should contain reaction SMILES strings." -# ) -# self.assertEqual(result[0], "C=CCC.[H][H]>>CCCC") -# self.assertEqual(result[1], "[H][H].C(C)=CC>>CCCC") - -# # Check if the result SMILES format matches expected output format -# for reaction_smiles in result: -# self.assertIn(">>", reaction_smiles, "Reaction SMILES format is incorrect.") -# parts = reaction_smiles.split(">>") -# self.assertTrue(len(parts[0]) > 0, "Product SMILES should be non-empty.") -# self.assertEqual( -# parts[1], -# ".".join(self.initial_smiles_bw), -# "Base SMILES are not correctly formatted.", -# ) - - -# if __name__ == "__main__": -# unittest.main() diff --git a/Test/Synthesis/Reactor/test_imba_engine.py b/Test/Synthesis/Reactor/test_imba_engine.py new file mode 100644 index 0000000..65dd508 --- /dev/null +++ b/Test/Synthesis/Reactor/test_imba_engine.py @@ -0,0 +1,94 @@ +import unittest +from synkit.IO import rsmi_to_its +from synkit.Graph.Wildcard.wildcard import WildCard +from synkit.Chem.Reaction.standardize import Standardize +from synkit.Synthesis.Reactor.imba_engine import ImbaEngine + + +class TestImbaEngine(unittest.TestCase): + def setUp(self): + # A complex standardized RSMI from your example + self.smart = ( + "[cH:1]1[cH:14][c:10]2[c:23]([cH:11][n:25]1)[cH:17][cH:12][cH:4][c:31]2[NH2:28]." + "[cH:2]1[c:20]([C:22]([OH:7])=[O:21])[s:18][c:24]([S:6][c:29]2[c:15]" + "([Cl:26])[cH:8][n:19][cH:9][c:16]2[Cl:27])[c:30]1[N+:5]([O-:3])=[O:13]>>" + "[cH:1]1[cH:14][c:10]2[c:23]([cH:11][n:25]1)[cH:17][cH:12][cH:4]" + "[c:31]2[NH:28][C:22]([c:20]1[cH:2][c:30]([N+:5]([O-:3])=[O:13])[c:24]([S:6]" + "[c:29]2[c:15]([Cl:26])[cH:8][n:19][cH:9][c:16]2[Cl:27])[s:18]1)=[O:21]" + ) + # Standardize removes AAM + self.rsmi = Standardize().fit(self.smart, remove_aam=True) + + def test_pipeline_forward(self): + """Test forward ImbaEngine pipeline end-to-end.""" + # Apply wildcard insertion + wild_smart = WildCard().rsmi_with_wildcards(self.smart) + # Build ITS graphs + temp = rsmi_to_its(wild_smart, core=True) + # substrate split from standardized RSMI + substrate_r, _ = self.rsmi.split(">>") + # Run engine forward with template without cleaning fragments + engine = ImbaEngine(substrate_r, temp, add_wildcard=True, clean_fragments=False) + out = engine.smarts_list + self.assertEqual(len(out), 1) + out_rsmi = Standardize().fit(out[0], remove_aam=True) + + self.assertIn("*", out_rsmi) + self.assertNotEqual(out_rsmi, self.rsmi) + + # Run engine forward with template with cleaning fragments + engine = ImbaEngine(substrate_r, temp, add_wildcard=True, clean_fragments=True) + out = engine.smarts_list + outs = [Standardize().fit(o, remove_aam=True) for o in out] + self.assertIn(self.rsmi, outs) + + def test_pipeline_backward(self): + """Test backward ImbaEngine pipeline end-to-end with and without fragment cleaning.""" + # Prepare wildcard and ITS template + wild_rsmi = WildCard().rsmi_with_wildcards(self.smart) + its = rsmi_to_its(wild_rsmi, core=True) + + _, substrate_p = self.rsmi.split(">>") + + # 1. Without fragment cleaning + engine = ImbaEngine( + substrate_p, + its, + add_wildcard=True, + clean_fragments=False, + invert=True, + partial=True, + ) + out = engine.smarts_list + self.assertEqual(len(out), 2) + out_rsmi = Standardize().fit(out[0], remove_aam=True) + self.assertIn("*", out_rsmi) + self.assertNotEqual(out_rsmi, self.rsmi) + + # 2. With fragment cleaning + engine_clean = ImbaEngine( + substrate_p, + its, + add_wildcard=True, + clean_fragments=True, + invert=True, + partial=True, + ) + + out_clean = engine_clean.smarts_list + self.assertEqual(len(out_clean), 2) + outs = [Standardize().fit(o, remove_aam=True) for o in out_clean] + self.assertIn(self.rsmi, outs) + + def test_invalid_rsmi(self): + """Invalid RSMI pipeline should raise an exception at Standardize or ITS step.""" + # Standardize should fail for invalid RSMI + with self.assertRaises(Exception): + # Attempt full pipeline + rsmi = Standardize().fit("not_a_rsmi", remove_aam=True) + wild = WildCard().rsmi_with_wildcards(rsmi) + _ = rsmi_to_its(wild, core=True) + + +if __name__ == "__main__": + unittest.main() diff --git a/doc/figures/mtg_mechanism.png b/doc/figures/mtg_mechanism.png new file mode 100644 index 0000000000000000000000000000000000000000..578932f667a132bd3a4400e39a06eede46a87930 GIT binary patch literal 1247567 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�RZnQ0kIey<<6IPVgM3 z5}xsS$hD>vW`Icv3YfJg%);BVT1R^lRf2wS~PdQy2B%l>#pXu8S1R zJ{@}?b=M-U^HU6XS6)^XzPq1EWuC0>$G!AG9=J!H1oGZDoB7qtQQ}lbYenHC(q9LQ z{7E3cWy(eW>zM)cT$;xzV!?G!o8Gsa%rQ-PwQx3lH}IX+ zKycap+Uu0Stx5kUlV9Cf;4Ewz9BnE+`z6&LsjB2b*W}Sq%Rzt`aHR*F#wsys?{LeN z_B-Uf`3x!a3#dRKsf|`)cO2>rsm5G7G2;^7lyez%CCKTR;hN)_hpX}gEI4?y z6Wrd0H_zJ21<7GxQue1?EZScj(!ZcgB7_{LSdkgv8$wd6GbxarM$068T)*QhBRUpg z?Fq;)d?1|W`RneZX0Z0$AM3PoIUPB1x$@8&^Vxw+6@NkPwBs0GaBG{Owr{-phE``| z(ZFp3Np=^o_k!TYz`V6zDHfO9RxyMN`X;{KYx1s=%BesYY@y;!3R3Mv|NZp(wpORM zM(ij=XLV$D6cdk+!^pr)!`8CMY`LtCI(~GjL?e5s!DY0}9%K2qXSGUh#HVXNzxar3 zWx`M$`sJk5xo>Ml{?KN$PR7$`=s96iAj&No;HzZJ52Pk$m76@4H{@raoXn%=YQG{- z=j#SmD5d-xsLKUyv`1=w6P#-QzVl&8v2bkbn@y#L7I#FOj83}i=4F4WJObAK{*O?S zI-jiV;H?cdUDlG9e+oLIo{h4Y~DCN@w6rFnJ}1PfN6I$*>OMZWP*K0j(&T3LNfO%Duo59{=utq@ZO z^r$mqy-(mOm$XH8wWJ>c+*V2T9pR5!V61X_uB5Nlc0E1#zPe zO9*xLAMYqTT~v{#t_Z&YX=84M&PjXL6Qh#erGl*;m~OrNx~hCvg8tWkYxClFs+`;p zPX*d6jB%R}e=b?_sN?f{p2J<0QSomaR+ZVW*)GRid(rqtkiHo;D3(oCAtPENKrj0# z9dN%bG3ot%4XXe0W1X8GSikC1wprmNAV0v>I+07C|7?L;Gy`@TJ~#na>l&$OS=X5! z*k9-dD!86Xo!icZ3b*IM{8oo7LlCxnopxD}Slr`b7DFi8C|K#D2+$I)Il=*xorwo z&P1z0g;x1FC#fC7La))2>?r}RZZDE95T?&@zFI7Vn=bs&jWhrr1q<1Yr4OrRXL@#% zTB~;buME$Avn}sM&Xga~8FHoWmal@lA|Y&XuA5qO&Qa%UYpGN;=zr}nmyv%^V*PfQ z?81ZG53f8X?jWh|{HpmsoH12r&j3~xEMk(cPthd-3Mto%t5@Kr&>bp;RHs>=SW=^|gr8!E;@vWkHcBfln5k z`%(Ym;w`cTi2bs!F+J|Hq(3}g&LDQc784eYnqMgn&DBcfz__nIdVjWM8SqN@Mxesx zo}N=ms%Y~iv;o0pOT2wbhGT4fcD6K0#eNY+r;lk!FBGxbdR@k$q$rGDue#(82GM>f zZ&H){;C~O3q49VTVme+gSLtsBZ8@~0gjY4@m#xSm`uU5+d&%wQkaN-dpIImi> zQPH2StlBD%6;F^Fn0CG-^tnf^)X+QiPa=K`OY+A|bs_Elk#xI+Rag z!#wk9YqwDrU~oEfPqrYz$$m|N)@I1IT6});jZf0XT)v{*1%&752Ha>Y0snyC1PeW> zFna?i3Tk{0E<5p0&nR%P>V=q73792hZa#P-+ieGkn?}54+;3Wuem1`kgwv##zx-`Acp!&Q61s{ZpV;l<|1>jNT$T_!dB++HJC zCK2^Qs{5q6`fiH!=)1jQ7n+}2Gr3OZPWHjLjNu22L}yVPQK9aD<)H(FROPl1TL|J+ zAU~reFAP@eXN&tKQb?%*(Ufz@)uzm%bc%ym9Vm*C#%Gyg^Yitg@#JuD4m;Qpa8Cwp zorsH1k+)GfOCVsm6s|gH2@)|pN?mDQsciWACm|RP-loF67yxZDP%4%u*bm_!XRkN% zH#YTer{zqiFaKiTKg+P@jB&faWaom9`fI-pCG&f^B^HA>oM_WlEaXW&a&)|?F79%G z_K$IU?#aMNCBm-pF>mJC|0hqKaD{7!+IppZ0}+%-7@-?Ah_b9bk7*+Bo%$2FNqjTN z*#U6t+E*QYcM1N(39xFbXMy#}E-hoP_XPUEoEdfG01+mh!WP_%sHVQGo3ed{Z<%lzcN$D5=1^UH zYFzFN2!z=7wd7&M*2Fv zsK3weFBj-2ZX{}ZvSD#P413N^N;r?>z@p0vg!<4WpUBKhRKgkKUn}mWcZ4a_d@Wkt z8CF&*JY2gm=0_cCzxhL&<3+XnIZB!t{4K2r(OTV}o!#PG1re$%zB&0Y^w2LqpaPeM zR~n#>6+gBMV^MN^I-yR+V5~HNQAxekIfC#zeSQ&tJgR(LUMC7S&OC4mELl*fnwr^B zVo<68j-quF9Tgph;;IA8h#vUTp z`NhUGbHaW`^o3%k6n_|`CuQrrasT-A;&e#lJAWcnvsM0d7d6(wijWLThN#XE1)DXN z3B*2%K*>=txZc0)&e1!}k&@oHd?li5Q3!sdVk#$HZRedH(k!uuS2>bbJ&L34e(8!x zD*gM>R|ND|KI(X^+hA<;O7X|vzbmPXzKfXb$J?J$%BQ~Z+anJ*EcRP`_viM9f%Jc< zY&cTATQJ`}d7FOf38f@)ifa093*H{arQs7w?V+jW_x6&uySL!C zcUt}`)aqZc$^ZLoEI3~}W6aGY+EQL4s(zbC{lg2?t@WHu!pEv@izX{jyX5Rhdq zxP-|q22p8Yw^?oeCj34{zK?&PnzTzmO-e3E$v7!Jh~D9feHb>eP2|du&lTuPRuqNB zSq8ro1_nt)_bEuJvOW)1)fayJY2n8KZuKZ2GvT#nA%ZYENTeY{)O`5xp^G+9HT@EG zZUO{p(nEn1dfHM9HN>{zRScDlBokoB!mURUUHvC{xplFA`z&gBC|2%Pe0maLS3dtt z43os4VnnDUn{x9fcf_esL5g~>YBmBWQyl_EY-2hvwPM4iAv0ur$oHkmK-`i>$EH03 zMn8y7dRaoq2*`|8FoVR9F2mFA4I?X1MbA(F+Wc2gdj|tS49=7qATd6=9RXuF%x7~; z1C6i}Y{!cc6n0V=&L=fX`wNW1io$Q`MG^qo0b-R5XGF`hIEwTqMn`-nEE{yq(9wc> zFf!mwBrZOfT+Hj@PY3Ned%f^3d)WE@Je3Sb{Mh3&{YmcT2MEs(X8{?W5|OyL6+gtk zuK`MD>+gh=xhj6ovn1@D42x)>RO7WzuOsyw{9sMYwaE=w_`0Sy$dd23e!NZ?vlnGzgqEC7mVg3a&>;6@Z5$ zk4ZJvP~C{NQW8B^)Cdv7LNf?&_Ko);DtZY!1Veg&Y);m-1Ub__LhCP@1pY`fZRI_Dovk9I@12Esi~C0%CnYQg&4 z3b~WSIPwq|g9!^jA3y;{l$YQ)pWtE?FYc6cm+{YY;B^(+=1Hg|4s*DlwV$221}6}4 zH%QrbnL%G7mX_KNAD%+wJGs}i933ud$*1Q2SRWbk0@UA6b&VVupe0cr@pw=d-3S3? z9H1A4lSDdGcxM-ut_h*9%GHcF$EV3h(!RRMxyqhLkM{1=dt`Q16dvoU-FJNr%9SK2 zreT5_F`Jizarjz3kxD}@b+_&GcL?uuX^rbmhr_eHRCuIzW4C7&v3Qo854xXVTB(WJ z)G_~*$wuE;MFQVezMk$N4NpX=M@^Tmd5IM#anE`PXX%~(Is_+DjjX2UbzePm?29HX zR#g$CK7}W`4#a%Y+1I!H^cD!>g-s$9qr6D{oet~)SKkNpYGB9&&5T6r^o-wU=j?s^ zSU?rR0^(QN3DJC{jq|7PQD)p`cRST|At!+@Fjz6~mJX;IYSWHuY$Wz=km!W{CG;KW z;)t#Zb#0n%WD2qUC>ZhFZYP03=SR<6n=lK03*#ku04zDO&gx z-HcS~a0R1~?x|$y1x6y#_SK`y5W}?5pLvkpmPyPZOHG6tY{9N?{{EHNKu}Hult^*M zOn>k@7b1D-F-Q88^=g;Rk>U*~uE1{bo9_l!JBx#!SCH*@hEy$pi*=-dh)Bp(xRJ)t z8QP2cIjS%-K>vPr{#tmkCI;f{qy;I(j=P9~DFVL&Khds@1YH+FnVaI9Gw(I=%J^_@ z9YNhh>PXSpv1tf(ujb*yH@>wXa!d6ib@7`|J6iA-J*(sDcVEcP&eeT_9I46)rtL?)WxI+V5v&OUB86<#n@nO z5^uJ~R1mHou5cj=$>%uS`+ylJv1WP(g-lPrTeaGZHa}5q?LMnR_Y``W57v*u=Gl*X z1uAiI;fL5uE`e1dgta0aTTqMNT=04id*&E$sX3UCSb47=7mPC6OaY-n3I4nN6lr~d zuY^M**UG3ktgPVtG z)jxg>!$BPuk#m0$0gPNILmV&>k@c|_4^R&fgXy``z1gRAboePiPLLQ4PDO7?P1jW2 zLZOldCz3pdKNpbXk$z?P9D$f5R**I=gpoe9l>loZMRo-g8Yd`?tQ*RsM+rb%Xy~Q> zWmbq73;q$!qU~EJdaIA6r>7H*v+k7P2GI9rmEnho76aZ@NunhJ7XZpOWL7PDBP5`! z=8KJ?z<@39QJWRkvxCQ8U0mu$A(^N6T`F;%hN9i3*H=9vM!l5Zv`ewqdw;~?O$q;5 z&n5)Jg#WXi_3-3QddPbCy6;NrnKvt7IeF;0_S2v;OJ8mNf{*dAjVMjO4M@-Xxma(- z79k%>-ykp$mZ%sn5z3-Jx_3NNONDZHbUr1E#{)U$yUxl}5hgjz;p5(#(Ct1pP0$3t*wngs}G*ENs7Qv-)~*}BZ0wG|BTSOzdvF0er3=5C&V|7iUCbwdT!I5 zM8raQtUfFq&`qKN^ssRdfu&%}2ipl9ivADiAH8@22NA#^*Ok7bm|QE+@Ugma3Se)u>!S+!h>0ASKMf>~+N2pmsQuo4XsK&vB9C0`*@ zt)w&G#hcSIfutXOfF3MdMxFu})!b_pQ(_A05BCmJ%7!?(L&2lT2@Ml!iMsRT)wZ{dHCOi~IVX5kKQA zOMc~p&Y)p0si~4iH`6!#t>J6cUaOa!_g8(+r&kMh$#08RZpq>~we>=qzP-b@KPtB? z28$~dqETmSQ8JzVf&NTvq{3(*DSc3qFQ=`g#SMEph04s#j9Z^J6nY2^l!B#>%(QUDEV1i{xt`b-C|N!M-(ySsaR@a&vl`9Jiw zA5g-P)xF~Xz>O`u7d8k=nUr1Rh{;f1?j$0K(>L|%F?*jMYSzy0RMvkvJw9`=0W`5K4gRRa>D*JXra6#A#90YvB8kPC( z`E?~FCDJK)nAn{m6_KO~l?IgXN(L~#IEjpJYG3z*1AwUQTXDFaB{fmfDORBH8 z7H<@=GIx=21M4HZ^>Xe4D+}44E58xmA^dRh*3#g2GQlO9JimoshZAmQekk(hI)sxd zj^5#-$WVvG72$+mt938pw14-K!kwm--&4CUk)k9lR$S(Q-I4I4nv)UT%Q|K;OJT=1 z@bBDhp#R<2^Zz4!_EZAd1E>?{Fm&qqs!0X-b-{@fzKlZk64{#Mj_r@AOa-SDPC^2q z5nnwUH_*KE7=s8lu!t)f7I|yX`CU5O2>yYHD7C$7PR2SQXxHO{HmxQ50%FE^&-=sZ z3k|{98i;*!i%u|s`+NVb5&CGk6ik!D?M0Stu9JT085(^X$peEdrURhN(T|E) zHiK&~o}fR<0uekxZ88NZX&IXz@~PT=@!I6vikRS9VU(7r1#A-KKT~uzC+_437ANUaS0hgXqt!lE@jcM`GxW1 zF7Nh^9T90Nr90Y<<%FHBeU+nW7n9Py46gvJ;i+dOwaF=_bN}hC;zqyQrX6m{rW|A4 zrRQWmZH!q0X`|);85qsl$gGMf_ElNrs~E2KUA&{Ll(a9Vx0Y)<#9(-})jOj*7UHq0 zWrboX=?QyZ901=~j{@%TNXedEH!A-R(P3v)$ON>9xd0R@@uSeK=;F;64`Y2mzWs2h zpg$=t4$afkE~89+DArD3m;Kl;$H_Zr8U-V6F7nHW}-o0 zFH)*v;}ZEO2^m1JCrjaIv4=KpjzSIm{ViFv&uup((z$R;A(PJ>expZS=gNA0NdS zTr+mm=VX_qQNqj1nWUXjtS#|AW$zw-$alv=I1Eg-|NkN4E9vMW&yeUZkPA@ta#$r7 zY$JPuPd~|48A+8GnXvxwE!ZkkjTD+oOSZ48UTA@6~O3|2&MMZ$o!-Q zk?<0vA-N^Tq5gr0A#o0#6y&l~E@ZMFYSklUwNKjZs_Af)2xy^f5NC2W1*L%@+aZtC zm8Je4V{ZaZW&Zw;tEQSM)y%Xftw%&zT8Kg$bqJ9y*$QP%SrXZ%bt;sDRMyIpU6w?) zsVSi-SxUuB$QH^P!tZrIDAVVAUH|{NKG&yZ!a2|Le%|llLDhG}ob_p{|1EB-0 z%_Sy?pV8t!s~5nozG}F0c_(N;o@=)i05)~F;z`8rHt5V&Lo30wCb*-h2Gu3oOWJi+ia9)e;2AvQ$7K?*pc1RV%Bw2cHTz z#{1dK#6j0EwOKe|`6`}@_ACAmb%dhor2kGz-&PT?SNLt(H9eu-fBtJqHyTX)f%w># zwfiHsdUMUbTWMaB-LK&AR@If%>+f7od?ywv6NdeZ^GE!PyRBZN3`ZY8oa$RQrEg+^ zWw5IGc7?}xTTW90YQRo;uc7&O*b`g)VeCsP1KS=Ox2x&|lSXm^3bMqqCiDSrLxUvl zMK`h<{*GZZ-4yg*oMv0Q$3jUp(sIS_P39O~RtQ&LHD^5(XyTrA9pt7rhthlxF2x{p zd*)~iH9UU_f+M}^gcYb4B-7l4x#h&=Fa)|AT(~(U^UuNA??oRskUxp^#*lR8;Cr;b zO(*R+WhCTZEyyW69OuU(CYLe)&FLyFD+BqdY4+Q4up)o-=+oPQ^(Y1yKoddiU}SM$ zd}EV8)|aeLbZD#Ug9X9Iq9MugA?9{P65Ngw2pAerZguP}K5vi7ecq|qrgCzL>F0qY zw{!dLBYjuTxrj4n%E=|?)9jp4^|9Es+~2RByK-cPN;BQLe=I`#QG6kN`8P-xb) z=Tnq7sb#ehEc&?cs3%QYk=gkj; zThT3-gPhryRb>KH5%*bm?+2HT{=19azTROW+5k4{h@+G-#9^X*Ac;uFc@-yqX`Iju zi#VE9z23T1XZXj*SUQihW5lWL=6r>!v-mYP{6}sL0B(->JMxb^yXH{@XM$0?&N3oDa z%cft_(S)!vEZlLpGatTpK@%UoV1#C(Q;u%YuTy$DY!$sTP5koRcbf1mw-=mW1_^Dr zqHxrhYKwnt^Jc0jL;bw~h=apk`*5m0Q)sk*8T~p|e!oFR5ayA3{k5a63y{&wHJ`vE zjD2n1Az@N53uP8bt)YsZG7%-`nf=XOexIv^(0O1hA(PDro&nsGubStjcw0<_+DbHE zjWGKud2J!!7!Zsi;51Vo?JIebk%Z@%cNx{~Hn%2*8a4GosB^=lVdJD!9{|QB&{VrT zI{J2n)d9-n052C!F8^{J^i}8|cfo!WM8k~KGqaJ!vk%%BpD^QU#i5N53!z}Ur9_^o z6u*+Lc+@lCtdcw5R1azvNp_*1E=sSfn(K_YT}Tee4^XVgLzy_&z*!ZOc16J6#sAA! zUU~O*l=yCeS)ek!Lde>Le*&|Sf!dp@iZR#c`5dWYA2<4oAbsJ2QK3rc?4aWZObXHm zoy`7y@Lf7)>Sj|XE1`OJv;v;!?@%x#k#IIDsLy39FJzH3KCO!v9e zDgAe=P7^aQTMIJ%|Lf+YnZ6+1A9gE-z`_QBqkdN2(U?+;T^;vtlR}7so$3RtT>D#C z!FNmGVDI|)dhL0$*I#V>=Ea2VDqo{9r&1ZdzS1;n1BzJEmYT5%jv!_$2`#G;D06`# zlKM%KO`ae$4BZeA%X(w>{; z`wTPM!A1EZhwtYv$e!}DOyIs1QQ9$uJ@=oKzbwP1X}3uxdb5|G;Qx3d)U|MlS$OZp z*HSf+gskD>;&QRh531?@SX5D@iD#|W&)jhSMqI6pB*|9^oG|h5M!3>CWGb;iu~^NS zv3oVP4m4&2PQ!QiF!52(5D0Nn+lrru+hmygrwF4x^UmfwzNd^Hs!NS_y|zlW6Yc}$ z*hQBMH3t0l;Bi=NGV+Gqm4-?DdAWk*Cw~`xah2&0ccW;5vNOLcrIxI&cv9aidEX?! zdkWSLEj8Qec&t-3%XPZistrhq)c(tui!C*?>LwM~&-d5fat8gpjVic9acE2H{F4W| zOD6I7evdD40dXW!ll;eWrU=|@@@TEmdYRJOSlgwY2Yl3TAKSkAwMJ~l$8x!@yi%c# zcOI5!g%9Gum~gQ`i*Dtz!B*V>0)=CG0h49S*(d^{ad^iN|t@1&9VlzJ?78w?rGXQb3qU@LHX1 zNP-F@lNZKe`(*Z@m};+ThTf+Xb^g`BjoDFZC)^Nz-3eE`g8AXLkz4{I&zQYKvIxkC zsoA2${1pK^b)24cld~P;B(Thl_a3TYNQzE~&9zEO7D6^%A4t2@Z4@X5_r|I-Lf8Id zMxGt_>C5N($$C37Mvu)x8~@>oQVB`O(G%nYt66gIXo-)C!ai%g#wVlKrbSN1psM?t z$KOjVnZHu^z?r;KZgqyO_Z*1?DsrNvoPK|n1fsM z{B!T^@e}IL{jc7w3A#%)TiL|~-J`g){qXb!*cP2Q{f^gPc$s|!CUExkO^!(jM@x0` zKQ#EYm-sbQN4iDp9c&2hM;=u~%!7g&sE^f9IQmp)iOB=8`g#02h<2{ay1542J&*o} z41a)w5$cBa2F||>8X6cD=ej3+;5hO}*P(G@_ieof;P#UZtZs=(f_k#Jd;5TeqYnb- z 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

2xgi~%yZEav+-c)-=Z!*)q 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�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[CH:4]([H:8])=[CH:5][O:6]([H:7])', # tautomerization - '[CH3:1][C:2]=[O:3].[CH:4]([H:8])=[CH:5][O:6]([H:7])>>' - '[CH3:1][C:2]([O:3][H:7])[CH:4]([H:8])[CH:5]=[O:6]' # nucleophilic addition - ] - rc_graphs = [rsmi_to_its(r, core=True) for r in rsmi_list] - # 2) Find MCS mapping between the two ITS graphs - mcs = MCSMatcher(node_label_names=['element', 'charge'], edge_attribute='order') - mcs.find_rc_mapping(rc_graphs[0], rc_graphs[1], mcs=True) - mapping = mcs.get_mappings()[0] + data = load_example("aldol") - # 3) Build the Mechanistic Transition Graph (MTG) - mtg = MTG(rc_graphs[0], rc_graphs[1], mapping) - mtg_graph = mtg.get_graph() + mech_neutral = data[0]['mechanisms'][1]['steps'] + smart_neutral = [i['smart_string'] for i in mech_neutral] - # 4) Also build the composite ITS by directly gluing the two RC graphs - its_composite = clean_graph_keep_largest_component(mtg_graph) + mech_acid = data[0]['mechanisms'][2]['steps'] + smart_acid = [i['smart_string'] for i in mech_acid] + # neutral + mtg = MTG(smart_neutral, mcs_mol=True) + mtg_its_neutral = mtg.get_compose_its() + mtg_rc_neutral = get_rc(mtg_its_neutral, keep_mtg=True) + rc_neutral = get_rc(mtg_its_neutral, keep_mtg=False) - # 5) Visualize all four graphs: two RCs, the composite ITS, and the MTG - fig, axes = plt.subplots(2, 2, figsize=(14, 6)) + # acid + mtg = MTG(smart_acid, mcs_mol=True) + mtg_its_acid = mtg.get_compose_its() + mtg_rc_acid = get_rc(mtg_its_acid, keep_mtg=True) + rc_acid = get_rc(mtg_its_acid, keep_mtg=False) + + # Visualize + fig, ax = plt.subplots(2, 2, figsize=(16, 8)) vis = GraphVisualizer() - vis.plot_its(rc_graphs[0], axes[0, 0], use_edge_color=True, title='A. Tautomerization RC') - vis.plot_its(rc_graphs[1], axes[0, 1], use_edge_color=True, title='B. Nucleophilic Addition RC') - vis.plot_its(its_composite, axes[1, 0], use_edge_color=True, title='C. Composite ITS') - vis.plot_its(mtg_graph, axes[1, 1], use_edge_color=True, title='D. Mechanistic TG', og=True) + vis.plot_its( + mtg_rc_neutral, + ax=ax[0, 0], + use_edge_color=True, + og=True, + title='A. MTG for aldol addition (neutral)', + title_font_size=20, + title_font_weight='medium', + title_font_style='normal' + ) + vis.plot_its( + rc_neutral, + ax=ax[0, 1], + use_edge_color=True, + og=True, + title='B. Reaction center (neutral)', + title_font_size=20, + title_font_weight='medium', + title_font_style='normal' + ) + vis.plot_its( + mtg_rc_acid, + ax=ax[1, 0], + use_edge_color=True, + og=True, + title='C. MTG for aldol addition (acid)', + title_font_size=20, + title_font_weight='medium', + title_font_style='normal' + ) + vis.plot_its( + rc_acid, + ax=ax[1, 1], + use_edge_color=True, + og=True, + title='D. Reaction center (acid)', + title_font_size=20, + title_font_weight='medium', + title_font_style='normal' + ) plt.tight_layout() plt.show() @@ -243,16 +278,13 @@ This example builds two reaction-center ITS graphs, computes their MCS mapping, .. container:: figure - .. image:: ./figures/mtg.png + .. image:: ./figures/mtg_mechanism.png :alt: Composite ITS and MTG visualization :align: center :width: 1000px *Figure:* - (A) Reaction‐center graph for the tautomerization step - (B) Reaction‐center graph for the nucleophilic addition step - (C) Composite ITS graph "gluing" both transformations - (D) Mechanistic Transition Graph (MTG) showing step-wise mechanism + Composition of the mechanistic sequences for aldol addition under neutral and acidic conditions, showing the composite MTG (left column) and the reaction center (right column). Context graph ------------- diff --git a/lint.sh b/lint.sh index 085068d..ba8fdd7 100755 --- a/lint.sh +++ b/lint.sh @@ -18,7 +18,9 @@ benchmark_reactor.py:W292,C901,\ syn_reactor.py:C901,\ sing.py:C901,\ turbo_iso.py:C901,\ -rule_vis.py:C901" \ +rule_vis.py:C901, +gml_to_graph.py:C901, +wildcard.py:C901" \ --exclude=venv,\ core_engine.py,\ rule_apply.py,\ diff --git a/pyproject.toml b/pyproject.toml index 657e6cf..9f3382e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "synkit" -version = "0.0.13" +version = "0.0.14" license = { text = "MIT" } license-files = ["LICENSE"] authors = [ diff --git a/recipe/meta.yaml b/recipe/meta.yaml index 4705fba..d4c6256 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -1,6 +1,6 @@ package: name: synkit - version: 0.0.13 + version: 0.0.14 source: path: .. diff --git a/synkit/Chem/Reaction/radical_wildcard.py b/synkit/Chem/Reaction/radical_wildcard.py index 166d981..7fe1f1e 100644 --- a/synkit/Chem/Reaction/radical_wildcard.py +++ b/synkit/Chem/Reaction/radical_wildcard.py @@ -4,6 +4,63 @@ from typing import Tuple, List, Optional, Dict +def clean_wc( + rsmi: str, invert: bool = False, max_frag: bool = False, wild_card: bool = True +) -> str: + """ + Clean wildcard-containing fragments from one side of a reaction SMILES, + optionally selecting the largest remaining fragment. + + :param rsmi: Reaction SMILES string in the form 'R>>P'. + :type rsmi: str + :param invert: If True, process the reactant side; otherwise the product side. + :type invert: bool + :param max_frag: If True, force fragment selection (implies wild_card=True). + :type max_frag: bool + :param wild_card: If True, remove fragments containing '*' before selection. + :type wild_card: bool + :returns: The processed reaction SMILES. + :rtype: str + :raises ValueError: If input does not split into reactant and product. + + Example + ------- + >>> clean_wc('A.B>>C.*', invert=False, wild_card=True) + 'A.B>>C' + >>> clean_wc('A.B>>C.D', invert=False, max_frag=True) + 'A.B>>C' + """ + # Ensure max_frag implies wild_card + if max_frag: + wild_card = True + + # Split into reactant and product + parts = rsmi.split(">>") + if len(parts) != 2: + raise ValueError("Reaction SMILES must contain exactly one '>>'.") + react, prod = parts + + # Select side to process + side = react if invert else prod + + processed = side + if wild_card: + frags = side.split(".") + # Filter out fragments containing wildcards + filtered = [frag for frag in frags if "*" not in frag] + if len(filtered) > 1: + # select the longest fragment + processed = max(filtered, key=len) + elif len(filtered) == 1: + processed = filtered[0] + # if no filtered fragments or single fragment, keep original side + + # Reconstruct and return + if invert: + return f"{processed}>>{prod}" + return f"{react}>>{processed}" + + class RadicalWildcardAdder: """A utility for adding wildcard dummy atoms ([*]) to radical centers in reaction SMILES, with unique incremental atom-map indices and correct diff --git a/synkit/Graph/Feature/Fingerprint/__init__.py b/synkit/Graph/Feature/Fingerprint/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/synkit/Graph/Feature/Fingerprint/wl_rxn_fps.py b/synkit/Graph/Feature/Fingerprint/wl_rxn_fps.py new file mode 100644 index 0000000..a1cb4e2 --- /dev/null +++ b/synkit/Graph/Feature/Fingerprint/wl_rxn_fps.py @@ -0,0 +1,231 @@ +from __future__ import annotations +import networkx as nx +from collections import Counter +from hashlib import blake2b +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Any, Union +import numpy as np +from synkit.IO import rsmi_to_graph + + +@dataclass +class WLRnxFps: + """ + Weisfeiler-Lehman Reaction Fingerprint Sketch (NetworkX). + + :param radius: number of WL refinement iterations + :type radius: int + :param size: bit budget for the parity sketch + :type size: int + :param to_array: whether to return fingerprint as NumPy array + :type to_array: bool + + Usage example: + >>> rsmi = ('COc1cc(NC(N)=S)ccc1-n1cnc(C)c1.O=C1C(Br)CCCC1c1ccc(Cl)cc1Cl' + >>> +'>>Br.COc1cc(Nc2nc3c(s2)CCCC3c2ccc(Cl)cc2Cl)ccc1-n1cnc(C)c1.O') + >>> react, prod = rsmi_to_graph(rsmi, drop_non_aam=False, use_index_as_atom_map=False) + >>> fps = WLRnxFps(radius=2, size=1024, to_array=False).fit(react, prod) + >>> bits = fps.fingerprint + """ + + radius: int = 2 + size: int = 1024 + to_array: bool = False + + _tokens_R: Optional[Counter] = field(init=False, default=None) + _tokens_P: Optional[Counter] = field(init=False, default=None) + _delta: Optional[Counter] = field(init=False, default=None) + _support: Optional[List[int]] = field(init=False, default=None) + _fingerprint: Optional[Union[List[int], np.ndarray]] = field( + init=False, default=None + ) + + def fit(self, react: nx.Graph, prod: nx.Graph) -> WLRnxFps: + """ + Compute WL tokens for reactant and product graphs, then build parity sketch on Δ-support. + + :param react: reactant graph with node attrs 'element','aromatic','hcount','charge' + :type react: nx.Graph + :param prod: product graph with same node/edge attrs + :type prod: nx.Graph + :returns: self + :rtype: WLRnxFps + :raises ValueError: if size is not positive + """ + if self.size <= 0: + raise ValueError("size must be a positive integer") + + def wl_tokens(G: nx.Graph) -> Counter: + labels: Dict[int, int] = {} + for n, attrs in G.nodes(data=True): + atom_tuple = ( + attrs.get("element"), + bool(attrs.get("aromatic", False)), + int(attrs.get("charge", 0)), + int(attrs.get("hcount", 0)), + G.degree(n), + ) + labels[n] = _h64(("init", atom_tuple)) + cnt = Counter(labels.values()) + for k in range(1, self.radius + 1): + new_labels: Dict[int, int] = {} + for n in G.nodes(): + neigh = [] + for m in G.neighbors(n): + bond_order = float(G.edges[n, m].get("order", 1.0)) + neigh.append((_h64(("bond", bond_order)), labels[m])) + neigh.sort() + new_labels[n] = _h64(("wl", k, labels[n], tuple(neigh))) + labels = new_labels + cnt.update(labels.values()) + return cnt + + TR = wl_tokens(react) + TP = wl_tokens(prod) + + Delta = Counter(TP) + for h, v in TR.items(): + Delta[h] -= v + if Delta[h] == 0: + del Delta[h] + + support = list(Delta.keys()) + self._tokens_R = TR + self._tokens_P = TP + self._delta = Delta + self._support = support + + bits = np.zeros(self.size, dtype=int) if self.to_array else [0] * self.size + for h in support: + idx = h % self.size + if self.to_array: + bits[idx] ^= 1 + else: + bits[idx] = bits[idx] ^ 1 + self._fingerprint = bits + + return self + + @classmethod + def from_rsmi( + cls, + rsmi: str, + radius: int = 2, + size: int = 1024, + to_array: bool = False, + drop_non_aam: bool = False, + use_index_as_atom_map: bool = False, + ) -> WLRnxFps: + """ + Build WLRnxFps directly from a reaction SMILES string. + + :param rsmi: reaction SMILES string + :type rsmi: str + :param radius: number of WL refinement iterations + :type radius: int + :param size: bit budget for the parity sketch + :type size: int + :param to_array: return fingerprint as NumPy array if True + :type to_array: bool + :param drop_non_aam: drop atoms without atom-atom mapping + :type drop_non_aam: bool + :param use_index_as_atom_map: interpret node indices as atom map numbers + :type use_index_as_atom_map: bool + :returns: fitted WLRnxFps instance + :rtype: WLRnxFps + :raises ValueError: on invalid SMILES parsing + """ + try: + react, prod = rsmi_to_graph( + rsmi, + drop_non_aam=drop_non_aam, + use_index_as_atom_map=use_index_as_atom_map, + ) + except Exception as e: + raise ValueError(f"Failed to parse rsmi: {e}") + + return cls(radius=radius, size=size, to_array=to_array).fit(react, prod) + + @property + def tokens_R(self) -> Counter: + """ + :returns: WL token counts for reactant + :rtype: Counter + :raises AttributeError: if fit() has not been called + """ + if self._tokens_R is None: + raise AttributeError("Call fit() before accessing tokens_R") + return self._tokens_R + + @property + def tokens_P(self) -> Counter: + """ + :returns: WL token counts for product + :rtype: Counter + :raises AttributeError: if fit() has not been called + """ + if self._tokens_P is None: + raise AttributeError("Call fit() before accessing tokens_P") + return self._tokens_P + + @property + def delta(self) -> Counter: + """ + :returns: Signed token difference (product - reactant) + :rtype: Counter + :raises AttributeError: if fit() has not been called + """ + if self._delta is None: + raise AttributeError("Call fit() before accessing delta") + return self._delta + + @property + def support(self) -> List[int]: + """ + :returns: Tokens with non-zero delta + :rtype: List[int] + :raises AttributeError: if fit() has not been called + """ + if self._support is None: + raise AttributeError("Call fit() before accessing support") + return self._support + + @property + def fingerprint(self) -> Union[List[int], np.ndarray]: + """ + :returns: Parity sketch bit vector (0/1) + :rtype: Union[List[int], numpy.ndarray] + :raises AttributeError: if fit() has not been called + """ + if self._fingerprint is None: + raise AttributeError("Call fit() before accessing fingerprint") + return self._fingerprint + + def __repr__(self) -> str: + support_len = len(self._support) if self._support is not None else 0 + return ( + f"" + ) + + def help(self) -> None: + """ + Print usage examples and class docstring. + + :returns: None + """ + print(self.__doc__) + + +def _h64(obj: Any) -> int: + """ + Compute a stable 64-bit hash of an object. + + :param obj: any hashable representation + :type obj: Any + :returns: 64-bit integer hash + :rtype: int + """ + h = blake2b(digest_size=8) + h.update(repr(obj).encode("utf-8")) + return int.from_bytes(h.digest(), "little") diff --git a/synkit/Graph/ITS/its_decompose.py b/synkit/Graph/ITS/its_decompose.py index df9a1fd..416fd33 100644 --- a/synkit/Graph/ITS/its_decompose.py +++ b/synkit/Graph/ITS/its_decompose.py @@ -1,6 +1,6 @@ import re import networkx as nx -from typing import Optional, List +from typing import Optional, List, Any from rdkit import Chem from rdkit.Chem.MolStandardize import rdMolStandardize @@ -8,75 +8,300 @@ __all__ = ["get_rc", "its_decompose"] +# def get_rc( +# ITS: nx.Graph, +# element_key: List[str] = ["element", "charge", "typesGH", "atom_map"], +# bond_key: str = "order", +# standard_key: str = "standard_order", +# disconnected: bool = False, +# ) -> nx.Graph: +# """Extract the reaction-center (RC) subgraph from an ITS graph. + +# This function identifies: +# 1. All bonds whose standard order (difference between ITS orders) is non-zero. +# 2. All H–H bonds, ensuring they are included even if no order change is detected. +# 3. (Optional) Additional nodes with charge changes and reconnection of edges +# if `disconnected=True`. + +# :param ITS: The integrated transition-state graph with composite node/edge attributes. +# :type ITS: nx.Graph +# :param element_key: List of node‐attribute keys to copy into the RC graph. +# :type element_key: List[str] +# :param bond_key: Edge attribute key representing the tuple of bond orders. +# :type bond_key: str +# :param standard_key: Edge attribute key for the computed standard_order. +# :type standard_key: str +# :param disconnected: If True, also include nodes with charge changes and +# reconnect any ITS edges between RC nodes. +# :type disconnected: bool +# :returns: A new graph containing only the reaction-center nodes and edges. +# :rtype: nx.Graph + +# :example: +# >>> ITS = nx.Graph() +# >>> # ... populate ITS with 'order', 'standard_order', 'typesGH', etc. ... +# >>> RC = get_rc(ITS, disconnected=True) +# >>> isinstance(RC, nx.Graph) +# True +# """ +# rc = nx.Graph() +# _add_bond_order_changes(ITS, rc, element_key, bond_key, standard_key) + +# # 1.5) H-H bonds (force inclusion, with fallback typesGH) +# for u, v, data in ITS.edges(data=True): +# elem_u = ITS.nodes[u].get("element") +# elem_v = ITS.nodes[v].get("element") +# if elem_u == "H" and elem_v == "H": +# for n in (u, v): +# node_data = dict(ITS.nodes[n]) +# if "typesGH" not in node_data: +# node_data["typesGH"] = ( +# ("H", False, 0, 0, []), +# ("*", False, 0, 0, []), +# ) +# # Ensure typesGH is available even if not in original element_key +# final_attrs = {k: node_data[k] for k in element_key if k in node_data} +# final_attrs["typesGH"] = node_data["typesGH"] +# rc.add_node(n, **final_attrs) + +# rc.add_edge( +# u, +# v, +# **{ +# bond_key: data.get(bond_key), +# standard_key: data.get(standard_key), +# }, +# ) +# if disconnected: +# _add_charge_change_nodes(ITS, rc, element_key) +# _reconnect_rc_edges(ITS, rc, bond_key, standard_key) + +# return rc + + +# def get_rc( +# ITS: nx.Graph, +# element_key: list[str] = ["element", "charge", "typesGH", "atom_map"], +# bond_key: str = "order", +# standard_key: str = "standard_order", +# disconnected: bool = False, +# keep_mtg: bool = False, +# ) -> nx.Graph: +# """ +# Extract the reaction-center (RC) subgraph from an ITS graph. + +# This function identifies: +# 1. All bonds whose standard order is non-zero. +# 2. (Optional) All bonds labeled with 'is_mtg=True' if keep_mtg is True. +# 3. All H-H bonds, ensuring they are included even if no order change is detected. +# 4. (Optional) Additional nodes with charge changes and reconnection of edges +# if `disconnected=True`. + +# :param ITS: The integrated transition-state graph with composite node/edge attributes. +# :type ITS: nx.Graph +# :param element_key: List of node-attribute keys to copy into the RC graph. +# :type element_key: List[str] +# :param bond_key: Edge attribute key representing the tuple of bond orders. +# :type bond_key: str +# :param standard_key: Edge attribute key for the computed standard_order. +# :type standard_key: str +# :param disconnected: If True, also include nodes with charge changes and +# reconnect any ITS edges between RC nodes. +# :type disconnected: bool +# :param keep_mtg: If True, also include edges where 'is_mtg' attribute is True. +# :type keep_mtg: bool +# :returns: A new graph containing only the reaction-center nodes and edges. +# :rtype: nx.Graph +# """ +# rc = nx.Graph() +# # 1) Bonds with standard order change or mechanistic transition +# for u, v, data in ITS.edges(data=True): +# std = data.get(standard_key) +# is_mtg_attr = data.get("is_mtg", False) +# include = False +# if isinstance(std, (int, float)) and std != 0: +# include = True +# if keep_mtg and is_mtg_attr: +# include = True +# if not include: +# continue +# # add nodes +# for n in (u, v): +# if not rc.has_node(n): +# node_data = dict(ITS.nodes[n]) +# final_attrs = {k: node_data[k] for k in element_key if k in node_data} +# rc.add_node(n, **final_attrs) +# # add edge +# edge_attrs = { +# bond_key: data.get(bond_key), +# standard_key: std, +# "is_mtg": is_mtg_attr, +# } +# rc.add_edge(u, v, **edge_attrs) + +# # 2) H-H bonds (force inclusion, with fallback typesGH) +# for u, v, data in ITS.edges(data=True): +# elem_u = ITS.nodes[u].get("element") +# elem_v = ITS.nodes[v].get("element") +# if elem_u == "H" and elem_v == "H": +# for n in (u, v): +# if not rc.has_node(n): +# node_data = dict(ITS.nodes[n]) +# if "typesGH" not in node_data: +# node_data["typesGH"] = ( +# ("H", False, 0, 0, []), +# ("*", False, 0, 0, []), +# ) +# final_attrs = { +# k: node_data[k] for k in element_key if k in node_data +# } +# final_attrs["typesGH"] = node_data["typesGH"] +# rc.add_node(n, **final_attrs) +# if not rc.has_edge(u, v): +# rc.add_edge( +# u, +# v, +# **{ +# bond_key: data.get(bond_key), +# standard_key: data.get(standard_key), +# "is_mtg": data.get("is_mtg", False), +# }, +# ) + +# if disconnected: +# _add_charge_change_nodes(ITS, rc, element_key) +# _reconnect_rc_edges(ITS, rc, bond_key, standard_key) + +# return rc + +# import networkx as nx +# from typing import List, Any + + def get_rc( ITS: nx.Graph, element_key: List[str] = ["element", "charge", "typesGH", "atom_map"], bond_key: str = "order", standard_key: str = "standard_order", disconnected: bool = False, + keep_mtg: bool = False, ) -> nx.Graph: - """Extract the reaction-center (RC) subgraph from an ITS graph. - - This function identifies: - 1. All bonds whose standard order (difference between ITS orders) is non-zero. - 2. All H–H bonds, ensuring they are included even if no order change is detected. - 3. (Optional) Additional nodes with charge changes and reconnection of edges - if `disconnected=True`. - - :param ITS: The integrated transition-state graph with composite node/edge attributes. - :type ITS: nx.Graph - :param element_key: List of node‐attribute keys to copy into the RC graph. - :type element_key: List[str] - :param bond_key: Edge attribute key representing the tuple of bond orders. - :type bond_key: str - :param standard_key: Edge attribute key for the computed standard_order. - :type standard_key: str - :param disconnected: If True, also include nodes with charge changes and - reconnect any ITS edges between RC nodes. - :type disconnected: bool - :returns: A new graph containing only the reaction-center nodes and edges. - :rtype: nx.Graph - - :example: - >>> ITS = nx.Graph() - >>> # ... populate ITS with 'order', 'standard_order', 'typesGH', etc. ... - >>> RC = get_rc(ITS, disconnected=True) - >>> isinstance(RC, nx.Graph) - True + """ + Extract the reaction-center (RC) subgraph from an ITS graph. """ rc = nx.Graph() - _add_bond_order_changes(ITS, rc, element_key, bond_key, standard_key) + _add_changed_bonds(ITS, rc, element_key, bond_key, standard_key, keep_mtg) + _add_hh_bonds(ITS, rc, element_key, bond_key, standard_key) + if disconnected: + _add_charge_change_nodes(ITS, rc, element_key) + _reconnect_rc_edges(ITS, rc, bond_key, standard_key) + return rc - # 1.5) H-H bonds (force inclusion, with fallback typesGH) + +def _add_changed_bonds( + ITS: nx.Graph, + rc: nx.Graph, + element_key: List[str], + bond_key: str, + standard_key: str, + keep_mtg: bool, +) -> None: + """ + Add bonds with non-zero standard order or mechanistic transitions. + """ + for u, v, data in ITS.edges(data=True): + std = data.get(standard_key) + is_mtg_attr = data.get("is_mtg", False) + if not _should_include_edge(std, is_mtg_attr, keep_mtg): + continue + _ensure_node(rc, ITS, u, element_key) + _ensure_node(rc, ITS, v, element_key) + rc.add_edge( + u, + v, + **{bond_key: data.get(bond_key), standard_key: std, "is_mtg": is_mtg_attr}, + ) + + +def _add_hh_bonds( + ITS: nx.Graph, + rc: nx.Graph, + element_key: List[str], + bond_key: str, + standard_key: str, +) -> None: + """ + Force inclusion of H-H bonds, with fallback for typesGH. + """ for u, v, data in ITS.edges(data=True): - elem_u = ITS.nodes[u].get("element") - elem_v = ITS.nodes[v].get("element") - if elem_u == "H" and elem_v == "H": + if _is_hh_pair(ITS, u, v): for n in (u, v): - node_data = dict(ITS.nodes[n]) - if "typesGH" not in node_data: - node_data["typesGH"] = ( - ("H", False, 0, 0, []), - ("*", False, 0, 0, []), - ) - # Ensure typesGH is available even if not in original element_key - final_attrs = {k: node_data[k] for k in element_key if k in node_data} - final_attrs["typesGH"] = node_data["typesGH"] - rc.add_node(n, **final_attrs) + _ensure_node_hh(rc, ITS, n, element_key) + if not rc.has_edge(u, v): + rc.add_edge( + u, + v, + **{ + bond_key: data.get(bond_key), + standard_key: data.get(standard_key), + "is_mtg": data.get("is_mtg", False), + }, + ) - rc.add_edge( - u, - v, - **{ - bond_key: data.get(bond_key), - standard_key: data.get(standard_key), - }, - ) - if disconnected: - _add_charge_change_nodes(ITS, rc, element_key) - _reconnect_rc_edges(ITS, rc, bond_key, standard_key) - return rc +def _should_include_edge( + std: Any, + is_mtg_attr: bool, + keep_mtg: bool, +) -> bool: + """ + Determine if an edge should be included based on standard order and mechanistic flag. + """ + if isinstance(std, (int, float)) and std != 0: + return True + if keep_mtg and is_mtg_attr: + return True + return False + + +def _is_hh_pair(ITS: nx.Graph, u: Any, v: Any) -> bool: + """ + Check if both nodes of an edge are hydrogen. + """ + return ITS.nodes[u].get("element") == "H" and ITS.nodes[v].get("element") == "H" + + +def _ensure_node( + rc: nx.Graph, + ITS: nx.Graph, + node: Any, + element_key: List[str], +) -> None: + """ + Add a node to RC with selected attributes if not already present. + """ + if not rc.has_node(node): + node_data = ITS.nodes[node] + final_attrs = {k: node_data[k] for k in element_key if k in node_data} + rc.add_node(node, **final_attrs) + + +def _ensure_node_hh( + rc: nx.Graph, + ITS: nx.Graph, + node: Any, + element_key: List[str], +) -> None: + """ + Add H node to RC, ensuring typesGH fallback if missing. + """ + if not rc.has_node(node): + node_data = dict(ITS.nodes[node]) + if "typesGH" not in node_data: + node_data["typesGH"] = (("H", False, 0, 0, []), ("*", False, 0, 0, [])) + final_attrs = {k: node_data[k] for k in element_key if k in node_data} + final_attrs["typesGH"] = node_data["typesGH"] + rc.add_node(node, **final_attrs) def _carry_node_attrs(src: nx.Graph, dst: nx.Graph, n: int, keys: List[str]) -> None: @@ -139,172 +364,6 @@ def _add_bond_order_changes( ) -# def get_rc( -# ITS: nx.Graph, -# element_key: List[str] = ["element", "charge", "typesGH", "atom_map"], -# bond_key: str = "order", -# standard_key: str = "standard_order", -# disconnected: bool = False, -# ) -> nx.Graph: -# """ -# Extract the reaction center (RC) from ITS graph. - -# Enhancements: -# - Adds nodes and edges where bond order changes (core logic). -# - If disconnected=True: -# - Adds nodes with charge change based on typesGH. -# - Reconnects any ITS edge between two RC nodes. -# - NEW: Always includes H-H bonds in RC. Adds default typesGH if missing. -# """ -# rc = nx.Graph() - -# # 1) edges with bond-order change -# for u, v, data in ITS.edges(data=True): -# old, new = data.get(bond_key, [None, None]) -# if old != new: -# for n in (u, v): -# if not rc.has_node(n): -# rc.add_node( -# n, -# **{ -# k: ITS.nodes[n][k] for k in element_key if k in ITS.nodes[n] -# }, -# ) -# rc.add_edge( -# u, -# v, -# **{bond_key: data.get(bond_key), standard_key: data.get(standard_key)}, -# ) - -# # 1.5) H-H bonds (force inclusion, with fallback typesGH) -# for u, v, data in ITS.edges(data=True): -# elem_u = ITS.nodes[u].get("element") -# elem_v = ITS.nodes[v].get("element") -# if elem_u == "H" and elem_v == "H": -# for n in (u, v): -# node_data = dict(ITS.nodes[n]) -# if "typesGH" not in node_data: -# node_data["typesGH"] = ( -# ("H", False, 0, 0, []), -# ("*", False, 0, 0, []), -# ) -# # Ensure typesGH is available even if not in original element_key -# final_attrs = {k: node_data[k] for k in element_key if k in node_data} -# final_attrs["typesGH"] = node_data["typesGH"] -# rc.add_node(n, **final_attrs) - -# rc.add_edge( -# u, -# v, -# **{ -# bond_key: data.get(bond_key), -# standard_key: data.get(standard_key), -# }, -# ) - -# if disconnected: -# # 2) nodes with typesGH-based charge change -# for n, data in ITS.nodes(data=True): -# gh = data.get("typesGH") -# if ( -# isinstance(gh, (list, tuple)) -# and len(gh) >= 2 -# and len(gh[0]) > 3 -# and len(gh[1]) > 3 -# and gh[0][3] != gh[1][3] -# ): -# if not rc.has_node(n): -# rc.add_node(n, **{k: data[k] for k in element_key if k in data}) - -# # 3) reconnect RC nodes -# for u, v, data in ITS.edges(data=True): -# if rc.has_node(u) and rc.has_node(v) and not rc.has_edge(u, v): -# rc.add_edge( -# u, -# v, -# **{ -# bond_key: data.get(bond_key), -# standard_key: data.get(standard_key), -# }, -# ) - -# return rc - - -# def get_rc( -# ITS: nx.Graph, -# element_key: List[str] = ["element", "charge", "typesGH", "atom_map"], -# bond_key: str = "order", -# standard_key: str = "standard_order", -# disconnected: bool = False, -# ) -> nx.Graph: -# """ -# Extract the reaction center (RC) from ITS by: - -# 1. Always adding any edge whose bond order changes -# (bond_key[0] != bond_key[1]), plus its two end-nodes. -# 2. [if disconnected=True] Adding any node whose 'typesGH' record shows a charge change -# (typesGH[0][3] != typesGH[1][3]), even if isolated. -# 3. [if disconnected=True] Re-adding any ITS edge between two nodes already in RC -# (to preserve connectivity), carrying over bond_key & standard_key. - -# Parameters: -# - ITS (nx.Graph): input ITS graph. -# - element_key (List[str]): node attrs to carry over. -# - bond_key (str): edge attr key for bond order. -# - standard_key (str): edge attr key for standard order. -# - disconnected (bool): if True, include “charge-change” nodes (step 2) and -# reconnect any edges among RC nodes (step 3). If False, only performs step 1. -# """ -# rc = nx.Graph() - -# # 1) edges with bond-order change -# for u, v, data in ITS.edges(data=True): -# old, new = data.get(bond_key, [None, None]) -# if old != new: -# for n in (u, v): -# if not rc.has_node(n): -# rc.add_node( -# n, -# **{ -# k: ITS.nodes[n][k] for k in element_key if k in ITS.nodes[n] -# }, -# ) -# rc.add_edge( -# u, -# v, -# **{bond_key: data.get(bond_key), standard_key: data.get(standard_key)}, -# ) - -# if disconnected: -# # 2) nodes with a typesGH-based charge change -# for n, data in ITS.nodes(data=True): -# gh = data.get("typesGH") -# if ( -# isinstance(gh, (list, tuple)) -# and len(gh) >= 2 -# and len(gh[0]) > 3 -# and len(gh[1]) > 3 -# and gh[0][3] != gh[1][3] -# ): -# if not rc.has_node(n): -# rc.add_node(n, **{k: data[k] for k in element_key if k in data}) - -# # 3) re-add any ITS edge between RC nodes to preserve connectivity -# for u, v, data in ITS.edges(data=True): -# if rc.has_node(u) and rc.has_node(v) and not rc.has_edge(u, v): -# rc.add_edge( -# u, -# v, -# **{ -# bond_key: data.get(bond_key), -# standard_key: data.get(standard_key), -# }, -# ) - -# return rc - - def its_decompose(its_graph: nx.Graph, nodes_share="typesGH", edges_share="order"): """Decompose an ITS graph into two separate reactant (G) and product (H) graphs. diff --git a/synkit/Graph/MTG/mcs_matcher.py b/synkit/Graph/MTG/mcs_matcher.py index e8a6228..ea27d83 100644 --- a/synkit/Graph/MTG/mcs_matcher.py +++ b/synkit/Graph/MTG/mcs_matcher.py @@ -2,7 +2,7 @@ ================================================= A convenience wrapper around ``networkx.algorithms.isomorphism.GraphMatcher`` -that finds *all* common‑subgraph (or maximum‑common‑subgraph) node mappings +that finds *all* common-subgraph (or maximum-common-subgraph) node mappings between two molecular graphs. Highlights @@ -17,8 +17,9 @@ ``MCSMatcher(node_label_names, node_label_defaults, edge_attribute='order', allow_shift=True)`` Construct a matcher instance. -``matcher.find_common_subgraph(G1, G2, mcs=False)`` - Run the search (stores but does *not* return mappings). +``matcher.find_common_subgraph(G1, G2, mcs=False, mcs_mol=False)`` + Run the search (stores but does *not* return mappings). If ``mcs_mol=True``, + find mappings by matching entire connected components (largest molecules). ``matcher.get_mappings()`` Retrieve the stored mapping list. @@ -64,9 +65,6 @@ class MCSMatcher: Placeholder for future asymmetric rules (ignored for scalars). """ - # --------------------------------------------------------------------- - # Construction - # --------------------------------------------------------------------- def __init__( self, node_label_names: Optional[List[str]] | None = None, @@ -93,9 +91,6 @@ def __init__( self._mappings: List[Dict[int, int]] = [] self._last_size: int = 0 - # ------------------------------------------------------------------ - # Private helpers - # ------------------------------------------------------------------ def _edge_match( self, host_attrs: Dict[str, Any], pat_attrs: Dict[str, Any] ) -> bool: @@ -109,27 +104,74 @@ def _edge_match( @staticmethod def _invert_mapping(gm_mapping: Dict[int, int]) -> Dict[int, int]: - """Convert *host → pattern* dict to *pattern → host*.""" + """Convert *host→pattern* dict to *pattern→host*.""" return {pat: host for host, pat in gm_mapping.items()} - # ------------------------------------------------------------------ - # Public runners - # ------------------------------------------------------------------ + def _find_mcs_mol(self, G1: nx.Graph, G2: nx.Graph) -> Dict[int, int]: + """ + Match connected components of G1 to G2 of the same size, combining + each component's isomorphic mapping into one dict. + """ + # sort components by size descending + comps1 = sorted(nx.connected_components(G1), key=len, reverse=True) + comps2 = sorted(nx.connected_components(G2), key=len, reverse=True) + + used2: Set[frozenset[int]] = set() + combined: Dict[int, int] = {} + + for comp1 in comps1: + size = len(comp1) + sub1 = G1.subgraph(comp1) + + for comp2 in comps2: + if len(comp2) != size: + continue + key2 = frozenset(comp2) + if key2 in used2: + continue + + sub2 = G2.subgraph(comp2) + gm = GraphMatcher( + sub1, + sub2, + node_match=self.node_match, + edge_match=self._edge_match, + ) + if gm.is_isomorphic(): + combined.update(gm.mapping) + used2.add(key2) + break + + return combined + def find_common_subgraph( - self, G1: nx.Graph, G2: nx.Graph, *, mcs: bool = False + self, + G1: nx.Graph, + G2: nx.Graph, + *, + mcs: bool = False, + mcs_mol: bool = False, ) -> None: """Search for subgraph isomorphisms and cache the mappings. Parameters ---------- - G1 : nx.Graph – *pattern* graph (searched as a subgraph) - G2 : nx.Graph – *host* graph + G1 : nx.Graph - *pattern* graph (searched as a subgraph) + G2 : nx.Graph - *host* graph mcs : bool, optional If *True*, keep only mappings of maximum size. + mcs_mol : bool, optional + If *True*, match entire connected components (largest molecules). """ self._mappings.clear() self._last_size = 0 + if mcs_mol: + combined = self._find_mcs_mol(G1, G2) + self._mappings = [combined] + self._last_size = len(combined) + return + max_k = min(len(G1), len(G2)) sizes = range(max_k, 0, -1) seen: Set[tuple] = set() @@ -167,21 +209,22 @@ def find_common_subgraph( # final ordering – largest first then lexicographic self._mappings.sort(key=lambda d: (-len(d), tuple(sorted(d.items())))) - # ------------------------------------------------------------------ - # Convenience wrapper for ITS reaction‑centres - # ------------------------------------------------------------------ - def find_rc_mapping(self, rc1, rc2, *, mcs: bool = False) -> None: # type: ignore[override] + def find_rc_mapping( + self, + rc1, + rc2, + *, + mcs: bool = False, + mcs_mol: bool = False, + ) -> None: # type: ignore[override] if its_decompose is None: raise ImportError( "synkit is not available; cannot decompose reaction centres." ) _, r1 = its_decompose(rc1) l2, _ = its_decompose(rc2) - self.find_common_subgraph(r1, l2, mcs=mcs) + self.find_common_subgraph(r1, l2, mcs=mcs, mcs_mol=mcs_mol) - # ------------------------------------------------------------------ - # Properties and dunders - # ------------------------------------------------------------------ def get_mappings(self) -> List[Dict[int, int]]: """Return the cached mapping list (empty if `find_*` not yet called).""" @@ -192,7 +235,6 @@ def last_size(self) -> int: """Number of nodes in the most recent mapping set (0 if none).""" return self._last_size - # Pretty representations def __repr__(self) -> str: # noqa: D401 return ( f"MCSMatcher(mappings={len(self._mappings)}, last_size={self._last_size})" diff --git a/synkit/Graph/MTG/mtg.py b/synkit/Graph/MTG/mtg.py index 613ce68..6cc773d 100644 --- a/synkit/Graph/MTG/mtg.py +++ b/synkit/Graph/MTG/mtg.py @@ -1,208 +1,886 @@ +from __future__ import annotations + +"""MTG – Mechanistic Transition Graph fusion utility. + +This module exposes :class:`~MTG`, a helper that merges a chronological +sequence of **Intermediate Transition State** (ITS) graphs – or their RSMI +string representations – into a single *product* graph capturing the entire +bond-order history across the reaction trajectory. + +The implementation is self-contained except for the external *synkit* helpers +used for RSMI⇒ITS inter-conversion and canonicalisation. +""" + +from collections.abc import Iterator +from typing import Any, Dict, List, Mapping, MutableMapping, Set, Tuple, Union + import networkx as nx -from collections import defaultdict -from typing import Dict, List, Tuple, Any, Set, Union -# ----------------------------------------------------------------------------- -# Type aliases -# ----------------------------------------------------------------------------- +# --------------------------------------------------------------------------- +# Optional dependencies +# --------------------------------------------------------------------------- +try: + import pandas as pd # type: ignore +except ImportError: # pragma: no cover – pandas is only required for to_dataframe() + pd = None # noqa: N816 + +from synkit.Graph.Hyrogen._misc import h_to_explicit +from synkit.Graph.ITS.normalize_aam import NormalizeAAM +from synkit.Graph.MTG.mcs_matcher import MCSMatcher +from synkit.Graph.MTG.utils import ( + normalize_hcount_and_typesGH, + normalize_order, + label_mtg_edges, + compute_standard_order, +) +from synkit.Graph.canon_graph import GraphCanonicaliser +from synkit.IO import its_to_rsmi, rsmi_to_its + NodeID = int -Order = Tuple[float, float] -Node = Tuple[NodeID, Dict[str, Any]] -Edge = Tuple[NodeID, NodeID, Dict[str, Any]] +OrderPair = Tuple[float, float] +MissingOrder = Tuple[Set[float], Set[float]] +GraphMapping = Dict[NodeID, NodeID] + +_PLACEHOLDER: MissingOrder = (set(), set()) +_PLACEHOLDER_TYPESGH = (set(), set(), set(), set(), set()) __all__ = ["MTG"] class MTG: - """Fuse two molecular graphs via a pair‑groupoid edge‑composition rule. - - Parameters - ---------- - G1, G2 - Input :class:`networkx.Graph` (or *DiGraph*) objects. Nodes must carry an - ``"element"`` attribute; edges carry an ``"order"`` 2‑tuple *(x, y)*. - mapping - A partial node map **G1 → G2** indicating which atoms are chemically - identical (intersection). Keys are node IDs in *G1*, values in *G2*. - - Notes - ----- - 1. ``intersection_ids`` are created where mapping ``G1[i] → G2[j]``. - 2. Edges are inserted in two passes: - * *Pass 1* – all edges from *G1* are copied unchanged. - * *Pass 2* – edges from *G2* are remapped; when both endpoints are in - ``intersection_ids`` **and** their bond orders satisfy the *pair‐ - groupoid* condition - - ``(a₁, a₂) + (b₁, b₂) with a₂ == b₁ → (a₁, b₂)``, - - the edges are *composed* instead of duplicated. - - Examples - -------- - >>> mtg = MTG(G1, G2, {1: 3, 4: 6, 5: 1}) - >>> fused = mtg.get_graph() - >>> fused.nodes(data=True) - ... + """Fuse a chronological series of ITS graphs into a Mechanistic Transition Graph. + + :param sequences: A list of ITS-format NetworkX graphs or RSMI strings. + :param mappings: Optional list of precomputed mappings; computed via MCS if None. + :param node_label_names: Keys for node-label matching. + :param canonicaliser: Optional GraphCanonicaliser for snapshot canonicalisation. + :raises ValueError: On invalid sequence or mapping lengths. + :raises RuntimeError: On mapping failures. """ - # ------------------------------------------------------------------ - # Construction helpers - # ------------------------------------------------------------------ def __init__( self, - G1: Union[nx.Graph, nx.DiGraph], - G2: Union[nx.Graph, nx.DiGraph], - mapping: Dict[NodeID, NodeID], + sequences: Union[List[nx.Graph], List[str]], + mappings: List[GraphMapping] | None = None, + *, + node_label_names: List[str] | None = None, + canonicaliser: GraphCanonicaliser | None = None, + mcs_mol: bool = False, + mcs: bool = False, ) -> None: - # Store originals - self.G1 = G1 - self.G2 = G2 - self.mapping12 = mapping # G1 → G2 - - # ---- 1. Build fused node set --------------------------------- - ( - self.product_nodes, # list[(id, attrs)] in fused graph - self.map1, # G1 id → fused id - self.map2, # G2 id → fused id - self.intersection_ids, # list[fused id] - ) = self._fuse_nodes() - - # ---- 2. Fuse edges with groupoid rule ------------------------ - fused_edges_step1 = self._insert_edges_from(self.G1.edges(data=True), self.map1) - self.product_edges = self._insert_edges_from( - self.G2.edges(data=True), self.map2, existing=fused_edges_step1 + if len(sequences) < 2: + raise ValueError("Need at least two snapshots.") + + self._node_label_names = node_label_names or ["element", "charge", "hcount"] + self._canonicaliser = canonicaliser + self.mcs_mol = mcs_mol + self.mcs = mcs + + self._graphs = self._prepare_graph_sequence(sequences) + self._k = len(self._graphs) + + self._mappings = ( + mappings if mappings is not None else self._compute_mappings(self._graphs) ) + if len(self._mappings) != self._k - 1: + raise ValueError("Mappings must match snapshot pairs.") - # ------------------------------------------------------------------ - # Node fusion - # ------------------------------------------------------------------ - def _fuse_nodes(self): - merged: Dict[NodeID, Dict[str, Any]] = {} - map1: Dict[NodeID, NodeID] = {} - map2: Dict[NodeID, NodeID] = {} - used: Set[NodeID] = set() - - # --- copy G1 directly into fused graph ------------------------ - for v, attrs in self.G1.nodes(data=True): - merged[v] = attrs.copy() - map1[v] = v - used.add(v) - - # inverse mapping: G2 node → G1 node it merges to - inv_map = {g2: g1 for g1, g2 in self.mapping12.items()} - intersection: List[NodeID] = [] - - # --- process G2 nodes ----------------------------------------- - next_id = max(used) + 1 if used else 0 - for v, attrs in self.G2.nodes(data=True): - if v in inv_map: # merged node - tgt = inv_map[v] - map2[v] = tgt - intersection.append(tgt) - else: # unique node from G2 - while next_id in used: - next_id += 1 - merged[next_id] = attrs.copy() - map2[v] = next_id - used.add(next_id) - next_id += 1 - - nodes_sorted = sorted(merged.items()) # list[(id, attrs)] - return nodes_sorted, map1, map2, intersection - - # ------------------------------------------------------------------ - # Edge insertion & groupoid composition - # ------------------------------------------------------------------ - def _insert_edges_from( - self, edge_iter, node_map: Dict[NodeID, NodeID], existing: List[Edge] = None - ) -> List[Edge]: - """Insert edges into *existing* applying the groupoid rule when - possible.""" - existing = [] if existing is None else existing.copy() - - # Remap and append new edges - for u, v, attrs in edge_iter: - u3 = node_map[u] - v3 = node_map[v] - existing.append((u3, v3, attrs.copy())) - - # Canonicalize keys for undirected graphs - def key(u, v): - return (u, v) if isinstance(self.G1, nx.DiGraph) else tuple(sorted((u, v))) - - # Group edges by (u,v) - buckets: Dict[Tuple[NodeID, NodeID], List[Order]] = defaultdict(list) - bucket_src: Dict[Tuple[NodeID, NodeID], List[str]] = defaultdict(list) - for idx, (u, v, attrs) in enumerate(existing): - buckets[key(u, v)].append(tuple(attrs["order"])) - bucket_src[key(u, v)].append("G1" if idx < len(self.G1.edges) else "G2") - - fused_edges: List[Edge] = [] - for (u, v), orders in buckets.items(): - # src = bucket_src[(u, v)] - if ( - u in self.intersection_ids - and v in self.intersection_ids - and len(orders) >= 2 - ): - # Attempt pair‑wise composition between G1 (first) and any G2 edge - made_composite = False - for idx2, ord2 in enumerate(orders[1:], start=1): - a1, a2 = orders[0] - b1, b2 = ord2 - if a2 == b1: - fused_edges.append((u, v, {"order": (a1, b2)})) - made_composite = True - break - if not made_composite: - # fall back to *all* distinct orders - for ord_ in orders: - fused_edges.append((u, v, {"order": ord_})) - else: - for ord_ in orders: - fused_edges.append((u, v, {"order": ord_})) + self._prod_nodes: Dict[int, Dict[str, Any]] + self._node_map: Dict[Tuple[int, NodeID], int] + self._graph: nx.Graph + + self._build_node_map_and_attributes() + self._build_edge_history_and_graph() + + def __repr__(self) -> str: + return f"" + + def __len__(self) -> int: + return self._graph.number_of_nodes() + + def __iter__(self) -> Iterator[int]: + return iter(self._graph.nodes) + + def __getitem__(self, node_id: int) -> Dict[str, Any]: + return self._graph.nodes[node_id] - return self._dedupe_edges(fused_edges) + @staticmethod + def describe() -> str: + return ( + "# Usage example\n" + "mtg = MTG([G0, G1, G2])\n" + "mg = mtg.get_mtg()\n" + "rsmi = mtg.get_aam()\n" + ) + + def get_mtg(self, *, directed: bool = False) -> nx.Graph: + return self._graph.to_directed() if directed else self._graph + + def get_compose_its(self, *, directed: bool = False) -> nx.Graph: + g = self.get_mtg(directed=directed) + g = label_mtg_edges(g, inplace=False) + g = normalize_order(g) + g = normalize_hcount_and_typesGH(g) + return compute_standard_order(g) + + def get_aam(self, *, directed: bool = False, explicit_h: bool = False) -> str: + g = self.get_compose_its(directed=directed) + rsmi = its_to_rsmi(g, explicit_hydrogen=True) + return ( + NormalizeAAM().fit(rsmi, fix_aam_indice=False) if not explicit_h else rsmi + ) + + def to_dataframe(self): + if pd is None: + raise RuntimeError("pandas required for DataFrame export.") + return pd.DataFrame.from_dict( + dict(self._graph.nodes(data=True)), orient="index" + ) - # ------------------------------------------------------------------ @staticmethod - def _dedupe_edges(edges: List[Edge]) -> List[Edge]: - seen: Set[Tuple[int, int, Order]] = set() - out: List[Edge] = [] - for u, v, attrs in edges: - key = (min(u, v), max(u, v), tuple(attrs["order"])) - if key not in seen: - seen.add(key) - out.append((u, v, attrs)) + def _merge_attrs(lhs: MutableMapping[str, Any], rhs: Mapping[str, Any]) -> None: + for k, v in rhs.items(): + if not lhs.get(k) and v is not None: + lhs[k] = v + + def _build_node_map_and_attributes(self) -> None: + prod, node_map = {}, {} + last = self._graphs[-1] + for nid, attrs in last.nodes(data=True): + prod[nid] = attrs.copy() + node_map[(self._k - 1, nid)] = nid + pid_counter = max(prod, default=-1) + 1 + + # merge attributes backwards + for i in range(self._k - 2, -1, -1): + G, mp = self._graphs[i], self._mappings[i] + for nid, attrs in G.nodes(data=True): + tgt = mp.get(nid) + if tgt is not None and (i + 1, tgt) in node_map: + pid = node_map[(i + 1, tgt)] + self._merge_attrs(prod[pid], attrs) + else: + pid = pid_counter + prod[pid] = attrs.copy() + pid_counter += 1 + node_map[(i, nid)] = pid + + # assemble typesGH history per pid + first_idx: Dict[int, int] = {} + for (gi, n), p in node_map.items(): + # track the earliest snapshot index where pid appears + if p in first_idx: + first_idx[p] = min(first_idx[p], gi) + else: + first_idx[p] = gi + + for p, attrs in prod.items(): + hist: List[Any] = [] + fi = first_idx[p] + for i in range(self._k): + if i < fi: + hist.append(_PLACEHOLDER_TYPESGH) + elif i == fi: + val = ( + self._graphs[i] + .nodes[ + next( + n + for (gi, n), pp in node_map.items() + if gi == i and pp == p + ) + ] + .get("typesGH", (_PLACEHOLDER_TYPESGH, _PLACEHOLDER_TYPESGH)) + ) + hist.append(val) + else: + originals = [ + n for (gi, n), pp in node_map.items() if gi == i and pp == p + ] + if originals: + val = ( + self._graphs[i] + .nodes[originals[0]] + .get( + "typesGH", (_PLACEHOLDER_TYPESGH, _PLACEHOLDER_TYPESGH) + )[-1] + ) + hist.append(val) + else: + hist.append(_PLACEHOLDER_TYPESGH) + attrs["typesGH_history"] = tuple(hist) + attrs["typesGH"] = attrs["typesGH_history"] + + self._prod_nodes = prod + self._node_map = node_map + + def _build_edge_history_and_graph(self) -> None: + hist: Dict[Tuple[int, int], List[MissingOrder]] = {} + for i, G in enumerate(self._graphs): + for u, v, a in G.edges(data=True): + pu, pv = self._node_map[(i, u)], self._node_map[(i, v)] + key = tuple(sorted((pu, pv))) + lst = hist.setdefault(key, [_PLACEHOLDER] * self._k) + lst[i] = a.get("order", _PLACEHOLDER) + g = nx.Graph() + g.add_nodes_from(self._prod_nodes.items()) + for (u, v), lst in hist.items(): + g.add_edge(u, v, order=tuple(lst)) + if g.number_of_nodes() != len(self._prod_nodes): + raise RuntimeError("Node count mismatch.") + self._graph = g + + def _prepare_graph_sequence( + self, seq: List[nx.Graph] | List[str] + ) -> List[nx.Graph]: + out: List[nx.Graph] = [] + for item in seq: + g = rsmi_to_its(item, core=False) if isinstance(item, str) else item + if self._canonicaliser: + g = self._canonicaliser.canonicalise_graph(g).canonical_graph + g = h_to_explicit(g, its=True) + # out.append(g) + out.append(normalize_hcount_and_typesGH(g)) return out - # ------------------------------------------------------------------ - # Public helpers - # ------------------------------------------------------------------ - def get_nodes(self) -> List[Node]: - """List of `(id, attrs)` for fused graph.""" - return self.product_nodes - - def get_edges(self) -> List[Edge]: - """List of `(u, v, attrs)` for fused graph.""" - return self.product_edges - - def get_map1(self) -> Dict[NodeID, NodeID]: - return self.map1 - - def get_map2(self) -> Dict[NodeID, NodeID]: - return self.map2 - - def get_graph(self, *, directed: bool = False): - G = nx.DiGraph() if directed else nx.Graph() - G.add_nodes_from(self.product_nodes) - for u, v, attrs in self.product_edges: - o = attrs["order"] - attrs["standard_order"] = o[0] - o[1] if None not in o else None - G.add_edge(u, v, **attrs) - return G - - # ------------------------------------------------------------------ - def __repr__(self): - return f"MTG(|V|={len(self.product_nodes)}, |E|={len(self.product_edges)})" + def _compute_mappings(self, graphs: List[nx.Graph]) -> List[GraphMapping]: + mappings: List[GraphMapping] = [] + for i in range(len(graphs) - 1): + m = MCSMatcher(node_label_names=self._node_label_names) + m.find_rc_mapping( + graphs[i], graphs[i + 1], mcs=self.mcs, mcs_mol=self.mcs_mol + ) + if not m._mappings: + raise RuntimeError(f"No mapping between {i} and {i+1}") + mappings.append(m._mappings[0]) + return mappings + + @property + def node_mapping(self) -> Dict[Tuple[int, NodeID], int]: + return dict(self._node_map) + + @property + def k(self) -> int: + return self._k + + +# from __future__ import annotations + +# """MTG – Mechanistic Transition Graph fusion utility. + +# This module exposes :class:`~MTG`, a helper that merges a chronological +# sequence of **Intermediate Transition State** (ITS) graphs – or their RSMI +# string representations – into a single *product* graph capturing the entire +# bond-order history across the reaction trajectory. + +# The implementation is self-contained except for the external *synkit* helpers +# used for RSMI⇆ITS inter-conversion and canonicalisation. +# """ + +# from collections.abc import Iterator +# from typing import Any, Dict, List, Mapping, MutableMapping, Set, Tuple, Union + +# import networkx as nx + +# # --------------------------------------------------------------------------- +# # Optional dependencies +# # --------------------------------------------------------------------------- +# try: +# import pandas as pd # type: ignore +# except ImportError: # pragma: no cover – pandas is only required for to_dataframe() +# pd = None # noqa: N816 – keep lowercase alias even if stubbed + +# from synkit.Graph.Hyrogen._misc import h_to_explicit # noqa: WPS433 – external import +# from synkit.Graph.ITS.normalize_aam import NormalizeAAM # noqa: WPS433 +# from synkit.Graph.MTG.mcs_matcher import MCSMatcher # noqa: WPS433 +# from synkit.Graph.MTG.utils import ( +# normalize_hcount_and_typesGH, +# normalize_order, +# label_mtg_edges, +# compute_standard_order, +# ) # noqa: WPS433 +# from synkit.Graph.canon_graph import GraphCanonicaliser # noqa: WPS433 +# from synkit.IO import its_to_rsmi, rsmi_to_its # noqa: WPS433 + +# NodeID = int +# OrderPair = Tuple[float, float] +# MissingOrder = Tuple[Set[float], Set[float]] +# GraphMapping = Dict[NodeID, NodeID] + +# # A placeholder for a *missing* edge-order in a particular snapshot. Using +# # `set()` makes the value clearly distinguishable from genuine numeric orders. +# _PLACEHOLDER: MissingOrder = (set(), set()) + +# __all__ = [ +# "MTG", +# ] + + +# class MTG: # pylint: disable=too-many-instance-attributes +# """Fuse a chronological series of ITS graphs into a Mechanistic Transition Graph. + +# :param sequences: Either a list of ITS-format NetworkX graphs or a list of RSMI +# strings in chronological order. +# :type sequences: List[nx.Graph] or List[str] +# :param mappings: Pre-computed node mappings between each consecutive pair of graphs. +# If None, mappings are computed via MCSMatcher. +# :type mappings: List[GraphMapping] or None +# :param node_label_names: Node attribute keys used for MCS-based matching. +# :type node_label_names: List[str] or None +# :param canonicaliser: Optional GraphCanonicaliser to canonicalise each +# snapshot before fusion. +# :type canonicaliser: GraphCanonicaliser or None +# :raises ValueError: If fewer than two sequences are provided or mapping count mismatches. +# :raises TypeError: If sequence elements are neither NetworkX graphs nor RSMI strings. +# :raises RuntimeError: If automatic mapping fails for any adjacent graph pair. +# """ + +# # --------------------------------------------------------------------- +# # Construction helpers +# # --------------------------------------------------------------------- + +# def __init__( +# self, +# sequences: Union[List[nx.Graph], List[str]], +# mappings: List[GraphMapping] | None = None, +# *, +# node_label_names: List[str] | None = None, +# canonicaliser: GraphCanonicaliser | None = None, +# mcs_mol: bool = False, +# mcs: bool = False, +# ) -> None: # noqa: D401 – docstring handled above +# # --- Basic validation ------------------------------------------------ +# if len(sequences) < 2: # also covers non-list via __len__ check raising +# raise ValueError( +# "At least two ITS snapshots are required to construct an MTG.", +# ) + +# self._node_label_names: List[str] = node_label_names or [ +# "element", +# "charge", +# "hcount", +# ] +# self._canonicaliser = canonicaliser +# self.mcs_mol: bool = mcs_mol +# self.mcs: bool = mcs + +# # --- Input normalisation ------------------------------------------- +# self._graphs: List[nx.Graph] = self._prepare_graph_sequence(sequences) +# self._k: int = len(self._graphs) + +# # --- Graph-to-graph mappings --------------------------------------- +# self._mappings: List[GraphMapping] = ( +# self._compute_mappings(self._graphs) if mappings is None else mappings +# ) +# if len(self._mappings) != self._k - 1: +# raise ValueError( +# "Need exactly one mapping per pair of adjacent snapshots.", +# ) + +# # --- Core fusion machinery ----------------------------------------- +# self._prod_nodes: Dict[int, Dict[str, Any]] +# self._node_map: Dict[Tuple[int, NodeID], int] +# self._graph: nx.Graph # final fused graph – populated below + +# self._build_node_map_and_attributes() +# self._build_edge_history_and_graph() + +# # --------------------------------------------------------------------- +# # Python dunder & public helpers +# # --------------------------------------------------------------------- + +# def __repr__(self) -> str: # noqa: D401 – simple representation +# """Return a summary representation including snapshot count and graph size.""" +# return ( +# f"" +# ) + +# # Collection-like API --------------------------------------------------- + +# def __len__(self) -> int: +# """Return the number of fused nodes in the product graph.""" +# return self._graph.number_of_nodes() + +# def __iter__(self) -> Iterator[int]: +# """Iterate over fused node identifiers.""" +# return iter(self._graph.nodes) + +# def __getitem__(self, node_id: int) -> Dict[str, Any]: +# """Access the attribute dictionary of a fused node by its ID. + +# :param node_id: Fused node identifier +# :type node_id: int +# :returns: Node attribute mapping +# :rtype: Dict[str, Any] +# """ +# return self._graph.nodes[node_id] + +# # --------------------------------------------------------------------- +# # Public / user-facing API +# # --------------------------------------------------------------------- + +# @staticmethod +# def describe() -> str: # noqa: D401 – simple helper +# """Return an inline usage example for quick reference.""" + +# return ( +# "# Example usage\n" +# "mtg = MTG([G0, G1, G2])\n" +# "fused_graph = mtg.get_mtg()\n" +# "rsmi_with_aam = mtg.get_aam()\n" +# ) + +# # ------------------------------------------------------------------ +# # Graph export helpers +# # ------------------------------------------------------------------ + +# def get_mtg(self, *, directed: bool = False) -> nx.Graph: +# """Return the fused product graph. + +# :param directed: If True, return a directed copy of the fused graph +# :type directed: bool +# :returns: Fused product graph +# :rtype: networkx.Graph or networkx.DiGraph +# """ +# return self._graph.to_directed() if directed else self._graph + +# def get_compose_its(self, *, directed: bool = False) -> nx.Graph: +# """Return a graph with normalized edge orders for ITS export. + +# :param directed: If True, normalize a directed version +# :type directed: bool +# :returns: Graph with collapsed (order_G, order_H) tuples +# :rtype: networkx.Graph or networkx.DiGraph +# """ +# fused = self.get_mtg(directed=directed) +# fused = label_mtg_edges(fused, inplace=False) +# fused = normalize_order(fused) +# return compute_standard_order(fused) + +# def get_aam( +# self, +# *, +# directed: bool = False, +# explicit_h: bool = False, +# ) -> str: +# """Export fused graph to an RSMI string with atom-atom mapping. + +# :param directed: If True, use a directed ITS representation +# :type directed: bool +# :param explicit_h: If True, include explicit hydrogens; otherwise normalize AAM +# :type explicit_h: bool +# :returns: RSMI string with AAM +# :rtype: str +# """ + +# its_graph = self.get_compose_its(directed=directed) +# rsmi = its_to_rsmi(its_graph, explicit_hydrogen=True) +# if not explicit_h: +# rsmi = NormalizeAAM().fit(rsmi, fix_aam_indice=False) +# return rsmi + +# def to_dataframe(self): +# """Return a pandas DataFrame of fused node attributes. + +# :returns: DataFrame indexed by fused node IDs with attribute columns +# :rtype: pandas.DataFrame +# :raises RuntimeError: If pandas is not installed +# """ +# if pd is None: +# raise RuntimeError( +# "pandas is required for `to_dataframe()` but is not installed." +# ) +# return pd.DataFrame.from_dict( +# dict(self._graph.nodes(data=True)), orient="index" +# ) + +# # ------------------------------------------------------------------ +# # Node & edge fusion internals +# # ------------------------------------------------------------------ + +# @staticmethod +# def _merge_attrs(lhs: MutableMapping[str, Any], rhs: Mapping[str, Any]) -> None: +# """Update in-place, preferring non-empty or non-None values from rhs. + +# :param lhs: Target attribute dict to update +# :type lhs: MutableMapping[str, Any] +# :param rhs: Source attribute dict +# :type rhs: Mapping[str, Any] +# """ +# for key, value in rhs.items(): +# if ( +# not lhs.get(key) and value is not None +# ): # noqa: WPS501 – explicitly allow False/0 +# lhs[key] = value + +# # ................................................................. + +# def _build_node_map_and_attributes(self) -> None: +# """Construct fused nodes by merging snapshots backwards. + +# Builds: +# - self._prod_nodes: pid → attribute dict +# - self._node_map: (snapshot_index, original_node_id) → pid +# """ + +# prod: Dict[int, Dict[str, Any]] = {} +# node_map: Dict[Tuple[int, NodeID], int] = {} + +# # --- Seed with last snapshot ------------------------------------- +# last_graph = self._graphs[-1] +# for nid, attrs in last_graph.nodes(data=True): +# prod[nid] = attrs.copy() +# node_map[(self._k - 1, nid)] = nid +# next_pid: int = (max(prod) if prod else -1) + 1 + +# # --- Walk backwards and merge ------------------------------------ +# for idx in range(self._k - 2, -1, -1): +# G = self._graphs[idx] +# mapping = self._mappings[idx] +# for nid, attrs in G.nodes(data=True): +# target = mapping.get(nid) +# if target is not None and (idx + 1, target) in node_map: +# pid = node_map[(idx + 1, target)] +# self._merge_attrs(prod[pid], attrs) +# else: # new (unmapped) node – assign fresh pid +# while next_pid in prod: # safeguard although unlikely +# next_pid += 1 +# pid = next_pid +# prod[pid] = attrs.copy() +# next_pid += 1 +# node_map[(idx, nid)] = pid + +# self._prod_nodes = prod +# self._node_map = node_map + +# # ................................................................. + +# def _build_edge_history_and_graph( +# self, +# ) -> None: # noqa: C901 – complex but contained +# """Assemble the fused graph with per-edge order histores. + +# Each edge in the result has an `order` attribute: a tuple of +# length `k`, where each element is either an order-pair or a placeholder. +# """ + +# history: Dict[Tuple[int, int], List[MissingOrder]] = {} + +# # Collect order trajectories ----------------------------------------------------- +# for gi, G in enumerate(self._graphs): +# for u, v, attrs in G.edges(data=True): +# pu, pv = ( +# self._node_map[(gi, u)], +# self._node_map[(gi, v)], +# ) +# key = tuple(sorted((pu, pv))) # undirected canonical ordering +# orders = history.setdefault(key, [_PLACEHOLDER] * self._k) +# orders[gi] = attrs.get("order", _PLACEHOLDER) # type: ignore[arg-type] + +# # Build fused NetworkX graph ----------------------------------------------------- +# graph = nx.Graph() +# graph.add_nodes_from(self._prod_nodes.items()) +# for (u, v), orders in history.items(): +# graph.add_edge(u, v, order=tuple(orders)) + +# # Sanity check ------------------------------------------------------------------ +# if graph.number_of_nodes() != len(self._prod_nodes): +# raise RuntimeError("Node count mismatch during MTG assembly.") + +# self._graph = graph + +# # ------------------------------------------------------------------ +# # Mapping helpers +# # ------------------------------------------------------------------ + +# def _prepare_graph_sequence( +# self, seq: List[nx.Graph] | List[str] +# ) -> List[nx.Graph]: +# """Convert input list to a cleaned sequence of ITS graphs. + +# :param seq: Raw sequence of graphs or RSMI strings +# :type seq: List[nx.Graph] or List[str] +# :returns: List of normalized ITS graphs +# :rtype: List[nx.Graph] +# :raises TypeError: If an element is neither nx.Graph nor str +# """ + +# prepared: List[nx.Graph] = [] +# for item in seq: +# if isinstance(item, str): +# graph = rsmi_to_its(item, core=False) +# elif isinstance(item, nx.Graph): +# graph = item +# else: # pragma: no cover – guard against future unsupported types +# raise TypeError( +# "Sequences must contain either NetworkX graphs or RSMI strings.", +# ) + +# # Canonicalise (optional) --------------------------------------------------- +# if self._canonicaliser is not None: +# graph = self._canonicaliser.canonicalise_graph(graph).canonical_graph # type: ignore[attr-defined] + +# # Ensure explicit hydrogens & normalised hcount / typesGH ---------- +# graph = h_to_explicit(graph, its=True) +# graph = normalize_hcount_and_typesGH(graph) +# prepared.append(graph) + +# return prepared + +# # .................................................................. + +# def _compute_mappings(self, graphs: List[nx.Graph]) -> List[GraphMapping]: +# """Compute atom mappings via MCS matching for each adjacent pair. + +# :param graphs: ITS graphs in chronological order +# :type graphs: List[nx.Graph] +# :returns: List of mappings of length k-1 +# :rtype: List[GraphMapping] +# :raises RuntimeError: If no mapping found for a pair +# """ + +# mappings: List[GraphMapping] = [] +# for idx in range(len(graphs) - 1): +# matcher = MCSMatcher(node_label_names=self._node_label_names) +# matcher.find_rc_mapping( +# graphs[idx], graphs[idx + 1], mcs=self.mcs, mcs_mol=self.mcs_mol +# ) +# if not matcher._mappings: # pylint: disable=protected-access +# raise RuntimeError( +# f"No MCS mapping found between snapshots {idx} and {idx + 1}.", +# ) +# mappings.append(matcher._mappings[0]) # pylint: disable=protected-access +# return mappings + +# # ------------------------------------------------------------------ +# # Convenience accessors (mostly for unit tests) +# # ------------------------------------------------------------------ + +# @property +# def node_mapping(self) -> Dict[Tuple[int, NodeID], int]: +# """Return the internal mapping from (snapshot_index, original_node_id) to fused pid. + +# :returns: Mapping dictionary +# :rtype: Dict[Tuple[int, NodeID], int] +# """ +# return dict(self._node_map) + +# @property +# def k(self) -> int: +# """Return the number of snapshots fused. + +# :returns: Snapshot count +# :rtype: int +# """ +# return self._k + + +# import networkx as nx +# from collections import defaultdict +# from typing import Dict, List, Tuple, Any, Set, Union + +# # ----------------------------------------------------------------------------- +# # Type aliases +# # ----------------------------------------------------------------------------- +# NodeID = int +# Order = Tuple[float, float] +# Node = Tuple[NodeID, Dict[str, Any]] +# Edge = Tuple[NodeID, NodeID, Dict[str, Any]] + +# __all__ = ["MTG"] + + +# class MTG: +# """Fuse two molecular graphs via a pair‑groupoid edge‑composition rule. + +# Parameters +# ---------- +# G1, G2 +# Input :class:`networkx.Graph` (or *DiGraph*) objects. Nodes must carry an +# ``"element"`` attribute; edges carry an ``"order"`` 2‑tuple *(x, y)*. +# mapping +# A partial node map **G1 → G2** indicating which atoms are chemically +# identical (intersection). Keys are node IDs in *G1*, values in *G2*. + +# Notes +# ----- +# 1. ``intersection_ids`` are created where mapping ``G1[i] → G2[j]``. +# 2. Edges are inserted in two passes: +# * *Pass 1* – all edges from *G1* are copied unchanged. +# * *Pass 2* – edges from *G2* are remapped; when both endpoints are in +# ``intersection_ids`` **and** their bond orders satisfy the *pair‐ +# groupoid* condition + +# ``(a₁, a₂) + (b₁, b₂) with a₂ == b₁ → (a₁, b₂)``, + +# the edges are *composed* instead of duplicated. + +# Examples +# -------- +# >>> mtg = MTG(G1, G2, {1: 3, 4: 6, 5: 1}) +# >>> fused = mtg.get_graph() +# >>> fused.nodes(data=True) +# ... +# """ + +# # ------------------------------------------------------------------ +# # Construction helpers +# # ------------------------------------------------------------------ +# def __init__( +# self, +# G1: Union[nx.Graph, nx.DiGraph], +# G2: Union[nx.Graph, nx.DiGraph], +# mapping: Dict[NodeID, NodeID], +# ) -> None: +# # Store originals +# self.G1 = G1 +# self.G2 = G2 +# self.mapping12 = mapping # G1 → G2 + +# # ---- 1. Build fused node set --------------------------------- +# ( +# self.product_nodes, # list[(id, attrs)] in fused graph +# self.map1, # G1 id → fused id +# self.map2, # G2 id → fused id +# self.intersection_ids, # list[fused id] +# ) = self._fuse_nodes() + +# # ---- 2. Fuse edges with groupoid rule ------------------------ +# fused_edges_step1 = self._insert_edges_from(self.G1.edges(data=True), self.map1) +# self.product_edges = self._insert_edges_from( +# self.G2.edges(data=True), self.map2, existing=fused_edges_step1 +# ) + +# # ------------------------------------------------------------------ +# # Node fusion +# # ------------------------------------------------------------------ +# def _fuse_nodes(self): +# merged: Dict[NodeID, Dict[str, Any]] = {} +# map1: Dict[NodeID, NodeID] = {} +# map2: Dict[NodeID, NodeID] = {} +# used: Set[NodeID] = set() + +# # --- copy G1 directly into fused graph ------------------------ +# for v, attrs in self.G1.nodes(data=True): +# merged[v] = attrs.copy() +# map1[v] = v +# used.add(v) + +# # inverse mapping: G2 node → G1 node it merges to +# inv_map = {g2: g1 for g1, g2 in self.mapping12.items()} +# intersection: List[NodeID] = [] + +# # --- process G2 nodes ----------------------------------------- +# next_id = max(used) + 1 if used else 0 +# for v, attrs in self.G2.nodes(data=True): +# if v in inv_map: # merged node +# tgt = inv_map[v] +# map2[v] = tgt +# intersection.append(tgt) +# else: # unique node from G2 +# while next_id in used: +# next_id += 1 +# merged[next_id] = attrs.copy() +# map2[v] = next_id +# used.add(next_id) +# next_id += 1 + +# nodes_sorted = sorted(merged.items()) # list[(id, attrs)] +# return nodes_sorted, map1, map2, intersection + +# # ------------------------------------------------------------------ +# # Edge insertion & groupoid composition +# # ------------------------------------------------------------------ +# def _insert_edges_from( +# self, edge_iter, node_map: Dict[NodeID, NodeID], existing: List[Edge] = None +# ) -> List[Edge]: +# """Insert edges into *existing* applying the groupoid rule when +# possible.""" +# existing = [] if existing is None else existing.copy() + +# # Remap and append new edges +# for u, v, attrs in edge_iter: +# u3 = node_map[u] +# v3 = node_map[v] +# existing.append((u3, v3, attrs.copy())) + +# # Canonicalize keys for undirected graphs +# def key(u, v): +# return (u, v) if isinstance(self.G1, nx.DiGraph) else tuple(sorted((u, v))) + +# # Group edges by (u,v) +# buckets: Dict[Tuple[NodeID, NodeID], List[Order]] = defaultdict(list) +# bucket_src: Dict[Tuple[NodeID, NodeID], List[str]] = defaultdict(list) +# for idx, (u, v, attrs) in enumerate(existing): +# buckets[key(u, v)].append(tuple(attrs["order"])) +# bucket_src[key(u, v)].append("G1" if idx < len(self.G1.edges) else "G2") + +# fused_edges: List[Edge] = [] +# for (u, v), orders in buckets.items(): +# # src = bucket_src[(u, v)] +# if ( +# u in self.intersection_ids +# and v in self.intersection_ids +# and len(orders) >= 2 +# ): +# # Attempt pair‑wise composition between G1 (first) and any G2 edge +# made_composite = False +# for idx2, ord2 in enumerate(orders[1:], start=1): +# a1, a2 = orders[0] +# b1, b2 = ord2 +# if a2 == b1: +# fused_edges.append((u, v, {"order": (a1, b2)})) +# made_composite = True +# break +# if not made_composite: +# # fall back to *all* distinct orders +# for ord_ in orders: +# fused_edges.append((u, v, {"order": ord_})) +# else: +# for ord_ in orders: +# fused_edges.append((u, v, {"order": ord_})) + +# return self._dedupe_edges(fused_edges) + +# # ------------------------------------------------------------------ +# @staticmethod +# def _dedupe_edges(edges: List[Edge]) -> List[Edge]: +# seen: Set[Tuple[int, int, Order]] = set() +# out: List[Edge] = [] +# for u, v, attrs in edges: +# key = (min(u, v), max(u, v), tuple(attrs["order"])) +# if key not in seen: +# seen.add(key) +# out.append((u, v, attrs)) +# return out + +# # ------------------------------------------------------------------ +# # Public helpers +# # ------------------------------------------------------------------ +# def get_nodes(self) -> List[Node]: +# """List of `(id, attrs)` for fused graph.""" +# return self.product_nodes + +# def get_edges(self) -> List[Edge]: +# """List of `(u, v, attrs)` for fused graph.""" +# return self.product_edges + +# def get_map1(self) -> Dict[NodeID, NodeID]: +# return self.map1 + +# def get_map2(self) -> Dict[NodeID, NodeID]: +# return self.map2 + +# def get_graph(self, *, directed: bool = False): +# G = nx.DiGraph() if directed else nx.Graph() +# G.add_nodes_from(self.product_nodes) +# for u, v, attrs in self.product_edges: +# o = attrs["order"] +# attrs["standard_order"] = o[0] - o[1] if None not in o else None +# G.add_edge(u, v, **attrs) +# return G + +# # ------------------------------------------------------------------ +# def __repr__(self): +# return f"MTG(|V|={len(self.product_nodes)}, |E|={len(self.product_edges)})" diff --git a/synkit/Graph/MTG/mtg_explore.py b/synkit/Graph/MTG/mtg_explore.py new file mode 100644 index 0000000..3895fb5 --- /dev/null +++ b/synkit/Graph/MTG/mtg_explore.py @@ -0,0 +1,74 @@ +from typing import Optional, List +from synkit.Graph.MTG.mtg import MTG +from synkit.Rule.Apply.rule_matcher import RuleMatcher +from synkit.Graph.MTG.mcs_matcher import MCSMatcher +from networkx import Graph + + +def find_mtg( + g1: Graph, + g2: Graph, + ground_truth: str, + node_label_names: Optional[List[str]] = None, +) -> Optional[MTG]: + """ + Attempt to construct a Mapping Transformation Graph (MTG) for two input graphs + by finding maximum common substructure mappings and validating against a ground truth. + + :param g1: The first input graph to match. + :type g1: networkx.Graph + :param g2: The second input graph to match. + :type g2: networkx.Graph + :param ground_truth: A string representation of the expected atom-atom mapping (AAM) + used to validate candidate mappings. + :type ground_truth: str + :param node_label_names: List of node attribute names to use for MCS matching. + Defaults to ["element", "charge", "hcount"]. + :type node_label_names: list of str, optional + :returns: An MTG instance if a valid mapping satisfying the ground truth is found; + otherwise, None. + :rtype: MTG or None + :raises ValueError: If input graphs are empty or ground_truth is invalid format. + + :example: + >>> from networkx import Graph + >>> g1, g2 = Graph(), Graph() + >>> # populate g1 and g2 with nodes/edges + >>> mtg = find_mtg( + ... g1, + ... g2, + ... ground_truth="{0:1, 1:0}", + ... node_label_names=["element", "charge", "hcount"] + ... ) + >>> if mtg: + ... print(mtg) + """ + # Validate inputs + if not g1 or not g2: + raise ValueError("Input graphs g1 and g2 must be non-empty.") + if not isinstance(ground_truth, str) or not ground_truth.strip(): + raise ValueError("Ground truth mapping must be a non-empty string.") + + # Set default node_label_names if not provided + if node_label_names is None: + node_label_names = ["element", "charge", "hcount"] + + # Initialize maximum common substructure matcher + mcs = MCSMatcher(node_label_names=node_label_names) + mcs.find_rc_mapping(g1, g2, mcs=False) + mappings = mcs._mappings + + for mapping in mappings: + # Construct MTG using current mapping + mtg = MTG([g1, g2], mappings=[mapping]) + aam = mtg.get_aam() + try: + # Validate generated AAM against ground truth + RuleMatcher(ground_truth, aam, explicit_h=False) + return mtg + except AssertionError: + # Mapping did not satisfy ground truth, try next + continue + + # No valid mapping found + return None diff --git a/synkit/Graph/MTG/utils.py b/synkit/Graph/MTG/utils.py new file mode 100644 index 0000000..5d78ebb --- /dev/null +++ b/synkit/Graph/MTG/utils.py @@ -0,0 +1,425 @@ +import networkx as nx +import copy +from typing import Tuple, Any, Optional, Sequence, Set, Union + +OrderPair = Tuple[float, float] +MissingOrder = Tuple[Set[float], Set[float]] + +GraphType = Union[nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph] +TypesGHTuple = Tuple[Any, ...] # e.g., ('H', 0, 0, 0, ['C']) + + +def _extract_leaf_candidates(orig_th: Tuple[Any, ...]) -> Tuple[TypesGHTuple, ...]: + """ + Flatten one level: if an element is a sequence whose elements are themselves sequences, + treat those inner sequences as candidates; otherwise the element itself is a candidate. + """ + candidates: list[TypesGHTuple] = [] + for item in orig_th: + if ( + isinstance(item, (list, tuple)) + and item + and all(isinstance(inner, (list, tuple)) for inner in item) + ): + # e.g., (('H',...), ('H',...)) -> take inner tuples + for inner in item: + if isinstance(inner, (list, tuple)): + candidates.append(tuple(inner)) + else: + if isinstance(item, (list, tuple)): + candidates.append(tuple(item)) + else: + # non-sequence fallback, wrap to tuple for uniformity + candidates.append((item,)) + return tuple(candidates) + + +def normalize_hcount_and_typesGH(G: GraphType) -> GraphType: + """ + Return a fresh copy of G where: + * each node's `hcount` attribute is set to 0 + * each node's `typesGH` is processed as follows: + 1. Flatten one level so that nested tuples-of-tuples are expanded. + 2. Drop any tuple that contains a `set` anywhere. + 3. From the remaining tuples, keep only the first and last (if more than two). + 4. Zero indices 1 and 2 in each kept tuple (if they exist). + 5. If nothing remains after dropping, result is an empty tuple. + + :param G: input NetworkX graph + :type G: nx.Graph or nx.DiGraph or nx.MultiGraph or nx.MultiDiGraph + :returns: a new graph with normalized hcount and typesGH + :rtype: same type as G + :raises: TypeError if G is not a supported NetworkX graph or if typesGH is malformed. + """ + if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)): + raise TypeError(f"Expected a NetworkX graph, got {type(G)!r}") + + H = G.__class__() + H.graph.update(copy.deepcopy(G.graph)) + + for node, data in G.nodes(data=True): + new_data = data.copy() + new_data["hcount"] = 0 + + orig_th = data.get("typesGH", ()) + if not isinstance(orig_th, (list, tuple)): + raise TypeError( + f"Node {node} has typesGH of unexpected type {type(orig_th)}; expected sequence" + ) + + # Step 1: flatten one level of nested tuples-of-tuples + candidates = _extract_leaf_candidates(tuple(orig_th)) + + # Step 2: drop any tuple containing a set + filtered = [ + t for t in candidates if not any(isinstance(elem, set) for elem in t) + ] + + # Step 3: select first and last (or whatever remains) + if not filtered: + selected: Tuple[TypesGHTuple, ...] = () + elif len(filtered) > 2: + selected = (filtered[0], filtered[-1]) + else: + selected = tuple(filtered) # 1 or 2 elements + + # Step 4: zero indices 1 and 2 + normalized: list[TypesGHTuple] = [] + for inner in selected: + if not isinstance(inner, (list, tuple)): + raise TypeError( + f"Inner element of typesGH for node {node} is not tuple-like: {inner!r}" + ) + inner_list = list(inner) + if len(inner_list) > 1: + inner_list[1] = 0 + if len(inner_list) > 2: + inner_list[2] = 0 + normalized.append(tuple(inner_list)) + + new_data["typesGH"] = tuple(normalized) + H.add_node(node, **new_data) + + # Copy edges appropriately + if G.is_multigraph(): + for u, v, key, edata in G.edges(keys=True, data=True): + H.add_edge(u, v, key=key, **copy.deepcopy(edata)) + else: + for u, v, edata in G.edges(data=True): + H.add_edge(u, v, **copy.deepcopy(edata)) + + return H + + +def extract_order_norm( + order_sequence: Sequence[Union[OrderPair, MissingOrder]], +) -> Optional[OrderPair]: + """ + Given a sequence of order tuples and/or placeholders (MissingOrder), + return the normalized bond order as a 2-tuple: + - left: the first tuple element 'a' in the sequence where not both parts are sets + - right: the second tuple element 'b' in the sequence where not both parts are sets, scanning from the end + + The input sequence must have length >= 2. + + :param order_sequence: A sequence of order tuples or placeholders + :type order_sequence: Sequence[tuple[float, float]] or Sequence[MissingOrder] + :returns: A 2-tuple (left, right) if found; otherwise None + :rtype: tuple[float, float] or None + :raises ValueError: If sequence length is less than 2 + + :example: + >>> seq = [({1}, {2}), (3.0, 4.0), ({5}, {6}), (7.0, 8.0)] + >>> extract_order_norm(seq) + (3.0, 8.0) + """ + if not isinstance(order_sequence, Sequence) or len(order_sequence) < 2: + raise ValueError("order_sequence must be a sequence of length >= 2") + + left: Any = None + right: Any = None + # Find first non-placeholder for left + for entry in order_sequence: + a, b = entry + if not (isinstance(a, set) and isinstance(b, set)): + left = a + break + # Find last non-placeholder for right + for entry in reversed(order_sequence): + a, b = entry + if not (isinstance(a, set) and isinstance(b, set)): + right = b + break + if left is not None and right is not None: + return (left, right) + return None + + +def normalize_order(G: nx.Graph) -> nx.Graph: + """ + Return a copy of the graph with each edge's 'order' attribute normalized. + If an edge has an 'order' attribute that is a sequence of length >= 2, + it is replaced by the 2-tuple returned by :func:`extract_order_norm`, + if that function returns a non-None result. + + :param G: Input NetworkX graph + :type G: nx.Graph, nx.DiGraph, nx.MultiGraph, or nx.MultiDiGraph + :returns: A new graph of the same type with normalized edge 'order' + :rtype: same as G + :raises TypeError: If G is not a NetworkX graph + + :example: + >>> import networkx as nx + >>> G = nx.Graph() + >>> G.add_edge(1, 2, order=[(1,2), ({3},{4}), (5,6)]) + >>> H = normalize_order(G) + >>> H.edges[1,2]['order'] + (1, 6) + """ + from copy import deepcopy + + if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)): + raise TypeError(f"Expected a NetworkX graph, got {type(G)}") + + H = deepcopy(G) + # Iterate edges appropriately + if H.is_multigraph(): + edge_iter = H.edges(keys=True, data=True) + for _, _, _, attr in edge_iter: + order = attr.get("order") + if isinstance(order, (list, tuple)) and len(order) >= 2: + norm = extract_order_norm(order) + if norm is not None: + attr["order"] = norm + else: + for _, _, attr in H.edges(data=True): + order = attr.get("order") + if isinstance(order, (list, tuple)) and len(order) >= 2: + norm = extract_order_norm(order) + if norm is not None: + attr["order"] = norm + return H + + +# def normalize_hcount_and_typesGH(G): +# """ +# Return a fresh copy of G where: +# - each node's `hcount` attribute is set to 0 +# - in each tuple of `typesGH`, indices 1 and 2 are set to 0 + +# :param G: input NetworkX graph +# :type G: nx.Graph or nx.DiGraph or nx.MultiGraph or nx.MultiDiGraph +# :returns: a new graph with normalized hcount and typesGH +# :rtype: same type as G +# :raises: TypeError if G is not a NetworkX graph + +# :example: +# >>> G = nx.Graph() +# >>> G.add_node(1, hcount=2, typesGH=(("C", 1, 2), ("O", 0, 1))) +# >>> H = normalize_hcount_and_typesGH(G) +# >>> H.nodes[1]['hcount'] +# 0 +# >>> H.nodes[1]['typesGH'] +# (('C', 0, 0), ('O', 0, 0)) +# """ +# if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)): +# raise TypeError(f"Expected NetworkX graph, got {type(G)}") + +# # Create empty graph of same class and copy graph-level attributes +# H = G.__class__() +# H.graph.update(copy.deepcopy(G.graph)) + +# # Copy and normalize node data +# for node, data in G.nodes(data=True): +# new_data = data.copy() +# new_data["hcount"] = 0 +# orig_th = data.get("typesGH", ()) +# new_th = [] +# for inner in orig_th: +# inner_list = list(inner) +# # Zero the aromatic slot (index 1) and the hcountGH slot (index 2) +# if len(inner_list) > 1: +# inner_list[1] = 0 +# if len(inner_list) > 2: +# inner_list[2] = 0 +# new_th.append(tuple(inner_list)) +# new_data["typesGH"] = tuple(new_th) +# H.add_node(node, **new_data) + +# # Copy edges (with keys for multigraphs) +# if G.is_multigraph(): +# for u, v, key, edata in G.edges(keys=True, data=True): +# H.add_edge(u, v, key=key, **copy.deepcopy(edata)) +# else: +# for u, v, edata in G.edges(data=True): +# H.add_edge(u, v, **copy.deepcopy(edata)) + +# return H + + +def label_mtg_edges(G: nx.Graph, inplace: bool = False) -> nx.Graph: + """ + Label each edge in the MTG graph with a boolean 'is_mtg' attribute based on two criteria: + 1. There are at least two steps where the standard order (order[0] - order[1]) is non-zero. + 2. The sum of all non-None standard orders is zero. + + :param G: Input MTG graph with 'order' history per edge + :type G: nx.Graph or nx.DiGraph + :param inplace: If True, modify G in place; otherwise work on a copy + :type inplace: bool + :returns: Graph with 'is_mtg' boolean attribute on each edge + :rtype: same type as G + :raises TypeError: If G is not a NetworkX Graph + + :example: + >>> import networkx as nx + >>> G = nx.Graph() + >>> # Single change only -> less than 2 non-zero steps => False + >>> G.add_edge(7,3, order=((1.0,1.0),(1.0,0))) + >>> H = label_mtg_edges(G) + >>> H.edges[7,3]['is_mtg'] + False + >>> # Two-step equal but opposite changes -> sum zero and count>=2 => True + >>> G = nx.Graph() + >>> G.add_edge(2,1, order=((1.0,2.0),(2.0,1.0))) + >>> H = label_mtg_edges(G) + >>> H.edges[2,1]['is_mtg'] + True + """ + if not isinstance(G, (nx.Graph, nx.DiGraph)): + raise TypeError(f"Expected a NetworkX Graph, got {type(G)}") + graph = G if inplace else G.copy() + for u, v, attr in graph.edges(data=True): + history = attr.get("order") + # Extract numeric standard orders + std_vals: list[float] = [] + if isinstance(history, (list, tuple)) and len(history) >= 2: + for entry in history: + if ( + isinstance(entry, tuple) + and len(entry) == 2 + and all(isinstance(x, (int, float)) for x in entry) + ): + std_vals.append(entry[0] - entry[1]) + # Apply criteria + non_zero_count = sum(1 for v in std_vals if v != 0) + total = sum(std_vals) + attr["is_mtg"] = non_zero_count >= 2 and total == 0 + return graph + + +def compute_standard_order(G: nx.Graph, inplace: bool = False) -> nx.Graph: + """ + Compute and assign the 'standard_order' attribute for each edge in the graph. + 'standard_order' is defined as the difference order[0] - order[1] + for edges whose 'order' attribute is a 2-tuple of numeric values. + + :param G: Input NetworkX graph + :type G: nx.Graph, nx.DiGraph, nx.MultiGraph, or nx.MultiDiGraph + :param inplace: If True, modify G in-place; otherwise operate on a copy + :type inplace: bool + :returns: Graph with 'standard_order' attributes set + :rtype: same type as G + :raises TypeError: If G is not a NetworkX graph + + :example: + >>> import networkx as nx + >>> G = nx.Graph() + >>> G.add_edge(7, 3, order=(1.0, 0)) + >>> H = compute_standard_order(G) + >>> H.edges[7,3]['standard_order'] + 1.0 + """ + if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)): + raise TypeError(f"Expected a NetworkX graph, got {type(G)}") + + graph = G if inplace else G.copy() + if graph.is_multigraph(): + for u, v, key, attr in graph.edges(keys=True, data=True): + order = attr.get("order") + if isinstance(order, tuple) and len(order) == 2: + a, b = order + try: + attr["standard_order"] = a - b + except Exception: + attr["standard_order"] = None + else: + for u, v, attr in graph.edges(data=True): + order = attr.get("order") + if isinstance(order, tuple) and len(order) == 2: + a, b = order + try: + attr["standard_order"] = a - b + except Exception: + attr["standard_order"] = None + return graph + + +# def extract_order_norm(order_tuple): +# """ +# Given a sequence of four 2-element tuples (order data), return the normalized order: +# - left: first element of the first tuple that is not both sets +# - right: second element of the last tuple that is not both sets + +# :param order_tuple: tuple of four 2-element tuples +# :type order_tuple: tuple(tuple, tuple, tuple, tuple) +# :returns: normalized (left, right) or None if not found +# :rtype: tuple or None +# :raises: ValueError if order_tuple is not length 4 + +# :example: +# >>> ot = (({1}, {2}), (3, 4), ({5}, {6}), (7, 8)) +# >>> extract_order_norm(ot) +# (3, 8) +# """ +# if not (isinstance(order_tuple, tuple) and len(order_tuple) == 4): +# raise ValueError("order_tuple must be a tuple of length 4") + +# left = None +# right = None +# # Find first non-all-set tuple for left +# for a, b in order_tuple: +# if not (isinstance(a, set) and isinstance(b, set)): +# left = a +# break +# # Find last non-all-set tuple for right +# for a, b in reversed(order_tuple): +# if not (isinstance(a, set) and isinstance(b, set)): +# right = b +# break +# return (left, right) if (left is not None and right is not None) else None + + +# def normalize_order(G): +# """ +# Return a copy of G with edge attribute 'order' normalized. +# For each edge, if the 'order' attribute is a 4-tuple, replace it with the +# normalized 2-tuple returned by extract_order_norm. + +# :param G: input NetworkX graph +# :type G: nx.Graph or nx.DiGraph or nx.MultiGraph or nx.MultiDiGraph +# :returns: a new graph with normalized edge orders +# :rtype: same type as G +# :raises: TypeError if G is not a NetworkX graph + +# :example: +# >>> G = nx.Graph() +# >>> G.add_edge(1, 2, order=((1,2), ({3},{4}), ({5},{6}), (7,8))) +# >>> H = copy_and_normalize_order(G) +# >>> H.edges[1,2]['order'] +# (1, 8) +# """ +# if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)): +# raise TypeError(f"Expected NetworkX graph, got {type(G)}") +# H = G.copy() +# for _, _, _, attr in ( +# H.edges(keys=True, data=True) +# if H.is_multigraph() +# else [(u, v, None, attr) for u, v, attr in H.edges(data=True)] +# ): +# order = attr.get("order") +# if isinstance(order, tuple) and len(order) == 4: +# norm = extract_order_norm(order) +# if norm is not None: +# attr["order"] = norm +# return H diff --git a/synkit/Graph/Matcher/subgraph_matcher.py b/synkit/Graph/Matcher/subgraph_matcher.py index 1eb5678..4c73810 100644 --- a/synkit/Graph/Matcher/subgraph_matcher.py +++ b/synkit/Graph/Matcher/subgraph_matcher.py @@ -548,3 +548,330 @@ def help(self) -> str: # noqa: D401 – property for convenience """Return the full module docstring.""" return __doc__ + + +# --------------------------------------------------------------------- +# Sub-graph search engine +# --------------------------------------------------------------------- +# class SubgraphSearchEngine: +# """Efficient sub-graph matching helpers (static, stateless). + +# Call :py:meth:`find_subgraph_mappings` as the single public entry-point. +# """ + +# # ------------------------------------------------------------------ +# # Public dispatcher ------------------------------------------------- +# # ------------------------------------------------------------------ +# @staticmethod +# def find_subgraph_mappings( +# host: nx.Graph, +# pattern: nx.Graph, +# *, +# node_attrs: List[str], +# edge_attrs: List[str], +# strategy: Strategy = Strategy.COMPONENT, +# max_results: Optional[int] = None, +# strict_cc_count: bool = True, +# unique: bool = True, +# ) -> List[MappingDict]: +# """Return *all* pattern→host embeddings. + +# Parameters +# ---------- +# host : nx.Graph +# The larger “substrate” graph. +# pattern : nx.Graph +# The smaller “template” graph. +# node_attrs : list[str] +# Attributes that must match exactly on nodes. +# edge_attrs : list[str] +# Attributes that must match exactly on edges. +# strategy : Strategy, default=COMPONENT +# Matching strategy (ALL | COMPONENT | BACKTRACK). +# max_results : int | None, default=None +# Stop after collecting this many embeddings (before duplicate +# suppression unless *unique* is False). +# strict_cc_count : bool, default=True +# Reject immediately if host has more CCs than pattern. +# unique : bool, default=False +# If True, collapse embeddings that differ only by an +# automorphism of *pattern*. + +# Returns +# ------- +# list[dict[int, int]] +# A list of node-ID maps *pattern → host*. +# """ + +# # ── defensive copy (do not mutate caller’s graphs) ──────────── +# host = host.copy() +# pattern = pattern.copy() + +# # ── run selected strategy ───────────────────────────────────── +# if strategy is Strategy.ALL: +# results = SubgraphSearchEngine._find_all_subgraph_mappings( +# host, +# pattern, +# node_attrs=node_attrs, +# edge_attrs=edge_attrs, +# max_results=max_results if not unique else None, +# ) +# elif strategy is Strategy.COMPONENT: +# results = ( +# SubgraphSearchEngine._find_component_aware_subgraph_mappings( +# host, +# pattern, +# node_attrs=node_attrs, +# edge_attrs=edge_attrs, +# max_results=max_results if not unique else None, +# strict_cc_count=strict_cc_count, +# ) +# ) +# elif strategy is Strategy.BACKTRACK: +# results = SubgraphSearchEngine._find_bt_subgraph_mappings( +# host, +# pattern, +# node_attrs=node_attrs, +# edge_attrs=edge_attrs, +# max_results=max_results if not unique else None, +# strict_cc_count=strict_cc_count, +# ) +# else: # defensive +# raise ValueError(f"Unsupported strategy: {strategy}") + +# # ── post-filter duplicates coming from automorphisms ────────── +# if unique and results: +# def node_match(nh: EdgeAttr, np: EdgeAttr) -> bool: +# return all(nh.get(k) == np.get(k) for k in node_attrs) and ( +# nh.get("hcount", 0) >= np.get("hcount", 0) +# ) + +# def edge_match(eh: EdgeAttr, ep: EdgeAttr) -> bool: +# return all(eh.get(k) == ep.get(k) for k in edge_attrs) + +# results = SubgraphSearchEngine._filter_automorphic_duplicates( +# pattern, +# results, +# node_match=node_match, +# edge_match=edge_match, +# ) + +# # ── honour *max_results* after uniqueness filter ────────────── +# if max_results is not None and len(results) > max_results: +# results = results[:max_results] + +# return results + +# # ------------------------------------------------------------------ +# # Strategy: ALL – classical VF2 on full host ----------------------- +# # ------------------------------------------------------------------ +# @staticmethod +# def _find_all_subgraph_mappings( +# host: nx.Graph, +# pattern: nx.Graph, +# *, +# node_attrs: List[str], +# edge_attrs: List[str], +# max_results: Optional[int] = None, +# ) -> List[MappingDict]: +# """Pure VF2 search without CC awareness (baseline).""" + +# def node_match(nh: EdgeAttr, np: EdgeAttr) -> bool: +# return all(nh.get(k) == np.get(k) for k in node_attrs) and nh.get( +# "hcount", 0 +# ) >= np.get("hcount", 0) + +# def edge_match(eh: EdgeAttr, ep: EdgeAttr) -> bool: +# return all(eh.get(k) == ep.get(k) for k in edge_attrs) + +# gm = GraphMatcher(host, pattern, node_match=node_match, edge_match=edge_match) +# results: List[MappingDict] = [] +# for iso in gm.subgraph_monomorphisms_iter(): +# results.append({p: h for h, p in iso.items()}) +# if max_results is not None and len(results) >= max_results: +# break +# return results + +# # ------------------------------------------------------------------ +# # Strategy: COMPONENT – improved component-aware matcher ----------- +# # ------------------------------------------------------------------ +# @staticmethod +# def _find_component_aware_subgraph_mappings( +# host: nx.Graph, +# pattern: nx.Graph, +# *, +# node_attrs: List[str], +# edge_attrs: List[str], +# max_results: Optional[int] = None, +# strict_cc_count: bool = False, +# ) -> List[MappingDict]: +# """Component-aware VF2 without attribute/degree/WL-1 pre-filters.""" + +# # 1) split into connected components +# host_ccs = [host.subgraph(c).copy() for c in nx.connected_components(host)] +# pat_ccs = [pattern.subgraph(c).copy() for c in nx.connected_components(pattern)] +# hcc, pcc = len(host_ccs), len(pat_ccs) + +# # empty pattern ⇒ single empty mapping +# if pcc == 0: +# return [{}] + +# # fallback to full VF2 if host has fewer CCs +# if hcc < pcc: +# return SubgraphSearchEngine._find_all_subgraph_mappings( +# host, +# pattern, +# node_attrs=node_attrs, +# edge_attrs=edge_attrs, +# max_results=max_results, +# ) + +# # strict count: reject when host has more CCs than pattern +# if hcc > pcc and strict_cc_count: +# return [] + +# # 2) define VF2 predicates +# def node_match(nh: EdgeAttr, np: EdgeAttr) -> bool: +# if any(nh.get(a) != np.get(a) for a in node_attrs): +# return False +# return nh.get("hcount", 0) >= np.get("hcount", 0) + +# def edge_match(eh: EdgeAttr, ep: EdgeAttr) -> bool: +# return all(eh.get(a) == ep.get(a) for a in edge_attrs) + +# # 3) collect embeddings for each pattern-CC +# per_cc: List[List[Tuple[int, MappingDict]]] = [] +# for pc in pat_ccs: +# sz = pc.number_of_nodes() +# cand_cc_idx = [ +# i for i, hc in enumerate(host_ccs) if hc.number_of_nodes() >= sz +# ] +# if not cand_cc_idx: +# return [] # impossible – no room for this component + +# cc_maps: List[Tuple[int, MappingDict]] = [] +# for hi in cand_cc_idx: +# gm = GraphMatcher( +# host_ccs[hi], pc, node_match=node_match, edge_match=edge_match +# ) +# for iso in gm.subgraph_monomorphisms_iter(): +# cc_maps.append((hi, {p: h for h, p in iso.items()})) +# if max_results and len(cc_maps) >= max_results: +# break +# if max_results and len(cc_maps) >= max_results: +# break + +# if not cc_maps: # this pattern-CC embeds nowhere +# return [] +# per_cc.append(cc_maps) + +# # 4) order pattern-CCs by fewest embeddings → best pruning +# order = sorted(range(pcc), key=lambda i: len(per_cc[i])) +# ordered = [per_cc[i] for i in order] + +# # 5) backtrack to build full-pattern mappings +# results: List[MappingDict] = [] +# used_host: Set[int] = set() + +# def backtrack(level: int, accum: MappingDict): +# if max_results and len(results) >= max_results: +# return +# if level == pcc: +# results.append(accum.copy()) +# return +# for hi, mapping in ordered[level]: +# if hi in used_host or any(p in accum for p in mapping): +# continue +# used_host.add(hi) +# accum.update(mapping) +# backtrack(level + 1, accum) +# for p in mapping: +# accum.pop(p) +# used_host.remove(hi) +# if max_results and len(results) >= max_results: +# return + +# backtrack(0, {}) +# return results + +# # ------------------------------------------------------------------ +# # Strategy: BACKTRACK – component first, fallback to VF2 ----------- +# # ------------------------------------------------------------------ +# @staticmethod +# def _find_bt_subgraph_mappings( +# host: nx.Graph, +# pattern: nx.Graph, +# *, +# node_attrs: List[str], +# edge_attrs: List[str], +# max_results: Optional[int] = None, +# strict_cc_count: bool = False, +# ) -> List[MappingDict]: +# """Component-aware search *with* classic fallback if any CC fails.""" + +# primary = SubgraphSearchEngine._find_component_aware_subgraph_mappings( +# host, +# pattern, +# node_attrs=node_attrs, +# edge_attrs=edge_attrs, +# max_results=max_results, +# strict_cc_count=strict_cc_count, +# ) +# if primary: +# return primary +# return SubgraphSearchEngine._find_all_subgraph_mappings( +# host, +# pattern, +# node_attrs=node_attrs, +# edge_attrs=edge_attrs, +# max_results=max_results, +# ) + +# # ------------------------------------------------------------------ +# # Duplicate suppression via automorphisms -------------------------- +# # ------------------------------------------------------------------ +# @staticmethod +# def _filter_automorphic_duplicates( +# pattern: nx.Graph, +# mappings: List[MappingDict], +# *, +# node_match, +# edge_match, +# ) -> List[MappingDict]: +# """Throw away mappings that differ only by a pattern automorphism.""" + +# # 1) compute automorphism group of *pattern* +# gm_self = GraphMatcher( +# pattern, pattern, node_match=node_match, edge_match=edge_match +# ) +# autos = list(gm_self.subgraph_isomorphisms_iter()) +# if len(autos) <= 1: +# return mappings # no non-trivial symmetry + +# sorted_nodes = tuple(sorted(pattern.nodes())) +# seen: Set[Tuple[int, ...]] = set() +# unique: List[MappingDict] = [] + +# # 2) canonical fingerprint for each mapping +# for m in mappings: +# canon = min( +# tuple(m[phi[n]] for n in sorted_nodes) for phi in autos +# ) +# if canon not in seen: +# seen.add(canon) +# unique.append(m) + +# return unique + +# # ------------------------------------------------------------------ +# # Niceties ---------------------------------------------------------- +# # ------------------------------------------------------------------ +# def __repr__(self) -> str: # noqa: D401 – simple repr +# return "" + +# __str__ = __repr__ + +# @property +# def help(self) -> str: # noqa: D401 – property for convenience +# """Return the full module docstring.""" +# return __doc__ diff --git a/synkit/Graph/Wildcard/radwc.py b/synkit/Graph/Wildcard/radwc.py new file mode 100644 index 0000000..0c92c98 --- /dev/null +++ b/synkit/Graph/Wildcard/radwc.py @@ -0,0 +1,117 @@ +import re +from rdkit import Chem +from rdkit.Chem import SanitizeFlags +from typing import Tuple, Optional + + +class RadWC: + """ + Static utility for appending wildcard dummy atoms ([*]) with atom-map + indices to all radical centers **in the product block** of a reaction SMILES. + + - Reactant and agent blocks are not modified. + - Only atoms in the product with unpaired electrons are considered. + - Each product radical gets a new [*:N] with unique map number (auto or user-supplied). + + Example + ------- + >>> rxn = '[CH2:1][OH:2]>>[CH2:1][O:2]' + >>> RadWC.transform(rxn) + '[CH2:1][OH:2]>>[CH2:1][O:2]' + >>> rxn2 = '[CH2:1][OH:2]>>[CH:1].[OH:2]' + >>> RadWC.transform(rxn2) + '[CH2:1][OH:2]>>[CH:1]([*:3]).[OH:2]' + """ + + @staticmethod + def transform(rxn_smiles: str, start_map: Optional[int] = None) -> str: + """ + Add [*] wildcards (with atom-map index) to every radical in the + product block of the input reaction SMILES. + + :param rxn_smiles: Reaction SMILES, 2 or 3 blocks (R>>P or R>A>P). + :type rxn_smiles: str + :param start_map: Optional; first atom-map index for wildcards. + :type start_map: int or None + :returns: Modified reaction SMILES with product wildcards. + :rtype: str + :raises ValueError: On parse error or invalid input. + + Example + ------- + >>> RadWC.transform('[CH2:1][OH:2]>>[CH:1].[OH:2]') + '[CH2:1][OH:2]>>[CH:1]([*:3]).[OH:2]' + """ + react_blk, agents_blk, prod_blk = RadWC._split_reaction(rxn_smiles) + # Determine atom-map to use for wildcards + existing = [int(n) for n in re.findall(r":(\d+)", rxn_smiles)] + next_map = ( + start_map if start_map is not None else (max(existing, default=0) + 1) + ) + + prod_frags = prod_blk.split(".") if prod_blk else [] + new_prod_frags = [] + + keep_ops = SanitizeFlags.SANITIZE_ALL & ~SanitizeFlags.SANITIZE_ADJUSTHS + + for smi in prod_frags: + if not smi: + continue + mol = Chem.MolFromSmiles(smi, sanitize=False) + if mol is None: + raise ValueError(f"Cannot parse product SMILES fragment: {smi}") + Chem.SanitizeMol(mol, sanitizeOps=keep_ops) + rw = Chem.RWMol(mol) + atoms = list(rw.GetAtoms()) + changed = False + for atom in atoms: + rad = atom.GetNumRadicalElectrons() + if rad > 0: + for _ in range(rad): + dummy = Chem.Atom(0) + dummy.SetAtomMapNum(next_map) + dummy.SetNoImplicit(True) + rw.AddAtom(dummy) + rw.AddBond( + atom.GetIdx(), rw.GetNumAtoms() - 1, Chem.BondType.SINGLE + ) + next_map += 1 + changed = True + if changed: + Chem.SanitizeMol(rw.GetMol(), sanitizeOps=keep_ops) + new_prod_frags.append( + Chem.MolToSmiles(rw.GetMol(), isomericSmiles=True, allHsExplicit=True) + ) + + prod_str = ".".join(new_prod_frags) + if agents_blk is None: + return f"{react_blk}>>{prod_str}" + return f"{react_blk}>{agents_blk}>{prod_str}" + + @staticmethod + def _split_reaction(rxn: str) -> Tuple[str, Optional[str], str]: + """ + Split a reaction SMILES into (reactant, agent or None, product). + + :param rxn: Reaction SMILES string. + :type rxn: str + :returns: (reactant, agent, product) tuple (agent may be None). + :rtype: Tuple[str, Optional[str], str] + :raises ValueError: If the SMILES does not contain 2 or 3 '>'s. + """ + parts = rxn.split(">") + if len(parts) == 2: + return parts[0], None, parts[1] + if len(parts) == 3: + return parts[0], parts[1], parts[2] + raise ValueError("Reaction SMILES must contain 2 or 3 '>' symbols") + + @staticmethod + def describe(): + """ + Print a description and minimal example. + """ + print(RadWC.__doc__) + + def __repr__(self): + return "" diff --git a/synkit/Graph/Wildcard/wildcard.py b/synkit/Graph/Wildcard/wildcard.py new file mode 100644 index 0000000..6766567 --- /dev/null +++ b/synkit/Graph/Wildcard/wildcard.py @@ -0,0 +1,230 @@ +import networkx as nx +from typing import Dict, Any, Tuple, Optional +from synkit.IO import rsmi_to_graph, graph_to_smi + + +class WildCard: + """ + Static utility class for generating reaction SMILES with wildcards by + augmenting the product graph with subgraphs unique to the reactant and + patching lost external connections with wildcard atoms ('*'). + + Optionally, can rebalance the reactant side to ensure both sides have + matching atom maps (by adding wildcard atoms if needed). + + All methods are static and do not store any internal state. + + Example + ------- + >>> WildCard.rsmi_with_wildcards('CCO>>CC') + 'CCO>>CC*' + + >>> WildCard.rsmi_with_wildcards('CCO>>CC', rebalance=True) + 'CCO*>>CC*' + """ + + @staticmethod + def rsmi_with_wildcards( + rsmi: str, + attributes_defaults: Optional[Dict[str, Any]] = None, + rebalance: bool = False, + ) -> str: + """ + Given a reaction SMILES string, returns a new reaction SMILES where the product + side contains any disconnected subgraphs unique to the reactant, with lost + external bonds patched with wildcard atoms. Optionally, also adds wildcards to + the reactant side to ensure matching atom maps (rebalance). + + :param rsmi: Reaction SMILES (e.g., 'CCO>>CC') + :type rsmi: str + :param attributes_defaults: Optional dictionary of default attributes for wildcards + :type attributes_defaults: dict, optional + :param rebalance: Whether to rebalance the reactant side by adding wildcards + :type rebalance: bool + :returns: Augmented reaction SMILES string + :rtype: str + :raises ValueError: If parsing or output generation fails. + + Example + ------- + >>> WildCard.rsmi_with_wildcards('CCO>>CC') + 'CCO>>CC*' + >>> WildCard.rsmi_with_wildcards('CCO>>CC', rebalance=True) + 'CCO*>>CC*' + """ + r, p = WildCard.from_rsmi(rsmi) + new_r, new_p = WildCard.add_unique_subgraph_with_wildcards( + r, p, attributes_defaults, rebalance=rebalance + ) + try: + return f"{WildCard.to_smi(new_r)}>>{WildCard.to_smi(new_p)}" + except Exception as e: + raise ValueError( + "Could not convert to RSMI after wildcard patching." + ) from e + + @staticmethod + def add_unique_subgraph_with_wildcards( + G: nx.Graph, + H: nx.Graph, + attributes_defaults: Optional[Dict[str, Any]] = None, + rebalance: bool = False, + ) -> Tuple[nx.Graph, nx.Graph]: + """ + Add the subgraph unique to G as a disconnected union to H, + and patch lost external connections with plain wildcard bonds. + Optionally, rebalance the reactant side to ensure both sides have + matching atom maps by adding wildcards. + + :param G: Reactant graph + :type G: nx.Graph + :param H: Product graph + :type H: nx.Graph + :param attributes_defaults: Optional attribute defaults for wildcard nodes + :type attributes_defaults: dict, optional + :param rebalance: Whether to rebalance the reactant side with wildcards + :type rebalance: bool + :returns: Tuple (new_G, new_H) with both graphs possibly augmented by wildcards + :rtype: Tuple[nx.Graph, nx.Graph] + :raises ValueError: If G or H are not valid graphs. + + Example + ------- + >>> r, p = WildCard.from_rsmi('CCO>>CC') + >>> r2, p2 = WildCard.add_unique_subgraph_with_wildcards(r, p, rebalance=True) + """ + if not isinstance(G, nx.Graph) or not isinstance(H, nx.Graph): + raise ValueError("G and H must be networkx.Graph instances") + if G.number_of_nodes() == 0 or H.number_of_nodes() == 0: + raise ValueError("Both G and H must have at least one node.") + if not all("atom_map" in d for _, d in G.nodes(data=True)): + raise ValueError( + "All reactant nodes must have 'atom_map' attributes for unique subgraph logic." + ) + if not all("atom_map" in d for _, d in H.nodes(data=True)): + raise ValueError( + "All product nodes must have 'atom_map' attributes for unique subgraph logic." + ) + + if attributes_defaults is None: + attributes_defaults = { + "element": "*", + "aromatic": False, + "hcount": 0, + "charge": 0, + "neighbors": [], + } + + # Make working copies + G_new = G.copy() + H_new = H.copy() + + # --------------------------- + # 1. PATCH PRODUCT SIDE (add unique reactant subgraphs and wildcards) + # --------------------------- + react_atom_maps = {d["atom_map"] for _, d in G.nodes(data=True)} + prod_atom_maps = {d["atom_map"] for _, d in H.nodes(data=True)} + + # Identify nodes and subgraphs unique to reactant + unique_atom_maps = react_atom_maps - prod_atom_maps + node_map = {d["atom_map"]: n for n, d in G.nodes(data=True)} + unique_nodes = [node_map[a] for a in unique_atom_maps if a in node_map] + + G_unique = G.subgraph(unique_nodes).copy() + # Add unique reactant fragments to product + for n, d in G_unique.nodes(data=True): + H_new.add_node(n, **d) + for u, v, d in G_unique.edges(data=True): + H_new.add_edge(u, v, **d) + + # Add wildcards to patch lost external bonds (reactant → outside) + existing_ids = set(H_new.nodes) + next_id = ( + max([n for n in existing_ids if isinstance(n, int)], default=0) + 1 + if existing_ids + else 1 + ) + + for n in unique_nodes: + for nbr in G.neighbors(n): + nbr_map = G.nodes[nbr]["atom_map"] + if nbr_map not in unique_atom_maps: + wc_id = next_id + next_id += 1 + H_new.add_node( + wc_id, + **attributes_defaults, + atom_map=wc_id, + typesGH=(("*", False, 0, 0, []), ("*", False, 0, 0, [])), + ) + H_new.add_edge(n, wc_id) + + # --------------------------- + # 2. REBALANCE REACTANT SIDE (add wildcards to reactant if required) + # --------------------------- + if rebalance: + prod_atom_maps = {d["atom_map"] for _, d in H_new.nodes(data=True)} + missing_in_react = prod_atom_maps - react_atom_maps + if missing_in_react: + react_existing_ids = set(G_new.nodes) + react_next_id = ( + max( + [n for n in react_existing_ids if isinstance(n, int)], default=0 + ) + + 1 + if react_existing_ids + else 1 + ) + for missing_map in missing_in_react: + wc_id = react_next_id + react_next_id += 1 + G_new.add_node( + wc_id, + **attributes_defaults, + atom_map=missing_map, + typesGH=(("*", False, 0, 0, []), ("*", False, 0, 0, [])), + ) + + return G_new, H_new + + @staticmethod + def from_rsmi(rsmi: str) -> Tuple[nx.Graph, nx.Graph]: + """ + Convert a reaction SMILES string into reactant and product graphs. + + :param rsmi: Reaction SMILES string + :type rsmi: str + :returns: Tuple (reactant_graph, product_graph) + :rtype: Tuple[nx.Graph, nx.Graph] + :raises ValueError: If input cannot be parsed. + """ + try: + return rsmi_to_graph(rsmi) + except Exception as e: + raise ValueError(f"Could not parse RSMI: {rsmi}") from e + + @staticmethod + def to_smi(G: nx.Graph) -> str: + """ + Convert a networkx molecular graph to a canonical SMILES string. + + :param G: Molecular graph + :type G: nx.Graph + :returns: SMILES string + :rtype: str + :raises ValueError: If conversion fails. + """ + try: + return graph_to_smi(G) + except Exception as e: + raise ValueError("Could not convert graph to SMILES") from e + + @staticmethod + def describe(): + """ + Print a description and usage example for this class. + """ + print(WildCard.__doc__) + + def __repr__(self): + return "" diff --git a/synkit/IO/combinatorial/__init__.py b/synkit/IO/combinatorial/__init__.py new file mode 100644 index 0000000..5b3e638 --- /dev/null +++ b/synkit/IO/combinatorial/__init__.py @@ -0,0 +1,8 @@ +import warnings + +warnings.warn( + "⚠️ This module is under active development and may be unstable. " + "APIs, behaviors, or outputs are subject to change without notice.", + category=UserWarning, + stacklevel=2, +) diff --git a/synkit/IO/combinatorial/gml_to_graph.py b/synkit/IO/combinatorial/gml_to_graph.py new file mode 100644 index 0000000..a01b23a --- /dev/null +++ b/synkit/IO/combinatorial/gml_to_graph.py @@ -0,0 +1,254 @@ +import re +import networkx as nx +from typing import Tuple, List, Dict + + +class GMLToGraph: + """ + Parses a GML-like reaction rule into three NetworkX graphs: reactant (left), + conserved context (context), and product (right). Preserves atom-map indices + and original SMARTS labels, and attaches placeholder constraints (both + table-style and multi-block Rest-style) to node and edge attributes. + + Parameters + ---------- + gml_text : str + The GML-like reaction rule text, containing 'left', 'context', 'right', + and 'constrainLabelAny' sections. + + Attributes + ---------- + graphs : Dict[str, nx.Graph] + A mapping of section names ('left', 'context', 'right') to the + corresponding parsed graphs. + placeholder_constraints : Dict[str, List[str]] + A mapping from placeholder labels (e.g. '_X') to their allowed values, + extracted from the 'constrainLabelAny' block. + """ + + def __init__(self, gml_text: str): + """ + Initialize the parser with the full GML text. + + Parameters + ---------- + gml_text : str + GML-like rule text to be parsed. + + Raises + ------ + ValueError + If the provided gml_text is empty. + """ + if not gml_text: + raise ValueError("gml_text must be a non-empty string") + self.gml_text = gml_text + self.graphs: Dict[str, nx.Graph] = { + sec: nx.Graph() for sec in ("left", "context", "right") + } + self.placeholder_constraints: Dict[str, List[str]] = {} + + def _parse_element(self, line: str, sec: str) -> None: + """ + Parse a single GML line describing a node or an edge and insert it + into the specified graph section. + + Parameters + ---------- + line : str + A line starting with 'node' or 'edge', e.g., 'node [ id 1 label "C" ]'. + sec : str + The target section in 'left', 'context', or 'right'. + + Raises + ------ + ValueError + If `sec` is not one of 'left', 'context', or 'right'. + """ + if sec not in self.graphs: + raise ValueError(f"Unknown section: {sec}") + tokens = line.split() + order_map = {"-": 1, ":": 1.5, "=": 2, "#": 3} + if tokens[0] == "node": + # Extract node attributes + nid = int(tokens[tokens.index("id") + 1]) + raw_label = tokens[tokens.index("label") + 1].strip('"') + m = re.fullmatch(r"([A-Za-z*_]+?)(\d+)?([+-])?", raw_label) + if m: + element = m.group(1) + charge = int(m.group(2)) if m.group(2) else 1 if m.group(3) else 0 + if m.group(3) == "-": + charge = -charge + else: + element = raw_label + charge = 0 + attrs = { + "element": element, + "charge": charge, + "atom_map": nid, + "hcount": 0, + "label": raw_label, + } + self.graphs[sec].add_node(nid, **attrs) + elif tokens[0] == "edge": + # Extract edge attributes + src = int(tokens[tokens.index("source") + 1]) + tgt = int(tokens[tokens.index("target") + 1]) + lbl = tokens[tokens.index("label") + 1].strip('"') + order = order_map.get(lbl, 0) + self.graphs[sec].add_edge(src, tgt, order=order, label=lbl) + + def _synchronize(self) -> None: + """ + Ensure that every node and edge in the 'context' graph also appears in + both 'left' and 'right' graphs, carrying over node and edge attributes. + """ + ctx = self.graphs["context"] + # Nodes + for n, data in ctx.nodes(data=True): + for sec in ("left", "right"): + if n not in self.graphs[sec]: + self.graphs[sec].add_node(n, **data) + else: + self.graphs[sec].nodes[n].update(data) + # Edges + for u, v, data in ctx.edges(data=True): + for sec in ("left", "right"): + if not self.graphs[sec].has_edge(u, v): + self.graphs[sec].add_edge(u, v, **data) + + def _parse_constraints(self, lines: List[str], idx: int) -> int: + """ + Parse placeholder constraints from the 'constrainLabelAny' block, + supporting both table and multi-block Rest styles. + + Parameters + ---------- + lines : List[str] + All lines of the GML text. + idx : int + The index of the first line inside the '[' of the block. + + Returns + ------- + int + The index of the closing ']' line of the block. + """ + while idx < len(lines): + line = lines[idx].strip() + if line.startswith("]"): + return idx + m = re.match(r'label\s+"[^\(]+\(([^)]+)\)"', line) + if m: + placeholders = [p.strip() for p in m.group(1).split(",")] + # Table style if multiple placeholders + if len(placeholders) > 1: + table: List[Tuple[str, ...]] = [] + idx += 1 + # Find rows + while idx < len(lines) and "labels [" not in lines[idx]: + idx += 1 + while idx < len(lines): + row_line = lines[idx].strip() + matches = re.findall(r'label\s+"[^\(]+\(([^)]+)\)"', row_line) + for row in matches: + table.append(tuple(val.strip() for val in row.split(","))) + if "]" in row_line: + break + idx += 1 + for j, ph in enumerate(placeholders): + self.placeholder_constraints[ph] = [row[j] for row in table] + idx += 1 + continue + # Multi-block Rest style + ph = placeholders[0] + self.placeholder_constraints[ph] = [] + idx += 1 + while idx < len(lines) and "labels [" not in lines[idx]: + idx += 1 + if idx < len(lines): + while idx < len(lines): + lbl_line = lines[idx].strip() + matches = re.findall(r'label\s+"[^\(]+\(([^)]+)\)"', lbl_line) + self.placeholder_constraints[ph].extend(matches) + if "]" in lbl_line: + break + idx += 1 + idx += 1 + continue + idx += 1 + return idx + + def _attach_constraints(self) -> None: + """ + Attach the parsed placeholder constraints to nodes (as 'constraint') + and edges (as 'bond_constraint') across all graphs, and store the + raw mapping on the context graph's .graph metadata. + """ + pc = self.placeholder_constraints + for graph in self.graphs.values(): + for n, data in graph.nodes(data=True): + lbl = data.get("label") + if lbl in pc: + data["constraint"] = pc[lbl] + for u, v, data in graph.edges(data=True): + lbl = data.get("label") + if lbl in pc: + data["bond_constraint"] = pc[lbl] + self.graphs["context"].graph["placeholder_constraints"] = pc + + def transform(self) -> Tuple[nx.Graph, nx.Graph, nx.Graph]: + """ + Parse the GML text, build the left, right, and context graphs, and return them. + + Returns + ------- + Tuple[nx.Graph, nx.Graph, nx.Graph] + A tuple (left_graph, right_graph, context_graph), each with attached constraints. + """ + lines = self.gml_text.splitlines() + section: str = "" + i = 0 + while i < len(lines): + line = lines[i].strip() + if line.startswith("constrainLabelAny"): + while i < len(lines) and not lines[i].strip().endswith("["): + i += 1 + i = self._parse_constraints(lines, i + 1) + elif any(line.startswith(x) for x in ("left", "right", "context")): + section = line.split("[")[0].strip() + elif line.startswith(("node", "edge")) and section: + self._parse_element(line, section) + i += 1 + self._synchronize() + self._attach_constraints() + return (self.graphs["left"], self.graphs["right"], self.graphs["context"]) + + def __repr__(self) -> str: + """ + Return a summary indicating the number of nodes in each graph. + + Returns + ------- + str + A brief representation with node counts. + """ + return ( + f"" + ) + + def help(self) -> str: + """ + Return a usage summary for the GMLToGraph parser. + + Returns + ------- + str + Multi-line help text explaining the API. + """ + return ( + "GMLToGraph(gml_text) -> (left, right, context) graphs\n" + "Supports Nuc-style and Rest-style constraint parsing." + ) diff --git a/synkit/IO/combinatorial/graph_to_gml.py b/synkit/IO/combinatorial/graph_to_gml.py new file mode 100644 index 0000000..b9c2651 --- /dev/null +++ b/synkit/IO/combinatorial/graph_to_gml.py @@ -0,0 +1,291 @@ +import networkx as nx +from typing import Dict, List, Tuple, Any, Set + + +class GraphToGML: + """ + Convert two NetworkX graphs into a minimal GML reaction rule string, + using canonical context detection and SynKit-style constraint annotations. + + This class identifies the conserved context (nodes/edges unchanged between + reactant and product graphs), extracts minimal changing portions, + and generates a GML block including left, context, right, and placeholder constraints. + + :param left: Graph representing the reactant state. + :type left: nx.Graph + :param right: Graph representing the product state. + :type right: nx.Graph + :param rule_id: Identifier for the reaction rule (default: "1"). + :type rule_id: str + :raises ValueError: If input graphs have mismatched mapping nodes. + + :Example: + >>> from networkx import Graph + >>> G1, G2 = Graph(), Graph() + >>> # populate G1, G2 with atom_map nodes, constraints, etc. + >>> g2g = GraphToGML(G1, G2, rule_id="rxn1") + >>> gml = g2g.to_gml() + >>> print(gml) + """ + + def __init__(self, left: nx.Graph, right: nx.Graph, rule_id: str = "1") -> None: + """ + Initialize the GraphToGML converter. + + :param left: Reactant graph with node and edge attributes. + :type left: nx.Graph + :param right: Product graph with node and edge attributes. + :type right: nx.Graph + :param rule_id: Unique identifier for the rule output. + :type rule_id: str + :returns: None + :rtype: None + :raises ValueError: If graphs have nodes without atom_map attribute. + """ + self.left: nx.Graph = left + self.right: nx.Graph = right + self.rule_id: str = rule_id + + self.context_nodes: Set[int] = set() + self.context_edges: Set[Tuple[int, int]] = set() + self.left_nodes: Set[int] = set() + self.right_nodes: Set[int] = set() + self.left_edges: List[Tuple[int, int, Dict[str, Any]]] = [] + self.right_edges: List[Tuple[int, int, Dict[str, Any]]] = [] + self.context: nx.Graph = nx.Graph() + self.constraint_dict: Dict[str, List[str]] = {} + + @staticmethod + def same_node(nl: Dict[str, Any], nr: Dict[str, Any]) -> bool: + """ + Compare two node attribute dictionaries, ignoring 'atom_map'. + + :param nl: Node attribute dict from left graph. + :type nl: Dict[str, Any] + :param nr: Node attribute dict from right graph. + :type nr: Dict[str, Any] + :returns: True if all attributes (except 'atom_map') match. + :rtype: bool + """ + keys = set(nl) | set(nr) + keys.discard("atom_map") + return all(nl.get(k) == nr.get(k) for k in keys) + + @staticmethod + def same_edge(el: Dict[str, Any], er: Dict[str, Any]) -> bool: + """ + Compare two edge attribute dictionaries for equality. + + :param el: Edge attribute dict from left graph. + :type el: Dict[str, Any] + :param er: Edge attribute dict from right graph. + :type er: Dict[str, Any] + :returns: True if all edge attributes match. + :rtype: bool + """ + keys = set(el) | set(er) + return all(el.get(k) == er.get(k) for k in keys) + + def compute(self) -> None: + """ + Compute conserved context and minimal changing nodes/edges, + then collect placeholder constraints from context graph. + + :returns: None + :rtype: None + """ + # Identify conserved context nodes + self.context_nodes = { + n + for n in set(self.left.nodes) & set(self.right.nodes) + if self.same_node(self.left.nodes[n], self.right.nodes[n]) + } + # Identify conserved context edges + self.context_edges = { + tuple(sorted((u, v))) + for u, v in set(self.left.edges) & set(self.right.edges) + if u in self.context_nodes + and v in self.context_nodes + and self.same_edge(self.left.edges[u, v], self.right.edges[u, v]) + } + # Compute minimal changing edges and nodes + self.left_edges, left_extra = self.get_changing_edges_and_nodes( + self.left, self.context_edges, self.context_nodes + ) + self.right_edges, right_extra = self.get_changing_edges_and_nodes( + self.right, self.context_edges, self.context_nodes + ) + self.left_nodes = { + n for n in self.left.nodes if n not in self.context_nodes + } | left_extra + self.right_nodes = { + n for n in self.right.nodes if n not in self.context_nodes + } | right_extra + # Build context subgraph + self.context = nx.Graph() + for n in sorted(self.context_nodes): + self.context.add_node(n, **self.left.nodes[n]) + for u, v in self.context_edges: + self.context.add_edge(u, v, **self.left.edges[u, v]) + # Gather constraints from context + self.constraint_dict.clear() + for _, d in self.context.nodes(data=True): + if "constraint" in d: + self.constraint_dict[d["label"]] = d["constraint"] + for _, _, d in self.context.edges(data=True): + if "bond_constraint" in d: + self.constraint_dict[d["label"]] = d["bond_constraint"] + + @staticmethod + def get_changing_edges_and_nodes( + G: nx.Graph, context_edges: Set[Tuple[int, int]], context_nodes: Set[int] + ) -> Tuple[List[Tuple[int, int, Dict[str, Any]]], Set[int]]: + """ + Identify edges and nodes in G that are not in the conserved context. + + :param G: Input graph. + :type G: nx.Graph + :param context_edges: Edges part of context. + :type context_edges: Set[Tuple[int, int]] + :param context_nodes: Nodes part of context. + :type context_nodes: Set[int] + :returns: A tuple of (changed_edges, changed_nodes). + :rtype: Tuple[List[Tuple[int, int, Dict[str, Any]]], Set[int]] + """ + changed_edges: List[Tuple[int, int, Dict[str, Any]]] = [] + changed_nodes: Set[int] = set() + for u, v in G.edges(): + key = tuple(sorted((u, v))) + if key not in context_edges: + changed_edges.append((u, v, G.edges[u, v])) + if u not in context_nodes: + changed_nodes.add(u) + if v not in context_nodes: + changed_nodes.add(v) + return changed_edges, changed_nodes + + @staticmethod + def graph_section( + name: str, + G: nx.Graph, + nodes: Set[int], + edges: List[Tuple[int, int, Dict[str, Any]]], + ) -> List[str]: + """ + Render a GML block for a subgraph section. + + :param name: Section name ('left', 'context', or 'right'). + :type name: str + :param G: Graph containing nodes/edges. + :type G: nx.Graph + :param nodes: Node IDs to include. + :type nodes: Set[int] + :param edges: Edges to include (u,v,attr). + :type edges: List[Tuple[int,int,Dict[str,Any]]] + :returns: Lines of GML representing the section. + :rtype: List[str] + """ + lines: List[str] = [f" {name} ["] + for n in sorted(nodes): + d = G.nodes[n] + lbl = d.get("label", d.get("element", str(n))) + lines.append(f' node [ id {n} label "{lbl}" ]') + order_map = {1: "-", 1.5: ":", 2: "=", 3: "#"} + for u, v, d in sorted(edges): + lbl = d.get("label", order_map.get(d.get("order", 1), "-")) + lines.append(f' edge [ source {u} target {v} label "{lbl}" ]') + lines.append(" ]") + return lines + + @staticmethod + def context_section(G: nx.Graph) -> List[str]: + """ + Render the conserved context GML block. + + :param G: Context graph. + :type G: nx.Graph + :returns: Lines of GML for context. + :rtype: List[str] + """ + lines: List[str] = [" context []"] # placeholder, updated in full block + lines = [" context ["] + for n, d in sorted(G.nodes(data=True)): + lbl = d.get("label", d.get("element", str(n))) + lines.append(f' node [ id {n} label "{lbl}" ]') + order_map = {1: "-", 1.5: ":", 2: "=", 3: "#"} + for u, v, d in sorted(G.edges(data=True)): + lbl = d.get("label", order_map.get(d.get("order", 1), "-")) + lines.append(f' edge [ source {u} target {v} label "{lbl}" ]') + lines.append(" ]") + return lines + + def constraints_section(self) -> List[str]: + """ + Render placeholder constraints as one constrainLabelAny block. + + :returns: Lines of GML for constraints. + :rtype: List[str] + """ + lines: List[str] = [] + if not self.constraint_dict: + return lines + lines.append(" constrainLabelAny [") + for ph, children in self.constraint_dict.items(): + lines.append(f' label "Rest({ph})"') + if children: + labels_inner = " ".join(f'label "Rest({c})"' for c in children) + lines.append(f" labels [{labels_inner}]") + else: + lines.append(" labels []") + lines.append(" ]") + return lines + + def to_gml(self) -> str: + """ + Generate the full GML reaction rule string. + + :returns: Complete GML string for the reaction rule. + :rtype: str + """ + self.compute() + out: List[str] = ["rule [", f' ruleID "{self.rule_id}"'] + out += self.graph_section("left", self.left, self.left_nodes, self.left_edges) + out += self.context_section(self.context) + out += self.graph_section( + "right", self.right, self.right_nodes, self.right_edges + ) + out += self.constraints_section() + out.append("]") + return "\n".join(out) + + def __repr__(self) -> str: + """ + Return a summary of the rule converter. + + :returns: Brief description with node counts. + :rtype: str + """ + return ( + f"" + ) + + def help(self) -> str: + """ + Show usage instructions for GraphToGML. + + :returns: Multi-line help text. + :rtype: str + """ + return ( + "GraphToGML(left, right, rule_id='374')\n" + " - left: nx.Graph for reactant state\n" + " - right: nx.Graph for product state\n" + " - rule_id: identifier for the GML rule\n" + "\n" + "Usage:\n" + " g2g = GraphToGML(G_left, G_right, rule_id='374')\n" + " print(g2g.to_gml())\n" + ) diff --git a/synkit/IO/combinatorial/graph_to_smarts.py b/synkit/IO/combinatorial/graph_to_smarts.py new file mode 100644 index 0000000..959ab28 --- /dev/null +++ b/synkit/IO/combinatorial/graph_to_smarts.py @@ -0,0 +1,189 @@ +import networkx as nx +from typing import List, Dict, Any, Set, Optional +from rdkit import Chem +from rdkit.Chem import AllChem + + +class GraphToSMARTS: + """ + Convert NetworkX graphs (with placeholder nodes/constraints) into SMARTS or reaction SMARTS strings. + + :param placeholder_labels: Set of labels recognized as placeholders (e.g., '_R', 'X', 'Y', 'Z'). + :type placeholder_labels: Optional[Set[str]] + :param validate: If True, validate generated SMARTS or reaction SMARTS using RDKit. + :type validate: bool + :raises: None + + :Example: + >>> G = nx.Graph() + >>> G.add_node(1, label='C', constraint=None) + >>> G.add_node(2, label='O', constraint=None) + >>> G.add_edge(1, 2, order=1) + >>> g2s = GraphToSMARTS() + >>> smarts = g2s.graph_to_smarts(G) + >>> isinstance(smarts, str) + True + """ + + def __init__( + self, placeholder_labels: Optional[Set[str]] = None, validate: bool = True + ) -> None: + """ + Initialize the GraphToSMARTS converter. + + :param placeholder_labels: Labels to treat as wildcard placeholders; defaults to {'_R','X','Y','Z'}. + :type placeholder_labels: Optional[Set[str]] + :param validate: Whether to validate SMARTS with RDKit if available. + :type validate: bool + :returns: None + :rtype: None + """ + if placeholder_labels is None: + placeholder_labels = {"_R", "X", "Y", "Z"} + self.placeholder_labels: Set[str] = placeholder_labels + self.validate: bool = validate + + def graph_to_smarts(self, G: nx.Graph) -> str: + """ + Convert a NetworkX graph into a SMARTS string representation. + + :param G: NetworkX Graph with node attributes: + - 'label': str atomic label or placeholder + - 'constraint': Optional[List[str]] allowed element list for placeholders + and edge attribute: + - 'order': float bond order (1,1.5,2,3) + :type G: nx.Graph + :returns: SMARTS string encoding the graph structure. + :rtype: str + :raises ValueError: If RDKit fails to parse the generated SMARTS when validate=True. + + :Example: + >>> G = nx.Graph() + >>> G.add_node(1, label='C', constraint=None) + >>> G.add_node(2, label='O', constraint=None) + >>> G.add_edge(1, 2, order=1) + >>> smarts = GraphToSMARTS().graph_to_smarts(G) + >>> smarts + '[C:1](-[O:2])' + """ + bond_sym: Dict[float, str] = {1: "-", 1.5: ":", 2: "=", 3: "#"} + + def bracket(node: Any, data: Dict[str, Any]) -> str: + map_num = node + if data.get("constraint"): + core = ",".join(data["constraint"]) + else: + core = data["label"] + return f"[{core}:{map_num}]" + + def choose_root(sub: nx.Graph) -> Any: + real_nodes = [ + n + for n, d in sub.nodes(data=True) + if d.get("label") not in self.placeholder_labels + ] + if real_nodes: + return max(real_nodes, key=sub.degree) + return min(sub.nodes) + + def rec(node: Any, parent: Any, sub: nx.Graph, visited: Set[Any]) -> str: + visited.add(node) + s = bracket(node, sub.nodes[node]) + for nbr in sub.neighbors(node): + if nbr == parent: + continue + order = sub[node][nbr].get("order", 1) + bond = bond_sym.get(order, "-") + s += f"({bond}{rec(nbr, node, sub, visited)})" + return s + + frags: List[str] = [] + for comp in nx.connected_components(G): + subgraph = G.subgraph(comp) + root = choose_root(subgraph) + frags.append(rec(root, None, subgraph, set())) + + smarts: str = ".".join(frags) + + if self.validate: + try: + if not Chem.MolFromSmarts(smarts): + raise ValueError("RDKit could not parse generated SMARTS.") + except ImportError: + pass + + return smarts + + def graphs_to_rxn_smarts(self, reactant: nx.Graph, product: nx.Graph) -> str: + """ + Construct a reaction SMARTS string from reactant and product graphs. + + :param reactant: Reactant NetworkX graph. + :type reactant: nx.Graph + :param product: Product NetworkX graph. + :type product: nx.Graph + :returns: Reaction SMARTS in the form 'reactants>>products'. + :rtype: str + :raises ValueError: If RDKit fails to parse the reaction SMARTS when validate=True. + + :Example: + >>> G1 = nx.Graph() + >>> G1.add_node(1, label='C', constraint=None) + >>> G1.add_node(2, label='O', constraint=None) + >>> G1.add_edge(1, 2, order=1) + >>> G2 = nx.Graph() + >>> G2.add_node(1, label='C', constraint=None) + >>> G2.add_node(2, label='O', constraint=None) + >>> G2.add_edge(1, 2, order=2) + >>> rxn = GraphToSMARTS().graphs_to_rxn_smarts(G1, G2) + >>> rxn + '[C:1](-[O:2])>>[C:1]([O:2])=' + """ + sm_r: str = self.graph_to_smarts(reactant) + sm_p: str = self.graph_to_smarts(product) + rxn: str = f"{sm_r}>>{sm_p}" + + if self.validate: + try: + if not AllChem.ReactionFromSmarts(rxn): + raise ValueError("RDKit could not parse generated reaction SMARTS.") + except ImportError: + pass + + return rxn + + def __repr__(self) -> str: + """ + Return an unambiguous representation of the converter instance. + + :returns: String showing placeholder labels and validation setting. + :rtype: str + """ + return ( + f"" + ) + + def help(self) -> str: + """ + Provide usage information for GraphToSMARTS. + + :returns: Multi-line help string describing available methods. + :rtype: str + + :Example: + >>> print(GraphToSMARTS().help()) # doctest:+NORMALIZE_WHITESPACE + GraphToSMARTS(placeholder_labels=None, validate=True) + - Use .graph_to_smarts(G) for a single graph + - Use .graphs_to_rxn_smarts(G_react, G_prod) for reaction SMARTS + """ + return ( + "GraphToSMARTS(placeholder_labels=None, validate=True)\n" + " - Use .graph_to_smarts(G) for a single graph\n" + " - Use .graphs_to_rxn_smarts(G_react, G_prod) for reaction SMARTS\n" + "Node attributes expected:\n" + " label: str (e.g., 'C', '_R', 'H+')\n" + " constraint: Optional[List[str]] for placeholders\n" + "Edge attribute:\n" + " order: float (1, 1.5, 2, 3) mapped to '-', ':', '=', '#'\n" + ) diff --git a/synkit/IO/combinatorial/smarts_expander.py b/synkit/IO/combinatorial/smarts_expander.py new file mode 100644 index 0000000..e66b97f --- /dev/null +++ b/synkit/IO/combinatorial/smarts_expander.py @@ -0,0 +1,152 @@ +import re +import itertools +from typing import List, Dict, Tuple, Iterator, Union + + +class SMARTSExpander: + """ + Efficiently enumerate all valid reaction SMARTS by expanding atom-list + placeholders like [C,N,O,P,S:9], ensuring that each atom-map uses the same + element everywhere it appears (on both sides of a reaction). + + :param smarts: SMARTS string, possibly containing one or more atom-list placeholders. + :type smarts: str + :returns: Expanded SMARTS strings without atom-list placeholders. + :rtype: List[str] + :raises ValueError: If no valid expansions exist due to incompatible element lists. + + Example usage:: + + >>> rxn = ( + ... '[H+:6].[C:7](-[O:8](-[H:12]))(-[C,N,O,P,S:9])' + ... '(-[C,N,O,P,S:10])(-[H:11]).' + ... '[C:2](-[S:4](-[C,N,O,P,S:5]))(-[C,N,O,P,S:1])(=[O:3])>>' + ... '[S:4](-[H:6])(-[C,N,O,P,S:5]).[H+:12].' + ... '[C:7](-[O:8](-[C:2](-[C,N,O,P,S:1])(=[O:3])))(-[C,N,O,P,S:9])' + ... '(-[C,N,O,P,S:10])(-[H:11])' + ... ) + >>> ex = SMARTSExpander.expand(rxn) + >>> len(ex) + 625 + >>> ex[:3] # first three expansions + ['[H+:6].[C:7](-[O:8](-[H:12]))(-[C:9])(-[C:10])(-[H:11]).[C:2]...' + '>>', + '...', + '...'] + """ + + _PAT = re.compile(r"\[([A-Z][a-z]?(?:,[A-Z][a-z]?)*)(:[0-9]+)\]") + + @staticmethod + def _extract_map_to_elements(matches: List[re.Match]) -> Dict[str, List[str]]: + """ + Build a mapping from atom-map to the intersection of allowed elements. + + :param matches: List of regex match objects for placeholders. + :type matches: List[re.Match] + :returns: Dictionary mapping ":map" to sorted list of shared elements. + :rtype: Dict[str, List[str]] + """ + amap2set: Dict[str, set] = {} + for m in matches: + elems = set(m.group(1).split(",")) + amap = m.group(2) + if amap not in amap2set: + amap2set[amap] = elems + else: + amap2set[amap] &= elems + for amap, s in amap2set.items(): + if not s: + raise ValueError(f"No overlapping elements for atom-map {amap}") + return {amap: sorted(s) for amap, s in amap2set.items()} + + @staticmethod + def _build_template( + smarts: str, matches: List[re.Match] + ) -> Tuple[List[Union[str, str]], List[str]]: + """ + Build a list of string segments and placeholders for reconstruction. + + :param smarts: Original SMARTS string. + :type smarts: str + :param matches: Regex matches for placeholders. + :type matches: List[re.Match] + :returns: Tuple of list of segments and placeholder order. + :rtype: Tuple[List[Union[str, str]], List[str]] + """ + segments: List[Union[str, str]] = [] + placeholder_order: List[str] = [] + last = 0 + for m in matches: + # fmt: off + segments.append(smarts[last: m.start()]) + # fmt: on + amap = m.group(2) + segments.append(amap) + placeholder_order.append(amap) + last = m.end() + segments.append(smarts[last:]) + return segments, placeholder_order + + @classmethod + def expand_iter(cls, smarts: str) -> Iterator[str]: + """ + Yield expanded SMARTS strings lazily. + + :param smarts: SMARTS string with placeholders. + :type smarts: str + :yields: One expanded SMARTS string at a time. + :rtype: Iterator[str] + + :raises ValueError: If no valid expansions due to incompatible lists. + """ + matches = list(cls._PAT.finditer(smarts)) + if not matches: + yield smarts + return + + amap2els = cls._extract_map_to_elements(matches) + segments, order = cls._build_template(smarts, matches) + unique_maps = list(dict.fromkeys(order)) + + pools = [amap2els[am] for am in unique_maps] + for combo in itertools.product(*pools): + mapping = dict(zip(unique_maps, combo)) + out = [] + for seg in segments: + if seg in mapping: + out.append(f"[{mapping[seg]}{seg}]") + else: + out.append(seg) + yield "".join(out) + + @classmethod + def expand(cls, smarts: str) -> List[str]: + """ + Return a list of all expanded SMARTS. + + :param smarts: SMARTS string with placeholders. + :type smarts: str + :returns: List of expanded SMARTS strings. + :rtype: List[str] + + :raises ValueError: If no valid expansions exist. + """ + return list(cls.expand_iter(smarts)) + + +# # --- Example usage --- + +# if __name__ == "__main__": +# rxn = ( +# '[H+:6].[C:7](-[O:8](-[H:12]))(-[C,N,O,P,S:9])(-[C,N,O,P,S:10])(-[H:11]).' +# '[C:2](-[S:4](-[C,N,O,P,S:5]))(-[C,N,O,P,S:1])(=[O:3])>>' +# '[S:4](-[H:6])(-[C,N,O,P,S:5]).[H+:12].' +# '[C:7](-[O:8](-[C:2](-[C,N,O,P,S:1])(=[O:3])))(-[C,N,O,P,S:9])(-[C,N,O,P,S:10])(-[H:11])' +# ) +# n = 0 +# for i, s in enumerate(SMARTSExpander.expand_iter(rxn)): +# if i < 3 or i > 621: +# print(f"{i+1}: {s}") +# n += 1 +# print(f"Total: {n} enumerated SMARTS.") diff --git a/synkit/IO/combinatorial/smarts_generalizer.py b/synkit/IO/combinatorial/smarts_generalizer.py new file mode 100644 index 0000000..42854df --- /dev/null +++ b/synkit/IO/combinatorial/smarts_generalizer.py @@ -0,0 +1,134 @@ +import re +from typing import List, Set +from rdkit import Chem +from rdkit.Chem import rdChemReactions + + +class SMARTSGeneralizer: + """ + Generalizes a list of atom-mapped (reaction) SMARTS into one combinatorial SMARTS + with element-list placeholders at mapped atom positions. + Optionally validates output using RDKit. + + :param sanity_check: If True, validate the output SMARTS with RDKit. + :type sanity_check: bool + + Example + ------- + >>> input_smarts = [ + ... '[C:1]-[N:2]>>[N:1]-[C:2]', + ... '[N:1]-[N:2]>>[N:1]-[N:2]', + ... '[O:1]-[N:2]>>[N:1]-[N:2]' + ... ] + >>> gen = SMARTSGeneralizer() + >>> print(gen.generalize(input_smarts)) + [C,N,O:1]-[N:2]>>[N:1]-[C,N,O:2] + """ + + _atom_pat = re.compile(r"\[([A-Z][a-z]?):(\d+)\]") + + def __init__(self, sanity_check: bool = True): + """ + Initialize SMARTSGeneralizer. + + :param sanity_check: If True, validate the output SMARTS with RDKit. + :type sanity_check: bool + """ + self.sanity_check = sanity_check + + def generalize(self, smarts_list: List[str]) -> str: + """ + Generalize a list of SMARTS/reaction SMARTS into one combinatorial SMARTS, + with element-list placeholders per atom-map index and position. + + :param smarts_list: List of atom-mapped SMARTS strings (same topology/order). + :type smarts_list: list[str] + :return: Generalized SMARTS with atom-list placeholders. + :rtype: str + :raises ValueError: If input list is empty, topology is inconsistent, or output is invalid. + """ + if not smarts_list: + raise ValueError("Input list is empty.") + + if len(smarts_list) == 1: + combined = smarts_list[0] + else: + pos_list: List[List[re.Match]] = [ + list(self._atom_pat.finditer(s)) for s in smarts_list + ] + n_atoms = len(pos_list[0]) + for pl in pos_list: + if len(pl) != n_atoms: + raise ValueError( + "All input SMARTS must have same atom-mapped topology and order." + ) + + pos2map: List[str] = [] + pos2elems: List[Set[str]] = [] + for i in range(n_atoms): + mapnum = pos_list[0][i].group(2) + elems = set(match[i].group(1) for match in pos_list) + pos2map.append(mapnum) + pos2elems.append(elems) + + # Template assembly: alternate static and atom-map segments + first = smarts_list[0] + atoms = list(self._atom_pat.finditer(first)) + segments = [] + last = 0 + for m in atoms: + # fmt: off + segments.append(first[last: m.start()]) + # fmt: on + segments.append(m.group(2)) # mapnum as marker + last = m.end() + segments.append(first[last:]) + + # Reconstruct combinatorial SMARTS + out = [] + idx = 0 + for seg in segments: + if idx < len(pos2map) and seg == pos2map[idx]: + els = sorted(pos2elems[idx]) + out.append(f"[{','.join(els)}:{seg}]") + idx += 1 + else: + out.append(seg) + combined = "".join(out) + + # RDKit validation + if self.sanity_check: + if ">>" in combined: + rxn = rdChemReactions.ReactionFromSmarts(combined) + if rxn is None or rxn.GetNumProductTemplates() == 0: + raise ValueError(f"Invalid reaction SMARTS generated: {combined}") + else: + mol = Chem.MolFromSmarts(combined) + if mol is None: + raise ValueError(f"Invalid molecule SMARTS generated: {combined}") + + return combined + + def describe(self) -> None: + """ + Print usage instructions and an example. + + :return: None + """ + print( + "SMARTSGeneralizer: Generalize a list of atom-mapped (reaction) SMARTS into a single " + "SMARTS with element-list placeholders at each mapped atom position.\n" + "Usage example:\n" + " >>> gen = SMARTSGeneralizer(sanity_check=True)\n" + " >>> smarts_list = ['[C:1]-[N:2]', '[O:1]-[N:2]', '[N:1]-[N:2]']\n" + " >>> print(gen.generalize(smarts_list)) # [C,N,O:1]-[N:2]" + ) + + def __repr__(self) -> str: + """ + String representation. + + :return: Description with current sanity_check setting. + :rtype: str + """ + return f"SMARTSGeneralizer(sanity_check={self.sanity_check})" diff --git a/synkit/IO/combinatorial/smarts_to_graph.py b/synkit/IO/combinatorial/smarts_to_graph.py new file mode 100644 index 0000000..bb8bad4 --- /dev/null +++ b/synkit/IO/combinatorial/smarts_to_graph.py @@ -0,0 +1,183 @@ +import re +import networkx as nx +from typing import Optional, Set, Tuple, List, Dict + +from rdkit import Chem + + +class SMARTSToGraph: + """ + Convert SMARTS or reaction SMARTS strings into NetworkX graphs with full atom and constraint data. + + :param placeholder_labels: Optional set of labels to treat as placeholders (e.g., wildcard atoms). + :type placeholder_labels: Optional[Set[str]] + :raises: None + """ + + def __init__(self, placeholder_labels: Optional[Set[str]] = None) -> None: + """ + Initialize a SMARTSToGraph converter. + + :param placeholder_labels: Set of placeholder labels used in SMARTS to identify wildcard positions. + Defaults to {'_R', 'X', 'Y', 'Z'} if None. + :type placeholder_labels: Optional[Set[str]] + :returns: None + :rtype: None + """ + self.placeholder_labels: Set[str] = placeholder_labels or {"_R", "X", "Y", "Z"} + + @staticmethod + def _safe_total_hs(atom: "Chem.Atom") -> int: + """ + Compute the total number of hydrogens (explicit + implicit) for an RDKit Atom safely. + + :param atom: RDKit Atom instance whose hydrogen count is desired. + :type atom: Chem.Atom + :returns: Total hydrogen count for the atom. + :rtype: int + :raises: Exception if RDKit property cache update fails. + + :Example: + >>> from rdkit import Chem + >>> atom = Chem.MolFromSmiles('C').GetAtomWithIdx(0) + >>> SMARTSToGraph._safe_total_hs(atom) + 3 + """ + try: + atom.UpdatePropertyCache(strict=False) + return int(atom.GetTotalNumHs(includeExplicit=True)) + except Exception: + return 0 + + def smarts_to_graph(self, smarts: str) -> nx.Graph: + """ + Parse a SMARTS string into a NetworkX graph representation, extracting wildcard constraints. + + :param smarts: SMARTS pattern to convert (e.g., '[C:1]C[O:2]'). + :type smarts: str + :returns: NetworkX Graph with: + - node attributes: element (str), charge (int), hcount (int), + label (str), constraint (Optional[List[str]]), atom_map (int) + - edge attributes: order (float), standard_order (float) + :rtype: nx.Graph + :raises ImportError: If RDKit is not available. + :raises ValueError: If the SMARTS string is invalid or atoms lack mapping numbers. + + :Example: + >>> stg = SMARTSToGraph() + >>> graph = stg.smarts_to_graph('[CH3:1]-[OH:2]') + >>> graph.nodes[1]['element'] + 'C' + >>> graph.nodes[2]['element'] + 'O' + """ + if Chem is None: + raise ImportError("RDKit is required for SMARTS parsing.") + + # Pre-scan SMARTS for wildcard constraint lists (e.g., [C,N:5]) + constraint_map: Dict[int, List[str]] = {} + for match in re.finditer(r"\[([^:\]]+?):(\d+)\]", smarts): + atom_expr, atom_idx = match.groups() + idx = int(atom_idx) + if "," in atom_expr: + constraint_map[idx] = [s.strip() for s in atom_expr.split(",")] + + mol = Chem.MolFromSmarts(smarts) + if mol is None: + raise ValueError(f"Invalid SMARTS string: {smarts!r}") + + G = nx.Graph() + idx_to_map: Dict[int, int] = {} + + # Add nodes with full atom data + for atom in mol.GetAtoms(): + amap = atom.GetAtomMapNum() + if amap == 0: + raise ValueError( + "All atoms in SMARTS must have a mapping number (atom map)." + ) + idx_to_map[atom.GetIdx()] = amap + + # Determine element, label, and constraints + raw_label = atom.GetSymbol() + if amap in constraint_map: + constraint = constraint_map[amap] + label = next(iter(self.placeholder_labels)) + element = "*" + else: + constraint = None + label = raw_label + element = "*" if raw_label in self.placeholder_labels else raw_label + + charge = atom.GetFormalCharge() + hcount = self._safe_total_hs(atom) + + G.add_node( + amap, + element=element, + charge=charge, + hcount=hcount, + label=label, + constraint=constraint, + atom_map=amap, + ) + + # Add edges with bond order data + for bond in mol.GetBonds(): + u = idx_to_map[bond.GetBeginAtomIdx()] + v = idx_to_map[bond.GetEndAtomIdx()] + order = bond.GetBondTypeAsDouble() + G.add_edge(u, v, order=order, standard_order=order) + + return G + + def rxn_smarts_to_graphs(self, rxn: str) -> Tuple[nx.Graph, nx.Graph]: + """ + Split a reaction SMARTS into separate reactant and product graphs. + + :param rxn: Reaction SMARTS in the format 'reactants>>products'. + :type rxn: str + :returns: Tuple of (reactant_graph, product_graph). + :rtype: Tuple[nx.Graph, nx.Graph] + :raises ValueError: If the reaction SMARTS string does not contain '>>'. + + :Example: + >>> stg = SMARTSToGraph() + >>> react, prod = stg.rxn_smarts_to_graphs('[CH3:1]-[OH:2]>>[CH2:1]=[O:2]') + >>> react.nodes + [1, 2] + >>> prod.nodes + [1, 2] + """ + if ">>" not in rxn: + raise ValueError("Reaction SMARTS must contain '>>' separator.") + lhs, rhs = rxn.split(">>", 1) + return self.smarts_to_graph(lhs), self.smarts_to_graph(rhs) + + def __repr__(self) -> str: + """ + Return an unambiguous string representation of this converter. + + :returns: Representation of the instance showing placeholder labels. + :rtype: str + """ + return f"" + + def describe(self) -> str: + """ + Provide a usage summary for SMARTSToGraph. + + :returns: Multi-line string explaining available methods and usage. + :rtype: str + + :Example: + >>> print(SMARTSToGraph().describe()) + SMARTSToGraph(placeholder_labels=None) + smarts_to_graph(smarts_str) -> Graph + rxn_smarts_to_graphs(rxn_smarts) -> (Graph_react, Graph_prod) + """ + return ( + "SMARTSToGraph(placeholder_labels=None)\n" + " smarts_to_graph(smarts_str) -> Graph\n" + " rxn_smarts_to_graphs(rxn_smarts) -> (Graph_react, Graph_prod)\n" + ) diff --git a/synkit/Rule/Apply/rule_matcher.py b/synkit/Rule/Apply/rule_matcher.py index 4e9b88f..b587fd9 100644 --- a/synkit/Rule/Apply/rule_matcher.py +++ b/synkit/Rule/Apply/rule_matcher.py @@ -17,10 +17,10 @@ >>> smarts, rule = matcher.get_result() """ -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union import networkx as nx -from synkit.IO import rsmi_to_graph +from synkit.IO import rsmi_to_graph, rsmi_to_its from synkit.Chem.Reaction.standardize import Standardize from synkit.Chem.Reaction.balance_check import BalanceReactionCheck from synkit.Synthesis.Reactor.syn_reactor import SynReactor @@ -54,13 +54,15 @@ class RuleMatcher: :vartype result: Tuple[str, nx.Graph] """ - def __init__(self, rsmi: str, rule: nx.Graph) -> None: + def __init__( + self, rsmi: str, rule: Union[str, nx.Graph], explicit_h: bool = True + ) -> None: """Initialize the matcher by standardizing the RSMI, building graphs, checking balance, and computing the match. :param rsmi: Reaction SMILES in 'reactant>>product' format. :type rsmi: str - :param rule: Transformation‑rule graph. + :param rule: Transformation-rule graph. :type rule: nx.Graph :raises ValueError: If no SMARTS reproduces the RSMI under the given rule. @@ -68,7 +70,10 @@ def __init__(self, rsmi: str, rule: nx.Graph) -> None: self.std = Standardize() self.rsmi = self.std.fit(rsmi) self.r_graph, self.p_graph = rsmi_to_graph(self.rsmi, drop_non_aam=False) + if isinstance(rule, str): + rule = rsmi_to_its(rule, core=True) self.rule = rule + self.explicit_h = explicit_h self.balanced = BalanceReactionCheck(n_jobs=1).rsmi_balance_check(self.rsmi) # Compute and store the match result @@ -124,7 +129,12 @@ def _match_reverse(self) -> Optional[Tuple[str, nx.Graph]]: return smarts, self.rule # Reactant‑side with inverted template - reactor = SynReactor(substrate=self.p_graph, template=self.rule, invert=True) + reactor = SynReactor( + substrate=self.p_graph, + template=self.rule, + invert=True, + explicit_h=self.explicit_h, + ) for smarts in reactor.smarts_list: std_r = self.std.fit(smarts) if self.all_in( diff --git a/synkit/Synthesis/Reactor/imba_engine.py b/synkit/Synthesis/Reactor/imba_engine.py new file mode 100644 index 0000000..310b1f6 --- /dev/null +++ b/synkit/Synthesis/Reactor/imba_engine.py @@ -0,0 +1,165 @@ +import networkx as nx +from typing import Union, Optional, List +from synkit.Graph.Canon.canon_graph import GraphCanonicaliser +from synkit.Synthesis.Reactor.syn_reactor import SynReactor, Strategy +from synkit.Graph.syn_graph import SynGraph +from synkit.Rule.syn_rule import SynRule +from synkit.Graph.Wildcard.radwc import RadWC +from synkit.Chem.Reaction.radical_wildcard import clean_wc + + +class ImbaEngine: + """ + Reactor for applying a SynKit reaction template to a substrate, with + options for inversion, canonicalisation, strategy, partial ITS, and + radical wildcard appending and fragment cleaning in products. + + :param substrate: Input substrate; SMILES string, networkx.Graph, or SynGraph. + :type substrate: Union[str, nx.Graph, SynGraph] + :param template: Reaction template; SMARTS (bracketed) string, networkx.Graph, or SynRule. + :type template: Union[str, nx.Graph, SynRule] + :param add_wildcard: If True, apply radical wildcard transform to each product SMARTS. + :type add_wildcard: bool + :param clean_fragments: If True, remove wildcard fragments and optionally keep max fragment. + :type clean_fragments: bool + :param max_frag: If True, force maximal fragment selection when cleaning. + :type max_frag: bool + :param invert: If True, apply the template in reverse (product → reactant). + :type invert: bool + :param canonicaliser: Optional GraphCanonicaliser for preprocessing or postprocessing. + :type canonicaliser: Optional[GraphCanonicaliser] + :param strategy: Enumeration strategy (Strategy enum or string). + :type strategy: Union[Strategy, str] + :param partial: If True, perform partial ITS graph construction on results. + :type partial: bool + """ + + def __init__( + self, + substrate: Union[str, nx.Graph, SynGraph], + template: Union[str, nx.Graph, SynRule], + add_wildcard: bool = True, + clean_fragments: bool = False, + max_frag: bool = False, + invert: bool = False, + canonicaliser: Optional[GraphCanonicaliser] = None, + strategy: Union[Strategy, str] = Strategy.ALL, + partial: bool = False, + ) -> None: + # Assign parameters + self.substrate = substrate + self.template = template + self.add_wildcard = add_wildcard + self.clean_fragments = clean_fragments + self.max_frag = max_frag + self.invert = invert + self.canonicaliser = canonicaliser + self.strategy = strategy + self.partial = partial + self._results: List[str] = [] + # Auto-run fit on init + self.fit() + + def __repr__(self) -> str: + return ( + f"" + ) + + @staticmethod + def describe() -> None: + """ + Print class documentation and usage examples. + """ + print(ImbaEngine.__doc__) + + def fit(self) -> "ImbaEngine": + """ + Apply the reaction template to the substrate, producing product SMARTS. + Optionally clean wildcard fragments and add radical wildcards. + Results are stored internally and self is returned. + + :returns: self + :rtype: ImbReactor + :raises ValueError: If substrate cannot be parsed or reaction fails. + """ + from synkit.IO import graph_to_smi + + # Determine reactant SMILES + if isinstance(self.substrate, (nx.Graph, SynGraph)): + react_smiles = graph_to_smi(self.substrate) + elif isinstance(self.substrate, str): + react_smiles = self.substrate + else: + raise ValueError(f"Unsupported substrate type: {type(self.substrate)}") + + reactor = SynReactor( + react_smiles, + template=self.template, + invert=self.invert, + strategy=self.strategy, + partial=self.partial, + implicit_temp=True, + explicit_h=False, + ) + raw_smarts: List[str] = reactor.smarts_list + + # Add radical wildcards if requested + if self.add_wildcard: + wc = [] + for s in raw_smarts: + try: + wc.append(RadWC.transform(s)) + except Exception as e: + print(e) + else: + wc = raw_smarts + # Clean fragments if requested + if self.clean_fragments: + self._results = [ + clean_wc(s, invert=False, max_frag=self.max_frag, wild_card=True) + for s in wc + ] + else: + self._results = wc + + return self + + @property + def smarts_list(self) -> List[str]: + """ + Product SMARTS results from the last fit() invocation. + + :returns: List of SMARTS strings. + :rtype: List[str] + """ + return self._results.copy() + + def __len__(self) -> int: + """ + Number of product SMARTS results. + """ + return len(self._results) + + def __getitem__(self, idx: int) -> str: + """ + Get the product SMARTS at index `idx`. + + :param idx: Index of desired SMARTS. + :type idx: int + :returns: SMARTS string at position `idx`. + :rtype: str + :raises IndexError: If idx is out of bounds. + """ + return self._results[idx] + + def to_list(self) -> List[str]: + """ + Return all product SMARTS as a list. + + :returns: List of SMARTS strings. + :rtype: List[str] + """ + return self._results.copy() diff --git a/synkit/Synthesis/Reactor/syn_reactor.py b/synkit/Synthesis/Reactor/syn_reactor.py index 7ba82f8..cd9e0f9 100644 --- a/synkit/Synthesis/Reactor/syn_reactor.py +++ b/synkit/Synthesis/Reactor/syn_reactor.py @@ -80,14 +80,14 @@ class SynReactor: :vartype _graph: Optional[SynGraph] :ivar _rule: Cached SynRule for the template. :vartype _rule: Optional[SynRule] - :ivar _mappings: Cached list of subgraph‐mapping dicts. + :ivar _mappings: Cached list of subgraph-mapping dicts. :vartype _mappings: Optional[List[MappingDict]] :ivar _its: Cached list of ITS graphs. :vartype _its: Optional[List[nx.Graph]] :ivar _smarts: Cached list of SMARTS strings. :vartype _smarts: Optional[List[str]] :ivar _flag_pattern_has_explicit_H: Internal flag indicating - explicit‑H constraints. + explicit-H constraints. :vartype _flag_pattern_has_explicit_H: bool """ diff --git a/synkit/Vis/graph_visualizer.py b/synkit/Vis/graph_visualizer.py index f197c49..4030548 100644 --- a/synkit/Vis/graph_visualizer.py +++ b/synkit/Vis/graph_visualizer.py @@ -122,6 +122,9 @@ def plot_its( font_size: int = 12, og: bool = False, rule: bool = False, + title_font_size: str = 20, + title_font_weight: str = "bold", + title_font_style: str = "italic", ) -> None: # --- original implementation preserved verbatim ------------------ ax.clear() @@ -130,7 +133,12 @@ def plot_its( ax.axis("equal") ax.axis("off") if title: - ax.set_title(title) + ax.set_title( + title, + fontsize=title_font_size, + fontweight=title_font_weight, + fontstyle=title_font_style, + ) if use_edge_color: edge_colors = [ ( diff --git a/synkit/Vis/rule_vis.py b/synkit/Vis/rule_vis.py index 8a630f5..940f75c 100644 --- a/synkit/Vis/rule_vis.py +++ b/synkit/Vis/rule_vis.py @@ -40,7 +40,7 @@ def vis(self, input: Union[str, Tuple[nx.Graph, nx.Graph, nx.Graph]], **kwargs): if input.strip().startswith("graph [") or "rule [" in input: return self.mod_vis(input, **kwargs) else: - r, p = rsmi_to_graph(input, light_weight=True) + r, p = rsmi_to_graph(input) its = ITSConstruction().ITSGraph(r, p) gml_str = smart_to_gml(input, core=False, sanitize=False) return self.mod_vis(gml_str, **kwargs) @@ -72,7 +72,7 @@ def nx_vis( try: # 1) Parse input if isinstance(input, str): - r, p = rsmi_to_graph(input, light_weight=True, sanitize=sanitize) + r, p = rsmi_to_graph(input, sanitize=sanitize) its = ITSConstruction().ITSGraph(r, p) elif isinstance(input, tuple) and len(input) == 3: r, p, its = input diff --git a/synkit/examples.py b/synkit/examples.py new file mode 100644 index 0000000..f94dd07 --- /dev/null +++ b/synkit/examples.py @@ -0,0 +1,50 @@ +# synkit/examples.py + +from __future__ import annotations + +from functools import lru_cache +from importlib.resources import files, as_file + +from synkit.IO import load_database # adjust if the import path differs + + +def _sanitize_slug(slug: str) -> str: + if ".." in slug or slug.startswith(("/", "\\")): + raise ValueError(f"Invalid example name: {slug!r}") + return slug + + +def list_examples() -> list[str]: + """ + Returns all available example slugs (without extensions), e.g. ['paracetamol', ...] + """ + data_dir = files("synkit").joinpath("Data") + slugs: set[str] = set() + for p in data_dir.iterdir(): + if not p.is_file(): + continue + if p.name.endswith(".json.gz"): + slugs.add(p.name[: -len(".json.gz")]) + elif p.name.endswith(".json"): + slugs.add(p.name[: -len(".json")]) + return sorted(slugs) + + +@lru_cache(maxsize=32) +def load_example(slug: str): + """ + Load an example by slug, preferring compressed (.json.gz) over plain (.json). + Delegates actual parsing to synkit.IO.load_database. + """ + slug = _sanitize_slug(slug) + data_dir = files("synkit").joinpath("Data") + candidates = [f"{slug}.json.gz", f"{slug}.json"] + for name in candidates: + resource = data_dir.joinpath(name) + if resource.is_file(): + # importlib.resources may keep resources inside archives; as_file gives a real path + with as_file(resource) as real_path: + return load_database(real_path) + raise FileNotFoundError( + f"Example '{slug}' not found (looked for {', '.join(candidates)})" + )