Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finding diverse solution for MiniZinc instances #87

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/minizinc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@
"Result",
"Solver",
"Status",
"Diversity",
]
116 changes: 116 additions & 0 deletions src/minizinc/analyse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# 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 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

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],
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]] = [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")

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(
tool_run_cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE
)
if proc.returncode != 0:
raise MiniZincError(message=str(proc.stderr))
return json.loads(proc.stdout)
2 changes: 2 additions & 0 deletions src/minizinc/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
98 changes: 96 additions & 2 deletions src/minizinc/helpers.py
Original file line number Diff line number Diff line change
@@ -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, Iterable, List, Optional, Sequence, Union

import minizinc

Expand Down Expand Up @@ -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))
Expand All @@ -109,3 +109,97 @@ def check_solution(
if status == minizinc.Status.ERROR:
return True
return False


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'
inst[varprevname] = []

# Fix the solution to given once
if sol_fix is not None:
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"]
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":
inst.add_string(f"solve minimize {obj_annots['name']};\n")
else:
inst.add_string(f"solve maximize {obj_annots['name']};\n")
else:
inst.add_string("solve satisfy;\n")

return inst


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
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
Loading