Attention une mise à jour du service Gitlab va être effectuée le mardi 18 janvier (et non lundi 17 comme annoncé précédemment) entre 18h00 et 18h30. Cette mise à jour va générer une interruption du service dont nous ne maîtrisons pas complètement la durée mais qui ne devrait pas excéder quelques minutes.

solution_search.py 26.4 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
144
145
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.
    """
    dimacs_start = list()
VIGNET Pierre's avatar
VIGNET Pierre committed
146
147
148
149
150
151
    [
        dimacs_start.append(
            [-mcla.unfolder.var_dimacs_code(place) for place in frontier_places]
        )
        for frontier_places in previous_frontier_places
    ]
152
153


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

VIGNET Pierre's avatar
VIGNET Pierre committed
159
160
161
162
163
164
165
    :param chart_file: Model file (bcx, xml, cal).
    :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.
166
167
168
169
170
171
172
173
    :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.
    :param inv_prop: Formula: Invariant property that will never occur during
        the simulation.
        The given logical formula will be enclose by a logical not() and
        will be checked at each step of the simulation.
VIGNET Pierre's avatar
VIGNET Pierre committed
174
    :param all_macs: If set to True (not default), search all macs with
175
176
177
178
179
180
181
182
183
184
185
186
187
188
        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.
        Example:
            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
189
    :param continue_run: If set to True (not default), previous macs from a previous
VIGNET Pierre's avatar
VIGNET Pierre committed
190
        run, will be reloaded.
VIGNET Pierre's avatar
VIGNET Pierre committed
191
192
193
194
195
196
197
198
199
200
201
202
203
    :param limit: Limit the number of solutions.
    :type chart_file: <str>
    :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>
204
205
206
207
208
209
210
211
212
213
214
215
216

    .. 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+")"
    """
217
218
    # Build MCLA with Error Reporter
    mcla = MCLAnalyser(ErrorRep())
219

VIGNET Pierre's avatar
VIGNET Pierre committed
220
    # Load model file
221
    detect_model_type(mcla, chart_file)(chart_file)
222
    if mcla.reporter.error:
223
224
225
        raise "Error during loading of file"

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

243
244
    # Compute the number of previously computed frontier places
    current_nb_sols = len(previous_frontier_places) if previous_frontier_places else 0
245

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

253
254
    ## 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)
255
256
257
    # with PyCallGraph(output=GraphvizOutput()):
    find_macs(mcla,
              mac_file, mac_step_file, mac_complete_file,
258
259
              steps, final_prop, start_prop, inv_prop, limit, current_nb_sols,
              previous_frontier_places)
260
261

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

271
272
        ret = \
            find_mac(mcla,
273
                     mac_file, mac_step_file, mac_complete_file,
274
275
                     steps, final_prop, start_prop, inv_prop,
                     previous_frontier_places)
276

VIGNET Pierre's avatar
VIGNET Pierre committed
277
        if not ret:
278
279
280
            # No new solution/not satisfiable
            return

VIGNET Pierre's avatar
VIGNET Pierre committed
281
282
        frontier_places, min_steps = ret

283
284
285
        # 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
286
287
288
        LOGGER.debug(
            "%s:: Prev frontier places: %s", final_prop, previous_frontier_places
        )
289

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

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

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

308

309
def find_macs(mcla,
310
              mac_file, mac_step_file, mac_complete_file,
311
312
              steps, final_prop, start_prop, inv_prop, limit, current_nb_sols,
              previous_frontier_places):
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
    """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.
328
329
    :param previous_frontier_places: Set of frontier places tuples from previous
        solutions. These tuples will be banned from the future solutions.
330
331
    :type limit: <int>
    :type current_nb_sols: <int>
332
    :type previous_frontier_places: <set <tuple <str>>>
333
334
335
336
    :return: None
    """
    # Build query
    query = MCLSimpleQuery(start_prop, inv_prop, final_prop)
337
338
    # Translate frontier places to their numerical values
    query.dim_start = get_dimacs_start_properties(mcla, previous_frontier_places)
339

340
341
342
    # 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)
343

VIGNET Pierre's avatar
VIGNET Pierre committed
344
        LOGGER.debug("%s:: Next MAC object:\n%s", final_prop, frontier_sol)
345
346

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

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

        # Save min steps
        # min_step = mcla.unfolder.get_current_step() - 1 # Magic number !
        # 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')
361

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

    LOGGER.info("%s:: STOP the search! No more MAC.", final_prop)
371
372
373


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

    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
387

388
389
390
    :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>>>
391
392
    :return: A tuple of activated frontiers and the current step.
        None if there is no new Solution or if problem is not satisfiable.
393
394
395
    """
    # Build query
    query = MCLSimpleQuery(start_prop, inv_prop, final_prop)
396
397
    # Translate frontier places to their numerical values
    query.dim_start = get_dimacs_start_properties(mcla, previous_frontier_places)
398
399
400
401

    # Is the property reacheable ?
    reacheable = mcla.sq_is_satisfiable(query, steps)
    # If yes, in how many steps ?
402
    min_step = mcla.unfolder.get_current_step()
403

404
    if reacheable and (min_step <= steps):
405
406
        LOGGER.info("%s:: Property %s is reacheable in %s steps",
                    final_prop, final_prop, min_step)
407
    else:
408
409
410
        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)
411
412
        return

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

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

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

        # Save min steps
VIGNET Pierre's avatar
VIGNET Pierre committed
429
        min_step = mcla.unfolder.get_current_step() - 1  # Magic number !
430
431
432
        # 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')
433

VIGNET Pierre's avatar
VIGNET Pierre committed
434
        return frontier_sol.activated_frontier, min_step
435
436

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


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
459
460
461
462
463
    build_func = {
        ".bcx": mclanalyser.build_from_chart_file,
        ".cal": mclanalyser.build_from_cadlang,
        ".xml": mclanalyser.build_from_pid_file,
    }
464
465
466
467
468
469

    _, 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)
470
471
472
473
474
        exit()

    return build_func[extension]


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

        :Example:

            ``('TGFB1', 'COL1A1'), ('TGFB1', 'decorin')``
            gives: ``['TGFB1 and COL1A1', 'TGFB1 and decorin']``
581
582
583
584
585
586
587
588
589
590
591

    :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
592
        negated_places = " and not ".join(final_properties - data)
593
        return "{}{}{}".format(
VIGNET Pierre's avatar
VIGNET Pierre committed
594
595
596
            " and ".join(data),
            " and not " if negated_places != "" else "",
            negated_places,
597
        )
598
599
600
601
602
603
604
605
606
607

    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)]
608
609
    LOGGER.debug("Combinations: %s, Length: %s", all_combinations, all_combinations)
    LOGGER.info("Number of computed combinations: %s", len(all_combinations))
610
611
612
613

    return all_combinations


614
615
616
def solutions_search(params):
    """Launch the search for Minimum Activation Conditions (MAC) for entities
    of interest.
617

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

    else:
VIGNET Pierre's avatar
VIGNET Pierre committed
627
628
        # Multiple properties in input file
        # => multiprocessing: 1 process for each property
629

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

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

VIGNET Pierre's avatar
VIGNET Pierre committed
635
        if params["combinations"]:
636
637
638
639
            # 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
640
        LOGGER.debug("Final properties: %s", final_properties)
641
        # Output combinations of final_properties
642
643
#        with open(params['input_file'] + '_combinations.txt', 'w') as f_d:
#            f_d.write('\n'.join(final_properties) + '\n')
644
645
646
#
#        g = (elem for elem in final_properties)
#        for i in range(1, len(final_properties) + 1):
647
#            with open(params['input_file'][:-4] + '_combinations' + str(i) + '.txt', 'w') as f_d:
648
#                try:
649
650
#                    f_d.write(next(g) + '\n')
#                    f_d.write(next(g) + '\n')
651
652
653
654
655
656
#                except StopIteration:
#                    break
#
#        exit()

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

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

667
        with ProcessPoolExecutor(max_workers=mp.cpu_count()) as executor:
668

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

        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
683
684
685
                LOGGER.error(
                    "%s generated an exception: \n%s", job_name, future.exception()
                )
686
687
688
                nb_errors += 1
            else:
                # The end
689
                LOGGER.info("%s... \t\t[Done]", job_name)
690
691
                nb_done += 1

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


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
VIGNET Pierre's avatar
VIGNET Pierre committed
709
    model_filename = os.path.basename(os.path.splitext(params["chart_file"])[0])
710
711
712

    # FILES
    # Add trailing '/' if not present
VIGNET Pierre's avatar
VIGNET Pierre committed
713
714
    output = params["output"] if params["output"][-1] == "/" else params["output"] + "/"
    mac_file_prefix = output + model_filename + "_" + params["final_prop"] + "_mac"
715
716
717
718
719
720
721
722
    # 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"
723
724

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

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

    # MAC research
740
    search_entry_point(
VIGNET Pierre's avatar
VIGNET Pierre committed
741
        params["chart_file"],  # chart_file
VIGNET Pierre's avatar
VIGNET Pierre committed
742
743
744
745
        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
746
747
748
749
750
751
752
        params["steps"],
        params["final_prop"],
        params["start_prop"],
        params["inv_prop"],
        params["all_macs"],
        params["continue"],
        params["limit"],
753
    )
VIGNET Pierre's avatar
VIGNET Pierre committed
754

VIGNET Pierre's avatar
VIGNET Pierre committed
755

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

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

VIGNET Pierre's avatar
VIGNET Pierre committed
762
763
764
765
766
767
768
769
    .. 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
770
771
    with open(file, "r") as f_d:
        return {tuple(line.rstrip("\n").split(" ")) for line in f_d}