rdkit-cli 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.
- rdkit_cli/__init__.py +4 -0
- rdkit_cli/__main__.py +6 -0
- rdkit_cli/cli.py +162 -0
- rdkit_cli/commands/__init__.py +1 -0
- rdkit_cli/commands/conformers.py +220 -0
- rdkit_cli/commands/convert.py +162 -0
- rdkit_cli/commands/depict.py +311 -0
- rdkit_cli/commands/descriptors.py +251 -0
- rdkit_cli/commands/diversity.py +232 -0
- rdkit_cli/commands/enumerate.py +229 -0
- rdkit_cli/commands/filter.py +384 -0
- rdkit_cli/commands/fingerprints.py +179 -0
- rdkit_cli/commands/fragment.py +284 -0
- rdkit_cli/commands/mcs.py +162 -0
- rdkit_cli/commands/reactions.py +191 -0
- rdkit_cli/commands/scaffold.py +243 -0
- rdkit_cli/commands/similarity.py +359 -0
- rdkit_cli/commands/standardize.py +138 -0
- rdkit_cli/core/__init__.py +1 -0
- rdkit_cli/core/conformers.py +197 -0
- rdkit_cli/core/depict.py +241 -0
- rdkit_cli/core/descriptors.py +248 -0
- rdkit_cli/core/diversity.py +174 -0
- rdkit_cli/core/enumerate.py +190 -0
- rdkit_cli/core/filters.py +443 -0
- rdkit_cli/core/fingerprints.py +265 -0
- rdkit_cli/core/fragment.py +237 -0
- rdkit_cli/core/mcs.py +128 -0
- rdkit_cli/core/reactions.py +159 -0
- rdkit_cli/core/scaffold.py +174 -0
- rdkit_cli/core/similarity.py +206 -0
- rdkit_cli/core/standardizer.py +141 -0
- rdkit_cli/io/__init__.py +7 -0
- rdkit_cli/io/formats.py +109 -0
- rdkit_cli/io/readers.py +352 -0
- rdkit_cli/io/writers.py +275 -0
- rdkit_cli/parallel/__init__.py +5 -0
- rdkit_cli/parallel/batch.py +181 -0
- rdkit_cli/parallel/executor.py +180 -0
- rdkit_cli/progress/__init__.py +5 -0
- rdkit_cli/progress/ninja.py +195 -0
- rdkit_cli/utils/__init__.py +1 -0
- rdkit_cli-0.1.0.dist-info/METADATA +380 -0
- rdkit_cli-0.1.0.dist-info/RECORD +47 -0
- rdkit_cli-0.1.0.dist-info/WHEEL +4 -0
- rdkit_cli-0.1.0.dist-info/entry_points.txt +2 -0
- rdkit_cli-0.1.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Fragment command implementation."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rdkit_cli.cli import RdkitHelpFormatter, add_common_io_options, add_common_processing_options
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_parser(subparsers):
|
|
10
|
+
"""Register the fragment command and subcommands."""
|
|
11
|
+
parser = subparsers.add_parser(
|
|
12
|
+
"fragment",
|
|
13
|
+
help="Fragment molecules",
|
|
14
|
+
description="Fragment molecules using BRICS, RECAP, or functional group analysis.",
|
|
15
|
+
formatter_class=RdkitHelpFormatter,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
frag_subparsers = parser.add_subparsers(
|
|
19
|
+
title="Subcommands",
|
|
20
|
+
dest="subcommand",
|
|
21
|
+
metavar="<subcommand>",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# fragment brics
|
|
25
|
+
brics_parser = frag_subparsers.add_parser(
|
|
26
|
+
"brics",
|
|
27
|
+
help="Fragment using BRICS algorithm",
|
|
28
|
+
formatter_class=RdkitHelpFormatter,
|
|
29
|
+
)
|
|
30
|
+
add_common_io_options(brics_parser)
|
|
31
|
+
add_common_processing_options(brics_parser)
|
|
32
|
+
brics_parser.add_argument(
|
|
33
|
+
"--min-size",
|
|
34
|
+
type=int,
|
|
35
|
+
default=1,
|
|
36
|
+
metavar="N",
|
|
37
|
+
help="Minimum fragment heavy atom count (default: 1)",
|
|
38
|
+
)
|
|
39
|
+
brics_parser.set_defaults(func=run_brics)
|
|
40
|
+
|
|
41
|
+
# fragment recap
|
|
42
|
+
recap_parser = frag_subparsers.add_parser(
|
|
43
|
+
"recap",
|
|
44
|
+
help="Fragment using RECAP algorithm",
|
|
45
|
+
formatter_class=RdkitHelpFormatter,
|
|
46
|
+
)
|
|
47
|
+
add_common_io_options(recap_parser)
|
|
48
|
+
add_common_processing_options(recap_parser)
|
|
49
|
+
recap_parser.add_argument(
|
|
50
|
+
"--min-size",
|
|
51
|
+
type=int,
|
|
52
|
+
default=1,
|
|
53
|
+
metavar="N",
|
|
54
|
+
help="Minimum fragment heavy atom count (default: 1)",
|
|
55
|
+
)
|
|
56
|
+
recap_parser.set_defaults(func=run_recap)
|
|
57
|
+
|
|
58
|
+
# fragment functional-groups
|
|
59
|
+
fg_parser = frag_subparsers.add_parser(
|
|
60
|
+
"functional-groups",
|
|
61
|
+
help="Extract functional group counts",
|
|
62
|
+
formatter_class=RdkitHelpFormatter,
|
|
63
|
+
)
|
|
64
|
+
add_common_io_options(fg_parser)
|
|
65
|
+
add_common_processing_options(fg_parser)
|
|
66
|
+
fg_parser.set_defaults(func=run_functional_groups)
|
|
67
|
+
|
|
68
|
+
# fragment analyze
|
|
69
|
+
analyze_parser = frag_subparsers.add_parser(
|
|
70
|
+
"analyze",
|
|
71
|
+
help="Analyze fragment frequency distribution",
|
|
72
|
+
formatter_class=RdkitHelpFormatter,
|
|
73
|
+
)
|
|
74
|
+
analyze_parser.add_argument(
|
|
75
|
+
"-i", "--input",
|
|
76
|
+
required=True,
|
|
77
|
+
metavar="FILE",
|
|
78
|
+
help="Input file with fragment_smiles column",
|
|
79
|
+
)
|
|
80
|
+
analyze_parser.add_argument(
|
|
81
|
+
"-o", "--output",
|
|
82
|
+
metavar="FILE",
|
|
83
|
+
help="Output file (optional, prints to stdout if not specified)",
|
|
84
|
+
)
|
|
85
|
+
analyze_parser.add_argument(
|
|
86
|
+
"--fragment-column",
|
|
87
|
+
default="fragment_smiles",
|
|
88
|
+
help="Name of fragment column (default: fragment_smiles)",
|
|
89
|
+
)
|
|
90
|
+
analyze_parser.add_argument(
|
|
91
|
+
"--top",
|
|
92
|
+
type=int,
|
|
93
|
+
default=20,
|
|
94
|
+
help="Number of top fragments to show (default: 20)",
|
|
95
|
+
)
|
|
96
|
+
analyze_parser.add_argument(
|
|
97
|
+
"--no-header",
|
|
98
|
+
action="store_true",
|
|
99
|
+
help="Input file has no header row",
|
|
100
|
+
)
|
|
101
|
+
analyze_parser.set_defaults(func=run_analyze)
|
|
102
|
+
|
|
103
|
+
# Set default for main parser
|
|
104
|
+
parser.set_defaults(func=lambda args: parser.print_help() or 1)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_brics(args) -> int:
|
|
108
|
+
"""Run BRICS fragmentation."""
|
|
109
|
+
from rdkit_cli.core.fragment import BRICSFragmenter
|
|
110
|
+
from rdkit_cli.io import create_reader, create_writer
|
|
111
|
+
|
|
112
|
+
fragmenter = BRICSFragmenter(
|
|
113
|
+
min_fragment_size=args.min_size,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
input_path = Path(args.input)
|
|
117
|
+
if not input_path.exists():
|
|
118
|
+
print(f"Error: Input file not found: {input_path}", file=sys.stderr)
|
|
119
|
+
return 1
|
|
120
|
+
|
|
121
|
+
reader = create_reader(
|
|
122
|
+
input_path,
|
|
123
|
+
smiles_column=args.smiles_column,
|
|
124
|
+
name_column=args.name_column,
|
|
125
|
+
has_header=not args.no_header,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
output_path = Path(args.output)
|
|
129
|
+
writer = create_writer(output_path)
|
|
130
|
+
|
|
131
|
+
total_input = 0
|
|
132
|
+
total_fragments = 0
|
|
133
|
+
|
|
134
|
+
with reader, writer:
|
|
135
|
+
for record in reader:
|
|
136
|
+
total_input += 1
|
|
137
|
+
results = fragmenter.fragment(record)
|
|
138
|
+
for result in results:
|
|
139
|
+
writer.write_row(result)
|
|
140
|
+
total_fragments += 1
|
|
141
|
+
|
|
142
|
+
if not args.quiet:
|
|
143
|
+
print(
|
|
144
|
+
f"Generated {total_fragments} BRICS fragments from {total_input} molecules",
|
|
145
|
+
file=sys.stderr,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def run_recap(args) -> int:
|
|
152
|
+
"""Run RECAP fragmentation."""
|
|
153
|
+
from rdkit_cli.core.fragment import RECAPFragmenter
|
|
154
|
+
from rdkit_cli.io import create_reader, create_writer
|
|
155
|
+
|
|
156
|
+
fragmenter = RECAPFragmenter(
|
|
157
|
+
min_fragment_size=args.min_size,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
input_path = Path(args.input)
|
|
161
|
+
if not input_path.exists():
|
|
162
|
+
print(f"Error: Input file not found: {input_path}", file=sys.stderr)
|
|
163
|
+
return 1
|
|
164
|
+
|
|
165
|
+
reader = create_reader(
|
|
166
|
+
input_path,
|
|
167
|
+
smiles_column=args.smiles_column,
|
|
168
|
+
name_column=args.name_column,
|
|
169
|
+
has_header=not args.no_header,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
output_path = Path(args.output)
|
|
173
|
+
writer = create_writer(output_path)
|
|
174
|
+
|
|
175
|
+
total_input = 0
|
|
176
|
+
total_fragments = 0
|
|
177
|
+
|
|
178
|
+
with reader, writer:
|
|
179
|
+
for record in reader:
|
|
180
|
+
total_input += 1
|
|
181
|
+
results = fragmenter.fragment(record)
|
|
182
|
+
for result in results:
|
|
183
|
+
writer.write_row(result)
|
|
184
|
+
total_fragments += 1
|
|
185
|
+
|
|
186
|
+
if not args.quiet:
|
|
187
|
+
print(
|
|
188
|
+
f"Generated {total_fragments} RECAP fragments from {total_input} molecules",
|
|
189
|
+
file=sys.stderr,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return 0
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def run_functional_groups(args) -> int:
|
|
196
|
+
"""Run functional group extraction."""
|
|
197
|
+
from rdkit_cli.core.fragment import FunctionalGroupExtractor
|
|
198
|
+
from rdkit_cli.io import create_reader, create_writer
|
|
199
|
+
|
|
200
|
+
extractor = FunctionalGroupExtractor()
|
|
201
|
+
|
|
202
|
+
input_path = Path(args.input)
|
|
203
|
+
if not input_path.exists():
|
|
204
|
+
print(f"Error: Input file not found: {input_path}", file=sys.stderr)
|
|
205
|
+
return 1
|
|
206
|
+
|
|
207
|
+
reader = create_reader(
|
|
208
|
+
input_path,
|
|
209
|
+
smiles_column=args.smiles_column,
|
|
210
|
+
name_column=args.name_column,
|
|
211
|
+
has_header=not args.no_header,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
output_path = Path(args.output)
|
|
215
|
+
writer = create_writer(output_path)
|
|
216
|
+
|
|
217
|
+
# Note: Running single-threaded because RDKit FragmentCatalog
|
|
218
|
+
# objects can't be pickled for multiprocessing
|
|
219
|
+
total = 0
|
|
220
|
+
successful = 0
|
|
221
|
+
|
|
222
|
+
with reader, writer:
|
|
223
|
+
for record in reader:
|
|
224
|
+
total += 1
|
|
225
|
+
result = extractor.extract(record)
|
|
226
|
+
if result is not None:
|
|
227
|
+
writer.write_row(result)
|
|
228
|
+
successful += 1
|
|
229
|
+
|
|
230
|
+
if not args.quiet:
|
|
231
|
+
print(
|
|
232
|
+
f"Extracted functional groups for {successful}/{total} molecules "
|
|
233
|
+
f"({total - successful} failed)",
|
|
234
|
+
file=sys.stderr,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return 0
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def run_analyze(args) -> int:
|
|
241
|
+
"""Run fragment frequency analysis."""
|
|
242
|
+
import pandas as pd
|
|
243
|
+
from rdkit_cli.core.fragment import analyze_fragments
|
|
244
|
+
|
|
245
|
+
input_path = Path(args.input)
|
|
246
|
+
if not input_path.exists():
|
|
247
|
+
print(f"Error: Input file not found: {input_path}", file=sys.stderr)
|
|
248
|
+
return 1
|
|
249
|
+
|
|
250
|
+
# Read fragment data
|
|
251
|
+
header = 0 if not args.no_header else None
|
|
252
|
+
df = pd.read_csv(input_path, header=header)
|
|
253
|
+
|
|
254
|
+
if args.no_header:
|
|
255
|
+
fragment_col = df.columns[0]
|
|
256
|
+
else:
|
|
257
|
+
fragment_col = args.fragment_column
|
|
258
|
+
|
|
259
|
+
if fragment_col not in df.columns:
|
|
260
|
+
print(f"Error: Fragment column '{fragment_col}' not found", file=sys.stderr)
|
|
261
|
+
return 1
|
|
262
|
+
|
|
263
|
+
fragments = df[fragment_col].dropna().tolist()
|
|
264
|
+
results = analyze_fragments(fragments, top_n=args.top)
|
|
265
|
+
|
|
266
|
+
# Output
|
|
267
|
+
output_lines = ["fragment,count,percentage"]
|
|
268
|
+
for fragment, count, pct in results:
|
|
269
|
+
fragment_escaped = fragment.replace('"', '""')
|
|
270
|
+
output_lines.append(f'"{fragment_escaped}",{count},{pct}')
|
|
271
|
+
|
|
272
|
+
output_text = "\n".join(output_lines)
|
|
273
|
+
|
|
274
|
+
if args.output:
|
|
275
|
+
output_path = Path(args.output)
|
|
276
|
+
with open(output_path, "w") as f:
|
|
277
|
+
f.write(output_text + "\n")
|
|
278
|
+
print(f"Wrote fragment analysis to {output_path}", file=sys.stderr)
|
|
279
|
+
else:
|
|
280
|
+
print(output_text)
|
|
281
|
+
|
|
282
|
+
print(f"\nTotal fragments: {len(fragments)}, Unique: {len(set(fragments))}", file=sys.stderr)
|
|
283
|
+
|
|
284
|
+
return 0
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""MCS (Maximum Common Substructure) command implementation."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rdkit_cli.cli import RdkitHelpFormatter, add_common_processing_options
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_parser(subparsers):
|
|
10
|
+
"""Register the mcs command."""
|
|
11
|
+
parser = subparsers.add_parser(
|
|
12
|
+
"mcs",
|
|
13
|
+
help="Find Maximum Common Substructure",
|
|
14
|
+
description="Find the Maximum Common Substructure (MCS) of molecules.",
|
|
15
|
+
formatter_class=RdkitHelpFormatter,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"-i", "--input",
|
|
20
|
+
required=True,
|
|
21
|
+
metavar="FILE",
|
|
22
|
+
help="Input file with molecules",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"-o", "--output",
|
|
26
|
+
metavar="FILE",
|
|
27
|
+
help="Output file (optional, prints to stdout if not specified)",
|
|
28
|
+
)
|
|
29
|
+
add_common_processing_options(parser)
|
|
30
|
+
|
|
31
|
+
# MCS options
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--timeout",
|
|
34
|
+
type=int,
|
|
35
|
+
default=60,
|
|
36
|
+
metavar="SEC",
|
|
37
|
+
help="Maximum time in seconds (default: 60)",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--threshold",
|
|
41
|
+
type=float,
|
|
42
|
+
default=1.0,
|
|
43
|
+
metavar="T",
|
|
44
|
+
help="Fraction of molecules that must contain MCS (default: 1.0)",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--maximize",
|
|
48
|
+
choices=["atoms", "bonds"],
|
|
49
|
+
default="atoms",
|
|
50
|
+
help="What to maximize (default: atoms)",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--no-ring-matches-ring",
|
|
54
|
+
action="store_true",
|
|
55
|
+
help="Allow ring atoms to match non-ring atoms",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--no-complete-rings",
|
|
59
|
+
action="store_true",
|
|
60
|
+
help="Allow partial ring matches",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--match-valences",
|
|
64
|
+
action="store_true",
|
|
65
|
+
help="Match atom valences",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--match-chirality",
|
|
69
|
+
action="store_true",
|
|
70
|
+
help="Match chirality",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--atom-compare",
|
|
74
|
+
choices=["any", "elements", "isotopes"],
|
|
75
|
+
default="elements",
|
|
76
|
+
help="Atom comparison method (default: elements)",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--bond-compare",
|
|
80
|
+
choices=["any", "order", "orderexact"],
|
|
81
|
+
default="order",
|
|
82
|
+
help="Bond comparison method (default: order)",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
parser.set_defaults(func=run_mcs)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def run_mcs(args) -> int:
|
|
89
|
+
"""Run MCS finding."""
|
|
90
|
+
from rdkit_cli.core.mcs import find_mcs
|
|
91
|
+
from rdkit_cli.io import create_reader
|
|
92
|
+
|
|
93
|
+
input_path = Path(args.input)
|
|
94
|
+
if not input_path.exists():
|
|
95
|
+
print(f"Error: Input file not found: {input_path}", file=sys.stderr)
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
reader = create_reader(
|
|
99
|
+
input_path,
|
|
100
|
+
smiles_column=args.smiles_column,
|
|
101
|
+
name_column=args.name_column,
|
|
102
|
+
has_header=not args.no_header,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if not args.quiet:
|
|
106
|
+
print("Reading molecules...", file=sys.stderr)
|
|
107
|
+
|
|
108
|
+
# Read all molecules
|
|
109
|
+
records = list(reader)
|
|
110
|
+
mols = [r.mol for r in records if r.mol is not None]
|
|
111
|
+
|
|
112
|
+
if len(mols) < 2:
|
|
113
|
+
print("Error: Need at least 2 valid molecules for MCS", file=sys.stderr)
|
|
114
|
+
return 1
|
|
115
|
+
|
|
116
|
+
if not args.quiet:
|
|
117
|
+
print(f"Finding MCS for {len(mols)} molecules...", file=sys.stderr)
|
|
118
|
+
|
|
119
|
+
# Find MCS
|
|
120
|
+
result = find_mcs(
|
|
121
|
+
mols,
|
|
122
|
+
timeout=args.timeout,
|
|
123
|
+
threshold=args.threshold,
|
|
124
|
+
maximize=args.maximize,
|
|
125
|
+
ring_matches_ring_only=not args.no_ring_matches_ring,
|
|
126
|
+
complete_rings_only=not args.no_complete_rings,
|
|
127
|
+
match_valences=args.match_valences,
|
|
128
|
+
match_chiral_tag=args.match_chirality,
|
|
129
|
+
atom_compare=args.atom_compare,
|
|
130
|
+
bond_compare=args.bond_compare,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if result is None:
|
|
134
|
+
print("Error: MCS computation failed", file=sys.stderr)
|
|
135
|
+
return 1
|
|
136
|
+
|
|
137
|
+
if result.get("error"):
|
|
138
|
+
print(f"Error: {result['error']}", file=sys.stderr)
|
|
139
|
+
return 1
|
|
140
|
+
|
|
141
|
+
# Output results
|
|
142
|
+
if args.output:
|
|
143
|
+
from rdkit_cli.io import create_writer
|
|
144
|
+
output_path = Path(args.output)
|
|
145
|
+
writer = create_writer(output_path)
|
|
146
|
+
with writer:
|
|
147
|
+
writer.write_row(result)
|
|
148
|
+
if not args.quiet:
|
|
149
|
+
print(f"Wrote MCS result to {output_path}", file=sys.stderr)
|
|
150
|
+
else:
|
|
151
|
+
print("\nMCS Results")
|
|
152
|
+
print("=" * 50)
|
|
153
|
+
|
|
154
|
+
if result.get("canceled"):
|
|
155
|
+
print(f"WARNING: Search timed out after {args.timeout}s")
|
|
156
|
+
|
|
157
|
+
print(f"SMARTS: {result.get('smarts', 'N/A')}")
|
|
158
|
+
print(f"Atoms: {result.get('num_atoms', 0)}")
|
|
159
|
+
print(f"Bonds: {result.get('num_bonds', 0)}")
|
|
160
|
+
print("=" * 50)
|
|
161
|
+
|
|
162
|
+
return 0
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Reactions command implementation."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rdkit_cli.cli import RdkitHelpFormatter, add_common_io_options, add_common_processing_options
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_parser(subparsers):
|
|
10
|
+
"""Register the reactions command and subcommands."""
|
|
11
|
+
parser = subparsers.add_parser(
|
|
12
|
+
"reactions",
|
|
13
|
+
help="Apply chemical reactions and transformations",
|
|
14
|
+
description="Apply SMIRKS transformations and enumerate reaction products.",
|
|
15
|
+
formatter_class=RdkitHelpFormatter,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
rxn_subparsers = parser.add_subparsers(
|
|
19
|
+
title="Subcommands",
|
|
20
|
+
dest="subcommand",
|
|
21
|
+
metavar="<subcommand>",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# reactions transform
|
|
25
|
+
transform_parser = rxn_subparsers.add_parser(
|
|
26
|
+
"transform",
|
|
27
|
+
help="Apply SMIRKS transformation",
|
|
28
|
+
formatter_class=RdkitHelpFormatter,
|
|
29
|
+
)
|
|
30
|
+
add_common_io_options(transform_parser)
|
|
31
|
+
add_common_processing_options(transform_parser)
|
|
32
|
+
transform_parser.add_argument(
|
|
33
|
+
"-s", "--smirks",
|
|
34
|
+
required=True,
|
|
35
|
+
metavar="SMIRKS",
|
|
36
|
+
help="SMIRKS transformation pattern",
|
|
37
|
+
)
|
|
38
|
+
transform_parser.add_argument(
|
|
39
|
+
"--max-products",
|
|
40
|
+
type=int,
|
|
41
|
+
default=100,
|
|
42
|
+
help="Maximum products per molecule (default: 100)",
|
|
43
|
+
)
|
|
44
|
+
transform_parser.set_defaults(func=run_transform)
|
|
45
|
+
|
|
46
|
+
# reactions enumerate
|
|
47
|
+
enum_parser = rxn_subparsers.add_parser(
|
|
48
|
+
"enumerate",
|
|
49
|
+
help="Enumerate reaction products",
|
|
50
|
+
formatter_class=RdkitHelpFormatter,
|
|
51
|
+
)
|
|
52
|
+
add_common_io_options(enum_parser)
|
|
53
|
+
add_common_processing_options(enum_parser)
|
|
54
|
+
enum_parser.add_argument(
|
|
55
|
+
"-t", "--template",
|
|
56
|
+
required=True,
|
|
57
|
+
metavar="SMARTS",
|
|
58
|
+
help="Reaction SMARTS template",
|
|
59
|
+
)
|
|
60
|
+
enum_parser.add_argument(
|
|
61
|
+
"--reactant2",
|
|
62
|
+
metavar="FILE",
|
|
63
|
+
help="Second reactant file (if reaction has 2 reactants)",
|
|
64
|
+
)
|
|
65
|
+
enum_parser.add_argument(
|
|
66
|
+
"--max-products",
|
|
67
|
+
type=int,
|
|
68
|
+
default=1000,
|
|
69
|
+
help="Maximum total products (default: 1000)",
|
|
70
|
+
)
|
|
71
|
+
enum_parser.set_defaults(func=run_enumerate)
|
|
72
|
+
|
|
73
|
+
# Set default for main parser
|
|
74
|
+
parser.set_defaults(func=lambda args: parser.print_help() or 1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run_transform(args) -> int:
|
|
78
|
+
"""Run SMIRKS transformation."""
|
|
79
|
+
# Lazy imports
|
|
80
|
+
from rdkit_cli.core.reactions import ReactionTransformer
|
|
81
|
+
from rdkit_cli.io import create_reader, create_writer
|
|
82
|
+
from rdkit_cli.parallel.batch import process_molecules
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
transformer = ReactionTransformer(
|
|
86
|
+
smirks=args.smirks,
|
|
87
|
+
max_products=args.max_products,
|
|
88
|
+
)
|
|
89
|
+
except ValueError as e:
|
|
90
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
input_path = Path(args.input)
|
|
94
|
+
if not input_path.exists():
|
|
95
|
+
print(f"Error: Input file not found: {input_path}", file=sys.stderr)
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
reader = create_reader(
|
|
99
|
+
input_path,
|
|
100
|
+
smiles_column=args.smiles_column,
|
|
101
|
+
name_column=args.name_column,
|
|
102
|
+
has_header=not args.no_header,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
output_path = Path(args.output)
|
|
106
|
+
writer = create_writer(output_path)
|
|
107
|
+
|
|
108
|
+
with reader, writer:
|
|
109
|
+
result = process_molecules(
|
|
110
|
+
reader=reader,
|
|
111
|
+
writer=writer,
|
|
112
|
+
processor=transformer.transform,
|
|
113
|
+
n_workers=args.ncpu,
|
|
114
|
+
quiet=args.quiet,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if not args.quiet:
|
|
118
|
+
print(
|
|
119
|
+
f"Transformed {result.successful}/{result.total_processed} molecules "
|
|
120
|
+
f"({result.total_processed - result.successful - result.failed} no reaction, "
|
|
121
|
+
f"{result.failed} failed) in {result.elapsed_time:.1f}s",
|
|
122
|
+
file=sys.stderr,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def run_enumerate(args) -> int:
|
|
129
|
+
"""Run reaction enumeration."""
|
|
130
|
+
# Lazy imports
|
|
131
|
+
from rdkit_cli.core.reactions import ReactionEnumerator
|
|
132
|
+
from rdkit_cli.io import create_reader, create_writer
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
enumerator = ReactionEnumerator(
|
|
136
|
+
reaction_smarts=args.template,
|
|
137
|
+
max_products=args.max_products,
|
|
138
|
+
)
|
|
139
|
+
except ValueError as e:
|
|
140
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
141
|
+
return 1
|
|
142
|
+
|
|
143
|
+
# Read reactants
|
|
144
|
+
input_path = Path(args.input)
|
|
145
|
+
if not input_path.exists():
|
|
146
|
+
print(f"Error: Input file not found: {input_path}", file=sys.stderr)
|
|
147
|
+
return 1
|
|
148
|
+
|
|
149
|
+
if not args.quiet:
|
|
150
|
+
print("Reading reactants...", file=sys.stderr)
|
|
151
|
+
|
|
152
|
+
reader1 = create_reader(
|
|
153
|
+
input_path,
|
|
154
|
+
smiles_column=args.smiles_column,
|
|
155
|
+
has_header=not args.no_header,
|
|
156
|
+
)
|
|
157
|
+
mols1 = [r.mol for r in reader1 if r.mol is not None]
|
|
158
|
+
|
|
159
|
+
reactant_lists = [mols1]
|
|
160
|
+
|
|
161
|
+
# Read second reactant file if provided
|
|
162
|
+
if args.reactant2:
|
|
163
|
+
reactant2_path = Path(args.reactant2)
|
|
164
|
+
if not reactant2_path.exists():
|
|
165
|
+
print(f"Error: Reactant2 file not found: {reactant2_path}", file=sys.stderr)
|
|
166
|
+
return 1
|
|
167
|
+
|
|
168
|
+
reader2 = create_reader(reactant2_path, smiles_column=args.smiles_column)
|
|
169
|
+
mols2 = [r.mol for r in reader2 if r.mol is not None]
|
|
170
|
+
reactant_lists.append(mols2)
|
|
171
|
+
|
|
172
|
+
if not args.quiet:
|
|
173
|
+
print(f"Enumerating products from {len(mols1)} reactant(s)...", file=sys.stderr)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
products = enumerator.enumerate(reactant_lists)
|
|
177
|
+
except ValueError as e:
|
|
178
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
179
|
+
return 1
|
|
180
|
+
|
|
181
|
+
# Write output
|
|
182
|
+
output_path = Path(args.output)
|
|
183
|
+
writer = create_writer(output_path)
|
|
184
|
+
|
|
185
|
+
with writer:
|
|
186
|
+
writer.write_batch(products)
|
|
187
|
+
|
|
188
|
+
if not args.quiet:
|
|
189
|
+
print(f"Generated {len(products)} products. Wrote to {output_path}", file=sys.stderr)
|
|
190
|
+
|
|
191
|
+
return 0
|