cadbiom_cmd.py 21.8 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Copyright (C) 2017  IRISA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# The original code contained here was initially developed by:
#
#     Pierre Vignet.
#     IRISA
#     Dyliss team
#     IRISA Campus de Beaulieu
#     35042 RENNES Cedex, FRANCE
24
"""Entry point and argument parser for cadbiom_cmd package"""
25
26
27
from __future__ import unicode_literals
from __future__ import print_function

28
29
30
# Standard imports
import argparse
import os
31
import sys
32

33
# Custom imports
34
import cadbiom.commons as cm
35

36
37
LOGGER = cm.logger()

38
39
40
def solutions_search(args):
    """Launch the search for Minimum Activation Conditions (MAC) for entities
    of interest.
41
42
    """

43
44
    # Module import
    import solution_search
45
    solution_search.solutions_search(args) # !
46

47

48
def solutions_sort(args):
VIGNET Pierre's avatar
VIGNET Pierre committed
49
    """Read a solution file or a directory containing MAC solutions files
50
    (*mac* files), and sort all frontier places/boundaries in alphabetical order.
51
52
    """

53
54
    # Module import
    import solution_sort
55
    solution_sort.solutions_sort(args['path'])
56

57

58
def solutions_2_graph(args):
59
    """Create GraphML formated files containing a representation of the
60
    trajectories for every solution in complete MAC files (*mac_complete files).
61
62
63

    This is a function to visualize paths taken by the solver from the boundaries
    to the entities of interest.
64
65
    """

66
    # Module import
67
68
    import solution_sort
    solution_sort.solutions_2_graph(
69
70
71
        args['output'],
        args['chart_file'],
        args['path']
72
    )
73

74

75
def solutions_2_json(args):
76
    """Create a JSON formated file containing all data from complete MAC files
77
    (*mac_complete files). The file will contain frontier places/boundaries
78
    and decompiled steps with their respective events for each solution.
79
80
81
82
83
84

    This is a function to quickly search all transition attributes involved
    in a solution.
    """

    # Module import
85
86
    import solution_sort
    solution_sort.solutions_2_json(
87
88
89
90
        args['output'],
        args['chart_file'],
        args['path'],
        conditions=not args['no_conditions'], # Reverse the param...
91
    )
92

93
94
95
96
97
98
99
100
101
102
103
104
105

def json_2_interaction_graph(args):
    """Make an interaction weighted graph based on the searched molecule of interest.

    Read decompiled solutions files (*.json* files produced by the
    directive 'solutions_2_json') and make a graph of the relationships
    between one or more molecules of interest, the genes and other
    frontier places/boundaries found among all the solutions.
    """

    # Module import
    import interaction_graph
    interaction_graph.json_2_interaction_graph(
106
107
108
        args['output'],
        args['molecules_of_interest'],
        args['path'],
109
110
111
    )


112
113
def solutions_2_common_graph(args):
    """Create a GraphML formated file containing a representation of **all**
114
    trajectories for **all** solutions in complete MAC files (*mac_complete files).
115
116
117
118
119
120
121
122

    This is a function to visualize paths taken by the solver from the boundaries
    to the entities of interest.
    """

    # Module import
    import solution_sort
    solution_sort.solutions_2_common_graph(
123
124
125
        args['output'],
        args['chart_file'],
        args['path']
126
127
128
    )


VIGNET Pierre's avatar
VIGNET Pierre committed
129
130
def solutions_2_occcurrences_matrix(args):
    """Create a matrix of occurrences counting entities in the solutions found in
131
    *mac.txt files in the given path.
VIGNET Pierre's avatar
VIGNET Pierre committed
132
133
134
135
136
    """

    # Module import
    import solution_sort
    solution_sort.solutions_2_occcurrences_matrix(
137
138
139
140
        args['output'],
        args['chart_file'],
        args['path'],
        transposed=args['transpose_csv']
VIGNET Pierre's avatar
VIGNET Pierre committed
141
142
    )

143
144
145
146
147
148
149
150
151
152

def identifiers_mapping(args):
    """Mapping of identifiers from external databases.

    This function exports a CSV formated file presenting the list of known
    Cadbiom identifiers for each given external identifier.
    """

    # Module import
    import solution_repr
153
    solution_repr.identifiers_mapping(**args)
154
155


156
157
def model_comparison(args):
    """Isomorphism test.
158

159
160
    Check if the graphs based on the two given models have  the same topology,
    nodes & edges attributes/roles.
161
162
163
164
    """

    # Module import
    import solution_repr
165
166
    args['output'] = args['output'] if args['output'][-1] == '/' \
                        else args['output'] + '/'
167
    solution_repr.graph_isomorph_test(
168
169
170
171
172
        args['model_file_1'],
        args['model_file_2'],
        args['output'],
        args['graphs'],
        args['json'],
173
174
175
    )


176
177
178
def model_info(args):
    """Provide several levels of information about the structure of the model
    and its places/entities.
179
180
181
182
    """

    # Module import
    import solution_repr
183
184
185
    args['output_dir'] = args['output'] if args['output'][-1] == '/' \
                        else args['output'] + '/'
    solution_repr.model_info(**args)
186
187
188


def model_graph(args):
189
    """Information about the graph based on the model.
190
191

    Get centralities (degree, in_degree, out_degree, closeness, betweenness).
192
    Forge a GraphML file.
193
194
195
196
    """

    # Module import
    import solution_repr
197
198
199
    args['output_dir'] = args['output'] if args['output'][-1] == '/' \
                        else args['output'] + '/'
    solution_repr.model_graph(**args)
200
201


202
def merge_macs(args):
203
    """Merge solutions to a csv file."""
204
205
206

    # Module import
    import solution_merge
207
208
    solution_merge.merge_macs_to_csv(args['solutions_directory'],
                                     args['output'])
209
210


211
212
213
214
215
def args_to_param(args):
    """Return argparse namespace as a dict {variable name: value}"""
    return {k: v for k, v in vars(args).items() if k != 'func'}


216
217
218
def ensure_dir_presence(path):
    """Test write mode and eventually create the given directory"""

219
220
221
222
    if not os.path.dirname(path):
        # Current directory
        path = "."

223
224
225
226
227
228
229
230
231
    if not os.path.isdir(path):
        LOGGER.info("Creation of the directory <{}>".format(path))
        os.mkdir(path)

    if not os.access(path, os.W_OK):
        LOGGER.error("<{}> is not writable".format(path))
        exit()


232
233
234
235
236
237
238
239
240
class ReadableFile(argparse.Action):
    """
    http://stackoverflow.com/questions/11415570/directory-path-types-with-argparse
    """

    def __call__(self, parser, namespace, values, option_string=None):
        prospective_file = values

        if not os.path.isfile(prospective_file):
241
242
            LOGGER.error("readable_file:<{}> is not a valid file".format(prospective_file))
            exit()
243
244
245
246

        if os.access(prospective_file, os.R_OK):
            setattr(namespace, self.dest, prospective_file)
        else:
247
248
            LOGGER.error("readable_file:<{}> is not a readable file".format(prospective_file))
            exit()
249
250
251


class ReadableDir(argparse.Action):
252
253
254
255
256
257
258
259
    """
    http://stackoverflow.com/questions/11415570/directory-path-types-with-argparse
    """

    def __call__(self, parser, namespace, values, option_string=None):
        prospective_dir = values

        if not os.path.isdir(prospective_dir):
260
261
            LOGGER.error("readable_dir:<{}> is not a valid path".format(prospective_dir))
            exit()
262
263
264
265

        if os.access(prospective_dir, os.R_OK):
            setattr(namespace, self.dest, prospective_dir)
        else:
266
267
            LOGGER.error("readable_dir:<{}> is not a readable dir".format(prospective_dir))
            exit()
268
269


270
def main():
271
    """Argument parser"""
272
273
274
275

    # parser configuration
    parser = argparse.ArgumentParser(description=__doc__)
    # Default log level: debug
VIGNET Pierre's avatar
VIGNET Pierre committed
276
    parser.add_argument('-vv', '--verbose', nargs='?', default='info')
277
278
279
    # Subparsers
    subparsers = parser.add_subparsers(title='subcommands')

280
    # PS: nargs='?' = optional
281
282
283
284
285
    # subparser: Compute macs
    #    steps      = 10
    #    final_prop = "P"
    #    start_prop = None
    #    inv_prop   = None
286
    parser_input_file = subparsers.add_parser(
287
288
        'solutions_search',
        help=solutions_search.__doc__,
289
290
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
291
    parser_input_file.add_argument('chart_file')
VIGNET Pierre's avatar
VIGNET Pierre committed
292
    # Get final_property alone OR an input_file containing multiple properties
293
    group = parser_input_file.add_mutually_exclusive_group(required=True)
294
    group.add_argument('final_prop', nargs='?')
295
    group.add_argument('--input_file', action=ReadableFile, nargs='?',
296
297
298
        help="Without input file, there will be only one process. "
             "While there will be 1 process per line (per logical formula "
             "on each line)")
299
    parser_input_file.add_argument('--output', action=ReadableDir, nargs='?',
VIGNET Pierre's avatar
VIGNET Pierre committed
300
                                   default='result/', help="Output directory.")
301

302
    # default: False
303
    parser_input_file.add_argument('--combinations', action='store_true',
304
305
        help="If input_file is set, we can compute all combinations of "
             "given elements on each line")
306
    parser_input_file.add_argument('--steps', type=int, nargs='?', default=7,
307
        help="Maximum of allowed steps to find macs")
308
309
    parser_input_file.add_argument('--limit', type=int, nargs='?', default=400,
        help="Limit the number of solutions.")
310
    # https://docs.python.org/dev/library/argparse.html#action
311
    # all_macs to False by default
312
    parser_input_file.add_argument('--all_macs', action='store_true',
313
314
        help="Solver will try to search all macs with 0 to the maximum of "
             "allowed steps.")
VIGNET Pierre's avatar
VIGNET Pierre committed
315
    # continue to False by default
316
    parser_input_file.add_argument('--continue', action='store_true',
317
318
        help="Resume previous computations; if there is a mac file from a "
             "previous work, last frontier places/boundaries will be reloaded.")
319
320
    parser_input_file.add_argument('--start_prop', nargs='?', default=None)
    parser_input_file.add_argument('--inv_prop', nargs='?', default=None)
321

322
    parser_input_file.set_defaults(func=solutions_search)
323

324
    ## Solutions-related commands ##############################################
325
326
327

    # subparser: Sort solutions in alphabetical order in place
    # Solution file (complete or not)
328
329
    parser_solutions_sort = subparsers.add_parser('solutions_sort',
                                                  help=solutions_sort.__doc__)
330
    parser_solutions_sort.add_argument('path',
331
        help="Solution file or directory with MAC solutions files "
332
             "(*mac* files) generated with the 'solutions_search' command.")
333
    parser_solutions_sort.set_defaults(func=solutions_sort)
334
335
336
337


    # subparser: Representation of the trajectories of MACs in a complete file.
    # Model file (xml : cadbiom language)
338
    # Solution file (mac_complete)
339
    parser_trajectories = subparsers.add_parser(
340
341
        'solutions_2_graph',
        help=solutions_2_graph.__doc__,
342
343
344
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser_trajectories.add_argument('chart_file',
        help="bcx model file.")
345
    parser_trajectories.add_argument('path',
346
        help="Complete solution file or directory with MAC solutions files "
347
             "(*mac_complete.txt files) generated with the 'compute_macs' command.")
348
    parser_trajectories.add_argument('--output', action=ReadableDir,
349
        nargs='?', default='graphs/',
350
        help="Output directory for GraphML files.")
351
    parser_trajectories.set_defaults(func=solutions_2_graph)
352
353


354
    # subparser: Decompilation of trajectories of MACs in a complete file/dir.
355
    # Model file (xml : cadbiom language)
356
    # Solution file (mac_complete)
357
358
359
    parser_solutions_2_json = subparsers.add_parser(
        'solutions_2_json',
        help=solutions_2_json.__doc__,
360
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
361
    parser_solutions_2_json.add_argument('chart_file',
362
        help="bcx model file.")
363
364
    parser_solutions_2_json.add_argument('path',
        help="Complete solution file or directory with MAC solutions files "
365
             "(*mac_complete.txt files) generated with the 'compute_macs' command.")
366
    parser_solutions_2_json.add_argument('--output', action=ReadableDir,
367
        nargs='?', default='decompiled_solutions/',
368
        help="Directory for newly created files.")
369
370
    parser_solutions_2_json.add_argument('--no_conditions', action='store_true',
        help="Don't export conditions of transitions. This allows "
371
372
             "to have only places/entities that are used inside trajectories; "
             "thus, inhibitors nodes are not present in the json file")
373
    parser_solutions_2_json.set_defaults(func=solutions_2_json)
374
375


376
377
378
379
380
381
382
    # subparser: Make an interaction weighted graph based on the searched
    # molecule of interest
    # Require JSON decompilated solutions files
    parser_json_2_interaction_graph = subparsers.add_parser(
        'json_2_interaction_graph',
        help=json_2_interaction_graph.__doc__,
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
VIGNET Pierre's avatar
VIGNET Pierre committed
383
    parser_json_2_interaction_graph.add_argument('molecules_of_interest', nargs='+',
384
385
386
387
388
        help="One or multiple molecule of interest to search in the trajectories"
             " of every solutions")
    parser_json_2_interaction_graph.add_argument('--path',
        nargs='?', default='decompiled_solutions/',
        help="JSON formated file containing all data from complete MAC files"
389
             "(*mac_complete files) generated with the 'solutions_2_json' command.")
390
391
392
393
394
395
    parser_json_2_interaction_graph.add_argument('--output', action=ReadableDir,
        nargs='?', default='graphs/',
        help="Directory for the newly created file.")
    parser_json_2_interaction_graph.set_defaults(func=json_2_interaction_graph)


396
397
    # subparser: Common representation of the trajectories of MACs in a complete file.
    # Model file (xml : cadbiom language)
398
    # Solution file (mac_complete)
399
400
401
402
403
404
405
406
    parser_trajectories = subparsers.add_parser(
        'solutions_2_common_graph',
        help=solutions_2_common_graph.__doc__,
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser_trajectories.add_argument('chart_file',
        help="bcx model file.")
    parser_trajectories.add_argument('path',
        help="Complete solution file or directory with MAC solutions files "
407
             "(*mac_complete.txt files) generated with the 'compute_macs' command.")
408
409
410
411
412
413
    parser_trajectories.add_argument('--output', action=ReadableDir,
        nargs='?', default='graphs/',
        help="Output directory for GraphML files.")
    parser_trajectories.set_defaults(func=solutions_2_common_graph)


VIGNET Pierre's avatar
VIGNET Pierre committed
414
415
    # subparser: Create a matrix of occurrences counting entities in the solutions.
    # Model file (xml : cadbiom language)
416
    # Solution file (mac.txt)
VIGNET Pierre's avatar
VIGNET Pierre committed
417
418
419
420
421
422
423
424
    parser_occurrences_matrix = subparsers.add_parser(
        'solutions_2_occcurrences_matrix',
        help=solutions_2_occcurrences_matrix.__doc__,
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser_occurrences_matrix.add_argument('chart_file',
        help="bcx model file.")
    parser_occurrences_matrix.add_argument('path',
        help="Directory with MAC solutions files "
425
             "(*mac.txt files) generated with the 'compute_macs' command.")
VIGNET Pierre's avatar
VIGNET Pierre committed
426
427
428
429
430
431
432
433
    parser_occurrences_matrix.add_argument('--output', action=ReadableDir,
        nargs='?', default='./',
        help="Output directory for CSV files.")
    parser_occurrences_matrix.add_argument('--transpose_csv', action='store_true',
        help="Transpose the final matrix (switch columns and rows).")
    parser_occurrences_matrix.set_defaults(func=solutions_2_occcurrences_matrix)


434
    # subparser: Merge solutions to a csv file
435
    # Solution file (mac)
436
    # Output (csv)
437
438
439
    parser_merge_macs = subparsers.add_parser(
        'merge_macs',
        help=merge_macs.__doc__,
440
441
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
442
    parser_merge_macs.add_argument('solutions_directory', nargs='?',
443
        default='result/')
444
445
446
447
    parser_merge_macs.add_argument('--output', nargs='?',
        default='result/merged_macs.csv',
        help="CSV file: <Final property formula>;<mac>")
    parser_merge_macs.set_defaults(func=merge_macs)
448

449
    ## Model-related commands ##################################################
450

451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
    # subparser: Mapping of identifiers
    # output: CSV file
    parser_identifiers_mapping = subparsers.add_parser(
        'identifiers_mapping',
        help=identifiers_mapping.__doc__,
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    parser_identifiers_mapping.add_argument('model_file',
        help="bcx model file.")
    group = parser_identifiers_mapping.add_mutually_exclusive_group(required=True)
    group.add_argument('--external_file',
        help="File with 1 external identifiers to be mapped per line."
    )
    group.add_argument('--external_identifiers', nargs='+',
        help="Multiple external identifiers to be mapped."
    )
    parser_identifiers_mapping.set_defaults(func=identifiers_mapping)


470
471
    # subparser: Model comparison
    # 2 models
472
    parser_model_comparison = subparsers.add_parser(
473
474
        'model_comparison',
        help=model_comparison.__doc__,
475
476
477
478
479
480
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    parser_model_comparison.add_argument('model_file_1',
        help="bcx model file.")
    parser_model_comparison.add_argument('model_file_2',
        help="bcx model file.")
481
    # Export graphs for the 2 models; default: false
482
    parser_model_comparison.add_argument('--graphs', action='store_true',
483
        help="Create two GraphML files from the given models.")
484
485
    parser_model_comparison.add_argument('--json', action='store_true',
        help="Create a summary dumped into a json file.")
486
    parser_model_comparison.add_argument('--output', action=ReadableDir,
487
488
        nargs='?', default='graphs/',
        help="Directory for created graphs files.")
489
    parser_model_comparison.set_defaults(func=model_comparison)
490
491


492
    # subparser: Model info
493
    # 1 model
494
495
496
    parser_model_info = subparsers.add_parser(
        'model_info',
        help=model_info.__doc__,
497
498
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
499
    parser_model_info.add_argument('model_file')
500
    # Filters
501
502
    group = parser_model_info.add_mutually_exclusive_group(required=True)
    # PS: Argparse doesn't allow to select a default value here
503
504
    group.add_argument('--default', action='store_true',
        help="Display quick description of the model "
505
             "(Number of places/entities, transitions, entity types, locations)")
506
507
508
509
    group.add_argument('--all_entities', action='store_true',
        help="Retrieve data for all places/entities of the model.")
    group.add_argument('--boundaries', action='store_true',
        help="Retrieve data only for the frontier places/boundaries of the model.")
510
511
    group.add_argument('--genes', action='store_true',
        help="Retrieve data only for the genes in the model.")
512
513
    group.add_argument('--smallmolecules', action='store_true',
        help="Retrieve data only for the smallmolecules in the model.")
514
    # Outputs
515
516
    parser_model_info.add_argument('--csv', action='store_true',
        help="Create a CSV file containing data about previously filtered "
517
             "places/entities of the model.")
518
519
    parser_model_info.add_argument('--json', action='store_true',
        help="Create a JSON formated file containing data about previously "
520
521
522
             "filtered places/entities of the model, and a full summary about the "
             "model itself (boundaries, transitions, events, entities locations,"
             " entities types).")
523
    parser_model_info.add_argument('--output', action=ReadableDir,
524
525
        nargs='?', default='./',
        help="Directory for newly created files.")
526
    parser_model_info.set_defaults(func=model_info)
527

528
529
530
531
532
533
534
535
536

    # subparser: Model graph
    parser_model_graph = subparsers.add_parser(
        'model_graph',
        help=model_graph.__doc__,
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    parser_model_graph.add_argument('model_file')
    # Additional data
537
    parser_model_graph.add_argument('--centralities', action='store_true',
VIGNET Pierre's avatar
VIGNET Pierre committed
538
        help="Get centralities for each node of the graph "
539
540
             "(degree, in_degree, out_degree, closeness, betweenness). "
             "Works in conjunction with the ``--json`` option.")
541
542
    # Outputs
    parser_model_graph.add_argument('--graph', action='store_true',
543
        help="Translate the model into a GraphML formated file which can "
544
             "be opened in Cytoscape.")
545
    parser_model_graph.add_argument('--json', action='store_true',
546
        help="Create a JSON formated file containing a summary of the graph "
547
             "based on the model.")
VIGNET Pierre's avatar
VIGNET Pierre committed
548
549
    parser_model_graph.add_argument('--json_graph', action='store_true',
        help="Create a JSON formated file containing the graph based on the "
550
             "model, which can be opened by Web applications.")
551
552
    parser_model_graph.add_argument('--output', action=ReadableDir,
        nargs='?', default='graphs/',
VIGNET Pierre's avatar
VIGNET Pierre committed
553
        help="Directory for newly created files.")
554
555
556
    parser_model_graph.set_defaults(func=model_graph)


557
558
559
560
    # Workaround for sphinx-argparse module that require the object parser
    # before the call of parse_args()
    if 'html' in sys.argv:
        return parser
561

562
563
    # get program args and launch associated command
    args = parser.parse_args()
564

565
    # Set log level
566
567
    cm.log_level(vars(args)['verbose'])

568
569
570
571
572
573
574
575
576
577
    # Take argparse arguments and put them in a standard dict
    params = args_to_param(args)

    # Ultimate check of the presence of the directories
    know_directory_variables = ("output", "path", "solutions_directory")
    [ensure_dir_presence(params[var]) for var in know_directory_variables
     if params.get(var, None)]

    # Launch associated command
    args.func(params)