diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 523730f..b69dd94 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -37,6 +37,9 @@ Changed ``Instance.solve``, ``Instance.solve_async``, and ``Instance.solutions``. The ``timeout`` parameter is still accepted, but will add a ``DeprecationWarning`` and will be removed in future versions. +- The ``intermediate_solutions`` parameter can now be explicitly set to + ``False`` to avoid the ``-i`` flag to be passed to MiniZinc, which is + generally added to ensure that a final solution is available. Fixed ^^^^^ diff --git a/src/minizinc/instance.py b/src/minizinc/instance.py index 7031716..9d903b8 100644 --- a/src/minizinc/instance.py +++ b/src/minizinc/instance.py @@ -110,7 +110,7 @@ def solve( processes: Optional[int] = None, random_seed: Optional[int] = None, all_solutions: bool = False, - intermediate_solutions: bool = False, + intermediate_solutions: Optional[bool] = None, free_search: bool = False, optimisation_level: Optional[int] = None, timeout: Optional[timedelta] = None, @@ -141,10 +141,13 @@ def solve( all_solutions (bool): Request to solver to find all solutions. (Only available on satisfaction problems and when the ``-a`` flag is supported by the solver) - intermediate_solutions (bool): Request the solver to output any - intermediate solutions that are found during the solving - process. (Only available on optimisation problems and when the - ``-a`` flag is supported by the solver) + intermediate_solutions (Optional[bool]): Request the solver to + output any intermediate solutions that are found during the + solving process. If left to ``None``, then intermediate + solutions might still be requested to ensure that the solving + process gives its final solution. (Only available on + optimisation problems and when the ``-i`` or ``-a`` flag is + supported by the solver) optimisation_level (Optional[int]): Set the MiniZinc compiler optimisation level. @@ -189,6 +192,8 @@ def solve( ) return asyncio.run(coroutine) except RuntimeError as r: + coroutine.close() + del coroutine if "called from a running event loop" in r.args[0]: raise RuntimeError( "the synchronous MiniZinc Python `solve()` method was called from" @@ -207,8 +212,8 @@ async def solve_async( nr_solutions: Optional[int] = None, processes: Optional[int] = None, random_seed: Optional[int] = None, - all_solutions=False, - intermediate_solutions=False, + all_solutions: bool = False, + intermediate_solutions: Optional[bool] = None, free_search: bool = False, optimisation_level: Optional[int] = None, timeout: Optional[timedelta] = None, @@ -231,14 +236,14 @@ async def solve_async( """ status = Status.UNKNOWN - solution = None statistics: Dict[str, Any] = {} multiple_solutions = ( all_solutions or intermediate_solutions or nr_solutions is not None ) - if multiple_solutions: - solution = [] + solution: Union[Optional[Any], List[Any]] = ( + [] if multiple_solutions else None + ) async for result in self.solutions( time_limit=time_limit, @@ -256,6 +261,7 @@ async def solve_async( statistics.update(result.statistics) if result.solution is not None: if multiple_solutions: + assert isinstance(solution, list) solution.append(result.solution) else: solution = result.solution @@ -296,12 +302,12 @@ async def diverse_solutions( "mzn-analyse executable could not be located" ) - try: - # Create a temporary file in which the diversity model (generated by mzn-analyse) is placed - div_file = tempfile.NamedTemporaryFile( - prefix="mzn_div", suffix=".mzn", delete=False - ) + # Create a temporary file in which the diversity model (generated by mzn-analyse) is placed + div_file = tempfile.NamedTemporaryFile( + prefix="mzn_div", suffix=".mzn", delete=False + ) + try: # Extract the diversity annotations. with self.files() as files: div_anns = mzn_analyse.run( @@ -454,8 +460,8 @@ async def solutions( nr_solutions: Optional[int] = None, processes: Optional[int] = None, random_seed: Optional[int] = None, - all_solutions=False, - intermediate_solutions=False, + all_solutions: bool = False, + intermediate_solutions: Optional[bool] = None, free_search: bool = False, optimisation_level: Optional[int] = None, verbose: bool = False, @@ -537,12 +543,20 @@ async def solutions( "Solver does not support the -n-o flag" ) cmd.extend(["--num-optimal", str(nr_solutions)]) - elif ( - "-i" not in self._solver.stdFlags - or "-a" not in self._solver.stdFlags + elif intermediate_solutions: + if ( + "-i" not in self._solver.stdFlags + and "-a" not in self._solver.stdFlags + ): + raise NotImplementedError( + "Solver does not support the -i and -a flags" + ) + cmd.append("--intermediate-solutions") + elif (intermediate_solutions is None and time_limit is not None) and ( + "-i" in self._solver.stdFlags or "-a" in self._solver.stdFlags ): - # Enable intermediate solutions when possible - # (ensure that solvers always output their best solution) + # Enable intermediate solutions just in case to ensure that there is + # a best solution available at the time limit. cmd.append("--intermediate-solutions") # Set number of processes to be used if processes is not None: @@ -596,12 +610,11 @@ async def solutions( # Run the MiniZinc process proc = await self._driver._create_process(cmd, solver=solver) - try: - assert isinstance(proc.stderr, asyncio.StreamReader) - assert isinstance(proc.stdout, asyncio.StreamReader) - - read_stderr = asyncio.create_task(_read_all(proc.stderr)) + assert isinstance(proc.stderr, asyncio.StreamReader) + assert isinstance(proc.stdout, asyncio.StreamReader) + read_stderr = asyncio.create_task(_read_all(proc.stderr)) + try: async for obj in decode_async_json_stream( proc.stdout, cls=MZNJSONDecoder, enum_map=self._enum_map ): @@ -836,6 +849,7 @@ def analyse(self): if obj["type"] == "interface": interface = obj break + assert interface is not None old_method = self._method_cache self._method_cache = Method.from_string(interface["method"]) self._input_cache = {} @@ -1019,6 +1033,7 @@ def _parse_stream_obj(self, obj, statistics): if "_checker" in statistics: tmp["_checker"] = statistics.pop("_checker") + assert self.output_type is not None solution = self.output_type(**tmp) statistics["time"] = timedelta(milliseconds=obj["time"]) elif obj["type"] == "time":