cellmaps-utils 0.10.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.
@@ -0,0 +1,1000 @@
1
+
2
+ import os
3
+ import shutil
4
+ import uuid
5
+ import re
6
+ import logging
7
+ import pandas as pd
8
+ import random
9
+ import time
10
+ import string
11
+ import json
12
+ import numpy as np
13
+ import cellmaps_utils
14
+ import ndex2
15
+ from ndex2.cx2 import RawCX2NetworkFactory, CX2NetworkXFactory
16
+
17
+ from cellmaps_utils.basecmdtool import BaseCommandLineTool
18
+ from cellmaps_utils.exceptions import CellMapsError
19
+ from cellmaps_utils import constants
20
+ from cellmaps_utils import logutils
21
+ from cellmaps_utils.provenance import ProvenanceUtil
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ REPL_ONE_TSV = 'repl1.tsv'
27
+ REPL_TWO_TSV = 'repl2.tsv'
28
+ REPL_MAPPING = 'repl1_repl2_id_mapping.json'
29
+ REPL_ONE_TWO_COMBINED = 'repl1_repl2_combined.tsv'
30
+ NEW_ID_COLNAME = 'xxx'
31
+
32
+
33
+ def _get_dataframe(inputfile=None):
34
+ """
35
+ Loads TSV as dataframe
36
+
37
+ :param inputfile:
38
+ :type inputfile: str
39
+ :return: Loaded dataframe
40
+ :rtype: :py:class:`pandas.DataFrame`
41
+ """
42
+ return pd.read_csv(inputfile, sep='\t')
43
+
44
+
45
+ def _get_set_of_all_values_of_column_from_dataframes(dataframes=[], col_name=None):
46
+ """
47
+ Iterates across all dataframes and pulls values from column specified by **col_name**
48
+
49
+ :param dataframes: data frames
50
+ :type dataframes: list
51
+ :param col_name: Column where values will be obtained
52
+ :type col_name: str
53
+ :return: unique set of values from **col_name** column across all data frames
54
+ :rtype: set
55
+ """
56
+ if dataframes is None:
57
+ raise CellMapsError('dataframes cannot be None')
58
+
59
+ df_list = dataframes
60
+ if not isinstance(df_list, list):
61
+ df_list = [dataframes]
62
+
63
+ if col_name is None:
64
+ raise CellMapsError('Column name must be specified')
65
+ col_values = set()
66
+ for df in df_list:
67
+ col_values.update(df[col_name].unique())
68
+ return col_values
69
+
70
+
71
+ def _generate_mapping(col_vals=None, num_chars=9,
72
+ mapping_col_name=None,
73
+ input_repl_one=None,
74
+ input_repl_two=None):
75
+ """
76
+ Uses random string generator of capitalized characters and numbers
77
+ to create a mapping of values
78
+ in **col_vals** to another string of length **num_chars**
79
+
80
+ Output mapping looks like:
81
+
82
+ ```json
83
+
84
+ { 'mapped_column_name': 'foo',
85
+ 'forward': { 'a': 'ASDFEFDDD', 'b': 'GGGHIJKOK'},
86
+ 'reverse': {'ASDFEFDDD': 'a', 'GGGHIJKOK': 'b'}
87
+ }
88
+
89
+ ```
90
+
91
+ :param col_vals: Values to encode
92
+ :type col_vals: set
93
+ :param num_chars: length of random string
94
+ :type num_chars: int
95
+ :param mapping_col_name: Name of column the mapping was made for
96
+ :type mapping_col_name: str
97
+ :return: the mapping as a dict
98
+ :rtype: dict
99
+ """
100
+ forward_mapping = {}
101
+ new_id_set = set()
102
+ reverse_mapping = {}
103
+ new_rando_id = None
104
+ for cval in col_vals:
105
+ while new_rando_id is None or new_rando_id in new_id_set:
106
+ new_rando_id = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(num_chars))
107
+ new_id_set.add(new_rando_id)
108
+ forward_mapping[cval] = new_rando_id
109
+ reverse_mapping[new_rando_id] = cval
110
+ return {'mapped_column_name': mapping_col_name,
111
+ REPL_ONE_TSV: input_repl_one,
112
+ REPL_TWO_TSV: input_repl_two,
113
+ 'forward': forward_mapping,
114
+ 'reverse': reverse_mapping}
115
+
116
+
117
+ def set_id_col_and_rename_othercols(df=None, id_mapping=None, col_name=None,
118
+ id_col_name=NEW_ID_COLNAME,
119
+ cols_to_remove=['PG.ProteinGroups',
120
+ 'PG.ProteinAccessions',
121
+ 'PG.Genes',
122
+ 'PG.UniProtIds',
123
+ 'PG.ProteinNames']):
124
+ """
125
+ Updates DataFrame **df** so it has format of:
126
+
127
+ ```bash
128
+ <NEW_ID_COLNAME> 1 2 3 ... N
129
+ ```
130
+
131
+ :param df:
132
+ :param id_mapping: Used
133
+ :type id_mapping: dict
134
+ :param col_name: Column to swap values via **id_mapping**
135
+ :type col_name: str
136
+ :param cols_to_remove: columns to remove
137
+ :type cols_to_remove: list
138
+ :param id_col_name: New name for id column
139
+ :type id_col_name: str
140
+ :return:
141
+ :rtype: :py:class:`Pandas.DataFrame`
142
+ """
143
+ # swap values in column with new ids
144
+ df[id_col_name] = df[col_name].replace(to_replace=id_mapping['forward'])
145
+ for c in cols_to_remove:
146
+ if c in df:
147
+ df.drop(c, axis=1, inplace=True)
148
+ col_list = df.columns.tolist()
149
+ col_list.insert(0, col_list.pop(col_list.index(id_col_name)))
150
+
151
+ df = df.reindex(columns=col_list)
152
+ col_mapping = {id_col_name: id_col_name}
153
+ cntr = 1
154
+ for c in col_list[1:]:
155
+ col_mapping[c] = str(cntr)
156
+ cntr += 1
157
+ df.rename(mapper=col_mapping, axis=1, inplace=True)
158
+
159
+ return df
160
+
161
+
162
+ def write_tsv_file(df=None, outfile=None):
163
+ """
164
+ Writes out a pandas dataframe in tab separated
165
+ format with a header and no index
166
+
167
+ :param df: data to write
168
+ :type df: :py:class:`pandas.DataFrame`
169
+ :param outfile: destination file path
170
+ :type outfile: str
171
+ """
172
+ df.to_csv(outfile, index=False, sep='\t', header=True)
173
+
174
+
175
+ def get_col_repl_map(prefix=None, columns=None, idcol='xxx'):
176
+ """
177
+ Given a list of strings in **columns** create a
178
+ dict where the key is the value from **columns**
179
+ and the value is the **prefix** prepended to the value
180
+ from the **columns**
181
+
182
+ :param prefix:
183
+ :type prefix: str
184
+ :param columns:
185
+ :type columns: list
186
+ :return: column name => prefix + column name unless column name
187
+ matches **idcol** in which case do not add prefix
188
+ :rtype: dict
189
+ """
190
+ if prefix is None:
191
+ raise CellMapsError('prefix is None')
192
+ if columns is None:
193
+ raise CellMapsError('columns is None')
194
+ if idcol is None:
195
+ raise CellMapsError('idcol is None')
196
+
197
+ cmap = {}
198
+ for c in columns:
199
+ if c == idcol:
200
+ cmap[c] = c
201
+ else:
202
+ cmap[c] = prefix + str(c)
203
+ return cmap
204
+
205
+
206
+ def merge_replicate_dataframes(repl1_df=None, repl2_df=None, idcol='xxx'):
207
+ """
208
+ Merges two replicate dataframes by outer join on **idcol** column
209
+ When doing the merge the **repl1_df** column names are prefixed
210
+ with ``repl1_`` and the **repl2_df** column names are p
211
+
212
+ :param repl1_df:
213
+ :type repl1_df: :py:class:`pandas.DataFrame`
214
+ :param repl2_df:
215
+ :type repl2_df: :py:class:`pandas.DataFrame`
216
+ :param idcol: Name of column to join dataframes by
217
+ :type idcol: str
218
+ :return: Merged DataFrame
219
+ :rtype: :py:class:`pandas.DataFrame`
220
+ """
221
+ newrepl1 = repl1_df.rename(mapper=get_col_repl_map('repl1_',
222
+ columns=repl1_df.columns.tolist()),
223
+ inplace=False, axis=1)
224
+
225
+ newrepl2 = repl2_df.rename(mapper=get_col_repl_map('repl2_',
226
+ columns=repl2_df.columns.tolist()),
227
+ inplace=False, axis=1)
228
+ return pd.merge(newrepl1,newrepl2, on=idcol, how='outer')
229
+
230
+
231
+ def get_network(uuid_or_path_to_net):
232
+ client = ndex2.client.Ndex2()
233
+ factory = RawCX2NetworkFactory()
234
+ if os.path.isfile(uuid_or_path_to_net):
235
+ return factory.get_cx2network(uuid_or_path_to_net)
236
+
237
+ client_resp = client.get_network_as_cx2_stream(uuid_or_path_to_net)
238
+ net = factory.get_cx2network(json.loads(client_resp.content))
239
+ return net
240
+
241
+
242
+ def get_system_to_gene_count(gene_to_system_mapping):
243
+ """
244
+ Given a dict of gene => [ node ids ]
245
+ this function returns a new dict
246
+ where key is node id aka system and value is count of genes
247
+ in that system: node id => count of genes
248
+
249
+ :param gene_to_system_mapping: gene => [ node ids ]
250
+ :type gene_to_system_mapping: dict
251
+ :return: node id => count of genes
252
+ :rtype: dict
253
+ """
254
+ system_to_gene_count = {}
255
+ for gene, systems in gene_to_system_mapping.items():
256
+ for system in set(systems):
257
+ if system in system_to_gene_count:
258
+ system_to_gene_count[system] += 1
259
+ else:
260
+ system_to_gene_count[system] = 1
261
+ return system_to_gene_count
262
+
263
+
264
+ class TwoReplCoelutionChallengeGenerator(BaseCommandLineTool):
265
+ """
266
+ Creates coelution challenge dataset suitable for Kaggle from
267
+ raw CM4AI datasets
268
+ """
269
+ COMMAND = 'tworeplchallenge'
270
+
271
+ def __init__(self, theargs,
272
+ provenance_utils=ProvenanceUtil()):
273
+ """
274
+ Constructor
275
+
276
+ :param theargs: Command line arguments that at minimum need
277
+ to have the following attributes:
278
+ :type theargs: :py:class:`~python.argparse.Namespace`
279
+ """
280
+ super().__init__()
281
+ self._outdir = os.path.abspath(theargs.outdir)
282
+ self._name = theargs.name
283
+ self._seed = theargs.seed
284
+ self._repl1_tsv = theargs.repl1_tsv
285
+ self._repl2_tsv = theargs.repl2_tsv
286
+ self._mapping_col_name = theargs.mapping_col_name
287
+ self._organization_name = theargs.organization_name
288
+ self._project_name = theargs.project_name
289
+ self._cell_line = theargs.cell_line
290
+ self._tissue = theargs.tissue
291
+ self._treatment = theargs.treatment
292
+ self._author = theargs.author
293
+ self._provenance_utils = provenance_utils
294
+ self._softwareid = None
295
+ self._input_data_dict = theargs.__dict__
296
+ self._start_time = int(time.time())
297
+ self._end_time = -1
298
+
299
+ def run(self):
300
+ """
301
+ Runs the process of CRISPR data loading into a RO-Crate.
302
+ It includes generating the output directory,
303
+ linking and registering h5ad file and registering the
304
+ computation and software used in the process.
305
+
306
+ :return:
307
+ """
308
+ exitcode = 99
309
+ try:
310
+ self._generate_rocrate_dir_path()
311
+ if os.path.exists(self._outdir):
312
+ raise CellMapsError(self._outdir + ' already exists')
313
+
314
+ logger.debug('Creating directory ' + str(self._outdir))
315
+ os.makedirs(self._outdir, mode=0o755)
316
+
317
+
318
+ logutils.setup_filelogger(outdir=self._outdir,
319
+ handlerprefix='cellmaps_utilscmd')
320
+ self._write_task_start_json()
321
+
322
+ keywords = [self._project_name,
323
+ self._cell_line, self._treatment,
324
+ self._tissue]
325
+
326
+
327
+ description = ' '.join(keywords)
328
+
329
+ self._provenance_utils.register_rocrate(self._outdir,
330
+ name=self._name,
331
+ organization_name=self._organization_name,
332
+ project_name=self._project_name,
333
+ description=description,
334
+ keywords=keywords,
335
+ guid=self._get_fairscape_id())
336
+
337
+
338
+
339
+ gen_dsets = []
340
+ shutil.copy(self._repl1_tsv, self._outdir)
341
+ shutil.copy(self._repl2_tsv, self._outdir)
342
+
343
+ raw_repl1 = _get_dataframe(self._repl1_tsv)
344
+ raw_repl2 = _get_dataframe(self._repl2_tsv)
345
+
346
+ id_mapping = self._generate_mapping(raw_repl1, raw_repl2)
347
+
348
+ self.write_id_mapping_file(id_mapping=id_mapping)
349
+
350
+ repl1 = set_id_col_and_rename_othercols(raw_repl1,
351
+ col_name=self._mapping_col_name,
352
+ id_mapping=id_mapping)
353
+ repl2 = set_id_col_and_rename_othercols(raw_repl2,
354
+ col_name=self._mapping_col_name,
355
+ id_mapping=id_mapping)
356
+ merged_df = merge_replicate_dataframes(repl1_df=repl1, repl2_df=repl2)
357
+ write_tsv_file(merged_df,
358
+ os.path.join(self._outdir, REPL_ONE_TWO_COMBINED))
359
+ self._register_software(keywords=keywords, description=description)
360
+ self._register_computation(generated_dataset_ids=gen_dsets,
361
+ description=description,
362
+ keywords=keywords)
363
+ exitcode = 0
364
+ return exitcode
365
+ finally:
366
+ self._end_time = int(time.time())
367
+ # write a task finish file
368
+ logutils.write_task_finish_json(outdir=self._outdir,
369
+ start_time=self._start_time,
370
+ end_time=self._end_time,
371
+ status=exitcode)
372
+
373
+ def _write_task_start_json(self):
374
+ """
375
+ Writes task_start.json file with information about
376
+ what is to be run
377
+
378
+ """
379
+ data = {}
380
+
381
+ if self._input_data_dict is not None:
382
+ data.update({'commandlineargs': self._input_data_dict})
383
+
384
+ logutils.write_task_start_json(outdir=self._outdir,
385
+ start_time=self._start_time,
386
+ version=cellmaps_utils.__version__,
387
+ data=data)
388
+
389
+ def write_id_mapping_file(self, id_mapping=None):
390
+ """
391
+ Writes out mapping file as json to file with **REPL_MAPPING** as name
392
+
393
+ See generate_mapping function for expected format of **id_mapping**
394
+
395
+ :param outdir:
396
+ :param id_mapping:
397
+ :type id_mapping: dict
398
+ :return:
399
+ """
400
+ with open(os.path.join(self._outdir, REPL_MAPPING), 'w') as f:
401
+ json.dump(id_mapping, f, indent=2)
402
+
403
+ def _generate_mapping(self, raw_repl1, raw_repl2):
404
+ """
405
+
406
+ :param raw_repl1:
407
+ :param raw_repl2:
408
+ :return:
409
+ """
410
+ col_values = _get_set_of_all_values_of_column_from_dataframes([raw_repl1, raw_repl2],
411
+ col_name=self._mapping_col_name)
412
+
413
+ return _generate_mapping(col_vals=col_values,
414
+ mapping_col_name=self._mapping_col_name,
415
+ input_repl_one=self._repl1_tsv,
416
+ input_repl_two=self._repl2_tsv)
417
+
418
+
419
+
420
+ def _get_fairscape_id(self):
421
+ """
422
+ Creates a unique id
423
+ :return:
424
+ """
425
+ return str(uuid.uuid4()) + ':' + os.path.basename(self._outdir)
426
+
427
+ def _generate_rocrate_dir_path(self):
428
+ """
429
+ Generates the directory path for the RO-Crate based on project name, gene set, cell line, treatment type,
430
+ tissue, dataset type, and release version.
431
+ """
432
+ dir_name = self._project_name.lower() + '_'
433
+ dir_name += self._cell_line.lower() + '_'
434
+ dir_name += re.sub(r'[^a-zA-Z0-9\w\n\.]', '_', self._tissue.lower()) + '_'
435
+ dir_name += self._treatment.lower()
436
+ dir_name = dir_name.replace(' ', '_')
437
+ self._outdir = os.path.join(self._outdir, dir_name)
438
+
439
+ def _register_computation(self, generated_dataset_ids=[],
440
+ description='',
441
+ keywords=[]):
442
+ """
443
+ Registers the computation.
444
+ # Todo: added in used dataset, software and what is being generated
445
+ :return:
446
+ """
447
+ logger.debug('Getting id of input rocrate')
448
+ comp_keywords = keywords.copy()
449
+ comp_keywords.extend(['computation'])
450
+ description = description + ' run of ' + cellmaps_utils.__name__
451
+ self._provenance_utils.register_computation(self._outdir,
452
+ name=TwoReplCoelutionChallengeGenerator.COMMAND,
453
+ run_by=str(self._provenance_utils.get_login()),
454
+ command=str(self._input_data_dict),
455
+ description=description,
456
+ keywords=comp_keywords,
457
+ used_software=[self._softwareid],
458
+ generated=generated_dataset_ids,
459
+ guid=self._get_fairscape_id())
460
+
461
+ def _register_software(self, description='',
462
+ keywords=[]):
463
+ """
464
+ Registers this tool
465
+
466
+ :raises CellMapsImageEmbeddingError: If fairscape call fails
467
+ """
468
+ software_keywords = keywords.copy()
469
+ software_keywords.extend(['tools', cellmaps_utils.__name__])
470
+ software_description = description + ' ' + \
471
+ cellmaps_utils.__description__
472
+ self._softwareid = self._provenance_utils.register_software(self._outdir,
473
+ name=cellmaps_utils.__name__,
474
+ description=software_description,
475
+ author=cellmaps_utils.__author__,
476
+ version=cellmaps_utils.__version__,
477
+ file_format='py',
478
+ keywords=software_keywords,
479
+ url=cellmaps_utils.__repo_url__,
480
+ guid=self._get_fairscape_id())
481
+
482
+ def add_subparser(subparsers):
483
+ """
484
+ Adds a subparser for the coleution challenge creator.
485
+
486
+ :return:
487
+ """
488
+ desc = """
489
+
490
+ Version {version}
491
+
492
+ {cmd} Given raw coelution data, creates an RO-Crate
493
+ with a challenge dataset containing obfuscated TSV
494
+ file along
495
+
496
+ """.format(version=cellmaps_utils.__version__,
497
+ cmd=TwoReplCoelutionChallengeGenerator.COMMAND)
498
+
499
+ parser = subparsers.add_parser(TwoReplCoelutionChallengeGenerator.COMMAND,
500
+ help='Generates coleution challenge dataset',
501
+ description=desc,
502
+ formatter_class=constants.ArgParseFormatter)
503
+ parser.add_argument('outdir',
504
+ help='Directory where RO-Crate will be created')
505
+ parser.add_argument('--repl1_tsv',
506
+ required=True,
507
+ help='File used to generated ' + REPL_ONE_TSV)
508
+ parser.add_argument('--repl2_tsv',
509
+ required=True,
510
+ help='File used to generate ' + REPL_TWO_TSV)
511
+ parser.add_argument('--mapping_col_name',
512
+ default='PG.ProteinAccessions',
513
+ help='Column to use for mapping')
514
+ parser.add_argument('--seed',
515
+ default=round(time.time()),
516
+ help='Random seed. If unset uses current time of invocation')
517
+ parser.add_argument('--author', default='Ideker Lab',
518
+ help='Author that created this data')
519
+ parser.add_argument('--name', default='tworeplcoleution',
520
+ help='Name of this run, needed for FAIRSCAPE')
521
+ parser.add_argument('--organization_name', default='Ideker Lab',
522
+ help='Name of organization running this tool, needed '
523
+ 'for FAIRSCAPE. Usually set to lab')
524
+ parser.add_argument('--project_name', default='tworeplcoelution_challenge',
525
+ help='Name of project running this tool, '
526
+ 'needed for FAIRSCAPE. Usually set to '
527
+ 'funding source')
528
+ parser.add_argument('--treatment', default='untreated',
529
+ choices=['paclitaxel', 'vorinostat', 'untreated'],
530
+ help='Treatment of sample.')
531
+ parser.add_argument('--cell_line', default='KOLF2.1J',
532
+ choices=['MDA-MB-468', 'KOLF2.1J'],
533
+ help='Name of cell line. For example MDA-MB-468')
534
+ parser.add_argument('--tissue', choices=['undifferentiated', 'neuron',
535
+ 'cardiomyocytes', 'breast; mammary gland'],
536
+ default='undifferentiated',
537
+ help='Tissue for dataset. Since the default --cell_line '
538
+ 'is MDA-MB-468, this value is set to the tissue '
539
+ 'for that cell line')
540
+ return parser
541
+
542
+
543
+ class SolutionGenerator(BaseCommandLineTool):
544
+ """
545
+ Creates solution from a challenge dataset
546
+ """
547
+ COMMAND = 'solutiongenerator'
548
+
549
+ SOURCE = 'source'
550
+ COL_NAME = 'col_name'
551
+ PREFIX = 'prefix'
552
+ MINSIZE = 'minsize'
553
+ SKIP_PARTIAL = 'skip_partial'
554
+ def __init__(self, theargs,
555
+ provenance_utils=ProvenanceUtil()):
556
+ """
557
+ Constructor
558
+
559
+ :param theargs: Command line arguments that at minimum need
560
+ to have the following attributes:
561
+ :type theargs: :py:class:`~python.argparse.Namespace`
562
+ """
563
+ super().__init__()
564
+ self._outdir = os.path.abspath(theargs.outdir)
565
+ self._input = theargs.input
566
+ self._id_mapping_file = os.path.join(self._input, theargs.id_mapping_file)
567
+ self._standards = theargs.standards
568
+ self._provenance_utils = provenance_utils
569
+ self._softwareid = None
570
+ self._input_data_dict = theargs.__dict__
571
+
572
+ def _get_uniprot_gene_dicts(self):
573
+ """
574
+
575
+ :return:
576
+ """
577
+ with open(self._id_mapping_file, "r") as file:
578
+ data = json.load(file)
579
+
580
+ repl1 = data.get("repl1.tsv", None)
581
+ repl2 = data.get("repl2.tsv", None)
582
+ uniprot_gene_dict = None
583
+ gene_uniprot_dict = {}
584
+ if repl1 and repl2:
585
+ mapped_column = data.get("mapped_column_name", "PG.ProteinAccessions") # "PG.ProteinAccessions"
586
+ dir_path = os.path.dirname(self._id_mapping_file)
587
+ uniprot_gene_dict, gene_uniprot_dict = self._merge_uniprot_gene_mapping(os.path.join(dir_path, repl1),
588
+ os.path.join(dir_path, repl2),
589
+ mapped_column)
590
+ logger.debug('uniprot_gene_dict: ' + str(uniprot_gene_dict))
591
+ logger.debug('gene_uniprot_dict: ' + str(gene_uniprot_dict))
592
+ logger.debug('forward: ' + str(data['forward']))
593
+ return uniprot_gene_dict, gene_uniprot_dict, data.get('forward', {})
594
+
595
+ def _merge_uniprot_gene_mapping(self, file1, file2, mapped_column, col_name='PG.Genes'):
596
+ df1 = pd.read_table(file1)
597
+ df2 = pd.read_table(file2)
598
+ df1_filtered = df1[[mapped_column, col_name]]
599
+ df2_filtered = df2[[mapped_column, col_name]]
600
+
601
+ merged_df = pd.concat([df1_filtered, df2_filtered], ignore_index=True)
602
+
603
+ uniprot_gene_dict = {}
604
+ gene_uniprot_dict = {}
605
+ for _, row in merged_df.iterrows():
606
+ uniprot_ids = row[mapped_column].split(";")
607
+ gene_names = row[col_name].split(";")
608
+
609
+ for uniprot_id, gene_name in zip(uniprot_ids, gene_names):
610
+ uniprot_gene_dict[uniprot_id] = gene_name
611
+ gene_uniprot_dict[gene_name] = uniprot_id
612
+
613
+ return uniprot_gene_dict, gene_uniprot_dict
614
+
615
+ def _get_gene_to_system_mapping_from_network(self, genes_column=None, net=None, minsize=4, genes=None,
616
+ uniprots=None, gene_uniprot_dict=None, prefix=None,
617
+ skip_partial=True):
618
+ """
619
+
620
+ :return:
621
+ """
622
+ nodes_to_remove = []
623
+ gene_to_system_mapping = {}
624
+ logger.debug('Looking for systems with these genes' + str(genes))
625
+ for node_id, node_obj in net.get_nodes().items():
626
+ if genes_column not in node_obj['v']:
627
+ logger.debug('Skipping, ' + genes_column +
628
+ ' not found ' + str(node_obj))
629
+ nodes_to_remove.append(node_id)
630
+ continue
631
+ gene_names = node_obj['v'].get(genes_column, [])
632
+ if isinstance(gene_names, str):
633
+ gene_names = gene_names.split(',')
634
+ if len(gene_names) < minsize:
635
+ logger.debug('Skipping, system too small: ' + str(node_obj))
636
+ nodes_to_remove.append(node_id)
637
+ continue
638
+
639
+ if skip_partial is True and not all(gene in genes for gene in gene_names):
640
+ logger.debug('Skipping, not all genes in mapping ' +
641
+ str(node_obj))
642
+ nodes_to_remove.append(node_id)
643
+ continue
644
+
645
+ for gene in gene_names:
646
+ if gene in genes:
647
+ if uniprots:
648
+ gene_to_system_mapping.setdefault(gene_uniprot_dict[gene], []).append(prefix + str(node_id))
649
+ else:
650
+ gene_to_system_mapping.setdefault(gene, []).append(prefix + str(node_id))
651
+ return gene_to_system_mapping
652
+
653
+ def _get_genes_and_uniprots(self, forward_dict, uniprot_gene_dict):
654
+ uniprots = None
655
+ logger.debug('Found ' + str(forward_dict) + ' in forward_dict')
656
+ if uniprot_gene_dict is None:
657
+ genes = list(forward_dict.keys())
658
+ else:
659
+ uniprots = list(forward_dict.keys())
660
+ genes = []
661
+ for uniprot in uniprots:
662
+ gene = uniprot_gene_dict.get(uniprot, None)
663
+ if gene:
664
+ genes.append(gene)
665
+ logger.debug('Found ' + str(len(genes)) + ' genes in mapping file')
666
+ return genes, uniprots
667
+
668
+ def _get_gene_to_system_mapping(self, genes=None, uniprots=None, gene_uniprot_dict=None,
669
+ source=None, prefix=None, skip_partial=False):
670
+ """
671
+ Generates gene to system mapping
672
+ :param genes:
673
+ :type genes: list
674
+ :param uniprots:
675
+ :param gene_uniprot_dict:
676
+ :param source:
677
+ :param prefix:
678
+ :return:
679
+ """
680
+ gene_to_system_mapping = {}
681
+
682
+ df = pd.read_csv(source)
683
+ # the 1st row has a numeric counter value just
684
+ # use that for column labels
685
+ df.columns = df.iloc[0]
686
+
687
+ # drop the 1st row
688
+ df = df.iloc[1:].reset_index(drop=True)
689
+ logger.debug(df.head())
690
+ for system in df.columns:
691
+ logger.debug('Examining system: ' + system)
692
+ gene_names = set(df[system].dropna().values)
693
+ if skip_partial is True and not all(gene in genes for gene in gene_names):
694
+ logger.debug('Skipping, not all genes in mapping ' +
695
+ str(system))
696
+ continue
697
+ for gene in df[system].dropna().values:
698
+ if gene in genes:
699
+ if uniprots:
700
+ gene_to_system_mapping.setdefault(gene_uniprot_dict[gene], []).append(prefix + str(system))
701
+ else:
702
+ gene_to_system_mapping.setdefault(gene, []).append(prefix + str(system))
703
+ else:
704
+ logger.debug(gene + ' not in genes. Not adding to map')
705
+ return gene_to_system_mapping
706
+
707
+ def _generate_solution_for_standard(self, uniprot_gene_dict=None, gene_uniprot_dict=None,
708
+ forward_dict=None, source=None, genes_column=None,
709
+ minsize=4, prefix='', skip_partial=True):
710
+ """
711
+
712
+ :return:
713
+ """
714
+ genes, uniprots = self._get_genes_and_uniprots(forward_dict, uniprot_gene_dict)
715
+
716
+ if os.path.isfile(source) and source.endswith('.csv'):
717
+ logger.debug('Processing csv file: ' + source)
718
+ gene_to_system_mapping = self._get_gene_to_system_mapping(uniprots=uniprots, genes=genes,
719
+ gene_uniprot_dict=gene_uniprot_dict,
720
+ source=source, prefix=prefix,
721
+ skip_partial=skip_partial)
722
+ elif not os.path.isfile(source) or (source.endswith('.cx') or source.endswith('.cx2')):
723
+ logger.debug('Processing network: ' + source)
724
+ gene_to_system_mapping = self._get_gene_to_system_mapping_from_network(genes_column=genes_column,
725
+ minsize=minsize,
726
+ genes=genes,
727
+ gene_uniprot_dict=gene_uniprot_dict,
728
+ prefix=prefix,
729
+ net=get_network(source),
730
+ uniprots=uniprots,
731
+ skip_partial=skip_partial)
732
+ logger.debug('gene_to_system_mapping: ' + str(gene_to_system_mapping))
733
+ system_to_gene_count = get_system_to_gene_count(gene_to_system_mapping)
734
+
735
+ systems_too_small = set()
736
+ logger.debug('Found: ' + str(len(system_to_gene_count)) + ' raw systems')
737
+ for system, cnt in system_to_gene_count.items():
738
+ if cnt < minsize:
739
+ systems_too_small.add(system)
740
+ logger.debug('Found: ' + str(len(systems_too_small)) + ' too small systems')
741
+
742
+ self._write_solution(outfile=os.path.join(self._outdir, prefix + '_solution.csv'),
743
+ systems_too_small=systems_too_small, forward_dict=forward_dict,
744
+ gene_to_system_mapping=gene_to_system_mapping)
745
+
746
+ self._remove_too_small_systems(systems_too_small=systems_too_small,
747
+ system_to_gene_count=system_to_gene_count)
748
+
749
+ # Convert the values to a list
750
+ gene_counts = list(system_to_gene_count.values())
751
+ # Calculate mean and variance
752
+ mean_gene_count = np.mean(gene_counts)
753
+ variance_gene_count = np.var(gene_counts)
754
+
755
+ with open(os.path.join(self._outdir, prefix + '_readme.txt'), 'w') as f:
756
+ f.write(f"Mean number of genes per system: {mean_gene_count}\n")
757
+ f.write(f"Variance of number of genes per system: {variance_gene_count}\n")
758
+
759
+ with open(os.path.join(self._outdir, prefix + '_systemsizes.json'), 'w') as f:
760
+ json.dump(system_to_gene_count, f)
761
+
762
+ def _remove_too_small_systems(self, systems_too_small=None,
763
+ system_to_gene_count=None):
764
+ """
765
+
766
+ :param systems_to_small:
767
+ :param system_to_gene_count:
768
+ :return:
769
+ """
770
+ # remove the too small systems for stats
771
+ logger.debug('There are: ' + str(len(systems_too_small)) +
772
+ ' systems that are too small to use')
773
+ for system in systems_too_small:
774
+ del system_to_gene_count[system]
775
+
776
+ def _write_solution(self, outfile=None, gene_to_system_mapping=None,
777
+ forward_dict=None,
778
+ systems_too_small=set()):
779
+ """
780
+
781
+ :return:
782
+ """
783
+ cntr = 1
784
+ with open(outfile, 'w') as f:
785
+ f.write("id,xxx,solution,Usage\n")
786
+ for gene, systems in gene_to_system_mapping.items():
787
+ for system in set(systems):
788
+ if system in systems_too_small:
789
+ continue
790
+ f.write(f"{cntr},{forward_dict[gene]},{system},Public\n")
791
+ cntr += 1
792
+
793
+ def _get_combined_solution(self):
794
+ """
795
+ Looks in output directory for all _solution.csv files and
796
+ creates a new solution file concating the entries and resetting
797
+ id column values to start from 1 and go to N
798
+ :return:
799
+ """
800
+ dframes = []
801
+ for entry in os.listdir(self._outdir):
802
+ fp = os.path.join(self._outdir, entry)
803
+ if not os.path.isfile(fp):
804
+ continue
805
+ if not entry.endswith('_solution.csv'):
806
+ continue
807
+ dframes.append(pd.read_csv(fp))
808
+ df = pd.concat(dframes, ignore_index=True)
809
+ df['id'] = df.index
810
+ return df
811
+
812
+ def _write_combined_solution(self, df):
813
+ """
814
+ Writes **df** as a CSV file
815
+ :param df:
816
+ :type df: :py:class:`pandas.DataFrame`
817
+ """
818
+ df.to_csv(os.path.join(self._outdir, 'combined_sol.csv'),
819
+ index=False, header=True)
820
+
821
+ def run(self):
822
+ """
823
+
824
+
825
+ :return:
826
+ """
827
+ self._generate_rocrate_dir_path()
828
+ if os.path.exists(self._outdir):
829
+ raise CellMapsError(self._outdir + ' already exists')
830
+
831
+ logger.debug('Creating directory ' + str(self._outdir))
832
+ os.makedirs(self._outdir, mode=0o755)
833
+
834
+ self._provenance_utils.register_rocrate(self._outdir,
835
+ name=os.path.basename(self._input) + ' solution',
836
+ organization_name='NEED TO SET THIS',
837
+ project_name='NEED TO SET THIS',
838
+ description='NEED TO SET THIS',
839
+ keywords=['solution'],
840
+ guid=self._get_fairscape_id())
841
+
842
+ uniprot_gene_dict, gene_uniprot_dict, forward_dict = self._get_uniprot_gene_dicts()
843
+ logger.debug('uniprod_gene_dict: ' + str(uniprot_gene_dict))
844
+ logger.debug('gene_uniprot_dict: ' + str(gene_uniprot_dict))
845
+ logger.debug('forward_dict: ' + str(forward_dict))
846
+ for entry in self._get_standards_as_dicts():
847
+ #uniprot_gene_dict = None, gene_uniprot_dict = None,
848
+ #forward_dict = None, source = None, genes_column = None,
849
+ #minsize = 4, prefix = ''
850
+ self._generate_solution_for_standard(uniprot_gene_dict=uniprot_gene_dict,
851
+ gene_uniprot_dict=gene_uniprot_dict,
852
+ forward_dict=forward_dict,
853
+ source=entry[SolutionGenerator.SOURCE],
854
+ minsize=entry[SolutionGenerator.MINSIZE],
855
+ prefix=entry[SolutionGenerator.PREFIX],
856
+ genes_column=entry[SolutionGenerator.COL_NAME],
857
+ skip_partial=entry[SolutionGenerator.SKIP_PARTIAL])
858
+
859
+ df = self._get_combined_solution()
860
+
861
+ self._write_combined_solution(df)
862
+
863
+ self._register_software(keywords=['solution'], description='NEED TO SET THIS')
864
+ self._register_computation(generated_dataset_ids=[],
865
+ description='NEED TO SET THIS',
866
+ keywords=['solution'])
867
+ return 0
868
+
869
+ def _get_standards_as_dicts(self):
870
+ """
871
+
872
+ :return:
873
+ """
874
+ for entry in self._standards:
875
+ curstandard = re.split('\s*,\s*', entry)
876
+ if len(curstandard) == 4:
877
+ yield {SolutionGenerator.SOURCE: curstandard[0],
878
+ SolutionGenerator.PREFIX: curstandard[1],
879
+ SolutionGenerator.MINSIZE: int(curstandard[2]),
880
+ SolutionGenerator.SKIP_PARTIAL: str(curstandard[3]).lower() == 'true',
881
+ SolutionGenerator.COL_NAME: None}
882
+ elif len(curstandard) == 5:
883
+ yield {SolutionGenerator.SOURCE: curstandard[0],
884
+ SolutionGenerator.COL_NAME: curstandard[1],
885
+ SolutionGenerator.PREFIX: curstandard[2],
886
+ SolutionGenerator.MINSIZE: int(curstandard[3]),
887
+ SolutionGenerator.SKIP_PARTIAL: str(curstandard[4]).lower() == 'true'}
888
+ else:
889
+ raise CellMapsError('Expected 4 or 5 values got: ' +
890
+ str(curstandard))
891
+
892
+ def _get_fairscape_id(self):
893
+ """
894
+ Creates a unique id
895
+ :return:
896
+ """
897
+ return str(uuid.uuid4()) + ':' + os.path.basename(self._outdir)
898
+
899
+ def _generate_rocrate_dir_path(self):
900
+ """
901
+ Generates the directory path for the RO-Crate based on project name, gene set, cell line, treatment type,
902
+ tissue, dataset type, and release version.
903
+ """
904
+ input_rocrate = self._provenance_utils.get_rocrate_as_dict(self._input)
905
+ dir_name = os.path.basename(self._input) + '_solution'
906
+ self._outdir = os.path.join(self._outdir, dir_name)
907
+
908
+ def _register_computation(self, generated_dataset_ids=[],
909
+ description='',
910
+ keywords=[]):
911
+ """
912
+ Registers the computation.
913
+ # Todo: added in used dataset, software and what is being generated
914
+ :return:
915
+ """
916
+ logger.debug('Getting id of input rocrate')
917
+ comp_keywords = keywords.copy()
918
+ comp_keywords.extend(['computation'])
919
+ description = description + ' run of ' + cellmaps_utils.__name__
920
+ self._provenance_utils.register_computation(self._outdir,
921
+ name=SolutionGenerator.COMMAND,
922
+ run_by=str(self._provenance_utils.get_login()),
923
+ command=str(self._input_data_dict),
924
+ description=description,
925
+ keywords=comp_keywords,
926
+ used_software=[self._softwareid],
927
+ generated=generated_dataset_ids,
928
+ guid=self._get_fairscape_id())
929
+
930
+ def _register_software(self, description='',
931
+ keywords=[]):
932
+ """
933
+ Registers this tool
934
+
935
+ :raises CellMapsImageEmbeddingError: If fairscape call fails
936
+ """
937
+ software_keywords = keywords.copy()
938
+ software_keywords.extend(['tools', cellmaps_utils.__name__])
939
+ software_description = description + ' ' + \
940
+ cellmaps_utils.__description__
941
+ self._softwareid = self._provenance_utils.register_software(self._outdir,
942
+ name=cellmaps_utils.__name__,
943
+ description=software_description,
944
+ author=cellmaps_utils.__author__,
945
+ version=cellmaps_utils.__version__,
946
+ file_format='py',
947
+ keywords=software_keywords,
948
+ url=cellmaps_utils.__repo_url__,
949
+ guid=self._get_fairscape_id())
950
+
951
+ def add_subparser(subparsers):
952
+ """
953
+ Adds a subparser for the coleution challenge creator.
954
+
955
+ :return:
956
+ """
957
+ desc = """
958
+
959
+ Version {version}
960
+
961
+ {cmd} Given an RO-Crate with a Challenge dataset, this
962
+ tool creates a solution RO-Crate using standards passed
963
+ in via --standards flag
964
+
965
+ Each --standards should look like the following:
966
+
967
+ For NDEx UUID or CX2 file:
968
+ <NDEx UUID | CX2 file>,<COLUMN NAME>,<PREFIX>,<MINSIZE OF CLUSTER>,<SKIP PARTIAL>
969
+
970
+ For CSV file:
971
+
972
+ <CSV file>,<PREFIX>,<MINSIZE OF CLUSTER>,<SKIP PARTIAL>
973
+
974
+ <NDEx UUID | CX2 file (.cx|.cx2) | CSV file (.csv)>: One of the following UUID of network on NDEx https://www.ndexbio.org,
975
+ path to a cx file (.cx|.cx2) or path to CSV file (.csv)
976
+ <COLUMN NAME>: Name of column in network where genes reside (ONLY FOR NDEX UUID or CX2 file)
977
+ <PREFIX>: Name to prefix on solution
978
+ <MINSIZE OF CLUSTER>: Minimum number of genes needed in cluster to be included
979
+ <SKIP PARTIAL>: If set to 'true', clusters missing one or more genes in input will be omitted
980
+ any other value implies false.
981
+
982
+ """.format(version=cellmaps_utils.__version__,
983
+ cmd=SolutionGenerator.COMMAND)
984
+
985
+ parser = subparsers.add_parser(SolutionGenerator.COMMAND,
986
+ help='Generates solution for challenge dataset',
987
+ description=desc,
988
+ formatter_class=constants.ArgParseFormatter)
989
+ parser.add_argument('outdir',
990
+ help='Directory where RO-Crate will be created')
991
+ parser.add_argument("--input", type=str,
992
+ help="Path to Challenge RO-Crate")
993
+ parser.add_argument('--id_mapping_file', default='repl1_repl2_id_mapping.json',
994
+ help='Name of json file containing id mapping')
995
+ parser.add_argument('--standards', nargs='*',
996
+ help=('Standards to use which is a comma delimited list'
997
+ 'of <NDEx UUID|CX2 File (.cx|.cx2)>,<COLUMN NAME>,<PREFIX>,<MINSIZE OF CLUSTER><SKIP PARTIAL> or'
998
+ ' <CSV FILE (.csv)>>,<PREFIX>,<MINSIZE OF CLUSTER>,<SKIP PARTIAL>'))
999
+ return parser
1000
+