diff --git a/clyngor/answers.py b/clyngor/answers.py index 67d2534..659fd2e 100644 --- a/clyngor/answers.py +++ b/clyngor/answers.py @@ -53,7 +53,8 @@ def __init__(self, answers:iter, command:str='', statistics:dict={}, """ if not with_optimization: # emulate optimization with None value - answers = ((answer, None, False) for answer in answers) + # TODO: verify that in absence of optimization, all answers are ALWAYS enumerated from 1 to n. + answers = ((answer, None, False, idx) for idx, answer in enumerate(answers, start=1)) self._answers = iter(answers) self._command = str(command or '') self._statistics = statistics # will be updated by reading method @@ -72,6 +73,7 @@ def __init__(self, answers:iter, command:str='', statistics:dict={}, self._ignore_args = False self._with_optimization = False self._with_optimality = False + self._with_answer_number = False self.__on_end = on_end or (lambda: None) def __del__(self): @@ -101,6 +103,14 @@ def with_optimality(self): self._with_optimality = True return self + @property + def with_answer_number(self): + """Yield (model, optimization, optimality, answer_number) instead of just model""" + self._with_optimization = True + self._with_optimality = True + self._with_answer_number = True + return self + @property def first_arg_only(self): """Keep only the first argument, and do not enclose it in a tuple.""" @@ -190,10 +200,12 @@ def __next__(self): def __iter__(self): """Yield answer sets""" - for answer_set, optimization, optimality in self._answers: + for answer_set, optimization, optimality, answer_number in self._answers: answer_set = tuple(self._parse_answer(answer_set)) parsed = self._format(answer_set) - if self._with_optimality: + if self._with_answer_number: + yield parsed, optimization, optimality, answer_number + elif self._with_optimality: yield parsed, optimization, optimality elif self._with_optimization: yield parsed, optimization diff --git a/clyngor/parsing.py b/clyngor/parsing.py index 9bc6d00..535e747 100644 --- a/clyngor/parsing.py +++ b/clyngor/parsing.py @@ -198,6 +198,7 @@ def parse_clasp_output(output:iter or str, *, yield_stats:bool=False, # first answer begins while True: if line.startswith(ASW_FLAG): + yield 'answer_number', int(line[len(ASW_FLAG):]) yield 'answer', next(output) elif line.startswith(OPT_FLAG) and yield_opti: yield 'optimization', tuple(map(int, line[len(OPT_FLAG):].strip().split())) diff --git a/clyngor/solving.py b/clyngor/solving.py index 95ec7c8..8fa231d 100644 --- a/clyngor/solving.py +++ b/clyngor/solving.py @@ -197,18 +197,22 @@ def clingo_version(clingo_bin_path:str=None) -> dict: def _gen_answers(stdout:iter, stderr:iter, statistics:dict, - error_on_warning:bool) -> (str, int or None, bool): - """Yield 3-uplet (answer set, optimization, optimum found), + error_on_warning:bool) -> (str, int or None, bool, int): + """Yield 4-uplet (answer set, optimization, optimum found, answer number), and update given statistics dict with statistics payloads """ answer = None # is used to generate a model only when we are sur there is (no) optimization + answer_number = None optimization, optimum_found = None, False for ptype, payload in parse_clasp_output(stdout, yield_stats=True): - if ptype == 'answer': # yield previously found answer + if ptype == 'answer_number': if answer is not None: - yield answer, optimization, optimum_found - answer, optimization, optimum_found = payload, None, False + yield answer, optimization, optimum_found, answer_number + answer, optimization, optimum_found, answer_number = payload, None, False, None + answer_number = payload + elif ptype == 'answer': # yield previously found answer + answer = payload elif ptype == 'optimum found': optimum_found = payload elif ptype == 'optimization': @@ -220,7 +224,8 @@ def _gen_answers(stdout:iter, stderr:iter, statistics:dict, else: assert ptype in parse_clasp_output.out_types, 'solving.parse_clasp_output yields an unexpected type ' + repr(ptype) if answer is not None: # if no optimization, probably one miss - yield answer, optimization, optimum_found + assert answer_number is not None, answer_number + yield answer, optimization, optimum_found, answer_number # handle stderr for payload in validate_clasp_stderr(stderr): diff --git a/clyngor/test/test_answers.py b/clyngor/test/test_answers.py index 38c7b5b..daccdb1 100644 --- a/clyngor/test/test_answers.py +++ b/clyngor/test/test_answers.py @@ -58,11 +58,11 @@ def many_atoms_answers(): @pytest.fixture def optimized_answers(): return Answers(( - ('edge(4,"s…lp.") r_e_l(1,2)', 1, False), - ('edge(4,"s…lp.") r_e_l(1,2)', 1, False), - ('edge(4,"s…lp.") r_e_l(1,2)', 2, False), - ('edge(4,"s…lp.") r_e_l(1,2)', 3, False), - ('edge(4,"s…lp.") r_e_l(1,2)', 4, False), + ('edge(4,"s…lp.") r_e_l(1,2)', 1, False, 1), + ('edge(4,"s…lp.") r_e_l(1,2)', 1, False, 2), + ('edge(4,"s…lp.") r_e_l(1,2)', 2, False, 3), + ('edge(4,"s…lp.") r_e_l(1,2)', 3, False, 4), + ('edge(4,"s…lp.") r_e_l(1,2)', 4, False, 5), ), with_optimization=True) @@ -288,8 +288,8 @@ def test_optimization_access(optimized_answers): assert next(answers) == ({('edge', (4, 's…lp.')), ('r_e_l', (1, 2))}, 1) answers = answers.no_arg assert next(answers) == ({'edge', 'r_e_l'}, 2) - answers.atoms_as_string - assert next(answers) == ({'edge(4,"s…lp.")', 'r_e_l(1,2)'}, 3) - answers.with_optimality - assert next(answers) == ({'edge(4,"s…lp.")', 'r_e_l(1,2)'}, 4, False) + answers.atoms_as_string.with_optimality + assert next(answers) == ({'edge(4,"s…lp.")', 'r_e_l(1,2)'}, 3, False) + answers.with_answer_number + assert next(answers) == ({'edge(4,"s…lp.")', 'r_e_l(1,2)'}, 4, False, 5) assert next(answers, None) is None diff --git a/clyngor/test/test_parsing.py b/clyngor/test/test_parsing.py index 6717fbe..3cbaf30 100644 --- a/clyngor/test/test_parsing.py +++ b/clyngor/test/test_parsing.py @@ -9,11 +9,13 @@ def test_simple_case(): yield_stats=True, yield_info=True) models = [] for type, payload in parsed: - assert type in ('statistics', 'info', 'answer') + assert type in ('statistics', 'info', 'answer', 'answer_number') if type == 'statistics': stats = payload elif type == 'answer': models.append(payload) + elif type == 'answer_number': + pass else: assert type == 'info' info = payload @@ -73,6 +75,8 @@ def test_parse_termset_impossible(): def test_string(): """Show that string with comma in it is handled correctly""" parsed = Parser().parse_clasp_output(OUTCLASP_STRING.splitlines()) + type, answer_number = next(parsed) + assert type == 'answer_number' type, model = next(parsed) assert next(parsed, None) is None, "there is only one model" assert type == 'answer', "the model is an answer" @@ -84,6 +88,8 @@ def test_string(): def test_complex_atoms(): parsed = Parser().parse_clasp_output(OUTCLASP_COMPLEX_ATOMS.splitlines()) + type, answer_number = next(parsed) + assert type == 'answer_number' type, model = next(parsed) assert next(parsed, None) is None, "there is only one model" assert type == 'answer', "the model is an answer" @@ -135,6 +141,8 @@ def test_time_limit(): assert model == next(expected_info) elif type == 'answer': assert model == frozenset(next(expected_answer)) + elif type == 'answer_number': + assert isinstance(model, int) and model > 0, model elif type == 'optimization': assert model == (next(expected_optimization),) elif type == 'progression': @@ -162,6 +170,8 @@ def test_multiple_opt_values(): assert False, 'no info' elif type == 'answer': assert model == frozenset(next(expected_answer)) + elif type == 'answer_number': + assert isinstance(model, int) and model > 0, model elif type == 'optimization': assert model == next(expected_optimization) elif type == 'optimum found': @@ -196,6 +206,8 @@ def test_multithread_with_progression(): assert False, 'no info' elif type == 'answer': assert model == frozenset(next(expected_answer)) + elif type == 'answer_number': + assert isinstance(model, int) and model > 0, model elif type == 'optimization': assert model == next(expected_optimization) elif type == 'progression':