solution_search.py 26.3 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
25
"""
Search Minimal Accessibility Conditions
26
27
28

Simulation of the system until some halting condition (given with the final
property) is satisfied.
29
"""
30
31
32
from __future__ import unicode_literals
from __future__ import print_function

VIGNET Pierre's avatar
VIGNET Pierre committed
33
34
#from pycallgraph import PyCallGraph
#from pycallgraph.output import GraphvizOutput
35
36
37
38
39
40

# Standard imports
import os
from functools import partial
import sys
import itertools as it
VIGNET Pierre's avatar
VIGNET Pierre committed
41

42
43
44
45
# Multiprocessing
try:
    from concurrent.futures import ProcessPoolExecutor, as_completed
except ImportError:
VIGNET Pierre's avatar
VIGNET Pierre committed
46
47
48
49
    raise ImportError(
        "No module named concurrent.futures.\n"
        "You can try to install 'futures' module in Python 2.7"
    )
50
51
52
53
54
55
56
57
58
59
60
import multiprocessing as mp

# Custom imports
from cadbiom.models.clause_constraints.mcl.MCLAnalyser import MCLAnalyser
from cadbiom.models.clause_constraints.mcl.MCLQuery import MCLSimpleQuery
import cadbiom.commons as cm

LOGGER = cm.logger()


class ErrorRep(object):
61
62
63
    """Cf class CompilReporter(object):
    gt_gui/utils/reporter.py
    """
VIGNET Pierre's avatar
VIGNET Pierre committed
64

65
66
67
68
69
70
    def __init__(self):
        self.context = ""
        self.error = False

    def display(self, mess):
        self.error = True
71
        LOGGER.error(">> Context: %s; %s", self.context, mess)
72
73
74
        exit()

    def display_info(self, mess):
75
        LOGGER.error("-- Context: %s; %s", self.context, mess)
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
        exit()

    def set_context(self, cont):
        self.context = cont


def logical_operator(elements, operator):
    """Join elements with the given logical operator.

    :param arg1: Iterable of elements to join with a logical operator
    :param arg2: Logical operator to use 'and' or 'or'
    :return: logical_formula: str - AND/OR of the input list
    :type arg1: <list>
    :type arg2: <str>
    :rtype: <str>
    """
VIGNET Pierre's avatar
VIGNET Pierre committed
92
93
    assert operator in ("and", "or")
    #    print(operator + ": elements:", elements)
94

VIGNET Pierre's avatar
VIGNET Pierre committed
95
    return "(" + " {} ".format(operator).join(elements) + ")"
96
97
98
99
100
101
102


def make_logical_formula(previous_frontier_places, start_prop):
    """Make a logical formula based on previous results of MAC.

    The aim is to exclude previous solution.

VIGNET Pierre's avatar
VIGNET Pierre committed
103
104
105
106
    - 1 line: ``"A B" => (A and B)``
    - another line: ``"B C" => (B and C)``
    - merge all lines: ``(A and B) or (B and C)``
    - forbid all combinaisons: ``not((A and B) or (B and C))``
107
108
109
110
111
112
113
114
115

    :param arg1: Set of previous frontier places (previous solutions).
    :param arg2: Original property (constraint) for the solver.
    :return: A logical formula which excludes all the previous solutions.
    :type arg1: <set>
    :type arg2: <str>
    :rtype: <str>
    """

VIGNET Pierre's avatar
VIGNET Pierre committed
116
117
    logical_and = partial(logical_operator, operator="and")
    logical_or = partial(logical_operator, operator="or")
118
119
120
121
122

    def add_start_prop(prev_frontier_places_formula):
        """Deal with start_prop if given"""

        if start_prop and prev_frontier_places_formula:
VIGNET Pierre's avatar
VIGNET Pierre committed
123
            return start_prop + " and (" + prev_frontier_places_formula + ")"
124
125
        elif prev_frontier_places_formula:
            return prev_frontier_places_formula
126
        return start_prop
127

VIGNET Pierre's avatar
VIGNET Pierre committed
128
129
130
    mac_list = [
        logical_and(frontier_places) for frontier_places in previous_frontier_places
    ]
131

132
    if mac_list:
133
        # Logical or between each line
VIGNET Pierre's avatar
VIGNET Pierre committed
134
135
        return add_start_prop("not(" + logical_or(mac_list) + ")")
    return add_start_prop("")
136
137


138
139
140
141
142
143
def get_dimacs_start_properties(mcla, previous_frontier_places):
    """Translate frontier places to their numerical values
    thanks to the current unfolder.

    It's much more efficient than using the ANTLR grammar to parse formulas
    for each new query.
144
145
146
147

    :return: List of previous solutions (list of negative values of frontier places)
        Ex: [[-1, -2], [-2, -3], ...]
    :rtype: <list <list <int>>
148
    """
149
150
151
    dimacs_start = [
        [-mcla.unfolder.var_dimacs_code(place) for place in frontier_places]
         for frontier_places in previous_frontier_places
VIGNET Pierre's avatar
VIGNET Pierre committed
152
    ]
153
    return dimacs_start
154
155


156
def search_entry_point(
157
    model_file, mac_file, mac_step_file, mac_complete_file, mac_strong_file,
158
    steps, final_prop, start_prop, inv_prop, all_macs, continue_run, limit):
VIGNET Pierre's avatar
VIGNET Pierre committed
159
    """Search solutions
160

161
    :param model_file: Model file (bcx, xml, cal).
VIGNET Pierre's avatar
VIGNET Pierre committed
162
163
164
165
166
167
    :param mac_file: File used to store Minimal Activation Condition (MAC/CAM).
    :param mac_step_file: File used to store Minimal step numbers for each solution.
    :param mac_complete_file: File used to store MAC & trajectories.
    :param mac_strong_file: ???
    :param steps: Maximal steps to reach the solutions.
    :param final_prop: Formula: Property that the solver looks for.
168
169
170
171
    :param start_prop: Formula: Property that will be part of the initial state
        of the model.
        In concrete terms, some entities can be activated by this mechanism
        without modifying the model.
172
    :param inv_prop: Formula: Invariant property that will always occur during
173
        the simulation.
174
        The given logical formula will be checked at each step of the simulation.
VIGNET Pierre's avatar
VIGNET Pierre committed
175
    :param all_macs: If set to True (not default), search all macs with
176
177
178
        less or equal the maxium of steps defined with the argument `steps`.
        If set to False: The solver will search all solutions with the minimum
        of steps found in the first returned solution.
VIGNET Pierre's avatar
VIGNET Pierre committed
179
180
181

        :Example:

182
183
184
185
186
187
188
189
190
191
            all_macs = False, steps = 10;
            First solution found with 4 steps;
            The next solution will be searched with a maximum of 4 steps;

            all_macs = True, steps = 10;
            First solution found with 4 steps;
            The next solution is not reachable with 4 steps but with 5 steps
            (which is still less than 10 steps);
            Get the solution for 5 steps;

VIGNET Pierre's avatar
VIGNET Pierre committed
192
    :param continue_run: If set to True (not default), previous macs from a previous
VIGNET Pierre's avatar
VIGNET Pierre committed
193
        run, will be reloaded.
VIGNET Pierre's avatar
VIGNET Pierre committed
194
    :param limit: Limit the number of solutions.
195
    :type model_file: <str>
VIGNET Pierre's avatar
VIGNET Pierre committed
196
197
198
199
200
201
202
203
204
205
206
    :type mac_file: <str>
    :type mac_step_file: <str>
    :type mac_complete_file: <str>
    :type mac_strong_file: <str>
    :type steps: <int>
    :type final_prop: <str>
    :type start_prop: <str>
    :type inv_prop: <str>
    :type all_macs: <boolean>
    :type continue_run: <boolean>
    :type limit: <int>
207
208
209
210
211
212
213
214
215
216
217
218
219

    .. todo: handle these QUERY PARAMETERS... from GUI program

            #    if self.possible:
            #        if len(inv_prop) == 0:
            #            inv_prop = None
            #        else :
            #            inv_prop = "not ("+inv_prop+")"
            #    else:
            #        if len(inv_prop) != 0:
            # sert pas:
            #            final_prop = "not ("+final_prop+" and "+inv_prop+")"
    """
220
221
    # Build MCLA with Error Reporter
    mcla = MCLAnalyser(ErrorRep())
222

VIGNET Pierre's avatar
VIGNET Pierre committed
223
    # Load model file
224
    detect_model_type(mcla, model_file)(model_file)
225
    if mcla.reporter.error:
226
227
228
        raise "Error during loading of file"

    # Frontier places asked
VIGNET Pierre's avatar
VIGNET Pierre committed
229
230
231
    if continue_run:
        # Reload previous working files
        try:
232
            ## TODO: see docstring of read_mac_file
233
            previous_frontier_places = read_mac_file(mac_file)
VIGNET Pierre's avatar
VIGNET Pierre committed
234
235
236
237
238
            LOGGER.info(
                "%s:: Reload previous frontier places: %s",
                final_prop,
                len(previous_frontier_places),
            )
VIGNET Pierre's avatar
VIGNET Pierre committed
239
        except IOError:
240
            LOGGER.warning("%s:: mac file not found!", final_prop)
VIGNET Pierre's avatar
VIGNET Pierre committed
241
242
243
244
245
            previous_frontier_places = set()
    else:
        # New run
        previous_frontier_places = set()

246
247
    # Compute the number of previously computed frontier places
    current_nb_sols = len(previous_frontier_places) if previous_frontier_places else 0
248

249
    if current_nb_sols >= limit:
250
        # EXIT
VIGNET Pierre's avatar
VIGNET Pierre committed
251
252
253
        LOGGER.info(
            "%s:: Reaching the limitation of the number of solutions!", final_prop
        )
254
255
        return

256
257
    ## Find macs in an optimized way with find_macs() (see the doc for more info)
    LOGGER.info("%s:: Start property: %s", final_prop, start_prop)
258
259
260
    # with PyCallGraph(output=GraphvizOutput()):
    find_macs(mcla,
              mac_file, mac_step_file, mac_complete_file,
261
262
              steps, final_prop, start_prop, inv_prop, limit, current_nb_sols,
              previous_frontier_places)
263
264

    return
265
266
267
    ############################################################################
    ############################################################################
    ## Alternative and deprecated less efficient way
268
269
    ## Find mac one by one with find_mac() (see the docstring for more info)
    while True:
270
        # Compute the formula of the next start_property
VIGNET Pierre's avatar
VIGNET Pierre committed
271
        current_start_prop = make_logical_formula(previous_frontier_places, start_prop)
272
        LOGGER.info("%s:: Start property: %s", final_prop, current_start_prop)
273

274
275
        ret = \
            find_mac(mcla,
276
                     mac_file, mac_step_file, mac_complete_file,
277
278
                     steps, final_prop, start_prop, inv_prop,
                     previous_frontier_places)
279

VIGNET Pierre's avatar
VIGNET Pierre committed
280
        if not ret:
281
282
283
            # No new solution/not satisfiable
            return

VIGNET Pierre's avatar
VIGNET Pierre committed
284
285
        frontier_places, min_steps = ret

286
287
288
        # Add theese frontier places to set of previous ones
        # (tuple is hashable)
        previous_frontier_places.add(tuple(frontier_places))
VIGNET Pierre's avatar
VIGNET Pierre committed
289
290
291
        LOGGER.debug(
            "%s:: Prev frontier places: %s", final_prop, previous_frontier_places
        )
292

293
294
        # If set to True (not default), search all macs with
        # less or equal the maxium of steps defined with the argument `steps`.
295
        # If all_macs == True: The limit is always the maximal number given
296
        # in 'steps' argument.
297
298
299
        if not all_macs:
            steps = min_steps

300
301
302
303
        # Stop when limitation is reached
        current_nb_sols += 1
        if current_nb_sols >= limit:
            # EXIT
VIGNET Pierre's avatar
VIGNET Pierre committed
304
305
306
            LOGGER.info(
                "%s:: Reaching the limitation of the number of solutions!", final_prop
            )
307
308
            return

309
        LOGGER.debug("%s:: Next solution will be in %s steps", final_prop, steps)
310

311

312
def find_macs(mcla,
313
              mac_file, mac_step_file, mac_complete_file,
314
315
              steps, final_prop, start_prop, inv_prop, limit, current_nb_sols,
              previous_frontier_places):
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
    """Search for many solutions, save timings, and save frontiers.

    For every new solution, the system is NOT reinitialized, and a satisfiability
    test is made ONLY when there is no solution for the current step.
    This test is made to evaluate the minimal number of steps for reachability.

    Unlike find_mac(), this function is autonomous and takes into account the
    limitation of the number of solutions.

    .. TODO:: Handle all_macs flag like the old method with find_mac()
        Not used very often but can be usefull sometimes...

    :param limit: Limit the number of solutions.
    :param current_nb_sols: The current number of solutions already found.
        This number is used to limit the number of searched solutions.
331
332
    :param previous_frontier_places: Set of frontier places tuples from previous
        solutions. These tuples will be banned from the future solutions.
333
334
    :type limit: <int>
    :type current_nb_sols: <int>
335
    :type previous_frontier_places: <set <tuple <str>>>
336
337
338
339
    :return: None
    """
    # Build query
    query = MCLSimpleQuery(start_prop, inv_prop, final_prop)
340
341
    # Translate frontier places to their numerical values
    query.dim_start = get_dimacs_start_properties(mcla, previous_frontier_places)
342

343
344
345
    # Iterate on solutions
    for i, frontier_sol in enumerate(mcla.mac_search(query, steps), current_nb_sols):
        LOGGER.info("%s:: Current solution n°%s", final_prop, i + 1)
346

VIGNET Pierre's avatar
VIGNET Pierre committed
347
        LOGGER.debug("%s:: Next MAC object:\n%s", final_prop, frontier_sol)
348
349

        # Save MAC and timings
350
        LOGGER.debug("%s:: Save MAC and timings...", final_prop)
VIGNET Pierre's avatar
VIGNET Pierre committed
351
        with open(mac_complete_file, "a") as file:
VIGNET Pierre's avatar
VIGNET Pierre committed
352
            frontier_sol.save(file)
353
354

        # Save MAC
VIGNET Pierre's avatar
VIGNET Pierre committed
355
        LOGGER.debug("%s:: Save next MAC: %s", final_prop)
VIGNET Pierre's avatar
VIGNET Pierre committed
356
        with open(mac_file, "a") as file:
VIGNET Pierre's avatar
VIGNET Pierre committed
357
358
359
            frontier_sol.save(file, only_macs=True)

        # Save min steps
360
        # min_step = mcla.unfolder.current_step - 1 # Magic number !
VIGNET Pierre's avatar
VIGNET Pierre committed
361
362
363
        # LOGGER.debug("%s:: Save minimal steps: %s", final_prop, min_step)
        # with open(mac_step_file, 'a') as file:
        #     file.write(str(min_step)+'\n')
364

365
366
367
        # Stop when limitation is reached
        if i + 1 >= limit:
            # EXIT
VIGNET Pierre's avatar
VIGNET Pierre committed
368
369
370
            LOGGER.info(
                "%s:: Reaching the limitation of the number of solutions!", final_prop
            )
371
372
373
            return

    LOGGER.info("%s:: STOP the search! No more MAC.", final_prop)
374
375
376


def find_mac(mcla,
377
             mac_file, mac_step_file, mac_complete_file,
378
             steps, final_prop, start_prop, inv_prop, previous_frontier_places):
379
    """Search for 1 solution, save timings, save frontiers, and return it with
380
    the current step (deprecated, see find_macs()).
381
382
383
384
385
386
387
388
389

    For every new solution, the system is reinitialized, and a satisfiability
    test is made on a new query to evaluate the minimal number of steps for
    reachability.

    The side effect is that this process is expensive in a general way,
    and that parsing the properties (logical formulas of the frontier places of
    the previous solutions for example) in text format is very expensive because
    realized by the grammar ANTLR.
VIGNET Pierre's avatar
VIGNET Pierre committed
390

391
392
393
    :param previous_frontier_places: Set of frontier places tuples from previous
        solutions. These tuples will be banned from the future solutions.
    :type previous_frontier_places: <set <tuple <str>>>
394
395
    :return: A tuple of activated frontiers and the current step.
        None if there is no new Solution or if problem is not satisfiable.
396
397
398
    """
    # Build query
    query = MCLSimpleQuery(start_prop, inv_prop, final_prop)
399
400
    # Translate frontier places to their numerical values
    query.dim_start = get_dimacs_start_properties(mcla, previous_frontier_places)
401
402
403
404

    # Is the property reacheable ?
    reacheable = mcla.sq_is_satisfiable(query, steps)
    # If yes, in how many steps ?
405
    min_step = mcla.unfolder.current_step
406

407
    if reacheable and (min_step <= steps):
408
409
        LOGGER.info("%s:: Property %s is reacheable in %s steps",
                    final_prop, final_prop, min_step)
410
    else:
411
412
413
        LOGGER.info("%s:: Property %s is NOT reacheable in %s steps",
                    final_prop, final_prop, min_step)
        LOGGER.info("%s:: STOP the search!", final_prop)
414
415
        return

VIGNET Pierre's avatar
VIGNET Pierre committed
416
    # Find next MAC: Get FrontierSolution object
VIGNET Pierre's avatar
VIGNET Pierre committed
417
418
419
    frontier_sol = mcla.next_mac(query, min_step)
    if frontier_sol:
        LOGGER.debug("%s:: Next MAC object:\n%s", final_prop, frontier_sol)
420
421

        # Save MAC and timings
422
        LOGGER.debug("%s:: Save MAC and timings...", final_prop)
VIGNET Pierre's avatar
VIGNET Pierre committed
423
        with open(mac_complete_file, "a") as file:
VIGNET Pierre's avatar
VIGNET Pierre committed
424
            frontier_sol.save(file)
425
426

        # Save MAC (in alphabetic order...)
VIGNET Pierre's avatar
VIGNET Pierre committed
427
        LOGGER.debug("%s:: Save next MAC: %s", final_prop)
VIGNET Pierre's avatar
VIGNET Pierre committed
428
        with open(mac_file, "a") as file:
VIGNET Pierre's avatar
VIGNET Pierre committed
429
            frontier_sol.save(file, only_macs=True)
430
431

        # Save min steps
432
        min_step = mcla.unfolder.current_step - 1  # Magic number !
433
434
435
        # LOGGER.debug("%s:: Save minimal steps: %s", final_prop, min_step)
        # with open(mac_step_file, 'a') as file:
        #     file.write(str(min_step)+'\n')
436

VIGNET Pierre's avatar
VIGNET Pierre committed
437
        return frontier_sol.activated_frontier, min_step
438
439

    LOGGER.info("%s:: STOP the search! No more MAC.", final_prop)
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461


def detect_model_type(mclanalyser, filepath):
    """Return the function to use to load the model.

    The detection is based on the file extension.

    bcx file: Build an MCLAnalyser from a .bcx file:
        build_from_chart_file()
    cal file: Build an MCLAnalyser from a .cal file of PID database
        build_from_cadlang()
    xml file: Build an MCLAnalyser from a .xml file of PID database:
        build_from_pid_file()

    :param arg1: MCLAnalyser.
    :param arg2: File that contains the model.
    :type arg1: <MCLAnalyser>
    :type arg2: <str>
    :return: The function to use to read the given file.
    :rtype: <func>
    """

VIGNET Pierre's avatar
VIGNET Pierre committed
462
463
464
465
466
    build_func = {
        ".bcx": mclanalyser.build_from_chart_file,
        ".cal": mclanalyser.build_from_cadlang,
        ".xml": mclanalyser.build_from_pid_file,
    }
467
468
469
470
471
472

    _, extension = os.path.splitext(filepath)
    LOGGER.debug("Found %s extension: %s", extension, filepath)

    if extension not in build_func:
        LOGGER.error("Unauthorized file: %s", filepath)
473
474
475
476
477
        exit()

    return build_func[extension]


478
# def main(model_file, mac_file, mac_step_file, mac_complete_file, mac_strong_file,
479
480
481
482
483
484
#         steps, final_prop, start_prop, inv_prop, all_macs):
#
#    LOGGER.debug("Params: start: {}, inv: {}, final: {}".format(start_prop,
#                                                                inv_prop,
#                                                                final_prop))
#
485
486
#    mac_p = None
#    # forbid previous mac
487
#    try :
488
489
#        mac_p = camFile2notOr(mac_file)
#        print("mac file:", mac_p)
490
491
492
#    except :
#        print('error in camFile2notOr')
#
493
494
495
496
#    if start_prop and mac_p :
#        start_prop += ' and ('+mac_p+')'
#    elif mac_p :
#        start_prop = mac_p
497
498
499
500
#
#    # BUILD MCLA
#    error_reporter = ErrorRep()
#    mcla = MCLAnalyser(error_reporter)
501
#    mcla.build_from_chart_file(model_file)
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
#
#    if error_reporter.error:
#        #cancel_warn(error_reporter.memory)
#        #error_reporter.reset()
#        #return
#        print("ERROR:: " + error_reporter.error)
#        raise
##
#    # BUILD QUERY
#    query = MCLSimpleQuery(start_prop, inv_prop, final_prop)
#
#    # Frontier init:
#    # on_yn function:
#    # reach = self.mcla.sq_is_satisfiable(query, max_step)
#
#    # Show solutions:
#    #on_cond function:
##    lsol = mcla.sq_frontier_solutions(query, step, 10)
##    print("DEBUG:: " + str(lsol))
##    print("DEBUG:: ", len( lsol))
#
#    # minimal activation conditions
524
525
526
#    # on_mac function:
##    mac_list = self.mcla.mac_search(query, self.max_step)
##            if len(mac_list)==0 :
527
528
529
530
##                ok_warn("The solver returns an empty list" +
##                        "\n"+" you should refine your query")
#
#    # OPTIMIZE STEP RESEARCH
531
532
#    if os.path.isfile(mac_step_file):
#        min_step = int(get_last_line(mac_step_file))
533
#        print("min_step opti:", min_step)
VIGNET Pierre's avatar
VIGNET Pierre committed
534
#        query.steps_before_check = min_step - 1
535
536
537
538
#
#
#    reacheable = mcla.sq_is_satisfiable(query, steps) #important step
#    print("reacheable:", reacheable)
539
#    min_step = mcla.unfolder.current_step
540
541
#    print("min_step:", min_step)
#    # Set max step authorized
VIGNET Pierre's avatar
VIGNET Pierre committed
542
#    query.steps_before_check = min_step - 1
543
544
545
546
547
548
549
#
#    # FIND NEXT MAC
#    next_mac_object = mcla.next_mac(query, min_step) #important step
#    print("next_mac_object:", next_mac_object)
#    if next_mac_object:
#
#        # SAVE MAC AND TIMING
550
#        with open(mac_complete_file, 'a') as file:
551
552
553
554
555
#            next_mac_object.save(file)
#
#        # SAVE MAC
#        next_mac = next_mac_object.activated_frontier
#        print("save next mac:", next_mac)
556
#        write_list(next_mac, mac_file)
557
558
#
#        # SAVE STEP
559
#        min_step = mcla.unfolder.current_step
560
#        print("save min step:", min_step)
561
#        with open(mac_step_file, 'a') as file:
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
#            file.write(str(min_step)+'\n')
#
#        return 0
#    else:
#        print("stop")
#        return 1


def compute_combinations(final_properties):
    """Return all combinations of final properties.

    .. note:: (in case of input_file and combinations set).

    :param: List of final properties.
    :type: <list>
    :return: List of str. Each str is a combination of final_properties linked
        by a logical 'and'.
VIGNET Pierre's avatar
VIGNET Pierre committed
579
580
581
582
583

        :Example:

            ``('TGFB1', 'COL1A1'), ('TGFB1', 'decorin')``
            gives: ``['TGFB1 and COL1A1', 'TGFB1 and decorin']``
584
585
586
587
588
589
590
591
592
593
594

    :rtype: <list <str>>
    """

    final_properties = set(final_properties)

    def get_formula(data):
        """Include all elements in the combinations and exclude explicitely,
        all other elements.
        """

VIGNET Pierre's avatar
VIGNET Pierre committed
595
        negated_places = " and not ".join(final_properties - data)
596
        return "{}{}{}".format(
VIGNET Pierre's avatar
VIGNET Pierre committed
597
598
599
            " and ".join(data),
            " and not " if negated_places != "" else "",
            negated_places,
600
        )
601
602
603
604
605
606
607
608
609
610

    all_combinations = list()
    for i in range(1, len(final_properties) + 1):

        all_combinations.append(
            {get_formula(set(comb)) for comb in it.combinations(final_properties, i)}
        )

    # Unpack combinations
    all_combinations = [comb for comb in it.chain(*all_combinations)]
611
612
    LOGGER.debug("Combinations: %s, Length: %s", all_combinations, all_combinations)
    LOGGER.info("Number of computed combinations: %s", len(all_combinations))
613
614
615
616

    return all_combinations


617
618
619
def solutions_search(params):
    """Launch the search for Minimum Activation Conditions (MAC) for entities
    of interest.
620

VIGNET Pierre's avatar
VIGNET Pierre committed
621
    * If there is no input file, there will be only one process.
VIGNET Pierre's avatar
VIGNET Pierre committed
622
623
    * If an input file is given, there will be 1 process per line
      (per logical formula on each line).
624
625
    """
    # No input file
VIGNET Pierre's avatar
VIGNET Pierre committed
626
    if params["final_prop"]:
627
628
629
        compute_macs(params)

    else:
VIGNET Pierre's avatar
VIGNET Pierre committed
630
631
        # Multiple properties in input file
        # => multiprocessing: 1 process for each property
632

VIGNET Pierre's avatar
VIGNET Pierre committed
633
634
        with open(params["input_file"], "r") as f_d:
            g = (line.rstrip("\n") for line in f_d)
635

VIGNET Pierre's avatar
VIGNET Pierre committed
636
            final_properties = [prop for prop in g if prop != ""]
637

VIGNET Pierre's avatar
VIGNET Pierre committed
638
        if params["combinations"]:
639
640
641
642
            # If input_file is set, we can compute all combinations of
            # final_properties. default: False
            final_properties = compute_combinations(final_properties)

VIGNET Pierre's avatar
VIGNET Pierre committed
643
        LOGGER.debug("Final properties: %s", final_properties)
644
        # Output combinations of final_properties
645
646
#        with open(params['input_file'] + '_combinations.txt', 'w') as f_d:
#            f_d.write('\n'.join(final_properties) + '\n')
647
648
649
#
#        g = (elem for elem in final_properties)
#        for i in range(1, len(final_properties) + 1):
650
#            with open(params['input_file'][:-4] + '_combinations' + str(i) + '.txt', 'w') as f_d:
651
#                try:
652
653
#                    f_d.write(next(g) + '\n')
#                    f_d.write(next(g) + '\n')
654
655
656
657
658
659
#                except StopIteration:
#                    break
#
#        exit()

        def update_params(prop):
660
661
            """Shallow copy of parameters and update final_prop for a new run"""
            new_params = params.copy()
VIGNET Pierre's avatar
VIGNET Pierre committed
662
            new_params["final_prop"] = prop
663
            return new_params
664
665
666

        # Fix number of processes
        # PS: the new solver is optimized for 8 threads
VIGNET Pierre's avatar
VIGNET Pierre committed
667
668
#        nb_cpu_required = mp.cpu_count() / 8
#        nb_cpu_required = 1 if nb_cpu_required == 0 else nb_cpu_required
669

670
        with ProcessPoolExecutor(max_workers=mp.cpu_count()) as executor:
671

VIGNET Pierre's avatar
VIGNET Pierre committed
672
673
674
675
            futures_and_output = {
                executor.submit(compute_macs, update_params(job_property)): job_property
                for job_property in final_properties
            }  # Job name
676
677
678
679
680
681
682
683
684
685

        nb_errors = 0
        nb_done = 0
        for future in as_completed(futures_and_output):

            job_name = futures_and_output[future]

            # On affiche les résultats si les futures en contiennent.
            # Si elles contiennent une exception, on affiche l'exception.
            if future.exception() is not None:
VIGNET Pierre's avatar
VIGNET Pierre committed
686
687
688
                LOGGER.error(
                    "%s generated an exception: \n%s", job_name, future.exception()
                )
689
690
691
                nb_errors += 1
            else:
                # The end
692
                LOGGER.info("%s... \t\t[Done]", job_name)
693
694
                nb_done += 1

695
        LOGGER.info("Ending: %s errors, %s done\nbye.", nb_errors, nb_done)
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711


def compute_macs(params):
    """Launch Cadbiom search of MACs (Minimal Activation Conditions).

    This function is called 1 or multiple times according to the necessity
    to use multiprocessing (Cf launch_researchs()).

    .. note:: Previous result files will be deleted.

    """

    # Limit recursion
    sys.setrecursionlimit(10000)

    # QUERY PARAMETERS
712
    model_filename = os.path.basename(os.path.splitext(params["model_file"])[0])
713
714

    # FILES
715
    mac_file_prefix = params["output"] + model_filename + "_" + params["final_prop"] + "_mac"
716
717
718
719
720
721
722
723
    # mac_file
    mac_file          = mac_file_prefix + ".txt"
    # mac_step_file
    mac_step_file     = mac_file_prefix + "_step.txt"
    # mac_complete_file
    mac_complete_file = mac_file_prefix + "_complete.txt"
    # mac_strong_file
    mac_strong_file   = mac_file_prefix + "_strongA.txt"
724
725

    def remove_file(file):
726
        """Reset files"""
727
728
        try:
            os.remove(file)
729
        except OSError:
730
731
            pass

VIGNET Pierre's avatar
VIGNET Pierre committed
732
    if not params["continue"]:
VIGNET Pierre's avatar
VIGNET Pierre committed
733
        # Reset previous working files
734
        # PS: the reload is done in search_entry_point() function
735
736
737
738
        remove_file(mac_file)
        remove_file(mac_step_file)
        remove_file(mac_complete_file)
        remove_file(mac_strong_file)
739
740

    # MAC research
741
    search_entry_point(
742
        params["model_file"],  # model_file
VIGNET Pierre's avatar
VIGNET Pierre committed
743
744
745
746
        mac_file,              # mac_file
        mac_step_file,         # mac_step_file
        mac_complete_file,     # mac_complete_file
        mac_strong_file,       # mac_strong_file
VIGNET Pierre's avatar
VIGNET Pierre committed
747
748
749
750
751
752
753
        params["steps"],
        params["final_prop"],
        params["start_prop"],
        params["inv_prop"],
        params["all_macs"],
        params["continue"],
        params["limit"],
754
    )
VIGNET Pierre's avatar
VIGNET Pierre committed
755

VIGNET Pierre's avatar
VIGNET Pierre committed
756

757
758
def read_mac_file(file):
    """Return a list a fontier places already found in mac file
VIGNET Pierre's avatar
VIGNET Pierre committed
759

760
761
762
    .. TODO: use tools/solutions.py functions to reload frontiers
        => put these functions into the library...

VIGNET Pierre's avatar
VIGNET Pierre committed
763
764
765
766
767
768
769
770
    .. note:: use make_logical_formula() to get the new start_prop of the run.

    :param: Mac file of a previous run
    :type: <str>
    :return: A set a frontier places.
    :rtype: <set>
    """

VIGNET Pierre's avatar
VIGNET Pierre committed
771
772
    with open(file, "r") as f_d:
        return {tuple(line.rstrip("\n").split(" ")) for line in f_d}