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.
Files changed (47) hide show
  1. rdkit_cli/__init__.py +4 -0
  2. rdkit_cli/__main__.py +6 -0
  3. rdkit_cli/cli.py +162 -0
  4. rdkit_cli/commands/__init__.py +1 -0
  5. rdkit_cli/commands/conformers.py +220 -0
  6. rdkit_cli/commands/convert.py +162 -0
  7. rdkit_cli/commands/depict.py +311 -0
  8. rdkit_cli/commands/descriptors.py +251 -0
  9. rdkit_cli/commands/diversity.py +232 -0
  10. rdkit_cli/commands/enumerate.py +229 -0
  11. rdkit_cli/commands/filter.py +384 -0
  12. rdkit_cli/commands/fingerprints.py +179 -0
  13. rdkit_cli/commands/fragment.py +284 -0
  14. rdkit_cli/commands/mcs.py +162 -0
  15. rdkit_cli/commands/reactions.py +191 -0
  16. rdkit_cli/commands/scaffold.py +243 -0
  17. rdkit_cli/commands/similarity.py +359 -0
  18. rdkit_cli/commands/standardize.py +138 -0
  19. rdkit_cli/core/__init__.py +1 -0
  20. rdkit_cli/core/conformers.py +197 -0
  21. rdkit_cli/core/depict.py +241 -0
  22. rdkit_cli/core/descriptors.py +248 -0
  23. rdkit_cli/core/diversity.py +174 -0
  24. rdkit_cli/core/enumerate.py +190 -0
  25. rdkit_cli/core/filters.py +443 -0
  26. rdkit_cli/core/fingerprints.py +265 -0
  27. rdkit_cli/core/fragment.py +237 -0
  28. rdkit_cli/core/mcs.py +128 -0
  29. rdkit_cli/core/reactions.py +159 -0
  30. rdkit_cli/core/scaffold.py +174 -0
  31. rdkit_cli/core/similarity.py +206 -0
  32. rdkit_cli/core/standardizer.py +141 -0
  33. rdkit_cli/io/__init__.py +7 -0
  34. rdkit_cli/io/formats.py +109 -0
  35. rdkit_cli/io/readers.py +352 -0
  36. rdkit_cli/io/writers.py +275 -0
  37. rdkit_cli/parallel/__init__.py +5 -0
  38. rdkit_cli/parallel/batch.py +181 -0
  39. rdkit_cli/parallel/executor.py +180 -0
  40. rdkit_cli/progress/__init__.py +5 -0
  41. rdkit_cli/progress/ninja.py +195 -0
  42. rdkit_cli/utils/__init__.py +1 -0
  43. rdkit_cli-0.1.0.dist-info/METADATA +380 -0
  44. rdkit_cli-0.1.0.dist-info/RECORD +47 -0
  45. rdkit_cli-0.1.0.dist-info/WHEEL +4 -0
  46. rdkit_cli-0.1.0.dist-info/entry_points.txt +2 -0
  47. 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