schemist 0.0.1__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.
schemist/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("schemist")
schemist/cleaning.py ADDED
@@ -0,0 +1,27 @@
1
+ """Chemical structure cleaning routines."""
2
+
3
+ from carabiner.decorators import vectorize
4
+
5
+ from datamol import sanitize_smiles
6
+ import selfies as sf
7
+
8
+ @vectorize
9
+ def clean_smiles(smiles: str,
10
+ *args, **kwargs) -> str:
11
+
12
+ """Sanitize a SMILES string or list of SMILES strings.
13
+
14
+ """
15
+
16
+ return sanitize_smiles(smiles, *args, **kwargs)
17
+
18
+
19
+ @vectorize
20
+ def clean_selfies(selfies: str,
21
+ *args, **kwargs) -> str:
22
+
23
+ """Sanitize a SELFIES string or list of SELFIES strings.
24
+
25
+ """
26
+
27
+ return sf.encode(sanitize_smiles(sf.decode(selfies), *args, **kwargs))
schemist/cli.py ADDED
@@ -0,0 +1,535 @@
1
+ """Command-line interface for schemist."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from argparse import FileType, Namespace
6
+ from collections import Counter, defaultdict
7
+ from functools import partial
8
+ import os
9
+ import sys
10
+ from tempfile import NamedTemporaryFile, TemporaryDirectory
11
+
12
+ from carabiner import pprint_dict, upper_and_lower
13
+ from carabiner.cliutils import clicommand, CLIOption, CLICommand, CLIApp
14
+ from carabiner.itertools import tenumerate
15
+ from carabiner.pd import get_formats, write_stream
16
+
17
+ from . import __version__
18
+ from .collating import collate_inventory, deduplicate_file
19
+ from .converting import _TO_FUNCTIONS, _FROM_FUNCTIONS
20
+ from .generating import AA, REACTIONS
21
+ from .io import _mutate_df_stream
22
+ from .tables import (converter, cleaner, featurizer, assign_groups,
23
+ _assign_splits, splitter, _peptide_table, reactor)
24
+ from .splitting import _SPLITTERS, _GROUPED_SPLITTERS
25
+
26
+ def _option_parser(x: Optional[List[str]]) -> Dict[str, Any]:
27
+
28
+ options = {}
29
+
30
+ try:
31
+ for opt in x:
32
+
33
+ try:
34
+ key, value = opt.split('=')
35
+ except ValueError:
36
+ raise ValueError(f"Option {opt} is misformatted. It should be in the format keyword=value.")
37
+
38
+ try:
39
+ value = int(value)
40
+ except ValueError:
41
+ try:
42
+ value = float(value)
43
+ except ValueError:
44
+ pass
45
+
46
+ options[key] = value
47
+
48
+ except TypeError:
49
+
50
+ pass
51
+
52
+ return options
53
+
54
+
55
+ def _sum_tally(tallies: Counter,
56
+ message: str = "Error counts",
57
+ use_length: bool = False):
58
+
59
+ total_tally = Counter()
60
+
61
+ for tally in tallies:
62
+
63
+ if use_length:
64
+ total_tally.update({key: len(value) for key, value in tally.items()})
65
+ else:
66
+ total_tally.update(tally)
67
+
68
+ if len(tallies) == 0:
69
+ raise ValueError(f"Nothing generated!")
70
+
71
+ pprint_dict(total_tally, message=message)
72
+
73
+ return total_tally
74
+
75
+
76
+ @clicommand(message="Cleaning file with the following parameters")
77
+ def _clean(args: Namespace) -> None:
78
+
79
+ error_tallies = _mutate_df_stream(input_file=args.input,
80
+ output_file=args.output,
81
+ function=partial(cleaner,
82
+ column=args.column,
83
+ input_representation=args.representation,
84
+ prefix=args.prefix),
85
+ file_format=args.format)
86
+
87
+ _sum_tally(error_tallies)
88
+
89
+ return None
90
+
91
+
92
+ @clicommand(message="Converting between string representations with the following parameters")
93
+ def _convert(args: Namespace) -> None:
94
+
95
+ options = _option_parser(args.options)
96
+
97
+ error_tallies = _mutate_df_stream(input_file=args.input,
98
+ output_file=args.output,
99
+ function=partial(converter,
100
+ column=args.column,
101
+ input_representation=args.representation,
102
+ output_representation=args.to,
103
+ prefix=args.prefix,
104
+ options=options),
105
+ file_format=args.format)
106
+
107
+ _sum_tally(error_tallies)
108
+
109
+ return None
110
+
111
+
112
+ @clicommand(message="Adding features to files with the following parameters")
113
+ def _featurize(args: Namespace) -> None:
114
+
115
+ error_tallies = _mutate_df_stream(input_file=args.input,
116
+ output_file=args.output,
117
+ function=partial(featurizer,
118
+ feature_type=args.feature,
119
+ column=args.column,
120
+ ids=args.id,
121
+ input_representation=args.representation,
122
+ prefix=args.prefix),
123
+ file_format=args.format)
124
+
125
+ _sum_tally(error_tallies)
126
+
127
+ return None
128
+
129
+
130
+ @clicommand(message="Splitting table with the following parameters")
131
+ def _split(args: Namespace) -> None:
132
+
133
+ split_type = args.type.casefold()
134
+
135
+ if split_type in _GROUPED_SPLITTERS:
136
+
137
+ chunk_processor, aggregator = _GROUPED_SPLITTERS[split_type]
138
+
139
+ with TemporaryDirectory() as dir:
140
+
141
+ with NamedTemporaryFile("w", dir=dir, delete=False) as f:
142
+
143
+ group_idxs = _mutate_df_stream(input_file=args.input,
144
+ output_file=f,
145
+ function=partial(assign_groups,
146
+ grouper=chunk_processor,
147
+ group_name=split_type,
148
+ column=args.column,
149
+ input_representation=args.representation),
150
+ file_format=args.format)
151
+ f.close()
152
+ new_group_idx = defaultdict(list)
153
+
154
+ totals = 0
155
+ for group_idx in group_idxs:
156
+ these_totals = 0
157
+ for key, value in group_idx.items():
158
+ these_totals += len(value)
159
+ new_group_idx[key] += [idx + totals for idx in value]
160
+ totals += these_totals
161
+
162
+ group_idx = aggregator(new_group_idx,
163
+ train=args.train,
164
+ test=args.test)
165
+
166
+ split_tallies = _mutate_df_stream(input_file=f.name,
167
+ output_file=args.output,
168
+ function=partial(_assign_splits,
169
+ split_idx=group_idx,
170
+ use_df_index=True),
171
+ file_format=args.format)
172
+ if os.path.exists(f.name):
173
+ os.remove(f.name)
174
+
175
+ else:
176
+
177
+ split_tallies = _mutate_df_stream(input_file=args.input,
178
+ output_file=args.output,
179
+ function=partial(splitter,
180
+ split_type=args.type,
181
+ column=args.column,
182
+ input_representation=args.representation,
183
+ train=args.train,
184
+ test=args.test,
185
+ set_seed=args.seed),
186
+ file_format=args.format)
187
+
188
+ _sum_tally(split_tallies,
189
+ message="Split counts")
190
+
191
+ return None
192
+
193
+
194
+ @clicommand(message="Collating files with the following parameters")
195
+ def _collate(args: Namespace) -> None:
196
+
197
+ root_dir = args.data_dir or '.'
198
+
199
+ error_tallies = _mutate_df_stream(input_file=args.input,
200
+ output_file=args.output,
201
+ function=partial(collate_inventory,
202
+ root_dir=root_dir,
203
+ drop_unmapped=not args.keep_extra_columns,
204
+ catalog_smiles_column=args.column,
205
+ id_column_name=args.id_column,
206
+ id_n_digits=args.digits,
207
+ id_prefix=args.prefix),
208
+ file_format=args.format)
209
+
210
+ _sum_tally(error_tallies,
211
+ message="Collated chemicals:")
212
+
213
+ return None
214
+
215
+
216
+ @clicommand(message="Deduplicating chemical structures with the following parameters")
217
+ def _dedup(args: Namespace) -> None:
218
+
219
+ report, deduped_df = deduplicate_file(args.input,
220
+ format=args.format,
221
+ column=args.column,
222
+ input_representation=args.representation,
223
+ index_columns=args.indexes)
224
+
225
+ if args.prefix is not None and 'inchikey' in deduped_df:
226
+ deduped_df = deduped_df.rename(columns={'inchikey': f'{args.prefix}inchikey'})
227
+
228
+ write_stream(deduped_df,
229
+ output=args.output,
230
+ format=args.format)
231
+
232
+ pprint_dict(report, message="Finished deduplicating:")
233
+
234
+ return None
235
+
236
+
237
+ @clicommand(message="Enumerating peptides with the following parameters")
238
+ def _enum(args: Namespace) -> None:
239
+
240
+ tables = _peptide_table(max_length=args.max_length,
241
+ min_length=args.min_length,
242
+ n=args.number,
243
+ indexes=args.slice,
244
+ set_seed=args.seed,
245
+ prefix=args.prefix,
246
+ suffix=args.suffix,
247
+ d_aa_only=args.d_aa_only,
248
+ include_d_aa=args.include_d_aa,
249
+ generator=True)
250
+
251
+ dAA_use = any(aa.islower() for aa in args.prefix + args.suffix)
252
+ dAA_use = dAA_use or args.include_d_aa or args.d_aa_only
253
+
254
+ tallies, error_tallies = [], []
255
+ options = _option_parser(args.options)
256
+ _converter = partial(converter,
257
+ column='peptide_sequence',
258
+ input_representation='minihelm' if dAA_use else 'aa_seq', ## affects performance
259
+ output_representation=args.to,
260
+ options=options)
261
+
262
+ for i, table in tenumerate(tables):
263
+
264
+ _err_tally, df = _converter(table)
265
+
266
+ tallies.append({"Number of peptides": df.shape[0]})
267
+ error_tallies.append(_err_tally)
268
+
269
+ write_stream(df,
270
+ output=args.output,
271
+ format=args.format,
272
+ mode='w' if i == 0 else 'a',
273
+ header=i == 0)
274
+
275
+ _sum_tally(tallies,
276
+ message="Enumerated peptides")
277
+ _sum_tally(error_tallies,
278
+ message="Conversion errors")
279
+
280
+ return None
281
+
282
+
283
+ @clicommand(message="Reacting peptides with the following parameters")
284
+ def _react(args: Namespace) -> None:
285
+
286
+ error_tallies = _mutate_df_stream(input_file=args.input,
287
+ output_file=args.output,
288
+ function=partial(reactor,
289
+ column=args.column,
290
+ input_representation=args.representation,
291
+ reaction=args.reaction,
292
+ product_name=args.name),
293
+ file_format=args.format)
294
+
295
+ _sum_tally(error_tallies)
296
+
297
+ return None
298
+
299
+
300
+ def main() -> None:
301
+
302
+ inputs = CLIOption('input',
303
+ default=sys.stdin,
304
+ type=FileType('r'),
305
+ nargs='?',
306
+ help='Input columnar Excel, CSV or TSV file. Default: STDIN.')
307
+ representation = CLIOption('--representation', '-r',
308
+ type=str,
309
+ default='SMILES',
310
+ choices=upper_and_lower(_FROM_FUNCTIONS),
311
+ help='Chemical representation to use for input. ')
312
+ column = CLIOption('--column', '-c',
313
+ default='smiles',
314
+ type=str,
315
+ help='Column to use as input string representation. ')
316
+ prefix = CLIOption('--prefix', '-p',
317
+ default=None,
318
+ type=str,
319
+ help='Prefix to add to new column name. Default: no prefix')
320
+ to = CLIOption('--to', '-2',
321
+ type=str,
322
+ default='SMILES',
323
+ nargs='*',
324
+ choices=upper_and_lower(_TO_FUNCTIONS),
325
+ help='Format to convert to.')
326
+ options = CLIOption('--options', '-x',
327
+ type=str,
328
+ default=None,
329
+ nargs='*',
330
+ help='Options to pass to converter, in the format '
331
+ '"keyword1=value1 keyword2=value2"')
332
+ output = CLIOption('--output', '-o',
333
+ type=FileType('w'),
334
+ default=sys.stdout,
335
+ help='Output file. Default: STDOUT')
336
+ formatting = CLIOption('--format', '-f',
337
+ type=str,
338
+ default=None,
339
+ choices=upper_and_lower(get_formats()),
340
+ help='Override file extensions for input and output. '
341
+ 'Default: infer from file extension.')
342
+
343
+ ## featurize
344
+ id_feat = CLIOption('--id', '-i',
345
+ type=str,
346
+ default=None,
347
+ nargs='*',
348
+ help='Columns to retain in output table. Default: use all')
349
+ feature = CLIOption('--feature', '-t',
350
+ type=str,
351
+ default='2d',
352
+ choices=['2d', 'fp'], ## TODO: implement 3d
353
+ help='Which feature type to generate.')
354
+
355
+ ## split
356
+ type_ = CLIOption('--type', '-t',
357
+ type=str,
358
+ default='random',
359
+ choices=upper_and_lower(_SPLITTERS),
360
+ help='Which split type to use.')
361
+ train = CLIOption('--train', '-a',
362
+ type=float,
363
+ default=1.,
364
+ help='Proportion of data to use for training. ')
365
+ test = CLIOption('--test', '-b',
366
+ type=float,
367
+ default=0.,
368
+ help='Proportion of data to use for testing. ')
369
+
370
+ ## collate
371
+ data_dir = CLIOption('--data-dir', '-d',
372
+ type=str,
373
+ default=None,
374
+ help='Directory containing data files. '
375
+ 'Default: current directory')
376
+ id_column = CLIOption('--id-column', '-s',
377
+ default=None,
378
+ type=str,
379
+ help='If provided, add a structure ID column with this name. '
380
+ 'Default: don\'t add structure IDs')
381
+ prefix_collate = CLIOption('--prefix', '-p',
382
+ default='ID-',
383
+ type=str,
384
+ help='Prefix to add to structure IDs. '
385
+ 'Default: no prefix')
386
+ digits = CLIOption('--digits', '-n',
387
+ default=8,
388
+ type=int,
389
+ help='Number of digits in structure IDs. ')
390
+ keep_extra_columns = CLIOption('--keep-extra-columns', '-x',
391
+ action='store_true',
392
+ help='Whether to keep columns not mentioned in the catalog. '
393
+ 'Default: drop extra columns.')
394
+ keep_invalid_smiles = CLIOption('--keep-invalid-smiles', '-y',
395
+ action='store_true',
396
+ help='Whether to keep rows with invalid SMILES. '
397
+ 'Default: drop invalid rows.')
398
+
399
+ ## dedup
400
+ indexes = CLIOption('--indexes', '-x',
401
+ type=str,
402
+ default=None,
403
+ nargs='*',
404
+ help='Columns to retain and collapse (if multiple values per unique structure). '
405
+ 'Default: retain no other columns than structure and InchiKey.')
406
+ drop_inchikey = CLIOption('--drop-inchikey', '-d',
407
+ action='store_true',
408
+ help='Whether to drop the calculated InchiKey column. '
409
+ 'Default: keep InchiKey.')
410
+
411
+ ### enum
412
+ max_length = CLIOption('--max-length', '-l',
413
+ type=int,
414
+ help='Maximum length of enumerated peptide. '
415
+ 'Required.')
416
+ min_length = CLIOption('--min-length', '-m',
417
+ type=int,
418
+ default=None,
419
+ help='Minimum length of enumerated peptide. '
420
+ 'Default: same as maximum, i.e. all peptides same length.')
421
+ number_to_gen = CLIOption('--number', '-n',
422
+ type=float,
423
+ default=None,
424
+ help='Number of peptides to sample from all possible '
425
+ 'within the constraints. If less than 1, sample '
426
+ 'that fraction of all possible. If greater than 1, '
427
+ 'sample that number. '
428
+ 'Default: return all peptides.')
429
+ slicer = CLIOption('--slice', '-z',
430
+ type=str,
431
+ default=None,
432
+ nargs='*',
433
+ help='Subset of (possibly sampled) population to return, in the format <stop> '
434
+ 'or <start> <stop> [<step>]. If "x" is used for <stop>, then it runs to the end. '
435
+ 'For example, 1000 gives the first 1000, 2 600 gives items 2-600, and '
436
+ '3 500 2 gives every other from 3 to 500. Default: return all.')
437
+ alphabet = CLIOption('--alphabet', '-b',
438
+ type=str,
439
+ default=''.join(AA),
440
+ help='Alphabet to use in sampling.')
441
+ suffix = CLIOption('--suffix', '-s',
442
+ type=str,
443
+ default='',
444
+ help='Sequence to add to end. Lowercase for D-amino acids. '
445
+ 'Default: no suffix.')
446
+ set_seed = CLIOption('--seed', '-e',
447
+ type=int,
448
+ default=None,
449
+ help='Seed to use for reproducible randomness. '
450
+ 'Default: don\'t enable reproducibility.')
451
+ d_aa_only = CLIOption('--d-aa-only', '-a',
452
+ action='store_true',
453
+ help='Whether to only use D-amino acids. '
454
+ 'Default: don\'t include.')
455
+ include_d_aa = CLIOption('--include-d-aa', '-y',
456
+ action='store_true',
457
+ help='Whether to include D-amino acids in enumeration. '
458
+ 'Default: don\'t include.')
459
+
460
+ ## reaction
461
+ name = CLIOption('--name', '-n',
462
+ type=str,
463
+ default=None,
464
+ help='Name of column for product. '
465
+ 'Default: same as reaction name.')
466
+ reaction_opt = CLIOption('--reaction', '-x',
467
+ type=str,
468
+ nargs='*',
469
+ choices=list(REACTIONS),
470
+ default='N_to_C_cyclization',
471
+ help='Reaction(s) to apply.')
472
+
473
+ clean = CLICommand('clean',
474
+ description='Clean and normalize SMILES column of a table.',
475
+ main=_clean,
476
+ options=[output, formatting, inputs, representation, column, prefix])
477
+ convert = CLICommand('convert',
478
+ description='Convert between string representations of chemical structures.',
479
+ main=_convert,
480
+ options=[output, formatting, inputs, representation, column, prefix, to, options])
481
+ featurize = CLICommand('featurize',
482
+ description='Convert between string representations of chemical structures.',
483
+ main=_featurize,
484
+ options=[output, formatting, inputs, representation, column, prefix,
485
+ id_feat, feature])
486
+ collate = CLICommand('collate',
487
+ description='Collect disparate tables or SDF files of libraries into a single table.',
488
+ main=_collate,
489
+ options=[output, formatting, inputs, representation,
490
+ data_dir, column.replace(default='input_smiles'), id_column, prefix_collate,
491
+ digits, keep_extra_columns, keep_invalid_smiles])
492
+ dedup = CLICommand('dedup',
493
+ description='Deduplicate chemical structures and retain references.',
494
+ main=_dedup,
495
+ options=[output, formatting, inputs, representation, column, prefix,
496
+ indexes, drop_inchikey])
497
+ enum = CLICommand('enumerate',
498
+ description='Enumerate bio-chemical structures within length and sequence constraints.',
499
+ main=_enum,
500
+ options=[output, formatting, to, options,
501
+ alphabet, max_length, min_length, number_to_gen,
502
+ slicer, set_seed,
503
+ prefix.replace(default='',
504
+ help='Sequence to prepend. Lowercase for D-amino acids. '
505
+ 'Default: no prefix.'),
506
+ suffix,
507
+ type_.replace(default='aa',
508
+ choices=['aa'],
509
+ help='Type of bio sequence to enumerate. '
510
+ 'Default: %(default)s.'),
511
+ d_aa_only, include_d_aa])
512
+ reaction = CLICommand('react',
513
+ description='React compounds in silico in indicated columns using a named reaction.',
514
+ main=_react,
515
+ options=[output, formatting, inputs, representation, column, name,
516
+ reaction_opt])
517
+ split = CLICommand('split',
518
+ description='Split table based on chosen algorithm, optionally taking account of chemical structure during splits.',
519
+ main=_split,
520
+ options=[output, formatting, inputs, representation, column, prefix,
521
+ type_, train, test, set_seed])
522
+
523
+ app = CLIApp("schemist",
524
+ version=__version__,
525
+ description="Tools for cleaning, collating, and augmenting chemical datasets.",
526
+ commands=[clean, convert, featurize, collate, dedup, enum, reaction, split])
527
+
528
+ app.run()
529
+
530
+ return None
531
+
532
+
533
+ if __name__ == "__main__":
534
+
535
+ main()