From 8bdcc200d0f0bf65c65bf8104ac5419095940b02 Mon Sep 17 00:00:00 2001 From: ilansen Date: Tue, 8 Aug 2023 16:05:10 +1000 Subject: [PATCH 01/15] Initial commit on diversity --- src/minizinc/__init__.py | 2 + src/minizinc/diversity.py | 338 ++++++++++++++++++++++++++++++++++++++ src/minizinc/instance.py | 37 +++++ 3 files changed, 377 insertions(+) create mode 100644 src/minizinc/diversity.py diff --git a/src/minizinc/__init__.py b/src/minizinc/__init__.py index 46f0447..0790958 100644 --- a/src/minizinc/__init__.py +++ b/src/minizinc/__init__.py @@ -12,6 +12,7 @@ from .result import Result, Status from .solver import Solver from .types import AnonEnum, ConstrEnum +from .diversity import Diversity __version__ = "0.9.1" @@ -48,4 +49,5 @@ "Result", "Solver", "Status", + "Diversity", ] diff --git a/src/minizinc/diversity.py b/src/minizinc/diversity.py new file mode 100644 index 0000000..97c02d5 --- /dev/null +++ b/src/minizinc/diversity.py @@ -0,0 +1,338 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +import os +import platform +# import re +import shutil +import json +import subprocess +import argparse +from pathlib import Path + +from typing import ( + Dict, + Iterator, + List, + Optional, + Union, +) + +import minizinc + +import numpy as np + +from dataclasses import asdict + +from .error import ConfigurationError + +MAC_LOCATIONS = [ + str(Path("/Applications/MiniZincIDE.app/Contents/Resources")), + str(Path("~/Applications/MiniZincIDE.app/Contents/Resources").expanduser()), +] +#: Default locations on Windows where the MiniZinc packaged release would be installed +WIN_LOCATIONS = [ + str(Path("c:/Program Files/MiniZinc")), + str(Path("c:/Program Files/MiniZinc IDE (bundled)")), + str(Path("c:/Program Files (x86)/MiniZinc")), + str(Path("c:/Program Files (x86)/MiniZinc IDE (bundled)")), +] + +class Diversity: + """Solves the Instance to find diverse solutions. + + Finds diverse solutions to the given MiniZinc instance using the given solver + configuration. + + """ + + _executable: Path + div_vars = ["div_orig_objective", "div_curr_var_0", "div_prev_var_0", "div_orig_opt_objective"] + # _version: Optional[Tuple[int, ...]] = None + + def __init__(self, executable: Path): + self._executable = executable + if not self._executable.exists(): + raise ConfigurationError( + f"No MiniZinc data annotator executable was found at '{self._executable}'." + ) + + @classmethod + def find( + cls, path: Optional[List[str]] = None, name: str = "mzn_tool" + ) -> Optional["Driver"]: + """Finds MiniZinc Data Annotator Driver on default or specified path. + + Find driver will look for the MiniZinc Data Annotator executable to + create a Driver for MiniZinc Python. If no path is specified, then the + paths given by the environment variables appended by default locations will be tried. + + Args: + path: List of locations to search. + name: Name of the executable. + + Returns: + Optional[Driver]: Returns a Driver object when found or None. + """ + + if path is None: + path = os.environ.get("PATH", "").split(os.pathsep) + # Add default MiniZinc locations to the path + if platform.system() == "Darwin": + path.extend(MAC_LOCATIONS) + elif platform.system() == "Windows": + path.extend(WIN_LOCATIONS) + + # Try to locate the MiniZinc executable + executable = shutil.which(name, path=os.pathsep.join(path)) + if executable is not None: + return cls(Path(executable)) + raise ConfigurationError( + f"No MiniZinc data annotator executable was found at '{path}'." + ) + return None + + def run( + self, + mzn_files: List[Path], + backend_solver: str, + solver_div: minizinc.Solver, + total_diverse_solutions: Optional[int] = None, + reference_solution: Optional[Dict] = None, + optimise_diverse_sol: Optional[bool] = True) -> Iterator[Dict]: + + verbose = False # if enabled, outputs the progress + path_tool = self._executable + + str_div_mzn = "out.mzn" + str_div_json = "out.json" + + path_div_mzn = Path(str_div_mzn) + path_div_json = Path(str_div_json) + path_div_sol_json = Path("div_sols.json") + + # Do not change the order of the arguments 'inline-includes', 'remove-items:output', 'remove-litter' and 'get-diversity-anns' + tool_run_cmd: List[Union[str, Path]] = [path_tool] + + tool_run_cmd.extend(mzn_files) + tool_run_cmd.extend([ + "inline-includes", + "remove-items:output", + "remove-anns:mzn_expression_name", + "remove-litter", + "get-diversity-anns", + f"out:{str_div_mzn}", + f"json_out:{str_div_json}", + ]) + + # Extract the diversity annotations. + subprocess.run(tool_run_cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + + assert path_div_mzn.exists() + assert path_div_json.exists() + + # Load the base model. + str_model = path_div_mzn.read_text() + div_annots = json.loads(path_div_json.read_text())["get-diversity-annotations"] + + # Objective annotations. + obj_annots = div_annots["objective"] + variables = div_annots["vars"] + + assert len(variables) > 0, "Distance measure not specified" + + base_m = minizinc.Model() + base_m.add_string(str_model) + + inst = minizinc.Instance(solver_div, base_m) + res: Result = None + + # Place holder for max gap. + max_gap = None + + # Place holder for prev solutions + prev_solutions = None + + # Number of total diverse solutions - If not provided use the count provided in the MiniZinc model + div_num = int(div_annots["k"]) if total_diverse_solutions is None else total_diverse_solutions + # Increase the solution count by one if a reference solution is provided + if reference_solution: + div_num += 1 + + for i in range(1, div_num+1): + + with inst.branch() as child: + + if i==1: + # Add constraints to the model that sets the decision variables to the reference solution, if provided + if reference_solution: + solution_obj = asdict(reference_solution.solution) + for key in solution_obj: + if key not in ['objective','_checker']: # ignore keys not found in the original model + child.add_string(f'constraint array1d({key}) = [{", ".join(np.array(solution_obj[key]).flatten().astype(str))}];\n') + + # We will extend the annotated model with the objective and vars. + child.add_string(add_diversity_to_opt_model(obj_annots, variables)) + + # Solve original model to optimality. + if verbose: + model_type = "opt" if obj_annots["sense"] != "0" else "sat" + print(f"[Sol 1] Solving the original ({model_type}) model to get a solution") + # inst = minizinc.Instance(solver_div, base_m) + res: Result = child.solve() + + # Ensure that the solution exists. + assert res.solution is not None + + if reference_solution is None: + yield res + + # Calculate max gap. + max_gap = ( + (1 - int(obj_annots["sense"]) * float(div_annots["gap"])) + * float(res["div_orig_opt_objective"]) + if obj_annots["sense"] != "0" + else 0 + ) + + # Store current solution as previous solution + prev_solutions = asdict(res.solution) + + else: + + if verbose: + print(f"[Sol {_+1}] Generating diverse solution {_}"+(" (optimal)" if optimise_diverse_sol else "")) + + # We will extend the annotated model with the objective and vars. + child.add_string(add_diversity_to_div_model(variables, obj_annots["sense"], max_gap, prev_solutions)) + + # Solve div model to get a diverse solution. + res = child.solve() + + # Ensure that the solution exists. + assert res.solution is not None + + # Solution as dictionary + sol_div = asdict(res.solution) + + # Solve diverse solution to optimality after fixing the diversity vars to the obtained solution + if optimise_diverse_sol: + + # COMMENDTED OUT FOR NOW: Merge the solution values. + # sol_dist = dict() + # for var in variables: + # distvarname = "dist_"+var["name"] + # sol_dist[distvarname] = (sol_div[distvarname]) + + # Solve opt model after fixing the diversity vars to the obtained solution + child_opt = minizinc.Instance(solver_div, base_m) + child_opt.add_string(add_diversity_to_opt_model(obj_annots, variables, sol_div)) + + # Solve the model + res = child_opt.solve() + + # Ensure that the solution exists. + assert res.solution is not None + + # COMMENDTED OUT FOR NOW: Add distance to previous solutions + # sol_opt = asdict(res.solution) + # sol_opt["distance_to_prev_vars"] = sol_dist + + yield res + + # Store current solution as previous solution + curr_solution = asdict(res.solution) + # Add the current solution to prev solution container + for var in variables: + prev_solutions[var["prev_name"]].append(curr_solution[var["name"]]) + +def add_diversity_to_opt_model(obj_annots, vars, sol_fix = None): + opt_model = "" + + for var in vars: + # Current and previous variables + varname = var["name"] + varprevname = var["prev_name"] + + # Add the 'previous solution variables' + opt_model += f'{varprevname} = [];\n' + + # Fix the solution to given once + if sol_fix is not None: + opt_model += f'constraint {varname} == {list(sol_fix[varname])};\n' + + # Add the optimal objective. + if obj_annots["sense"] != "0": + obj_type = obj_annots["type"] + opt_model += f"{obj_type}: div_orig_opt_objective :: output;\n" + opt_model += f'constraint div_orig_opt_objective == {obj_annots["name"]};\n' + if obj_annots["sense"] == "-1": + opt_model += f'solve minimize {obj_annots["name"]};\n' + else: + opt_model += f'solve maximize {obj_annots["name"]};\n' + else: + opt_model += f"solve satisfy;\n" + + return opt_model + +def add_diversity_to_div_model(vars, obj_sense, gap, sols): + opt_model = "" + + # Indices i,j,k,... + indices = [chr(ord("i") + x) for x in range(15)] + + # Add the 'previous solution variables' + for var in vars: + + # The array type ('array [1..5] of ...') and variable data type ('var float'). + vartype = var["type"] + vardtype = vartype[vartype.rindex("var") :] + + # Current and previous variables + varname = var["name"] + varprevname = var["prev_name"] + varprevisfloat = "float" in var["prev_type"] + + distfun = var["distance_function"] + prevsols = np.array(sols[varprevname] + [sols[varname]]) + prevsol = ( + np.round(prevsols, 6) + if varprevisfloat + else prevsols + ) # float values are rounded to six decimal places to avoid infeasibility due to decimal errors. + + # Re-derive the domain of the solution from the return variable. + domains = [f"1..{x}" for x in prevsol.shape] + domain = ", ".join(domains) + d = len(domains) + + # Derive the indices for the array reconstruction. + subindices = ",".join(indices[: (len(domains) - 1)]) + ranger = ", ".join( + [ + f"{idx} in {subdomain}" + for idx, subdomain in zip(subindices, domains[1:]) + ] + ) + + # Add the previous solutions to the model code. + opt_model += f"{varprevname} = array{d}d({domain}, {list(prevsol.flat)});\n" + + # Add the diversity distance measurement to the model code. + varprevtype = "float" if "float" in var["prev_type"] else "int" + opt_model += ( + f"array [{domains[0]}] of var {varprevtype}: dist_{varname} :: output;\n" + f"constraint (forall (sol in {domains[0]}) (dist_{varname}[sol] == {distfun}({varname}, [{varprevname}[sol,{subindices}] | {ranger}])));\n" + ) + + # Add the bound on the objective. + if obj_sense == "-1": + opt_model += f"constraint div_orig_objective <= {gap};\n" + elif obj_sense == "1": + opt_model += f"constraint div_orig_objective >= {gap};\n" + + # Add new objective: maximize diversity. + dist_sum = "+".join([f'sum(dist_{var["name"]})' for var in vars]) + opt_model += f"solve maximize {dist_sum};\n" + + return opt_model diff --git a/src/minizinc/instance.py b/src/minizinc/instance.py index 3fc5df2..3c92496 100644 --- a/src/minizinc/instance.py +++ b/src/minizinc/instance.py @@ -252,6 +252,43 @@ async def solve_async( solution = result.solution return Result(status, solution, statistics) + def diverse_solutions( + self, + num_diverse_solutions: Optional[int] = None, + reference_solution: Optional[Result] = None + # timeout: Optional[timedelta] = None, + # nr_solutions: Optional[int] = None, + # processes: Optional[int] = None, + # random_seed: Optional[int] = None, + # free_search: bool = False, + # optimisation_level: Optional[int] = None, + # verbose: bool = False, + # debug_output: Optional[Path] = None, + # **kwargs, + ) -> Iterator[Dict]: + """Solves the Instance to find diverse solutions using its given solver configuration. + + Finds diverse solutions to the given MiniZinc instance using the given solver + configuration. Every diverse solution is yielded one at a + time. If a reference solution is provided the diverse solutions are generated + around it. For more information regarding this methods and its + arguments, see the documentation of :func:`~MiniZinc.Instance.diverse_solutions`. + + Yields: + Result: (TODO) + A Result object containing the current solving status, values + assigned, and statistical information. + + """ + + # Loads diverse solution generator if MiniZinc Data Annotator is present + div_sols = minizinc.Diversity.find() + + with self.files() as files, self._solver.configuration() as solver: + # assert self.output_type is not None + for sol in div_sols.run(files, solver, self._solver, num_diverse_solutions, reference_solution): + yield sol + async def solutions( self, timeout: Optional[timedelta] = None, From 8ffbb86d0a6988a48c46913679994d3cf1917dea Mon Sep 17 00:00:00 2001 From: "Jip J. Dekker" Date: Wed, 9 Aug 2023 17:01:53 +1000 Subject: [PATCH 02/15] Fix the formatting and types --- src/minizinc/__init__.py | 1 - src/minizinc/diversity.py | 215 +++++++++++++++++++------------------- src/minizinc/driver.py | 2 + src/minizinc/instance.py | 12 ++- 4 files changed, 118 insertions(+), 112 deletions(-) diff --git a/src/minizinc/__init__.py b/src/minizinc/__init__.py index 0790958..ab7c1c7 100644 --- a/src/minizinc/__init__.py +++ b/src/minizinc/__init__.py @@ -12,7 +12,6 @@ from .result import Result, Status from .solver import Solver from .types import AnonEnum, ConstrEnum -from .diversity import Diversity __version__ = "0.9.1" diff --git a/src/minizinc/diversity.py b/src/minizinc/diversity.py index 97c02d5..79ed925 100644 --- a/src/minizinc/diversity.py +++ b/src/minizinc/diversity.py @@ -1,54 +1,40 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +import json import os import platform -# import re import shutil -import json import subprocess -import argparse +from dataclasses import asdict, is_dataclass from pathlib import Path - -from typing import ( - Dict, - Iterator, - List, - Optional, - Union, -) - -import minizinc +from typing import Any, Dict, Iterator, List, Optional, Union import numpy as np -from dataclasses import asdict - +from .driver import MAC_LOCATIONS, WIN_LOCATIONS from .error import ConfigurationError +from .instance import Instance +from .model import Model +from .result import Result +from .solver import Solver -MAC_LOCATIONS = [ - str(Path("/Applications/MiniZincIDE.app/Contents/Resources")), - str(Path("~/Applications/MiniZincIDE.app/Contents/Resources").expanduser()), -] -#: Default locations on Windows where the MiniZinc packaged release would be installed -WIN_LOCATIONS = [ - str(Path("c:/Program Files/MiniZinc")), - str(Path("c:/Program Files/MiniZinc IDE (bundled)")), - str(Path("c:/Program Files (x86)/MiniZinc")), - str(Path("c:/Program Files (x86)/MiniZinc IDE (bundled)")), -] - -class Diversity: - """Solves the Instance to find diverse solutions. - - Finds diverse solutions to the given MiniZinc instance using the given solver + +class MznAnalyse: + """Python interface to the mzn-analyse executable + + This tool is used to retrieve information about MiniZinc instance. This is used, for example, to diverse solutions to the given MiniZinc instance using the given solver configuration. """ _executable: Path - div_vars = ["div_orig_objective", "div_curr_var_0", "div_prev_var_0", "div_orig_opt_objective"] - # _version: Optional[Tuple[int, ...]] = None + div_vars = [ + "div_orig_objective", + "div_curr_var_0", + "div_prev_var_0", + "div_orig_opt_objective", + ] def __init__(self, executable: Path): self._executable = executable @@ -59,12 +45,12 @@ def __init__(self, executable: Path): @classmethod def find( - cls, path: Optional[List[str]] = None, name: str = "mzn_tool" - ) -> Optional["Driver"]: + cls, path: Optional[List[str]] = None, name: str = "mzn-analyse" + ) -> Optional["MznAnalyse"]: """Finds MiniZinc Data Annotator Driver on default or specified path. - Find driver will look for the MiniZinc Data Annotator executable to - create a Driver for MiniZinc Python. If no path is specified, then the + Find driver will look for the MiniZinc Data Annotator executable to + create a Driver for MiniZinc Python. If no path is specified, then the paths given by the environment variables appended by default locations will be tried. Args: @@ -94,14 +80,14 @@ def find( def run( self, - mzn_files: List[Path], + mzn_files: List[Path], backend_solver: str, - solver_div: minizinc.Solver, + solver_div: Solver, total_diverse_solutions: Optional[int] = None, - reference_solution: Optional[Dict] = None, - optimise_diverse_sol: Optional[bool] = True) -> Iterator[Dict]: - - verbose = False # if enabled, outputs the progress + reference_solution: Optional[Union[Result, Dict[str, Any]]] = None, + optimise_diverse_sol: Optional[bool] = True, + ) -> Iterator[Result]: + verbose = False # if enabled, outputs the progress path_tool = self._executable str_div_mzn = "out.mzn" @@ -109,21 +95,22 @@ def run( path_div_mzn = Path(str_div_mzn) path_div_json = Path(str_div_json) - path_div_sol_json = Path("div_sols.json") # Do not change the order of the arguments 'inline-includes', 'remove-items:output', 'remove-litter' and 'get-diversity-anns' tool_run_cmd: List[Union[str, Path]] = [path_tool] - + tool_run_cmd.extend(mzn_files) - tool_run_cmd.extend([ - "inline-includes", - "remove-items:output", - "remove-anns:mzn_expression_name", - "remove-litter", - "get-diversity-anns", - f"out:{str_div_mzn}", - f"json_out:{str_div_json}", - ]) + tool_run_cmd.extend( + [ + "inline-includes", + "remove-items:output", + "remove-anns:mzn_expression_name", + "remove-litter", + "get-diversity-anns", + f"out:{str_div_mzn}", + f"json_out:{str_div_json}", + ] + ) # Extract the diversity annotations. subprocess.run(tool_run_cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE) @@ -141,83 +128,102 @@ def run( assert len(variables) > 0, "Distance measure not specified" - base_m = minizinc.Model() + base_m = Model() base_m.add_string(str_model) - inst = minizinc.Instance(solver_div, base_m) - res: Result = None + inst = Instance(solver_div, base_m) # Place holder for max gap. max_gap = None - + # Place holder for prev solutions prev_solutions = None - - # Number of total diverse solutions - If not provided use the count provided in the MiniZinc model - div_num = int(div_annots["k"]) if total_diverse_solutions is None else total_diverse_solutions + + # Number of total diverse solutions - If not provided use the count provided in the MiniZinc model + div_num = ( + int(div_annots["k"]) + if total_diverse_solutions is None + else total_diverse_solutions + ) # Increase the solution count by one if a reference solution is provided if reference_solution: div_num += 1 - for i in range(1, div_num+1): - + for i in range(1, div_num + 1): with inst.branch() as child: - - if i==1: + if i == 1: # Add constraints to the model that sets the decision variables to the reference solution, if provided if reference_solution: - solution_obj = asdict(reference_solution.solution) + if isinstance(reference_solution, Result) and is_dataclass( + reference_solution.solution + ): + solution_obj = asdict(reference_solution.solution) + else: + assert isinstance(reference_solution, dict) + solution_obj = reference_solution for key in solution_obj: - if key not in ['objective','_checker']: # ignore keys not found in the original model - child.add_string(f'constraint array1d({key}) = [{", ".join(np.array(solution_obj[key]).flatten().astype(str))}];\n') - + if key not in [ + "objective", + "_checker", + ]: # ignore keys not found in the original model + child.add_string( + f'constraint array1d({key}) = [{", ".join(np.array(solution_obj[key]).flatten().astype(str))}];\n' + ) + # We will extend the annotated model with the objective and vars. child.add_string(add_diversity_to_opt_model(obj_annots, variables)) - + # Solve original model to optimality. if verbose: model_type = "opt" if obj_annots["sense"] != "0" else "sat" - print(f"[Sol 1] Solving the original ({model_type}) model to get a solution") + print( + f"[Sol 1] Solving the original ({model_type}) model to get a solution" + ) # inst = minizinc.Instance(solver_div, base_m) res: Result = child.solve() - + # Ensure that the solution exists. assert res.solution is not None - + if reference_solution is None: yield res - + # Calculate max gap. max_gap = ( - (1 - int(obj_annots["sense"]) * float(div_annots["gap"])) - * float(res["div_orig_opt_objective"]) - if obj_annots["sense"] != "0" - else 0 + (1 - int(obj_annots["sense"]) * float(div_annots["gap"])) + * float(res["div_orig_opt_objective"]) + if obj_annots["sense"] != "0" + else 0 ) - + # Store current solution as previous solution prev_solutions = asdict(res.solution) else: - if verbose: - print(f"[Sol {_+1}] Generating diverse solution {_}"+(" (optimal)" if optimise_diverse_sol else "")) - + print( + f"[Sol {i+1}] Generating diverse solution {i}" + + (" (optimal)" if optimise_diverse_sol else "") + ) + # We will extend the annotated model with the objective and vars. - child.add_string(add_diversity_to_div_model(variables, obj_annots["sense"], max_gap, prev_solutions)) + child.add_string( + add_diversity_to_div_model( + variables, obj_annots["sense"], max_gap, prev_solutions + ) + ) # Solve div model to get a diverse solution. res = child.solve() - + # Ensure that the solution exists. assert res.solution is not None - + # Solution as dictionary sol_div = asdict(res.solution) # Solve diverse solution to optimality after fixing the diversity vars to the obtained solution if optimise_diverse_sol: - # COMMENDTED OUT FOR NOW: Merge the solution values. # sol_dist = dict() # for var in variables: @@ -225,28 +231,32 @@ def run( # sol_dist[distvarname] = (sol_div[distvarname]) # Solve opt model after fixing the diversity vars to the obtained solution - child_opt = minizinc.Instance(solver_div, base_m) - child_opt.add_string(add_diversity_to_opt_model(obj_annots, variables, sol_div)) + child_opt = Instance(solver_div, base_m) + child_opt.add_string( + add_diversity_to_opt_model(obj_annots, variables, sol_div) + ) # Solve the model res = child_opt.solve() - + # Ensure that the solution exists. assert res.solution is not None - + # COMMENDTED OUT FOR NOW: Add distance to previous solutions # sol_opt = asdict(res.solution) # sol_opt["distance_to_prev_vars"] = sol_dist yield res - + # Store current solution as previous solution curr_solution = asdict(res.solution) # Add the current solution to prev solution container + assert prev_solutions is not None for var in variables: prev_solutions[var["prev_name"]].append(curr_solution[var["name"]]) -def add_diversity_to_opt_model(obj_annots, vars, sol_fix = None): + +def add_diversity_to_opt_model(obj_annots, vars, sol_fix=None): opt_model = "" for var in vars: @@ -255,11 +265,11 @@ def add_diversity_to_opt_model(obj_annots, vars, sol_fix = None): varprevname = var["prev_name"] # Add the 'previous solution variables' - opt_model += f'{varprevname} = [];\n' + opt_model += f"{varprevname} = [];\n" # Fix the solution to given once if sol_fix is not None: - opt_model += f'constraint {varname} == {list(sol_fix[varname])};\n' + opt_model += f"constraint {varname} == {list(sol_fix[varname])};\n" # Add the optimal objective. if obj_annots["sense"] != "0": @@ -271,10 +281,11 @@ def add_diversity_to_opt_model(obj_annots, vars, sol_fix = None): else: opt_model += f'solve maximize {obj_annots["name"]};\n' else: - opt_model += f"solve satisfy;\n" + opt_model += "solve satisfy;\n" return opt_model + def add_diversity_to_div_model(vars, obj_sense, gap, sols): opt_model = "" @@ -283,11 +294,6 @@ def add_diversity_to_div_model(vars, obj_sense, gap, sols): # Add the 'previous solution variables' for var in vars: - - # The array type ('array [1..5] of ...') and variable data type ('var float'). - vartype = var["type"] - vardtype = vartype[vartype.rindex("var") :] - # Current and previous variables varname = var["name"] varprevname = var["prev_name"] @@ -296,9 +302,7 @@ def add_diversity_to_div_model(vars, obj_sense, gap, sols): distfun = var["distance_function"] prevsols = np.array(sols[varprevname] + [sols[varname]]) prevsol = ( - np.round(prevsols, 6) - if varprevisfloat - else prevsols + np.round(prevsols, 6) if varprevisfloat else prevsols ) # float values are rounded to six decimal places to avoid infeasibility due to decimal errors. # Re-derive the domain of the solution from the return variable. @@ -309,10 +313,7 @@ def add_diversity_to_div_model(vars, obj_sense, gap, sols): # Derive the indices for the array reconstruction. subindices = ",".join(indices[: (len(domains) - 1)]) ranger = ", ".join( - [ - f"{idx} in {subdomain}" - for idx, subdomain in zip(subindices, domains[1:]) - ] + [f"{idx} in {subdomain}" for idx, subdomain in zip(subindices, domains[1:])] ) # Add the previous solutions to the model code. diff --git a/src/minizinc/driver.py b/src/minizinc/driver.py index cb3523e..2a1f4b8 100644 --- a/src/minizinc/driver.py +++ b/src/minizinc/driver.py @@ -26,7 +26,9 @@ #: Default locations on MacOS where the MiniZinc packaged release would be installed MAC_LOCATIONS = [ str(Path("/Applications/MiniZincIDE.app/Contents/Resources")), + str(Path("/Applications/MiniZincIDE.app/Contents/Resources/bin")), str(Path("~/Applications/MiniZincIDE.app/Contents/Resources").expanduser()), + str(Path("~/Applications/MiniZincIDE.app/Contents/Resources/bin").expanduser()), ] #: Default locations on Windows where the MiniZinc packaged release would be installed WIN_LOCATIONS = [ diff --git a/src/minizinc/instance.py b/src/minizinc/instance.py index 3c92496..822d423 100644 --- a/src/minizinc/instance.py +++ b/src/minizinc/instance.py @@ -30,6 +30,7 @@ import minizinc +from .diversity import MznAnalyse from .driver import Driver from .error import MiniZincError, parse_error from .json import ( @@ -265,12 +266,12 @@ def diverse_solutions( # verbose: bool = False, # debug_output: Optional[Path] = None, # **kwargs, - ) -> Iterator[Dict]: + ) -> Iterator[Result]: """Solves the Instance to find diverse solutions using its given solver configuration. Finds diverse solutions to the given MiniZinc instance using the given solver configuration. Every diverse solution is yielded one at a - time. If a reference solution is provided the diverse solutions are generated + time. If a reference solution is provided the diverse solutions are generated around it. For more information regarding this methods and its arguments, see the documentation of :func:`~MiniZinc.Instance.diverse_solutions`. @@ -282,11 +283,14 @@ def diverse_solutions( """ # Loads diverse solution generator if MiniZinc Data Annotator is present - div_sols = minizinc.Diversity.find() + div_sols = MznAnalyse.find() + assert div_sols is not None with self.files() as files, self._solver.configuration() as solver: # assert self.output_type is not None - for sol in div_sols.run(files, solver, self._solver, num_diverse_solutions, reference_solution): + for sol in div_sols.run( + files, solver, self._solver, num_diverse_solutions, reference_solution + ): yield sol async def solutions( From ea3e392270e7f125102ac55cab97c40ec9af6383 Mon Sep 17 00:00:00 2001 From: "Jip J. Dekker" Date: Wed, 9 Aug 2023 17:08:09 +1000 Subject: [PATCH 03/15] Make MznAnalyse.find consistent with Driver.find --- src/minizinc/diversity.py | 29 ++++++++++------------------- src/minizinc/instance.py | 17 ++++++----------- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/minizinc/diversity.py b/src/minizinc/diversity.py index 79ed925..c6e487d 100644 --- a/src/minizinc/diversity.py +++ b/src/minizinc/diversity.py @@ -23,18 +23,12 @@ class MznAnalyse: """Python interface to the mzn-analyse executable - This tool is used to retrieve information about MiniZinc instance. This is used, for example, to diverse solutions to the given MiniZinc instance using the given solver - configuration. - + This tool is used to retrieve information about or transform a MiniZinc + instance. This is used, for example, to diverse solutions to the given + MiniZinc instance using the given solver configuration. """ _executable: Path - div_vars = [ - "div_orig_objective", - "div_curr_var_0", - "div_prev_var_0", - "div_orig_opt_objective", - ] def __init__(self, executable: Path): self._executable = executable @@ -47,18 +41,18 @@ def __init__(self, executable: Path): def find( cls, path: Optional[List[str]] = None, name: str = "mzn-analyse" ) -> Optional["MznAnalyse"]: - """Finds MiniZinc Data Annotator Driver on default or specified path. + """Finds the mzn-analyse executable on default or specified path. - Find driver will look for the MiniZinc Data Annotator executable to - create a Driver for MiniZinc Python. If no path is specified, then the - paths given by the environment variables appended by default locations will be tried. + The find method will look for the mzn-analyse executable to create an + interface for MiniZinc Python. If no path is specified, then the paths + given by the environment variables appended by default locations will be + tried. Args: - path: List of locations to search. - name: Name of the executable. + path: List of locations to search. name: Name of the executable. Returns: - Optional[Driver]: Returns a Driver object when found or None. + Optional[MznAnalyse]: Returns a MznAnalyse object when found or None. """ if path is None: @@ -73,9 +67,6 @@ def find( executable = shutil.which(name, path=os.pathsep.join(path)) if executable is not None: return cls(Path(executable)) - raise ConfigurationError( - f"No MiniZinc data annotator executable was found at '{path}'." - ) return None def run( diff --git a/src/minizinc/instance.py b/src/minizinc/instance.py index 822d423..c59e6e2 100644 --- a/src/minizinc/instance.py +++ b/src/minizinc/instance.py @@ -256,15 +256,8 @@ async def solve_async( def diverse_solutions( self, num_diverse_solutions: Optional[int] = None, - reference_solution: Optional[Result] = None - # timeout: Optional[timedelta] = None, - # nr_solutions: Optional[int] = None, - # processes: Optional[int] = None, - # random_seed: Optional[int] = None, - # free_search: bool = False, - # optimisation_level: Optional[int] = None, - # verbose: bool = False, - # debug_output: Optional[Path] = None, + reference_solution: Optional[Result] = None, + mzn_analyse: Optional[MznAnalyse] = None, # **kwargs, ) -> Iterator[Result]: """Solves the Instance to find diverse solutions using its given solver configuration. @@ -283,8 +276,10 @@ def diverse_solutions( """ # Loads diverse solution generator if MiniZinc Data Annotator is present - div_sols = MznAnalyse.find() - assert div_sols is not None + if mzn_analyse is None: + mzn_analyse = MznAnalyse.find() + if mzn_analyse is None: + raise ConfigurationError("mzn-analyse executable could not be located") with self.files() as files, self._solver.configuration() as solver: # assert self.output_type is not None From ee3e3dc7f80178689a18385191d89ea6a9b9c9a1 Mon Sep 17 00:00:00 2001 From: "Jip J. Dekker" Date: Thu, 10 Aug 2023 13:24:33 +1000 Subject: [PATCH 04/15] Simplify the assignment of previous solutions --- src/minizinc/diversity.py | 17 ++++++----------- src/minizinc/helpers.py | 2 +- src/minizinc/instance.py | 4 ++-- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/minizinc/diversity.py b/src/minizinc/diversity.py index c6e487d..b58b660 100644 --- a/src/minizinc/diversity.py +++ b/src/minizinc/diversity.py @@ -13,9 +13,6 @@ import numpy as np from .driver import MAC_LOCATIONS, WIN_LOCATIONS -from .error import ConfigurationError -from .instance import Instance -from .model import Model from .result import Result from .solver import Solver @@ -78,6 +75,9 @@ def run( reference_solution: Optional[Union[Result, Dict[str, Any]]] = None, optimise_diverse_sol: Optional[bool] = True, ) -> Iterator[Result]: + from .instance import Instance + from .model import Model + verbose = False # if enabled, outputs the progress path_tool = self._executable @@ -152,14 +152,9 @@ def run( else: assert isinstance(reference_solution, dict) solution_obj = reference_solution - for key in solution_obj: - if key not in [ - "objective", - "_checker", - ]: # ignore keys not found in the original model - child.add_string( - f'constraint array1d({key}) = [{", ".join(np.array(solution_obj[key]).flatten().astype(str))}];\n' - ) + for k, v in solution_obj.items(): + if k not in ("objective", "_output_item", "_checker"): + child[k] = v # We will extend the annotated model with the objective and vars. child.add_string(add_diversity_to_opt_model(obj_annots, variables)) diff --git a/src/minizinc/helpers.py b/src/minizinc/helpers.py index 40b70ea..4e028d1 100644 --- a/src/minizinc/helpers.py +++ b/src/minizinc/helpers.py @@ -88,7 +88,7 @@ def check_solution( solution = asdict(solution) for k, v in solution.items(): - if k not in ("objective", "__output_item"): + if k not in ("objective", "_output_item", "_checker"): instance[k] = v try: check = instance.solve(timeout=timedelta(seconds=5)) diff --git a/src/minizinc/instance.py b/src/minizinc/instance.py index c59e6e2..db355f8 100644 --- a/src/minizinc/instance.py +++ b/src/minizinc/instance.py @@ -32,7 +32,7 @@ from .diversity import MznAnalyse from .driver import Driver -from .error import MiniZincError, parse_error +from .error import MiniZincError, parse_error, ConfigurationError from .json import ( MZNJSONDecoder, MZNJSONEncoder, @@ -283,7 +283,7 @@ def diverse_solutions( with self.files() as files, self._solver.configuration() as solver: # assert self.output_type is not None - for sol in div_sols.run( + for sol in mzn_analyse.run( files, solver, self._solver, num_diverse_solutions, reference_solution ): yield sol From 2e4f0a0ba0a23c7bff53a3e00bb40239daa9793a Mon Sep 17 00:00:00 2001 From: "Jip J. Dekker" Date: Fri, 11 Aug 2023 10:40:41 +1000 Subject: [PATCH 05/15] Remove the requirement of numpy --- src/minizinc/diversity.py | 64 +++++++++++++++++++-------------------- src/minizinc/instance.py | 2 +- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/minizinc/diversity.py b/src/minizinc/diversity.py index b58b660..c729115 100644 --- a/src/minizinc/diversity.py +++ b/src/minizinc/diversity.py @@ -10,8 +10,6 @@ from pathlib import Path from typing import Any, Dict, Iterator, List, Optional, Union -import numpy as np - from .driver import MAC_LOCATIONS, WIN_LOCATIONS from .result import Result from .solver import Solver @@ -193,10 +191,8 @@ def run( ) # We will extend the annotated model with the objective and vars. - child.add_string( - add_diversity_to_div_model( - variables, obj_annots["sense"], max_gap, prev_solutions - ) + child = add_diversity_to_div_model( + child, variables, obj_annots["sense"], max_gap, prev_solutions ) # Solve div model to get a diverse solution. @@ -272,12 +268,7 @@ def add_diversity_to_opt_model(obj_annots, vars, sol_fix=None): return opt_model -def add_diversity_to_div_model(vars, obj_sense, gap, sols): - opt_model = "" - - # Indices i,j,k,... - indices = [chr(ord("i") + x) for x in range(15)] - +def add_diversity_to_div_model(inst, vars, obj_sense, gap, sols): # Add the 'previous solution variables' for var in vars: # Current and previous variables @@ -286,40 +277,47 @@ def add_diversity_to_div_model(vars, obj_sense, gap, sols): varprevisfloat = "float" in var["prev_type"] distfun = var["distance_function"] - prevsols = np.array(sols[varprevname] + [sols[varname]]) + prevsols = sols[varprevname] + [sols[varname]] prevsol = ( - np.round(prevsols, 6) if varprevisfloat else prevsols + __round_elements(prevsols, 6) if True else prevsols ) # float values are rounded to six decimal places to avoid infeasibility due to decimal errors. - # Re-derive the domain of the solution from the return variable. - domains = [f"1..{x}" for x in prevsol.shape] - domain = ", ".join(domains) - d = len(domains) - - # Derive the indices for the array reconstruction. - subindices = ",".join(indices[: (len(domains) - 1)]) - ranger = ", ".join( - [f"{idx} in {subdomain}" for idx, subdomain in zip(subindices, domains[1:])] - ) - # Add the previous solutions to the model code. - opt_model += f"{varprevname} = array{d}d({domain}, {list(prevsol.flat)});\n" + inst[varprevname] = prevsol # Add the diversity distance measurement to the model code. + dim = __num_dim(prevsols) + dotdots = ", ".join([".." for _ in range(dim - 1)]) varprevtype = "float" if "float" in var["prev_type"] else "int" - opt_model += ( - f"array [{domains[0]}] of var {varprevtype}: dist_{varname} :: output;\n" - f"constraint (forall (sol in {domains[0]}) (dist_{varname}[sol] == {distfun}({varname}, [{varprevname}[sol,{subindices}] | {ranger}])));\n" + inst.add_string( + f"array [1..{len(prevsol)}] of var {varprevtype}: dist_{varname} :: output = [{distfun}({varname}, {varprevname}[sol,{dotdots}]) | sol in 1..{len(prevsol)}];\n" ) # Add the bound on the objective. if obj_sense == "-1": - opt_model += f"constraint div_orig_objective <= {gap};\n" + inst.add_string(f"constraint div_orig_objective <= {gap};\n") elif obj_sense == "1": - opt_model += f"constraint div_orig_objective >= {gap};\n" + inst.add_string(f"constraint div_orig_objective >= {gap};\n") # Add new objective: maximize diversity. dist_sum = "+".join([f'sum(dist_{var["name"]})' for var in vars]) - opt_model += f"solve maximize {dist_sum};\n" + inst.add_string(f"solve maximize {dist_sum};\n") - return opt_model + return inst + + +def __num_dim(x: List) -> int: + i = 1 + while isinstance(x[0], list): + i += 1 + x = x[0] + return i + + +def __round_elements(x: List, p: int) -> List: + for i in range(len(x)): + if isinstance(x[i], list): + x[i] = __round_elements(x[i], p) + elif isinstance(x[i], float): + x[i] = round(x[i], p) + return x diff --git a/src/minizinc/instance.py b/src/minizinc/instance.py index db355f8..37e29d1 100644 --- a/src/minizinc/instance.py +++ b/src/minizinc/instance.py @@ -32,7 +32,7 @@ from .diversity import MznAnalyse from .driver import Driver -from .error import MiniZincError, parse_error, ConfigurationError +from .error import ConfigurationError, MiniZincError, parse_error from .json import ( MZNJSONDecoder, MZNJSONEncoder, From 4be791fa4fed31c9c10d562cddd9d2e979e539fe Mon Sep 17 00:00:00 2001 From: "Jip J. Dekker" Date: Fri, 11 Aug 2023 13:07:36 +1000 Subject: [PATCH 06/15] Remove unused argument MznAnalyse.run --- src/minizinc/diversity.py | 1 - src/minizinc/instance.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/minizinc/diversity.py b/src/minizinc/diversity.py index c729115..98dad36 100644 --- a/src/minizinc/diversity.py +++ b/src/minizinc/diversity.py @@ -67,7 +67,6 @@ def find( def run( self, mzn_files: List[Path], - backend_solver: str, solver_div: Solver, total_diverse_solutions: Optional[int] = None, reference_solution: Optional[Union[Result, Dict[str, Any]]] = None, diff --git a/src/minizinc/instance.py b/src/minizinc/instance.py index 37e29d1..3b2f1aa 100644 --- a/src/minizinc/instance.py +++ b/src/minizinc/instance.py @@ -281,10 +281,10 @@ def diverse_solutions( if mzn_analyse is None: raise ConfigurationError("mzn-analyse executable could not be located") - with self.files() as files, self._solver.configuration() as solver: + with self.files() as files: # assert self.output_type is not None for sol in mzn_analyse.run( - files, solver, self._solver, num_diverse_solutions, reference_solution + files, self._solver, num_diverse_solutions, reference_solution ): yield sol From b5eeaeb573a9b6df9ddd547dc5e336b6d5a747c9 Mon Sep 17 00:00:00 2001 From: "Jip J. Dekker" Date: Fri, 11 Aug 2023 14:37:58 +1000 Subject: [PATCH 07/15] Refactor code to be in more logical places --- src/minizinc/analyse.py | 80 ++++++++++ src/minizinc/diversity.py | 322 -------------------------------------- src/minizinc/helpers.py | 87 +++++++++- src/minizinc/instance.py | 167 +++++++++++++++++++- 4 files changed, 325 insertions(+), 331 deletions(-) create mode 100644 src/minizinc/analyse.py delete mode 100644 src/minizinc/diversity.py diff --git a/src/minizinc/analyse.py b/src/minizinc/analyse.py new file mode 100644 index 0000000..2bf8984 --- /dev/null +++ b/src/minizinc/analyse.py @@ -0,0 +1,80 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +import os +import platform +import shutil +import subprocess +from pathlib import Path +from typing import List, Optional, Union + +from .driver import MAC_LOCATIONS, WIN_LOCATIONS +from .error import ConfigurationError, MiniZincError + + +class MznAnalyse: + """Python interface to the mzn-analyse executable + + This tool is used to retrieve information about or transform a MiniZinc + instance. This is used, for example, to diverse solutions to the given + MiniZinc instance using the given solver configuration. + """ + + _executable: Path + + def __init__(self, executable: Path): + self._executable = executable + if not self._executable.exists(): + raise ConfigurationError( + f"No MiniZinc data annotator executable was found at '{self._executable}'." + ) + + @classmethod + def find( + cls, path: Optional[List[str]] = None, name: str = "mzn-analyse" + ) -> Optional["MznAnalyse"]: + """Finds the mzn-analyse executable on default or specified path. + + The find method will look for the mzn-analyse executable to create an + interface for MiniZinc Python. If no path is specified, then the paths + given by the environment variables appended by default locations will be + tried. + + Args: + path: List of locations to search. name: Name of the executable. + + Returns: + Optional[MznAnalyse]: Returns a MznAnalyse object when found or None. + """ + + if path is None: + path = os.environ.get("PATH", "").split(os.pathsep) + # Add default MiniZinc locations to the path + if platform.system() == "Darwin": + path.extend(MAC_LOCATIONS) + elif platform.system() == "Windows": + path.extend(WIN_LOCATIONS) + + # Try to locate the MiniZinc executable + executable = shutil.which(name, path=os.pathsep.join(path)) + if executable is not None: + return cls(Path(executable)) + return None + + def run( + self, + mzn_files: List[Path], + args: List[str], + ) -> None: + # Do not change the order of the arguments 'inline-includes', 'remove-items:output', 'remove-litter' and 'get-diversity-anns' + tool_run_cmd: List[Union[str, Path]] = [self._executable] + + tool_run_cmd.extend(mzn_files) + tool_run_cmd.extend(args) + + # Extract the diversity annotations. + proc = subprocess.run( + tool_run_cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE + ) + if proc.returncode != 0: + raise MiniZincError(message=str(proc.stderr)) diff --git a/src/minizinc/diversity.py b/src/minizinc/diversity.py deleted file mode 100644 index 98dad36..0000000 --- a/src/minizinc/diversity.py +++ /dev/null @@ -1,322 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -import json -import os -import platform -import shutil -import subprocess -from dataclasses import asdict, is_dataclass -from pathlib import Path -from typing import Any, Dict, Iterator, List, Optional, Union - -from .driver import MAC_LOCATIONS, WIN_LOCATIONS -from .result import Result -from .solver import Solver - - -class MznAnalyse: - """Python interface to the mzn-analyse executable - - This tool is used to retrieve information about or transform a MiniZinc - instance. This is used, for example, to diverse solutions to the given - MiniZinc instance using the given solver configuration. - """ - - _executable: Path - - def __init__(self, executable: Path): - self._executable = executable - if not self._executable.exists(): - raise ConfigurationError( - f"No MiniZinc data annotator executable was found at '{self._executable}'." - ) - - @classmethod - def find( - cls, path: Optional[List[str]] = None, name: str = "mzn-analyse" - ) -> Optional["MznAnalyse"]: - """Finds the mzn-analyse executable on default or specified path. - - The find method will look for the mzn-analyse executable to create an - interface for MiniZinc Python. If no path is specified, then the paths - given by the environment variables appended by default locations will be - tried. - - Args: - path: List of locations to search. name: Name of the executable. - - Returns: - Optional[MznAnalyse]: Returns a MznAnalyse object when found or None. - """ - - if path is None: - path = os.environ.get("PATH", "").split(os.pathsep) - # Add default MiniZinc locations to the path - if platform.system() == "Darwin": - path.extend(MAC_LOCATIONS) - elif platform.system() == "Windows": - path.extend(WIN_LOCATIONS) - - # Try to locate the MiniZinc executable - executable = shutil.which(name, path=os.pathsep.join(path)) - if executable is not None: - return cls(Path(executable)) - return None - - def run( - self, - mzn_files: List[Path], - solver_div: Solver, - total_diverse_solutions: Optional[int] = None, - reference_solution: Optional[Union[Result, Dict[str, Any]]] = None, - optimise_diverse_sol: Optional[bool] = True, - ) -> Iterator[Result]: - from .instance import Instance - from .model import Model - - verbose = False # if enabled, outputs the progress - path_tool = self._executable - - str_div_mzn = "out.mzn" - str_div_json = "out.json" - - path_div_mzn = Path(str_div_mzn) - path_div_json = Path(str_div_json) - - # Do not change the order of the arguments 'inline-includes', 'remove-items:output', 'remove-litter' and 'get-diversity-anns' - tool_run_cmd: List[Union[str, Path]] = [path_tool] - - tool_run_cmd.extend(mzn_files) - tool_run_cmd.extend( - [ - "inline-includes", - "remove-items:output", - "remove-anns:mzn_expression_name", - "remove-litter", - "get-diversity-anns", - f"out:{str_div_mzn}", - f"json_out:{str_div_json}", - ] - ) - - # Extract the diversity annotations. - subprocess.run(tool_run_cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE) - - assert path_div_mzn.exists() - assert path_div_json.exists() - - # Load the base model. - str_model = path_div_mzn.read_text() - div_annots = json.loads(path_div_json.read_text())["get-diversity-annotations"] - - # Objective annotations. - obj_annots = div_annots["objective"] - variables = div_annots["vars"] - - assert len(variables) > 0, "Distance measure not specified" - - base_m = Model() - base_m.add_string(str_model) - - inst = Instance(solver_div, base_m) - - # Place holder for max gap. - max_gap = None - - # Place holder for prev solutions - prev_solutions = None - - # Number of total diverse solutions - If not provided use the count provided in the MiniZinc model - div_num = ( - int(div_annots["k"]) - if total_diverse_solutions is None - else total_diverse_solutions - ) - # Increase the solution count by one if a reference solution is provided - if reference_solution: - div_num += 1 - - for i in range(1, div_num + 1): - with inst.branch() as child: - if i == 1: - # Add constraints to the model that sets the decision variables to the reference solution, if provided - if reference_solution: - if isinstance(reference_solution, Result) and is_dataclass( - reference_solution.solution - ): - solution_obj = asdict(reference_solution.solution) - else: - assert isinstance(reference_solution, dict) - solution_obj = reference_solution - for k, v in solution_obj.items(): - if k not in ("objective", "_output_item", "_checker"): - child[k] = v - - # We will extend the annotated model with the objective and vars. - child.add_string(add_diversity_to_opt_model(obj_annots, variables)) - - # Solve original model to optimality. - if verbose: - model_type = "opt" if obj_annots["sense"] != "0" else "sat" - print( - f"[Sol 1] Solving the original ({model_type}) model to get a solution" - ) - # inst = minizinc.Instance(solver_div, base_m) - res: Result = child.solve() - - # Ensure that the solution exists. - assert res.solution is not None - - if reference_solution is None: - yield res - - # Calculate max gap. - max_gap = ( - (1 - int(obj_annots["sense"]) * float(div_annots["gap"])) - * float(res["div_orig_opt_objective"]) - if obj_annots["sense"] != "0" - else 0 - ) - - # Store current solution as previous solution - prev_solutions = asdict(res.solution) - - else: - if verbose: - print( - f"[Sol {i+1}] Generating diverse solution {i}" - + (" (optimal)" if optimise_diverse_sol else "") - ) - - # We will extend the annotated model with the objective and vars. - child = add_diversity_to_div_model( - child, variables, obj_annots["sense"], max_gap, prev_solutions - ) - - # Solve div model to get a diverse solution. - res = child.solve() - - # Ensure that the solution exists. - assert res.solution is not None - - # Solution as dictionary - sol_div = asdict(res.solution) - - # Solve diverse solution to optimality after fixing the diversity vars to the obtained solution - if optimise_diverse_sol: - # COMMENDTED OUT FOR NOW: Merge the solution values. - # sol_dist = dict() - # for var in variables: - # distvarname = "dist_"+var["name"] - # sol_dist[distvarname] = (sol_div[distvarname]) - - # Solve opt model after fixing the diversity vars to the obtained solution - child_opt = Instance(solver_div, base_m) - child_opt.add_string( - add_diversity_to_opt_model(obj_annots, variables, sol_div) - ) - - # Solve the model - res = child_opt.solve() - - # Ensure that the solution exists. - assert res.solution is not None - - # COMMENDTED OUT FOR NOW: Add distance to previous solutions - # sol_opt = asdict(res.solution) - # sol_opt["distance_to_prev_vars"] = sol_dist - - yield res - - # Store current solution as previous solution - curr_solution = asdict(res.solution) - # Add the current solution to prev solution container - assert prev_solutions is not None - for var in variables: - prev_solutions[var["prev_name"]].append(curr_solution[var["name"]]) - - -def add_diversity_to_opt_model(obj_annots, vars, sol_fix=None): - opt_model = "" - - for var in vars: - # Current and previous variables - varname = var["name"] - varprevname = var["prev_name"] - - # Add the 'previous solution variables' - opt_model += f"{varprevname} = [];\n" - - # Fix the solution to given once - if sol_fix is not None: - opt_model += f"constraint {varname} == {list(sol_fix[varname])};\n" - - # Add the optimal objective. - if obj_annots["sense"] != "0": - obj_type = obj_annots["type"] - opt_model += f"{obj_type}: div_orig_opt_objective :: output;\n" - opt_model += f'constraint div_orig_opt_objective == {obj_annots["name"]};\n' - if obj_annots["sense"] == "-1": - opt_model += f'solve minimize {obj_annots["name"]};\n' - else: - opt_model += f'solve maximize {obj_annots["name"]};\n' - else: - opt_model += "solve satisfy;\n" - - return opt_model - - -def add_diversity_to_div_model(inst, vars, obj_sense, gap, sols): - # Add the 'previous solution variables' - for var in vars: - # Current and previous variables - varname = var["name"] - varprevname = var["prev_name"] - varprevisfloat = "float" in var["prev_type"] - - distfun = var["distance_function"] - prevsols = sols[varprevname] + [sols[varname]] - prevsol = ( - __round_elements(prevsols, 6) if True else prevsols - ) # float values are rounded to six decimal places to avoid infeasibility due to decimal errors. - - # Add the previous solutions to the model code. - inst[varprevname] = prevsol - - # Add the diversity distance measurement to the model code. - dim = __num_dim(prevsols) - dotdots = ", ".join([".." for _ in range(dim - 1)]) - varprevtype = "float" if "float" in var["prev_type"] else "int" - inst.add_string( - f"array [1..{len(prevsol)}] of var {varprevtype}: dist_{varname} :: output = [{distfun}({varname}, {varprevname}[sol,{dotdots}]) | sol in 1..{len(prevsol)}];\n" - ) - - # Add the bound on the objective. - if obj_sense == "-1": - inst.add_string(f"constraint div_orig_objective <= {gap};\n") - elif obj_sense == "1": - inst.add_string(f"constraint div_orig_objective >= {gap};\n") - - # Add new objective: maximize diversity. - dist_sum = "+".join([f'sum(dist_{var["name"]})' for var in vars]) - inst.add_string(f"solve maximize {dist_sum};\n") - - return inst - - -def __num_dim(x: List) -> int: - i = 1 - while isinstance(x[0], list): - i += 1 - x = x[0] - return i - - -def __round_elements(x: List, p: int) -> List: - for i in range(len(x)): - if isinstance(x[i], list): - x[i] = __round_elements(x[i], p) - elif isinstance(x[i], float): - x[i] = round(x[i], p) - return x diff --git a/src/minizinc/helpers.py b/src/minizinc/helpers.py index 4e028d1..00d4fbc 100644 --- a/src/minizinc/helpers.py +++ b/src/minizinc/helpers.py @@ -1,7 +1,7 @@ import sys from dataclasses import asdict, is_dataclass from datetime import timedelta -from typing import Any, Dict, Optional, Sequence, Union +from typing import Any, Dict, List, Optional, Sequence, Union import minizinc @@ -109,3 +109,88 @@ def check_solution( if status == minizinc.Status.ERROR: return True return False + + +def _add_diversity_to_opt_model(obj_annots, vars, sol_fix=None): + opt_model = "" + + for var in vars: + # Current and previous variables + varname = var["name"] + varprevname = var["prev_name"] + + # Add the 'previous solution variables' + opt_model += f"{varprevname} = [];\n" + + # Fix the solution to given once + if sol_fix is not None: + opt_model += f"constraint {varname} == {list(sol_fix[varname])};\n" + + # Add the optimal objective. + if obj_annots["sense"] != "0": + obj_type = obj_annots["type"] + opt_model += f"{obj_type}: div_orig_opt_objective :: output;\n" + opt_model += f'constraint div_orig_opt_objective == {obj_annots["name"]};\n' + if obj_annots["sense"] == "-1": + opt_model += f'solve minimize {obj_annots["name"]};\n' + else: + opt_model += f'solve maximize {obj_annots["name"]};\n' + else: + opt_model += "solve satisfy;\n" + + return opt_model + + +def _add_diversity_to_div_model(inst, vars, obj_sense, gap, sols): + # Add the 'previous solution variables' + for var in vars: + # Current and previous variables + varname = var["name"] + varprevname = var["prev_name"] + varprevisfloat = "float" in var["prev_type"] + + distfun = var["distance_function"] + prevsols = sols[varprevname] + [sols[varname]] + prevsol = ( + __round_elements(prevsols, 6) if varprevisfloat else prevsols + ) # float values are rounded to six decimal places to avoid infeasibility due to decimal errors. + + # Add the previous solutions to the model code. + inst[varprevname] = prevsol + + # Add the diversity distance measurement to the model code. + dim = __num_dim(prevsols) + dotdots = ", ".join([".." for _ in range(dim - 1)]) + varprevtype = "float" if "float" in var["prev_type"] else "int" + inst.add_string( + f"array [1..{len(prevsol)}] of var {varprevtype}: dist_{varname} :: output = [{distfun}({varname}, {varprevname}[sol,{dotdots}]) | sol in 1..{len(prevsol)}];\n" + ) + + # Add the bound on the objective. + if obj_sense == "-1": + inst.add_string(f"constraint div_orig_objective <= {gap};\n") + elif obj_sense == "1": + inst.add_string(f"constraint div_orig_objective >= {gap};\n") + + # Add new objective: maximize diversity. + dist_sum = "+".join([f'sum(dist_{var["name"]})' for var in vars]) + inst.add_string(f"solve maximize {dist_sum};\n") + + return inst + + +def __num_dim(x: List) -> int: + i = 1 + while isinstance(x[0], list): + i += 1 + x = x[0] + return i + + +def __round_elements(x: List, p: int) -> List: + for i in range(len(x)): + if isinstance(x[i], list): + x[i] = __round_elements(x[i], p) + elif isinstance(x[i], float): + x[i] = round(x[i], p) + return x diff --git a/src/minizinc/instance.py b/src/minizinc/instance.py index 3b2f1aa..057f91f 100644 --- a/src/minizinc/instance.py +++ b/src/minizinc/instance.py @@ -9,7 +9,7 @@ import sys import tempfile import warnings -from dataclasses import field, make_dataclass +from dataclasses import asdict, field, is_dataclass, make_dataclass from datetime import timedelta from enum import EnumMeta from keyword import iskeyword @@ -30,7 +30,7 @@ import minizinc -from .diversity import MznAnalyse +from .analyse import MznAnalyse from .driver import Driver from .error import ConfigurationError, MiniZincError, parse_error from .json import ( @@ -256,8 +256,9 @@ async def solve_async( def diverse_solutions( self, num_diverse_solutions: Optional[int] = None, - reference_solution: Optional[Result] = None, + reference_solution: Optional[Union[Result, Dict]] = None, mzn_analyse: Optional[MznAnalyse] = None, + optimise_diverse_sol: Optional[bool] = True, # **kwargs, ) -> Iterator[Result]: """Solves the Instance to find diverse solutions using its given solver configuration. @@ -274,19 +275,169 @@ def diverse_solutions( assigned, and statistical information. """ + from .helpers import _add_diversity_to_div_model, _add_diversity_to_opt_model # Loads diverse solution generator if MiniZinc Data Annotator is present if mzn_analyse is None: mzn_analyse = MznAnalyse.find() if mzn_analyse is None: raise ConfigurationError("mzn-analyse executable could not be located") + verbose = False # if enabled, outputs the progress + str_div_mzn = "out.mzn" + str_div_json = "out.json" + + path_div_mzn = Path(str_div_mzn) + path_div_json = Path(str_div_json) + + # Extract the diversity annotations. with self.files() as files: - # assert self.output_type is not None - for sol in mzn_analyse.run( - files, self._solver, num_diverse_solutions, reference_solution - ): - yield sol + mzn_analyse.run( + files, + # Do not change the order of the arguments 'inline-includes', 'remove-items:output', 'remove-litter' and 'get-diversity-anns' + [ + "inline-includes", + "remove-items:output", + "remove-anns:mzn_expression_name", + "remove-litter", + "get-diversity-anns", + f"out:{str_div_mzn}", + f"json_out:{str_div_json}", + ], + ) + + assert path_div_mzn.exists() + assert path_div_json.exists() + + # Load the base model. + str_model = path_div_mzn.read_text() + div_annots = json.loads(path_div_json.read_text())["get-diversity-annotations"] + + # Objective annotations. + obj_annots = div_annots["objective"] + variables = div_annots["vars"] + + assert len(variables) > 0, "Distance measure not specified" + + base_m = Model() + base_m.add_string(str_model) + + inst = Instance(self._solver, base_m) + + # Place holder for max gap. + max_gap = None + + # Place holder for prev solutions + prev_solutions = None + + # Number of total diverse solutions - If not provided use the count provided in the MiniZinc model + div_num = ( + int(div_annots["k"]) + if num_diverse_solutions is None + else num_diverse_solutions + ) + # Increase the solution count by one if a reference solution is provided + if reference_solution: + div_num += 1 + + for i in range(1, div_num + 1): + with inst.branch() as child: + if i == 1: + # Add constraints to the model that sets the decision variables to the reference solution, if provided + if reference_solution: + if isinstance(reference_solution, Result) and is_dataclass( + reference_solution.solution + ): + solution_obj = asdict(reference_solution.solution) + else: + assert isinstance(reference_solution, dict) + solution_obj = reference_solution + for k, v in solution_obj.items(): + if k not in ("objective", "_output_item", "_checker"): + child[k] = v + + # We will extend the annotated model with the objective and vars. + child.add_string(_add_diversity_to_opt_model(obj_annots, variables)) + + # Solve original model to optimality. + if verbose: + model_type = "opt" if obj_annots["sense"] != "0" else "sat" + print( + f"[Sol 1] Solving the original ({model_type}) model to get a solution" + ) + # inst = minizinc.Instance(self._solver, base_m) + res: Result = child.solve() + + # Ensure that the solution exists. + assert res.solution is not None + + if reference_solution is None: + yield res + + # Calculate max gap. + max_gap = ( + (1 - int(obj_annots["sense"]) * float(div_annots["gap"])) + * float(res["div_orig_opt_objective"]) + if obj_annots["sense"] != "0" + else 0 + ) + + # Store current solution as previous solution + prev_solutions = asdict(res.solution) + + else: + if verbose: + print( + f"[Sol {i+1}] Generating diverse solution {i}" + + (" (optimal)" if optimise_diverse_sol else "") + ) + + # We will extend the annotated model with the objective and vars. + child = _add_diversity_to_div_model( + child, variables, obj_annots["sense"], max_gap, prev_solutions + ) + + # Solve div model to get a diverse solution. + res = child.solve() + + # Ensure that the solution exists. + assert res.solution is not None + + # Solution as dictionary + sol_div = asdict(res.solution) + + # Solve diverse solution to optimality after fixing the diversity vars to the obtained solution + if optimise_diverse_sol: + # COMMENDTED OUT FOR NOW: Merge the solution values. + # sol_dist = dict() + # for var in variables: + # distvarname = "dist_"+var["name"] + # sol_dist[distvarname] = (sol_div[distvarname]) + + # Solve opt model after fixing the diversity vars to the obtained solution + child_opt = Instance(self._solver, base_m) + child_opt.add_string( + _add_diversity_to_opt_model(obj_annots, variables, sol_div) + ) + + # Solve the model + res = child_opt.solve() + + # Ensure that the solution exists. + assert res.solution is not None + + # COMMENDTED OUT FOR NOW: Add distance to previous solutions + # sol_opt = asdict(res.solution) + # sol_opt["distance_to_prev_vars"] = sol_dist + + yield res + + # Store current solution as previous solution + curr_solution = asdict(res.solution) + # Add the current solution to prev solution container + assert prev_solutions is not None + for var in variables: + prev_solutions[var["prev_name"]].append(curr_solution[var["name"]]) async def solutions( self, From fbdf357d387b45544aa8c3019f11adbde08df78d Mon Sep 17 00:00:00 2001 From: "Jip J. Dekker" Date: Mon, 14 Aug 2023 14:31:34 +1000 Subject: [PATCH 08/15] Additional refactoring of diverse_solutions function --- src/minizinc/helpers.py | 35 +++++--- src/minizinc/instance.py | 176 +++++++++++++++++++-------------------- 2 files changed, 107 insertions(+), 104 deletions(-) diff --git a/src/minizinc/helpers.py b/src/minizinc/helpers.py index 00d4fbc..a1311b3 100644 --- a/src/minizinc/helpers.py +++ b/src/minizinc/helpers.py @@ -1,7 +1,7 @@ import sys from dataclasses import asdict, is_dataclass from datetime import timedelta -from typing import Any, Dict, List, Optional, Sequence, Union +from typing import Any, Dict, Iterable, List, Optional, Sequence, Union import minizinc @@ -111,37 +111,46 @@ def check_solution( return False -def _add_diversity_to_opt_model(obj_annots, vars, sol_fix=None): - opt_model = "" - +def _add_diversity_to_opt_model( + inst: minizinc.Instance, + obj_annots: Dict[str, Any], + vars: List[Dict[str, Any]], + sol_fix: Dict[str, Iterable] = None, +): for var in vars: # Current and previous variables varname = var["name"] varprevname = var["prev_name"] # Add the 'previous solution variables' - opt_model += f"{varprevname} = [];\n" + inst[varprevname] = [] # Fix the solution to given once if sol_fix is not None: - opt_model += f"constraint {varname} == {list(sol_fix[varname])};\n" + inst.add_string(f"constraint {varname} == {list(sol_fix[varname])};\n") # Add the optimal objective. if obj_annots["sense"] != "0": obj_type = obj_annots["type"] - opt_model += f"{obj_type}: div_orig_opt_objective :: output;\n" - opt_model += f'constraint div_orig_opt_objective == {obj_annots["name"]};\n' + inst.add_string(f"{obj_type}: div_orig_opt_objective :: output;\n") + inst.add_string(f"constraint div_orig_opt_objective == {obj_annots['name']};\n") if obj_annots["sense"] == "-1": - opt_model += f'solve minimize {obj_annots["name"]};\n' + inst.add_string(f"solve minimize {obj_annots['name']};\n") else: - opt_model += f'solve maximize {obj_annots["name"]};\n' + inst.add_string(f"solve maximize {obj_annots['name']};\n") else: - opt_model += "solve satisfy;\n" + inst.add_string("solve satisfy;\n") - return opt_model + return inst -def _add_diversity_to_div_model(inst, vars, obj_sense, gap, sols): +def _add_diversity_to_div_model( + inst: minizinc.Instance, + vars: List[Dict[str, Any]], + obj_sense: str, + gap: Union[int, float], + sols: Dict[str, Any], +): # Add the 'previous solution variables' for var in vars: # Current and previous variables diff --git a/src/minizinc/instance.py b/src/minizinc/instance.py index 057f91f..00fcdad 100644 --- a/src/minizinc/instance.py +++ b/src/minizinc/instance.py @@ -4,6 +4,7 @@ import asyncio import contextlib import json +import logging import os import re import sys @@ -282,7 +283,6 @@ def diverse_solutions( mzn_analyse = MznAnalyse.find() if mzn_analyse is None: raise ConfigurationError("mzn-analyse executable could not be located") - verbose = False # if enabled, outputs the progress str_div_mzn = "out.mzn" str_div_json = "out.json" @@ -324,11 +324,8 @@ def diverse_solutions( inst = Instance(self._solver, base_m) - # Place holder for max gap. - max_gap = None - - # Place holder for prev solutions - prev_solutions = None + max_gap = None # Place holder for max gap. + prev_solutions = None # Place holder for prev solutions # Number of total diverse solutions - If not provided use the count provided in the MiniZinc model div_num = ( @@ -340,104 +337,101 @@ def diverse_solutions( if reference_solution: div_num += 1 - for i in range(1, div_num + 1): + # Initial (re-)optimisation run + with inst.branch() as child: + # Add constraints to the model that sets the decision variables to the reference solution, if provided + if reference_solution: + if isinstance(reference_solution, Result) and is_dataclass( + reference_solution.solution + ): + solution_obj = asdict(reference_solution.solution) + else: + assert isinstance(reference_solution, dict) + solution_obj = reference_solution + for k, v in solution_obj.items(): + if k not in ("objective", "_output_item", "_checker"): + child[k] = v + + # We will extend the annotated model with the objective and vars. + child = _add_diversity_to_opt_model(child, obj_annots, variables) + + # Solve original model to optimality. + if minizinc.logger.isEnabledFor(logging.INFO): + model_type = "opt" if obj_annots["sense"] != "0" else "sat" + minizinc.logger.info( + f"[Sol 1] Solving the original ({model_type}) model to get a solution" + ) + res: Result = child.solve() + # TODO: I'm not sure this condition is guaranteed to hold + # Ensure that the solution exists. + assert res.solution is not None + + if reference_solution is None: + yield res + + # Calculate max gap. + max_gap = ( + (1 - int(obj_annots["sense"]) * float(div_annots["gap"])) + * float(res["div_orig_opt_objective"]) + if obj_annots["sense"] != "0" + else 0 + ) + + # Store current solution as previous solution + prev_solutions = asdict(res.solution) + + for i in range(2, div_num + 1): with inst.branch() as child: - if i == 1: - # Add constraints to the model that sets the decision variables to the reference solution, if provided - if reference_solution: - if isinstance(reference_solution, Result) and is_dataclass( - reference_solution.solution - ): - solution_obj = asdict(reference_solution.solution) - else: - assert isinstance(reference_solution, dict) - solution_obj = reference_solution - for k, v in solution_obj.items(): - if k not in ("objective", "_output_item", "_checker"): - child[k] = v - - # We will extend the annotated model with the objective and vars. - child.add_string(_add_diversity_to_opt_model(obj_annots, variables)) - - # Solve original model to optimality. - if verbose: - model_type = "opt" if obj_annots["sense"] != "0" else "sat" - print( - f"[Sol 1] Solving the original ({model_type}) model to get a solution" - ) - # inst = minizinc.Instance(self._solver, base_m) - res: Result = child.solve() + minizinc.logger.info( + f"[Sol {i}] Generating diverse solution {i}" + + (" (optimal)" if optimise_diverse_sol else "") + ) - # Ensure that the solution exists. - assert res.solution is not None + # We will extend the annotated model with the objective and vars. + child = _add_diversity_to_div_model( + child, variables, obj_annots["sense"], max_gap, prev_solutions + ) - if reference_solution is None: - yield res + # Solve div model to get a diverse solution. + res = child.solve() - # Calculate max gap. - max_gap = ( - (1 - int(obj_annots["sense"]) * float(div_annots["gap"])) - * float(res["div_orig_opt_objective"]) - if obj_annots["sense"] != "0" - else 0 - ) + # Ensure that the solution exists. + assert res.solution is not None - # Store current solution as previous solution - prev_solutions = asdict(res.solution) + # Solve diverse solution to optimality after fixing the diversity vars to the obtained solution + if optimise_diverse_sol: + # Solution as dictionary + sol_div = asdict(res.solution) - else: - if verbose: - print( - f"[Sol {i+1}] Generating diverse solution {i}" - + (" (optimal)" if optimise_diverse_sol else "") - ) - - # We will extend the annotated model with the objective and vars. - child = _add_diversity_to_div_model( - child, variables, obj_annots["sense"], max_gap, prev_solutions + # COMMENDTED OUT FOR NOW: Merge the solution values. + # sol_dist = dict() + # for var in variables: + # distvarname = "dist_"+var["name"] + # sol_dist[distvarname] = (sol_div[distvarname]) + + # Solve opt model after fixing the diversity vars to the obtained solution + with inst.branch() as child: + child = _add_diversity_to_opt_model( + child, obj_annots, variables, sol_div ) - # Solve div model to get a diverse solution. + # Solve the model res = child.solve() # Ensure that the solution exists. assert res.solution is not None - # Solution as dictionary - sol_div = asdict(res.solution) - - # Solve diverse solution to optimality after fixing the diversity vars to the obtained solution - if optimise_diverse_sol: - # COMMENDTED OUT FOR NOW: Merge the solution values. - # sol_dist = dict() - # for var in variables: - # distvarname = "dist_"+var["name"] - # sol_dist[distvarname] = (sol_div[distvarname]) - - # Solve opt model after fixing the diversity vars to the obtained solution - child_opt = Instance(self._solver, base_m) - child_opt.add_string( - _add_diversity_to_opt_model(obj_annots, variables, sol_div) - ) - - # Solve the model - res = child_opt.solve() - - # Ensure that the solution exists. - assert res.solution is not None - - # COMMENDTED OUT FOR NOW: Add distance to previous solutions - # sol_opt = asdict(res.solution) - # sol_opt["distance_to_prev_vars"] = sol_dist - - yield res - - # Store current solution as previous solution - curr_solution = asdict(res.solution) - # Add the current solution to prev solution container - assert prev_solutions is not None - for var in variables: - prev_solutions[var["prev_name"]].append(curr_solution[var["name"]]) + # COMMENDTED OUT FOR NOW: Add distance to previous solutions + # sol_opt = asdict(res.solution) + # sol_opt["distance_to_prev_vars"] = sol_dist + yield res + + # Store current solution as previous solution + curr_solution = asdict(res.solution) + # Add the current solution to prev solution container + assert prev_solutions is not None + for var in variables: + prev_solutions[var["prev_name"]].append(curr_solution[var["name"]]) async def solutions( self, From 625a2afb490151cc6f6d87bea7fb6357023da0ca Mon Sep 17 00:00:00 2001 From: "Jip J. Dekker" Date: Mon, 14 Aug 2023 14:45:36 +1000 Subject: [PATCH 09/15] Change Instance.diverse_solutions to be async --- src/minizinc/instance.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/minizinc/instance.py b/src/minizinc/instance.py index 00fcdad..3737594 100644 --- a/src/minizinc/instance.py +++ b/src/minizinc/instance.py @@ -254,14 +254,14 @@ async def solve_async( solution = result.solution return Result(status, solution, statistics) - def diverse_solutions( + async def diverse_solutions( self, num_diverse_solutions: Optional[int] = None, reference_solution: Optional[Union[Result, Dict]] = None, mzn_analyse: Optional[MznAnalyse] = None, optimise_diverse_sol: Optional[bool] = True, # **kwargs, - ) -> Iterator[Result]: + ) -> AsyncIterator[Result]: """Solves the Instance to find diverse solutions using its given solver configuration. Finds diverse solutions to the given MiniZinc instance using the given solver @@ -361,7 +361,7 @@ def diverse_solutions( minizinc.logger.info( f"[Sol 1] Solving the original ({model_type}) model to get a solution" ) - res: Result = child.solve() + res: Result = await child.solve_async() # TODO: I'm not sure this condition is guaranteed to hold # Ensure that the solution exists. assert res.solution is not None @@ -393,7 +393,7 @@ def diverse_solutions( ) # Solve div model to get a diverse solution. - res = child.solve() + res = await child.solve_async() # Ensure that the solution exists. assert res.solution is not None @@ -416,7 +416,7 @@ def diverse_solutions( ) # Solve the model - res = child.solve() + res = await child.solve_async() # Ensure that the solution exists. assert res.solution is not None From fc8a422979b1960fc3da75deb772e5ce7c965bd0 Mon Sep 17 00:00:00 2001 From: "Jip J. Dekker" Date: Tue, 15 Aug 2023 10:07:10 +1000 Subject: [PATCH 10/15] Add a more programmatic interface for the mzn-analyse tool --- src/minizinc/analyse.py | 48 +++++++- src/minizinc/instance.py | 258 +++++++++++++++++++-------------------- 2 files changed, 166 insertions(+), 140 deletions(-) diff --git a/src/minizinc/analyse.py b/src/minizinc/analyse.py index 2bf8984..eb91eab 100644 --- a/src/minizinc/analyse.py +++ b/src/minizinc/analyse.py @@ -1,17 +1,25 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +import json import os import platform import shutil import subprocess +from enum import Enum, auto from pathlib import Path -from typing import List, Optional, Union +from typing import Any, Dict, List, Optional, Union from .driver import MAC_LOCATIONS, WIN_LOCATIONS from .error import ConfigurationError, MiniZincError +class InlineOption(Enum): + DISABLED = auto() + NON_LIBRARY = auto() + ALL = auto() + + class MznAnalyse: """Python interface to the mzn-analyse executable @@ -64,13 +72,40 @@ def find( def run( self, mzn_files: List[Path], - args: List[str], - ) -> None: + inline_includes: InlineOption = InlineOption.DISABLED, + remove_litter: bool = False, + get_diversity_anns: bool = False, + get_solve_anns: bool = True, + output_all: bool = True, + mzn_output: Optional[Path] = None, + remove_anns: Optional[List[str]] = None, + remove_items: Optional[List[str]] = None, + ) -> Dict[str, Any]: # Do not change the order of the arguments 'inline-includes', 'remove-items:output', 'remove-litter' and 'get-diversity-anns' - tool_run_cmd: List[Union[str, Path]] = [self._executable] + tool_run_cmd: List[Union[str, Path]] = [str(self._executable), "json_out:-"] + + for f in mzn_files: + tool_run_cmd.append(str(f)) + + if inline_includes == InlineOption.ALL: + tool_run_cmd.append("inline-all_includes") + elif inline_includes == InlineOption.NON_LIBRARY: + tool_run_cmd.append("inline-includes") + + if remove_items is not None and len(remove_items) > 0: + tool_run_cmd.append(f"remove-items:{','.join(remove_items)}") + if remove_anns is not None and len(remove_anns) > 0: + tool_run_cmd.append(f"remove-anns:{','.join(remove_anns)}") + + if remove_litter: + tool_run_cmd.append("remove-litter") + if get_diversity_anns: + tool_run_cmd.append("get-diversity-anns") - tool_run_cmd.extend(mzn_files) - tool_run_cmd.extend(args) + if mzn_output is not None: + tool_run_cmd.append(f"out:{str(mzn_output)}") + else: + tool_run_cmd.append("no_out") # Extract the diversity annotations. proc = subprocess.run( @@ -78,3 +113,4 @@ def run( ) if proc.returncode != 0: raise MiniZincError(message=str(proc.stderr)) + return json.loads(proc.stdout) diff --git a/src/minizinc/instance.py b/src/minizinc/instance.py index 3737594..f18b060 100644 --- a/src/minizinc/instance.py +++ b/src/minizinc/instance.py @@ -31,7 +31,7 @@ import minizinc -from .analyse import MznAnalyse +from .analyse import InlineOption, MznAnalyse from .driver import Driver from .error import ConfigurationError, MiniZincError, parse_error from .json import ( @@ -284,154 +284,144 @@ async def diverse_solutions( if mzn_analyse is None: raise ConfigurationError("mzn-analyse executable could not be located") - str_div_mzn = "out.mzn" - str_div_json = "out.json" - - path_div_mzn = Path(str_div_mzn) - path_div_json = Path(str_div_json) - - # Extract the diversity annotations. - with self.files() as files: - mzn_analyse.run( - files, - # Do not change the order of the arguments 'inline-includes', 'remove-items:output', 'remove-litter' and 'get-diversity-anns' - [ - "inline-includes", - "remove-items:output", - "remove-anns:mzn_expression_name", - "remove-litter", - "get-diversity-anns", - f"out:{str_div_mzn}", - f"json_out:{str_div_json}", - ], + 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 ) - assert path_div_mzn.exists() - assert path_div_json.exists() - - # Load the base model. - str_model = path_div_mzn.read_text() - div_annots = json.loads(path_div_json.read_text())["get-diversity-annotations"] - - # Objective annotations. - obj_annots = div_annots["objective"] - variables = div_annots["vars"] - - assert len(variables) > 0, "Distance measure not specified" - - base_m = Model() - base_m.add_string(str_model) - - inst = Instance(self._solver, base_m) - - max_gap = None # Place holder for max gap. - prev_solutions = None # Place holder for prev solutions - - # Number of total diverse solutions - If not provided use the count provided in the MiniZinc model - div_num = ( - int(div_annots["k"]) - if num_diverse_solutions is None - else num_diverse_solutions - ) - # Increase the solution count by one if a reference solution is provided - if reference_solution: - div_num += 1 - - # Initial (re-)optimisation run - with inst.branch() as child: - # Add constraints to the model that sets the decision variables to the reference solution, if provided + # Extract the diversity annotations. + with self.files() as files: + div_anns = mzn_analyse.run( + files, + get_diversity_anns=True, + inline_includes=InlineOption.NON_LIBRARY, + remove_items=["output"], + remove_anns=["mzn_expression_name"], + remove_litter=True, + mzn_output=Path(div_file.name), + )["get-diversity-annotations"] + + # Objective annotations. + obj_anns = div_anns["objective"] + variables = div_anns["vars"] + + assert len(variables) > 0, "Distance measure not specified" + + inst = Instance(self._solver, Model(Path(div_file.name))) + + max_gap = None # Place holder for max gap. + prev_solutions = None # Place holder for prev solutions + + # Number of total diverse solutions - If not provided use the count provided in the MiniZinc model + div_num = ( + int(div_anns["k"]) + if num_diverse_solutions is None + else num_diverse_solutions + ) + # Increase the solution count by one if a reference solution is provided if reference_solution: - if isinstance(reference_solution, Result) and is_dataclass( - reference_solution.solution - ): - solution_obj = asdict(reference_solution.solution) - else: - assert isinstance(reference_solution, dict) - solution_obj = reference_solution - for k, v in solution_obj.items(): - if k not in ("objective", "_output_item", "_checker"): - child[k] = v - - # We will extend the annotated model with the objective and vars. - child = _add_diversity_to_opt_model(child, obj_annots, variables) - - # Solve original model to optimality. - if minizinc.logger.isEnabledFor(logging.INFO): - model_type = "opt" if obj_annots["sense"] != "0" else "sat" - minizinc.logger.info( - f"[Sol 1] Solving the original ({model_type}) model to get a solution" - ) - res: Result = await child.solve_async() - # TODO: I'm not sure this condition is guaranteed to hold - # Ensure that the solution exists. - assert res.solution is not None - - if reference_solution is None: - yield res - - # Calculate max gap. - max_gap = ( - (1 - int(obj_annots["sense"]) * float(div_annots["gap"])) - * float(res["div_orig_opt_objective"]) - if obj_annots["sense"] != "0" - else 0 - ) + div_num += 1 - # Store current solution as previous solution - prev_solutions = asdict(res.solution) - - for i in range(2, div_num + 1): + # Initial (re-)optimisation run with inst.branch() as child: - minizinc.logger.info( - f"[Sol {i}] Generating diverse solution {i}" - + (" (optimal)" if optimise_diverse_sol else "") - ) + # Add constraints to the model that sets the decision variables to the reference solution, if provided + if reference_solution: + if isinstance(reference_solution, Result) and is_dataclass( + reference_solution.solution + ): + solution_obj = asdict(reference_solution.solution) + else: + assert isinstance(reference_solution, dict) + solution_obj = reference_solution + for k, v in solution_obj.items(): + if k not in ("objective", "_output_item", "_checker"): + child[k] = v # We will extend the annotated model with the objective and vars. - child = _add_diversity_to_div_model( - child, variables, obj_annots["sense"], max_gap, prev_solutions - ) - - # Solve div model to get a diverse solution. - res = await child.solve_async() + child = _add_diversity_to_opt_model(child, obj_anns, variables) - # Ensure that the solution exists. - assert res.solution is not None - - # Solve diverse solution to optimality after fixing the diversity vars to the obtained solution - if optimise_diverse_sol: - # Solution as dictionary - sol_div = asdict(res.solution) + # Solve original model to optimality. + if minizinc.logger.isEnabledFor(logging.INFO): + model_type = "opt" if obj_anns["sense"] != "0" else "sat" + minizinc.logger.info( + f"[Sol 1] Solving the original ({model_type}) model to get a solution" + ) + res: Result = await child.solve_async() + # TODO: I'm not sure this condition is guaranteed to hold + # Ensure that the solution exists. + assert res.solution is not None + + if reference_solution is None: + yield res + + # Calculate max gap. + max_gap = ( + (1 - int(obj_anns["sense"]) * float(div_anns["gap"])) + * float(res["div_orig_opt_objective"]) + if obj_anns["sense"] != "0" + else 0 + ) - # COMMENDTED OUT FOR NOW: Merge the solution values. - # sol_dist = dict() - # for var in variables: - # distvarname = "dist_"+var["name"] - # sol_dist[distvarname] = (sol_div[distvarname]) + # Store current solution as previous solution + prev_solutions = asdict(res.solution) - # Solve opt model after fixing the diversity vars to the obtained solution + for i in range(2, div_num + 1): with inst.branch() as child: - child = _add_diversity_to_opt_model( - child, obj_annots, variables, sol_div + minizinc.logger.info( + f"[Sol {i}] Generating diverse solution {i}" + + (" (optimal)" if optimise_diverse_sol else "") ) - # Solve the model - res = await child.solve_async() - - # Ensure that the solution exists. - assert res.solution is not None + # We will extend the annotated model with the objective and vars. + child = _add_diversity_to_div_model( + child, variables, obj_anns["sense"], max_gap, prev_solutions + ) - # COMMENDTED OUT FOR NOW: Add distance to previous solutions - # sol_opt = asdict(res.solution) - # sol_opt["distance_to_prev_vars"] = sol_dist - yield res + # Solve div model to get a diverse solution. + res = await child.solve_async() - # Store current solution as previous solution - curr_solution = asdict(res.solution) - # Add the current solution to prev solution container - assert prev_solutions is not None - for var in variables: - prev_solutions[var["prev_name"]].append(curr_solution[var["name"]]) + # Ensure that the solution exists. + assert res.solution is not None + + # Solve diverse solution to optimality after fixing the diversity vars to the obtained solution + if optimise_diverse_sol: + # Solution as dictionary + sol_div = asdict(res.solution) + + # COMMENDTED OUT FOR NOW: Merge the solution values. + # sol_dist = dict() + # for var in variables: + # distvarname = "dist_"+var["name"] + # sol_dist[distvarname] = (sol_div[distvarname]) + + # Solve opt model after fixing the diversity vars to the obtained solution + with inst.branch() as child: + child = _add_diversity_to_opt_model( + child, obj_anns, variables, sol_div + ) + + # Solve the model + res = await child.solve_async() + + # Ensure that the solution exists. + assert res.solution is not None + + # COMMENDTED OUT FOR NOW: Add distance to previous solutions + # sol_opt = asdict(res.solution) + # sol_opt["distance_to_prev_vars"] = sol_dist + yield res + + # Store current solution as previous solution + curr_solution = asdict(res.solution) + # Add the current solution to prev solution container + assert prev_solutions is not None + for var in variables: + prev_solutions[var["prev_name"]].append(curr_solution[var["name"]]) + finally: + # Remove temporary file created for the diversity model + div_file.close() + os.remove(div_file.name) async def solutions( self, From bd9a1290c7d678992dd77facc971bf3bdf09e231 Mon Sep 17 00:00:00 2001 From: "Jip J. Dekker" Date: Tue, 15 Aug 2023 11:26:02 +1000 Subject: [PATCH 11/15] Add solver argument for Instance.diverse_solutions --- src/minizinc/instance.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/minizinc/instance.py b/src/minizinc/instance.py index f18b060..e1a43ed 100644 --- a/src/minizinc/instance.py +++ b/src/minizinc/instance.py @@ -260,7 +260,7 @@ async def diverse_solutions( reference_solution: Optional[Union[Result, Dict]] = None, mzn_analyse: Optional[MznAnalyse] = None, optimise_diverse_sol: Optional[bool] = True, - # **kwargs, + solver: Optional[Solver] = None, ) -> AsyncIterator[Result]: """Solves the Instance to find diverse solutions using its given solver configuration. @@ -306,9 +306,12 @@ async def diverse_solutions( obj_anns = div_anns["objective"] variables = div_anns["vars"] - assert len(variables) > 0, "Distance measure not specified" + if len(variables) > 0: + raise MiniZincError(message="No distance measure is specified") - inst = Instance(self._solver, Model(Path(div_file.name))) + if solver is None: + solver = self._solver + inst = Instance(solver, Model(Path(div_file.name)), self._driver) max_gap = None # Place holder for max gap. prev_solutions = None # Place holder for prev solutions From 913313e47ad0b541eb678ae706b3f5ec9412cc7a Mon Sep 17 00:00:00 2001 From: "Jip J. Dekker" Date: Tue, 15 Aug 2023 13:15:34 +1000 Subject: [PATCH 12/15] Change solution assertions in Instance.diverse_solutions to early return --- src/minizinc/instance.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/minizinc/instance.py b/src/minizinc/instance.py index e1a43ed..ef02b66 100644 --- a/src/minizinc/instance.py +++ b/src/minizinc/instance.py @@ -306,7 +306,7 @@ async def diverse_solutions( obj_anns = div_anns["objective"] variables = div_anns["vars"] - if len(variables) > 0: + if len(variables) <= 0: raise MiniZincError(message="No distance measure is specified") if solver is None: @@ -351,9 +351,9 @@ async def diverse_solutions( f"[Sol 1] Solving the original ({model_type}) model to get a solution" ) res: Result = await child.solve_async() - # TODO: I'm not sure this condition is guaranteed to hold - # Ensure that the solution exists. - assert res.solution is not None + # No (additional) solutions can be found, return from function + if res.solution is None: + return if reference_solution is None: yield res @@ -384,8 +384,9 @@ async def diverse_solutions( # Solve div model to get a diverse solution. res = await child.solve_async() - # Ensure that the solution exists. - assert res.solution is not None + # No (additional) solutions can be found, return from function + if res.solution is None: + return # Solve diverse solution to optimality after fixing the diversity vars to the obtained solution if optimise_diverse_sol: @@ -407,8 +408,9 @@ async def diverse_solutions( # Solve the model res = await child.solve_async() - # Ensure that the solution exists. - assert res.solution is not None + # No (additional) solutions can be found, return from function + if res.solution is None: + return # COMMENDTED OUT FOR NOW: Add distance to previous solutions # sol_opt = asdict(res.solution) From eb99bb7645351d5af53d1cacb69755fe97538c12 Mon Sep 17 00:00:00 2001 From: "Jip J. Dekker" Date: Fri, 20 Oct 2023 13:40:00 +1100 Subject: [PATCH 13/15] Add initial diversity library file --- src/minizinc/instance.py | 7 +- .../share/minizinc-python/diversity.mzn | 117 ++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/minizinc/share/minizinc-python/diversity.mzn diff --git a/src/minizinc/instance.py b/src/minizinc/instance.py index ef02b66..c46651a 100644 --- a/src/minizinc/instance.py +++ b/src/minizinc/instance.py @@ -10,6 +10,7 @@ import sys import tempfile import warnings +from importlib import resources from dataclasses import asdict, field, is_dataclass, make_dataclass from datetime import timedelta from enum import EnumMeta @@ -534,9 +535,13 @@ async def solutions( all_solutions or intermediate_solutions or (nr_solutions is not None) ) + mznpy_share = resources.files(minizinc) / "share/minizinc-python" # Add files as last arguments - with self.files() as files, self._solver.configuration() as solver: + with resources.as_file( + mznpy_share + ) as share, self.files() as files, self._solver.configuration() as solver: assert self.output_type is not None + cmd.extend(["-I", share]) cmd.extend(files) status = Status.UNKNOWN diff --git a/src/minizinc/share/minizinc-python/diversity.mzn b/src/minizinc/share/minizinc-python/diversity.mzn new file mode 100644 index 0000000..8ecdda1 --- /dev/null +++ b/src/minizinc/share/minizinc-python/diversity.mzn @@ -0,0 +1,117 @@ +/*** + @groupdef diversity MiniZinc definitions for the MiniZinc diversity extension. + + These annotations and predicates can be used to produce diverse set of + solutions.In order to use them in a model, include the file "diversity.mzn". +*/ + + +/*** + @groupdef diversity.annotations Annotations provided to guide the MiniZinc diversity extension. +*/ + +annotation diversity_incremental(int: k, float: gap); +annotation diversity_global(int: k, float: gap); +annotation diversity_pairwise(array[int] of var int: x, string: compare_fn); +annotation diversity_pairwise(array[int,int] of var int: x, string: compare_fn); +annotation diversity_pairwise(array[int] of var float: x, string: compare_fn); +annotation diversity_pairwise(array[int,int] of var float: x, string: compare_fn); +annotation diversity_aggregator(string); +annotation diversity_combinator(string); +annotation diversity_intra_constraint(string); +annotation diversity_inter_constraint(string); + +/*** + @groupdef diversity.distance Distance functions provided in the MiniZinc diversity extension. +*/ + +/** @group diversity.distance + Returns the Hamming distance between \a x and \a y. +*/ +function int: hamming_distance( + array[$A] of opt $T: x, + array[$A] of opt $T: y +) = assert( + index_sets_agree(x, y), + "hamming_distance: x and x must have identical index sets", + let { + any: xx = array1d(x); + any: yy = array1d(y); + } in count(i in index_set(array1d(xx))) (xx[i] != yy[i]) +); + +/** @group diversity.distance + Returns the Hamming distance between \a x and \a y. +*/ +function var int: hamming_distance( + array[$A] of var opt $T: x, + array[$A] of var opt $T: y +) = assert( + index_sets_agree(x, y), + "hamming_distance: x and x must have identical index sets", + let { + any: xx = array1d(x); + any: yy = array1d(y); + } in count(i in index_set(array1d(xx))) (xx[i] != yy[i]) +); + +/** @group diversity.distance + Returns the Hamming distance between \a x and \a y. +*/ +function int: manhattan_distance( + array[$A] of $$T: x, + array[$A] of $$T: y +) = assert( + index_sets_agree(x, y), + "manhattan_distance: x and x must have identical index sets", + let { + any: xx = array1d(x); + any: yy = array1d(y); + } in sum(i in index_set(xx)) (abs(xx[i] - yy[i])) +); + +/** @group diversity.distance + Returns the Hamming distance between \a x and \a y. +*/ +function var int: manhattan_distance( + array[$A] of var $$T: x, + array[$A] of var $$T: y +) = assert( + index_sets_agree(x, y), + "manhattan_distance: x and x must have identical index sets", + let { + any: xx = array1d(x); + any: yy = array1d(y); + } in sum(i in index_set(xx)) (abs(xx[i] - yy[i])) +); + +/** @group diversity.distance + Returns the Hamming distance between \a x and \a y. +*/ +function float: manhattan_distance( + array[$A] of float: x, + array[$A] of float: y +) = assert( + index_sets_agree(x, y), + "manhattan_distance: x and x must have identical index sets", + let { + any: xx = array1d(x); + any: yy = array1d(y); + } in sum(i in index_set(xx)) (abs(xx[i] - yy[i])) +); + + +/** @group diversity.distance + Returns the Hamming distance between \a x and \a y. +*/ +function var float: manhattan_distance( + array[$A] of var float: x, + array[$A] of var float: y +) = assert( + index_sets_agree(x, y), + "manhattan_distance: x and x must have identical index sets", + let { + any: xx = array1d(x); + any: yy = array1d(y); + } in sum(i in index_set(xx)) (abs(xx[i] - yy[i])) +); From 3df0cc4f1322fd28bf88b8bcd5e2c33cb1072034 Mon Sep 17 00:00:00 2001 From: ilansen Date: Tue, 31 Oct 2023 09:04:08 +1100 Subject: [PATCH 14/15] Diversity MZN: Added description of annotations. Removed unused annotations. --- .../share/minizinc-python/diversity.mzn | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/minizinc/share/minizinc-python/diversity.mzn b/src/minizinc/share/minizinc-python/diversity.mzn index 8ecdda1..eef0860 100644 --- a/src/minizinc/share/minizinc-python/diversity.mzn +++ b/src/minizinc/share/minizinc-python/diversity.mzn @@ -10,16 +10,27 @@ @groupdef diversity.annotations Annotations provided to guide the MiniZinc diversity extension. */ +/** @group diversity.annotations Produce at most \a k number of diverse solutions with thier objective values + bounded between optimal value and \a gap % from the optimal. Diverse solutions are obtained one after the other + and distance to kth diverse solution is sum of pairwise distance to all the previous diverse solutions +*/ annotation diversity_incremental(int: k, float: gap); -annotation diversity_global(int: k, float: gap); -annotation diversity_pairwise(array[int] of var int: x, string: compare_fn); -annotation diversity_pairwise(array[int,int] of var int: x, string: compare_fn); -annotation diversity_pairwise(array[int] of var float: x, string: compare_fn); -annotation diversity_pairwise(array[int,int] of var float: x, string: compare_fn); -annotation diversity_aggregator(string); -annotation diversity_combinator(string); -annotation diversity_intra_constraint(string); -annotation diversity_inter_constraint(string); +/** @group diversity.annotations + Returns the Hamming distance between \a x and \a compare_fn +*/ +annotation diverse_pairwise(array[int] of var int: x, string: compare_fn); +/** @group diversity.annotations + Returns the Hamming distance between \a x and \a compare_fn +*/ +annotation diverse_pairwise(array[$T] of var int: x, string: compare_fn) = diverse_pairwise(array1d(x), compare_fn); +/** @group diversity.annotations + Returns the Hamming distance between \a x and \a compare_fn +*/ +annotation diverse_pairwise(array[int] of var float: x, string: compare_fn); +/** @group diversity.annotations + Returns the Hamming distance between \a x and \a compare_fn +*/ +annotation diverse_pairwise(array[$T] of var float: x, string: compare_fn) = diverse_pairwise(array1d(x), compare_fn); /*** @groupdef diversity.distance Distance functions provided in the MiniZinc diversity extension. @@ -33,7 +44,7 @@ function int: hamming_distance( array[$A] of opt $T: y ) = assert( index_sets_agree(x, y), - "hamming_distance: x and x must have identical index sets", + "hamming_distance: x and y must have identical index sets", let { any: xx = array1d(x); any: yy = array1d(y); @@ -48,7 +59,7 @@ function var int: hamming_distance( array[$A] of var opt $T: y ) = assert( index_sets_agree(x, y), - "hamming_distance: x and x must have identical index sets", + "hamming_distance: x and y must have identical index sets", let { any: xx = array1d(x); any: yy = array1d(y); @@ -63,7 +74,7 @@ function int: manhattan_distance( array[$A] of $$T: y ) = assert( index_sets_agree(x, y), - "manhattan_distance: x and x must have identical index sets", + "manhattan_distance: x and y must have identical index sets", let { any: xx = array1d(x); any: yy = array1d(y); @@ -78,7 +89,7 @@ function var int: manhattan_distance( array[$A] of var $$T: y ) = assert( index_sets_agree(x, y), - "manhattan_distance: x and x must have identical index sets", + "manhattan_distance: x and y must have identical index sets", let { any: xx = array1d(x); any: yy = array1d(y); @@ -93,7 +104,7 @@ function float: manhattan_distance( array[$A] of float: y ) = assert( index_sets_agree(x, y), - "manhattan_distance: x and x must have identical index sets", + "manhattan_distance: x and y must have identical index sets", let { any: xx = array1d(x); any: yy = array1d(y); @@ -109,7 +120,7 @@ function var float: manhattan_distance( array[$A] of var float: y ) = assert( index_sets_agree(x, y), - "manhattan_distance: x and x must have identical index sets", + "manhattan_distance: x and y must have identical index sets", let { any: xx = array1d(x); any: yy = array1d(y); From caa32cb72ca7b3064e099412d125e94a62f12975 Mon Sep 17 00:00:00 2001 From: ilansen Date: Tue, 31 Oct 2023 10:42:57 +1100 Subject: [PATCH 15/15] Diversity MZN: more description to annotations --- .../share/minizinc-python/diversity.mzn | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/minizinc/share/minizinc-python/diversity.mzn b/src/minizinc/share/minizinc-python/diversity.mzn index eef0860..86eecb1 100644 --- a/src/minizinc/share/minizinc-python/diversity.mzn +++ b/src/minizinc/share/minizinc-python/diversity.mzn @@ -12,25 +12,33 @@ /** @group diversity.annotations Produce at most \a k number of diverse solutions with thier objective values bounded between optimal value and \a gap % from the optimal. Diverse solutions are obtained one after the other - and distance to kth diverse solution is sum of pairwise distance to all the previous diverse solutions + and distance to k-th diverse solution is sum of pairwise distance to all the previous diverse solutions */ annotation diversity_incremental(int: k, float: gap); /** @group diversity.annotations - Returns the Hamming distance between \a x and \a compare_fn + Use \a distance_metric on \a x when computing distance to solutions. Here same index on different solutions are compared. + For example, when obtaining the distance to k-th diverse solution using the incremental approach (diversity_incremental) each index of + \a x in k-th solution is compared to the corresponding index of all previously obtained diverse solutions. */ -annotation diverse_pairwise(array[int] of var int: x, string: compare_fn); +annotation diverse_pairwise(array[int] of var int: x, string: distance_metric); /** @group diversity.annotations - Returns the Hamming distance between \a x and \a compare_fn + Use \a distance_metric on \a x when computing distance to solutions. Here same index on different solutions are compared. + For example, when obtaining the distance to k-th diverse solution using the incremental approach (diversity_incremental) each index of + \a x in k-th solution is compared to the corresponding index of all previously obtained diverse solutions. */ -annotation diverse_pairwise(array[$T] of var int: x, string: compare_fn) = diverse_pairwise(array1d(x), compare_fn); +annotation diverse_pairwise(array[$T] of var int: x, string: distance_metric) = diverse_pairwise(array1d(x), distance_metric); /** @group diversity.annotations - Returns the Hamming distance between \a x and \a compare_fn + Use \a distance_metric on \a x when computing distance to solutions. Here same index on different solutions are compared. + For example, when obtaining the distance to k-th diverse solution using the incremental approach (diversity_incremental) each index of + \a x in k-th solution is compared to the corresponding index of all previously obtained diverse solutions. */ -annotation diverse_pairwise(array[int] of var float: x, string: compare_fn); +annotation diverse_pairwise(array[int] of var float: x, string: distance_metric); /** @group diversity.annotations - Returns the Hamming distance between \a x and \a compare_fn + Use \a distance_metric on \a x when computing distance to solutions. Here same index on different solutions are compared. + For example, when obtaining the distance to k-th diverse solution using the incremental approach (diversity_incremental) each index of + \a x in k-th solution is compared to the corresponding index of all previously obtained diverse solutions. */ -annotation diverse_pairwise(array[$T] of var float: x, string: compare_fn) = diverse_pairwise(array1d(x), compare_fn); +annotation diverse_pairwise(array[$T] of var float: x, string: distance_metric) = diverse_pairwise(array1d(x), distance_metric); /*** @groupdef diversity.distance Distance functions provided in the MiniZinc diversity extension.