polyscript-pypi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- MCP/server.py +770 -0
- polyscript/__init__.py +12 -0
- polyscript/brics/BRICSMod.py +1164 -0
- polyscript/brics/BRICS_utils.py +189 -0
- polyscript/brics/__init__.py +48 -0
- polyscript/classifier/__init__.py +270 -0
- polyscript/classifier/class_reference.py +951 -0
- polyscript/depolymerizer/__init__.py +576 -0
- polyscript/depolymerizer/mpi.py +390 -0
- polyscript/depolymerizer/reaction_reference.py +447 -0
- polyscript/polymerizer/__init__.py +22 -0
- polyscript/polymerizer/core.py +648 -0
- polyscript/polymerizer/mpi.py +860 -0
- polyscript/polymerizer/mpi_df.py +378 -0
- polyscript/polymerizer/utils.py +795 -0
- polyscript/utils/__init__.py +80 -0
- polyscript/utils/adapters.py +357 -0
- polyscript/utils/canon_analyze.py +193 -0
- polyscript/utils/executor.py +66 -0
- polyscript/utils/logger.py +331 -0
- polyscript/utils/mpi/__init__.py +33 -0
- polyscript/utils/mpi/checkpoint.py +263 -0
- polyscript/utils/mpi/logger.py +138 -0
- polyscript/utils/mpi/progress.py +134 -0
- polyscript/utils/mpi/protocol.py +113 -0
- polyscript/utils/mpi/resource.py +94 -0
- polyscript/utils/mpi/splitter.py +42 -0
- polyscript/utils/mpi_executor.py +167 -0
- polyscript/utils/mpi_validator.py +940 -0
- polyscript/utils/parsers.py +177 -0
- polyscript/utils/validators.py +201 -0
- polyscript_pypi-0.1.0.dist-info/METADATA +82 -0
- polyscript_pypi-0.1.0.dist-info/RECORD +57 -0
- polyscript_pypi-0.1.0.dist-info/WHEEL +5 -0
- polyscript_pypi-0.1.0.dist-info/licenses/LICENSE +21 -0
- polyscript_pypi-0.1.0.dist-info/top_level.txt +4 -0
- runners/adapt_dataframe.py +105 -0
- runners/brics_decompose.py +28 -0
- runners/canonicalization.py +46 -0
- runners/filter_candidates.py +54 -0
- runners/mpi_depolymerize.py +51 -0
- runners/mpi_runner.py +66 -0
- runners/mpi_runner_df.py +49 -0
- runners/mpi_validator.py +45 -0
- tests/conftest.py +10 -0
- tests/sample_gen.py +10 -0
- tests/test_brics.py +275 -0
- tests/test_classifier.py +201 -0
- tests/test_mpi_polymerize.py +170 -0
- tests/test_polymerize.py +206 -0
- tests/utils/test_adaptors.py +182 -0
- tests/utils/test_canonicalization.py +62 -0
- tests/utils/test_executor.py +101 -0
- tests/utils/test_logger.py +426 -0
- tests/utils/test_mpi_validator.py +159 -0
- tests/utils/test_parsers.py +150 -0
- tests/utils/test_validator.py +105 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from polyscript.brics.BRICSMod import BRICSBuild, BRICSDecompose
|
|
8
|
+
from polyscript.utils.logger import INFO, PolyLogger
|
|
9
|
+
from rdkit import Chem
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def read_df(df_path: str, column_name: str):
|
|
13
|
+
"""Read a column of data from a CSV file.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
df_path: Path to a CSV file.
|
|
17
|
+
column_name: Name of the column to extract.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
numpy.ndarray: Values from the specified column.
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
ValueError: If the file is not a CSV or the column is missing.
|
|
24
|
+
"""
|
|
25
|
+
if not df_path.split(".")[-1] == "csv":
|
|
26
|
+
raise ValueError("The given file path is not targeting a csv fil")
|
|
27
|
+
df = pd.read_csv(df_path)
|
|
28
|
+
if not column_name in df.columns:
|
|
29
|
+
raise ValueError("The given column is not in the df !")
|
|
30
|
+
data = df[column_name].values
|
|
31
|
+
return data
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write_data(data, file_path: str, new_line: bool = True):
|
|
35
|
+
"""Write data to a text file.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
data: A list of strings (written one per line) or a single
|
|
39
|
+
string (written as-is).
|
|
40
|
+
file_path: Destination file path.
|
|
41
|
+
new_line: If True (default) and *data* is a list, write each
|
|
42
|
+
element followed by a newline. Ignored for non-list data.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
bool: ``True`` on success.
|
|
46
|
+
"""
|
|
47
|
+
with open(file_path, "w+") as f:
|
|
48
|
+
if type(data) == list:
|
|
49
|
+
if new_line:
|
|
50
|
+
for d in data:
|
|
51
|
+
f.write(d + "\n")
|
|
52
|
+
else:
|
|
53
|
+
f.write(str(data))
|
|
54
|
+
else:
|
|
55
|
+
f.write(data)
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def apply_BRICS(
|
|
60
|
+
data: list[str],
|
|
61
|
+
max_depth: int = 1,
|
|
62
|
+
output_dir: Optional[str] = None,
|
|
63
|
+
fmt: str = "csv",
|
|
64
|
+
is_decomposed: bool = False,
|
|
65
|
+
log_level: int | str = 2,
|
|
66
|
+
logger: Optional[PolyLogger] = None,
|
|
67
|
+
) -> Optional[list[dict]]:
|
|
68
|
+
"""Decompose and rebuild molecules via BRICS.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
data : list[str]
|
|
73
|
+
SMILES strings to process. May be full molecules (the default)
|
|
74
|
+
or already-decomposed BRICS fragments when *is_decomposed* is
|
|
75
|
+
``True``.
|
|
76
|
+
max_depth : int
|
|
77
|
+
Maximum BRICS build depth (default 1).
|
|
78
|
+
output_dir : str, optional
|
|
79
|
+
Directory to save output files to. When provided, two files are
|
|
80
|
+
written: ``decomposed_unique.txt`` (fragments, one per line) and
|
|
81
|
+
``brics_results.{csv,parquet}`` (table with ``product_smiles`` and
|
|
82
|
+
``recipe`` columns). When ``None`` (default), no files are written
|
|
83
|
+
and the results are returned as a list of dicts instead.
|
|
84
|
+
fmt : str
|
|
85
|
+
Output table format: ``"csv"`` (default) or ``"parquet"``.
|
|
86
|
+
Ignored when *output_dir* is ``None``.
|
|
87
|
+
is_decomposed : bool
|
|
88
|
+
When ``False`` (default), *data* contains full molecules that will
|
|
89
|
+
be decomposed into BRICS fragments before building. When ``True``,
|
|
90
|
+
*data* already contains BRICS fragments (with wildcards like
|
|
91
|
+
``[15*]C1CCCCC1``) — the decomposition step is skipped and the
|
|
92
|
+
fragments go directly to ``BRICSBuild``.
|
|
93
|
+
log_level : int or str
|
|
94
|
+
Verbosity threshold used when *logger* is not provided.
|
|
95
|
+
As an **int**: ``0`` = silent, ``1`` = warnings,
|
|
96
|
+
``2`` = info (default), ``3`` = debug.
|
|
97
|
+
As a **str**: ``"SILENT"``, ``"WARNING"``, ``"INFO"``, ``"DEBUG"``.
|
|
98
|
+
logger : PolyLogger, optional
|
|
99
|
+
A pre-configured ``PolyLogger`` instance. When given, its
|
|
100
|
+
existing level and handlers are used as-is; *log_level* is
|
|
101
|
+
ignored. When ``None``, a console logger at *log_level* is
|
|
102
|
+
created automatically.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
list[dict] or None
|
|
107
|
+
When *output_dir* is ``None``, returns a list of dicts keyed by
|
|
108
|
+
``"product_smiles"``, ``"recipe"``, and ``"mol"``. When
|
|
109
|
+
*output_dir* is given, returns ``None`` (results are written to
|
|
110
|
+
disk instead).
|
|
111
|
+
"""
|
|
112
|
+
if logger is None:
|
|
113
|
+
logger = PolyLogger(name="brics", level=log_level)
|
|
114
|
+
logger.add_console(level=log_level)
|
|
115
|
+
|
|
116
|
+
# ── 1. Decompose (skip if already decomposed) ───────────────────────
|
|
117
|
+
if is_decomposed:
|
|
118
|
+
# Input is already BRICS fragments — use directly, deduplicate
|
|
119
|
+
uniques = list(set(data))
|
|
120
|
+
logger.info(
|
|
121
|
+
"Using %d pre-decomposed fragments (skipping decomposition)",
|
|
122
|
+
len(uniques),
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
Decompositions = []
|
|
126
|
+
for smiles in data:
|
|
127
|
+
mol = Chem.MolFromSmiles(smiles)
|
|
128
|
+
if mol is None:
|
|
129
|
+
logger.warning(f"Invalid SMILES >> {smiles}")
|
|
130
|
+
if mol is not None:
|
|
131
|
+
res = BRICSDecompose(mol)
|
|
132
|
+
Decompositions += res
|
|
133
|
+
|
|
134
|
+
uniques = list(set(Decompositions))
|
|
135
|
+
logger.info("Unique blocks found -> %d", len(uniques))
|
|
136
|
+
|
|
137
|
+
# ── 2. Build ─────────────────────────────────────────────────────────
|
|
138
|
+
build_mols = [Chem.MolFromSmiles(smiles) for smiles in uniques]
|
|
139
|
+
brics_iter = list(BRICSBuild(build_mols, maxDepth=max_depth))
|
|
140
|
+
|
|
141
|
+
# ── 3. Collect results ───────────────────────────────────────────────
|
|
142
|
+
results: list[dict] = []
|
|
143
|
+
for b in brics_iter:
|
|
144
|
+
if isinstance(b, tuple):
|
|
145
|
+
recipe, mol = b
|
|
146
|
+
product_smiles = Chem.MolToSmiles(mol)
|
|
147
|
+
results.append({
|
|
148
|
+
"product_smiles": product_smiles,
|
|
149
|
+
"recipe": recipe,
|
|
150
|
+
"mol": mol,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
logger.info("New blocks found -> %d", len(brics_iter))
|
|
154
|
+
|
|
155
|
+
# ── 4. Save or return ────────────────────────────────────────────────
|
|
156
|
+
if output_dir is not None:
|
|
157
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
158
|
+
|
|
159
|
+
# Fragments — human-readable, one SMILES per line
|
|
160
|
+
write_data(uniques, os.path.join(output_dir, "decomposed_unique.txt"))
|
|
161
|
+
|
|
162
|
+
# Results table — dataframe-compatible (CSV or Parquet)
|
|
163
|
+
if results:
|
|
164
|
+
df_out = pd.DataFrame([
|
|
165
|
+
{"product_smiles": r["product_smiles"], "recipe": r["recipe"]}
|
|
166
|
+
for r in results
|
|
167
|
+
])
|
|
168
|
+
else:
|
|
169
|
+
df_out = pd.DataFrame(columns=["product_smiles", "recipe"])
|
|
170
|
+
|
|
171
|
+
if fmt == "parquet":
|
|
172
|
+
out_path = os.path.join(output_dir, "brics_results.parquet")
|
|
173
|
+
df_out.to_parquet(out_path, index=False)
|
|
174
|
+
else:
|
|
175
|
+
out_path = os.path.join(output_dir, "brics_results.csv")
|
|
176
|
+
df_out.to_csv(out_path, index=False)
|
|
177
|
+
|
|
178
|
+
logger.info(
|
|
179
|
+
"Saved %d results to %s (decomposed_unique.txt, %s)",
|
|
180
|
+
len(results), output_dir, os.path.basename(out_path),
|
|
181
|
+
)
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
return results
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
smiles = read_df("./source.csv", "SMILES")
|
|
189
|
+
apply_BRICS(smiles)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BRICS (Breaking of Retrosynthetically Interesting Chemical Substructures)
|
|
3
|
+
sub-package for PolyScript.
|
|
4
|
+
|
|
5
|
+
Provides fragment-based decomposition and recombination of molecules:
|
|
6
|
+
|
|
7
|
+
- :func:`FindBRICSBonds` — locate cleavable bonds in a molecule.
|
|
8
|
+
- :func:`BreakBRICSBonds` — break those bonds, returning labelled fragments.
|
|
9
|
+
- :func:`BRICSDecompose` — full recursive decomposition into BRICS fragments.
|
|
10
|
+
- :func:`BRICSBuild` — enumerate new molecules by recombining fragments.
|
|
11
|
+
- :func:`BRICSBuildDistribWrite` — build + persist results to disk.
|
|
12
|
+
|
|
13
|
+
High-level convenience (from ``BRICS_utils``):
|
|
14
|
+
- :func:`apply_BRICS` — end-to-end: decompose → deduplicate → rebuild.
|
|
15
|
+
- :func:`read_df` — load a SMILES column from a CSV.
|
|
16
|
+
- :func:`write_data` — write text / list data to a file.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
# ── Core BRICS operations ────────────────────────────────────────────────────
|
|
22
|
+
from polyscript.brics.BRICSMod import (
|
|
23
|
+
BRICSBuild,
|
|
24
|
+
BRICSBuildDistribWrite,
|
|
25
|
+
BRICSDecompose,
|
|
26
|
+
BreakBRICSBonds,
|
|
27
|
+
FindBRICSBonds,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# ── High-level utilities ─────────────────────────────────────────────────────
|
|
31
|
+
from polyscript.brics.BRICS_utils import (
|
|
32
|
+
apply_BRICS,
|
|
33
|
+
read_df,
|
|
34
|
+
write_data,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# BRICSMod
|
|
39
|
+
"FindBRICSBonds",
|
|
40
|
+
"BreakBRICSBonds",
|
|
41
|
+
"BRICSDecompose",
|
|
42
|
+
"BRICSBuild",
|
|
43
|
+
"BRICSBuildDistribWrite",
|
|
44
|
+
# BRICS_utils
|
|
45
|
+
"apply_BRICS",
|
|
46
|
+
"read_df",
|
|
47
|
+
"write_data",
|
|
48
|
+
]
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Polymer-type classifier based on SMiPoly functional-group logic.
|
|
2
|
+
|
|
3
|
+
This module provides ``PolyScriptClassifier``, which classifies monomer
|
|
4
|
+
SMILES strings into polymer-relevant functional-group categories (mono-
|
|
5
|
+
and poly-functional, as well as olefinic) using SMARTS patterns and
|
|
6
|
+
exclusion rules derived from SMiPoly, with additional classes from OMG.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
from polyscript.classifier.class_reference import exc_lst, mon_index_map, mon_lst, mon_type_index
|
|
13
|
+
from polyscript.utils.executor import BaseExecutor
|
|
14
|
+
from polyscript.utils.logger import INFO
|
|
15
|
+
from rdkit import Chem
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PolyScriptClassifier(BaseExecutor):
|
|
19
|
+
_log_level = INFO # class-level default, change via set_log_level()
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
MinFG: int = 2,
|
|
24
|
+
MaxFG: int = 4,
|
|
25
|
+
include_carbonate: bool = True,
|
|
26
|
+
**kwargs,
|
|
27
|
+
):
|
|
28
|
+
"""Initialize the PolyScript classifier.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
MinFG: Minimum number of functional groups for
|
|
32
|
+
poly-functional classification (default 2).
|
|
33
|
+
MaxFG: Maximum number of functional groups for
|
|
34
|
+
poly-functional classification (default 4).
|
|
35
|
+
include_carbonate: If True, append CO and HCHO rows
|
|
36
|
+
to enable carbonate monomer classification.
|
|
37
|
+
**kwargs: Passed to ``BaseExecutor.__init__``.
|
|
38
|
+
"""
|
|
39
|
+
super().__init__(**kwargs)
|
|
40
|
+
self.MinFG = MinFG
|
|
41
|
+
self.MaxFG = MaxFG
|
|
42
|
+
self.mon_lst = mon_lst
|
|
43
|
+
self.exc_lst = exc_lst
|
|
44
|
+
self.mon_index_map = mon_index_map
|
|
45
|
+
self.mon_type_index = mon_type_index
|
|
46
|
+
self.include_carbonate = include_carbonate
|
|
47
|
+
|
|
48
|
+
self.logger.debug(
|
|
49
|
+
"PolyScriptClassifier initialised (MinFG=%d, MaxFG=%d)", MinFG, MaxFG
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def _extract_FG(self, mol, patt):
|
|
53
|
+
"""Count non-overlapping functional-group matches for a pattern.
|
|
54
|
+
|
|
55
|
+
Filters out consecutive duplicate matches that differ by a single
|
|
56
|
+
atom shift (symmetric difference of size 2), which typically
|
|
57
|
+
represent the same functional group seen from a different
|
|
58
|
+
orientation rather than distinct occurrences.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
mol: An RDKit ``Mol`` object.
|
|
62
|
+
patt: An RDKit ``Mol`` object representing a SMARTS pattern.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
int: Number of distinct functional-group matches.
|
|
66
|
+
"""
|
|
67
|
+
numFG = 0
|
|
68
|
+
matchs = mol.GetSubstructMatches(
|
|
69
|
+
patt
|
|
70
|
+
) # finding the substructures based on the pattern
|
|
71
|
+
if len(matchs) >= 2: # if higher number of substructures are found
|
|
72
|
+
not_match = []
|
|
73
|
+
for i in range(0, len(matchs) - 1):
|
|
74
|
+
# Checks if the current and next match are NOT extensively overlapping by a
|
|
75
|
+
# single atom shift (i.e., symmetric difference of atom indices is not 2).
|
|
76
|
+
if len(set(matchs[i]) ^ set(matchs[i + 1])) != 2:
|
|
77
|
+
pass
|
|
78
|
+
else:
|
|
79
|
+
not_match.append(i + 1)
|
|
80
|
+
numFG = len(
|
|
81
|
+
[matchs[i] for i in range(0, len(matchs)) if i not in not_match]
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
# if there are only 2 number of matches just add the length as number of functional group
|
|
85
|
+
numFG = len(matchs)
|
|
86
|
+
return numFG
|
|
87
|
+
|
|
88
|
+
def _get_mono_FG(self, mol, type_index):
|
|
89
|
+
"""Check for mono-functional (single-monomer) classification.
|
|
90
|
+
|
|
91
|
+
Tests whether *mol* contains at least one substructure match
|
|
92
|
+
from the monomer SMARTS list for *type_index* (ignoring
|
|
93
|
+
duplicate overlaps). Also verifies that none of the exclusion
|
|
94
|
+
patterns for that type match.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
mol: An RDKit ``Mol`` object, or ``None``.
|
|
98
|
+
type_index: String key into ``self.mon_lst`` and
|
|
99
|
+
``self.exc_lst`` (e.g. ``"1"`` for vinyl).
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
list: ``[bool, int]`` — the bool is ``True`` if the
|
|
103
|
+
monomer type is present with no exclusion violations;
|
|
104
|
+
the int is the total number of raw substructure matches.
|
|
105
|
+
"""
|
|
106
|
+
if mol is None:
|
|
107
|
+
return [False, 0]
|
|
108
|
+
else:
|
|
109
|
+
chk_c = 0
|
|
110
|
+
fchk_c = 0
|
|
111
|
+
chk = []
|
|
112
|
+
if len(self.mon_lst[str(type_index)]) != 0:
|
|
113
|
+
for mon in self.mon_lst[str(type_index)]:
|
|
114
|
+
patt = Chem.MolFromSmarts(mon)
|
|
115
|
+
if mol.HasSubstructMatch(patt):
|
|
116
|
+
chk_c = len(mol.GetSubstructMatches(patt))
|
|
117
|
+
fchk_c = fchk_c + chk_c
|
|
118
|
+
chk_excl = []
|
|
119
|
+
for excl in self.exc_lst[str(type_index)]:
|
|
120
|
+
excl_patt = Chem.MolFromSmarts(excl)
|
|
121
|
+
if mol.HasSubstructMatch(excl_patt):
|
|
122
|
+
chk_excl.append(False)
|
|
123
|
+
else:
|
|
124
|
+
chk_excl.append(True)
|
|
125
|
+
if False in chk_excl:
|
|
126
|
+
chk.append(False)
|
|
127
|
+
else:
|
|
128
|
+
chk.append(True)
|
|
129
|
+
else:
|
|
130
|
+
chk.append(False)
|
|
131
|
+
if True in chk:
|
|
132
|
+
fchk = True
|
|
133
|
+
else:
|
|
134
|
+
fchk = False
|
|
135
|
+
else:
|
|
136
|
+
fchk = False
|
|
137
|
+
return [fchk, fchk_c]
|
|
138
|
+
|
|
139
|
+
def _get_poly_FG(self, mol, type_index):
|
|
140
|
+
"""Check for poly-functional (multi-monomer) classification.
|
|
141
|
+
|
|
142
|
+
Counts all functional-group matches (using ``_extract_FG``) and
|
|
143
|
+
requires the total to fall within ``[MinFG, MaxFG]``. Also
|
|
144
|
+
validates that none of the exclusion patterns match.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
mol: An RDKit ``Mol`` object, or ``None``.
|
|
148
|
+
type_index: String key into ``self.mon_lst`` and
|
|
149
|
+
``self.exc_lst``.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
list: ``[bool, int]`` — the bool is ``True`` if the
|
|
153
|
+
monomer type is present within the FG count range and
|
|
154
|
+
has no exclusion violations; the int is the count of
|
|
155
|
+
functional-group matches.
|
|
156
|
+
"""
|
|
157
|
+
if mol is None:
|
|
158
|
+
return [False, 0]
|
|
159
|
+
chk_c = 0 # check count
|
|
160
|
+
fchk_c = 0 # functional check count
|
|
161
|
+
if len(self.mon_lst[str(type_index)]) != 0:
|
|
162
|
+
for mon in self.mon_lst[str(type_index)]:
|
|
163
|
+
patt = Chem.MolFromSmarts(mon)
|
|
164
|
+
chk_c = self._extract_FG(mol, patt)
|
|
165
|
+
fchk_c = fchk_c + chk_c
|
|
166
|
+
if self.MinFG <= fchk_c <= self.MaxFG:
|
|
167
|
+
chk = []
|
|
168
|
+
for excl in self.exc_lst[str(type_index)]:
|
|
169
|
+
excl_patt = Chem.MolFromSmarts(excl)
|
|
170
|
+
if mol.HasSubstructMatch(excl_patt):
|
|
171
|
+
chk.append(False)
|
|
172
|
+
else:
|
|
173
|
+
chk.append(True)
|
|
174
|
+
if False in chk:
|
|
175
|
+
fchk = False
|
|
176
|
+
else:
|
|
177
|
+
fchk = True
|
|
178
|
+
else:
|
|
179
|
+
fchk = False
|
|
180
|
+
else:
|
|
181
|
+
fchk = False
|
|
182
|
+
return [fchk, fchk_c]
|
|
183
|
+
|
|
184
|
+
def _classify(self, df, ids, mapper_func):
|
|
185
|
+
"""Apply a classification mapper to a group of monomer types.
|
|
186
|
+
|
|
187
|
+
Iterates over the type indices listed in ``self.mon_index_map[ids]``
|
|
188
|
+
and creates a new DataFrame column for each type name (from
|
|
189
|
+
``self.mon_type_index``), populated by applying *mapper_func*
|
|
190
|
+
to the ``ROMol`` column.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
df: A pandas DataFrame with a ``ROMol`` column.
|
|
194
|
+
ids: Index into ``self.mon_index_map`` (0 for mono, 1 for poly).
|
|
195
|
+
mapper_func: Callable ``(mol, type_index) -> [bool, int]``.
|
|
196
|
+
"""
|
|
197
|
+
for i in self.mon_index_map[ids]:
|
|
198
|
+
df[self.mon_type_index[str(i)]] = [
|
|
199
|
+
e[0] for e in df["ROMol"].apply(mapper_func, type_index=i)
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
def classify(self, df, col_name):
|
|
203
|
+
"""Run full mono- and poly-functional-group classification.
|
|
204
|
+
|
|
205
|
+
Parses SMILES in *df[col_name]*, optionally appends CO / HCHO
|
|
206
|
+
rows for carbonate classification, runs mono-functional
|
|
207
|
+
(``_get_mono_FG``) and poly-functional (``_get_poly_FG``)
|
|
208
|
+
classifiers, and returns the enriched DataFrame.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
df: Input DataFrame containing SMILES strings.
|
|
212
|
+
col_name: Column name holding SMILES strings.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
pandas.DataFrame: The input DataFrame with additional
|
|
216
|
+
classification columns (one per monomer type) and the
|
|
217
|
+
``ROMol`` column dropped. Returns the original DataFrame
|
|
218
|
+
unchanged if *col_name* is not found.
|
|
219
|
+
"""
|
|
220
|
+
# read source file
|
|
221
|
+
DF01 = df
|
|
222
|
+
n_rows = len(DF01)
|
|
223
|
+
self.logger.info(
|
|
224
|
+
"Starting classification (%d rows, column='%s')", n_rows, col_name
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# optionally append CO and HCHO for carbonate classification
|
|
228
|
+
DF02 = DF01.copy()
|
|
229
|
+
if col_name in DF02.columns.to_list():
|
|
230
|
+
if self.include_carbonate:
|
|
231
|
+
# adding two rows for CO and HCHO that will later be used
|
|
232
|
+
# to form carbonate with other monomers
|
|
233
|
+
DFadd = pd.DataFrame(
|
|
234
|
+
[
|
|
235
|
+
["[C-]#[O+]"], # CO
|
|
236
|
+
["C=O"], # HCHO
|
|
237
|
+
],
|
|
238
|
+
columns=[col_name],
|
|
239
|
+
)
|
|
240
|
+
DF02 = pd.concat([DF02, DFadd], ignore_index=True)
|
|
241
|
+
self.logger.debug(
|
|
242
|
+
"Appended CO / HCHO rows for carbonate classification"
|
|
243
|
+
)
|
|
244
|
+
else:
|
|
245
|
+
self.logger.error(
|
|
246
|
+
"Column '%s' not found in dataframe (available: %s)",
|
|
247
|
+
col_name,
|
|
248
|
+
DF02.columns.to_list(),
|
|
249
|
+
)
|
|
250
|
+
return df
|
|
251
|
+
|
|
252
|
+
# drop NA of smiles, and add chemical structure
|
|
253
|
+
DF02["ROMol"] = DF02[col_name].apply(lambda x: Chem.MolFromSmiles(x))
|
|
254
|
+
DF02[col_name] = DF02["ROMol"].apply(lambda x: Chem.MolToSmiles(x))
|
|
255
|
+
|
|
256
|
+
null_count = DF02["ROMol"].isna().sum()
|
|
257
|
+
if null_count:
|
|
258
|
+
self.logger.warning("%d SMILES could not be parsed", null_count)
|
|
259
|
+
|
|
260
|
+
# implementing mono func
|
|
261
|
+
self.logger.debug("Running mono-functional-group classification")
|
|
262
|
+
self._classify(DF02, 0, self._get_mono_FG)
|
|
263
|
+
|
|
264
|
+
# implementing poly func
|
|
265
|
+
self.logger.debug("Running poly-functional-group classification")
|
|
266
|
+
self._classify(DF02, 1, self._get_poly_FG)
|
|
267
|
+
|
|
268
|
+
DF02 = DF02.drop("ROMol", axis=1)
|
|
269
|
+
self.logger.info("Classification complete (%d rows)", len(DF02))
|
|
270
|
+
return DF02
|