Source code for moptipyapps.binpacking2d.instgen.hardness

"""
An objective function assessing the hardness of an instance.

>>> from moptipyapps.binpacking2d.instgen.instance_space import InstanceSpace
>>> orig = Instance.from_resource("a04")
>>> space = InstanceSpace(orig)
>>> print(f"{space.inst_name!r} with {space.n_different_items}/"
...       f"{space.n_items} items with area {space.total_item_area} "
...       f"in {space.min_bins} bins of "
...       f"size {space.bin_width}*{space.bin_height}.")
'a04n' with 2/16 items with area 7305688 in 3 bins of size 2750*1220.

>>> from moptipyapps.binpacking2d.instgen.inst_decoding import InstanceDecoder
>>> decoder = InstanceDecoder(space)
>>> import numpy as np
>>> x = np.array([ 0.0,  0.2, -0.1,  0.3,  0.5, -0.6, -0.7,  0.9,
...                0.0,  0.2, -0.1,  0.3,  0.5, -0.6, -0.7,  0.9,
...                0.0,  0.2, -0.1,  0.3,  0.5, -0.6, -0.7,  0.9,
...                0.0,  0.2, ])
>>> y = space.create()
>>> decoder.decode(x, y)
>>> space.validate(y)
>>> res: Instance = y[0]
>>> print(f"{res.name!r} with {res.n_different_items}/"
...       f"{res.n_items} items with area {res.total_item_area} "
...       f"in {res.lower_bound_bins} bins of "
...       f"size {res.bin_width}*{res.bin_height}.")
'a04n' with 15/16 items with area 10065000 in 3 bins of size 2750*1220.
>>> print(space.to_str(y))
a04n;15;2750;1220;1101,1098;2750,244;2750,98;1101,171;1649,171;2750,976;\
441,122;1649,122;2750,10;2750,1,2;2750,3;1649,1098;2750,878;2750,58;660,122

>>> hardness = Hardness(max_fes=100)
>>> hardness.lower_bound()
0.0
>>> hardness.upper_bound()
1.0
>>> hardness.evaluate(y)
0.8781459580052053

>>> y[0] = orig
>>> hardness.evaluate(y)
0.9876395399254564

>>> z = Instance.from_compact_str(
...     "cl04_020_01n;19;100;100;1,10;2,38;2,62;1,4,2;3,38;1,7;27,93;1,62;1,"
...     "3;13,38;1,38;1,17;1,45;36,62;39,3;1,2;20,10;3,24;12,4")
>>> hardness.evaluate(z)
0.9995959203471327
"""
from math import isfinite
from typing import Callable, Final, Iterable

from moptipy.algorithms.random_sampling import RandomSampling
from moptipy.algorithms.so.rls import RLS
from moptipy.api.execution import Execution
from moptipy.api.objective import Objective
from moptipy.operators.signed_permutations.op0_shuffle_and_flip import (
    Op0ShuffleAndFlip,
)
from moptipy.operators.signed_permutations.op1_swap_2_or_flip import (
    Op1Swap2OrFlip,
)
from moptipy.spaces.signed_permutations import SignedPermutations
from moptipy.utils.logger import KeyValueLogSection
from moptipy.utils.nputils import rand_seeds_from_str
from pycommons.types import check_int_range

from moptipyapps.binpacking2d.encodings.ibl_encoding_1 import (
    ImprovedBottomLeftEncoding1,
)
from moptipyapps.binpacking2d.instance import (
    Instance,
)
from moptipyapps.binpacking2d.objectives.bin_count import BinCount
from moptipyapps.binpacking2d.objectives.bin_count_and_last_skyline import (
    BinCountAndLastSkyline,
)
from moptipyapps.binpacking2d.packing_space import PackingSpace


[docs] def setup_rls_f7(instance: Instance) -> tuple[Execution, Objective]: """ Set up the randomized local search for an instance. :param instance: the instance :return: the execution and upper bound of the objective """ search_space = SignedPermutations( instance.get_standard_item_sequence()) # Create the search space. solution_space = PackingSpace(instance) # Create the space of packings. objective: Final[BinCountAndLastSkyline] = BinCountAndLastSkyline(instance) # Build a single execution of a single run of a single algorithm and # return the upper bound of the objective value return (Execution() .set_search_space(search_space) .set_solution_space(solution_space) .set_encoding(ImprovedBottomLeftEncoding1(instance)) .set_algorithm( # This is the algorithm: Randomized Local Search. RLS(Op0ShuffleAndFlip(search_space), Op1Swap2OrFlip())) .set_objective(objective), objective)
[docs] def setup_rls_f1(instance: Instance) -> tuple[Execution, Objective]: """ Set up the randomized local search for an instance. :param instance: the instance :return: the execution """ search_space = SignedPermutations( instance.get_standard_item_sequence()) # Create the search space. solution_space = PackingSpace(instance) # Create the space of packings. objective: Final[BinCount] = BinCount(instance) # Build a single execution of a single run of a single algorithm and # return the lower bound of the objective value return (Execution() .set_search_space(search_space) .set_solution_space(solution_space) .set_encoding(ImprovedBottomLeftEncoding1(instance)) .set_algorithm( # This is the algorithm: Randomized Local Search. RLS(Op0ShuffleAndFlip(search_space), Op1Swap2OrFlip())) .set_objective(objective), objective)
[docs] def setup_rs_f1(instance: Instance) -> tuple[Execution, Objective]: """ Set up the randomized sampling for an instance. :param instance: the instance :return: the execution """ search_space = SignedPermutations( instance.get_standard_item_sequence()) # Create the search space. solution_space = PackingSpace(instance) # Create the space of packings. objective: Final[BinCount] = BinCount(instance) # Build a single execution of a single run of a single algorithm and # return the lower bound of the objective value return (Execution() .set_search_space(search_space) .set_solution_space(solution_space) .set_encoding(ImprovedBottomLeftEncoding1(instance)) .set_algorithm( # This is the algorithm: Random Sampling RandomSampling(Op0ShuffleAndFlip(search_space))) .set_objective(objective), objective)
#: the default executors DEFAULT_EXECUTORS: Final[tuple[Callable[[Instance], tuple[ Execution, Objective]], ...]] = ( setup_rs_f1, setup_rls_f1, setup_rls_f7)
[docs] class Hardness(Objective): """Compute the hardness of an instance.""" def __init__( self, max_fes: int = 1_000_000, n_runs: int = 3, executors: Iterable[Callable[[Instance], tuple[ Execution, Objective]]] = DEFAULT_EXECUTORS) -> None: """ Initialize the hardness objective function. :param max_fes: the maximum FEs :param n_runs: the maximum runs :param executors: the functions creating the executions """ super().__init__() #: the maximum FEs per setup. self.max_fes: Final[int] = check_int_range( max_fes, "max_fes", 2, 1_000_000_000_000) #: the maximum FEs per setup. self.n_runs: Final[int] = check_int_range( n_runs, "n_runs", 1, 1_000_000) #: the executors self.executors: Final[tuple[Callable[[Instance], tuple[ Execution, Objective]], ...]] = tuple(executors) #: the last instance name self.__last_inst: str | None = None #: the last seeds name self.__last_seeds: tuple[int, ...] | None = None
[docs] def evaluate(self, x: list[Instance] | Instance) -> float: """ Compute the hardness of an instance. :param x: the instance :return: the hardness """ instance: Final[Instance] = x[0] if isinstance(x, list) else x seeds: tuple[int, ...] name: str = instance.name if (self.__last_seeds is None) or (self.__last_inst is None) or ( self.__last_inst != name): self.__last_seeds = seeds = tuple(rand_seeds_from_str( f"seed for {instance.name}", self.n_runs)) self.__last_inst = name else: seeds = self.__last_seeds max_fes: Final[int] = self.max_fes runs: int = 0 result: float = 0.0 for executor in self.executors: execs, f = executor(instance) lb: int | float = f.lower_bound() ub: int | float = f.upper_bound() if not (isfinite(lb) and isfinite(ub) and (lb < ub)): raise ValueError(f"Invalid lower and upper bound {lb}, {ub}.") execs.set_max_fes(max_fes) for seed in seeds: execs.set_rand_seed(seed) with execs.execute() as proc: runs += 1 quality: int | float = proc.get_best_f() if not (isfinite(quality) and (lb <= quality <= ub)): raise ValueError( f"quality={quality} invalid, must be in " f"[{lb}, {ub}] for objective {f}.") quality = (ub - quality) / (ub - lb) if not (0.0 <= quality <= 1.0): raise ValueError( f"invalid normalized quality {quality} " f"for objective {f}.") runtime: int | float = proc.get_last_improvement_fe() if not (0 < runtime <= max_fes): raise ValueError(f"invalid FEs {runtime}, must " f"be in 1..{max_fes}.") runtime = (max_fes - runtime) / (max_fes - 1) if not (0.0 <= runtime <= 1.0): raise ValueError( f"invalid normalized runtime {runtime}.") result += max(0.0, min(1.0, ( (quality * 1000.0) + runtime) / 1001.0)) return max(0.0, min(1.0, result / runs))
[docs] def lower_bound(self) -> float: """ Get the lower bound of the instance hardness. :return: the lower bound for the instance hardness :returns 0.0: always """ return 0.0
[docs] def upper_bound(self) -> float: """ Get the upper bound of the instance hardness. :return: the upper bound for the instance hardness :returns 1.0: always """ return 1.0
[docs] def is_always_integer(self) -> bool: """ Return `False` because the hardness function returns `float`. :retval False: always """ return False
def __str__(self) -> str: """ Get the name of the hardness objective function. :return: `hardness` :retval "hardness": always """ return "hardness"
[docs] def log_parameters_to(self, logger: KeyValueLogSection) -> None: """ Log the parameters of this instance. :param logger: the logger """ super().log_parameters_to(logger) logger.key_value("nRuns", self.n_runs) logger.key_value("maxFEs", self.max_fes) logger.key_value("nExecutors", len(self.executors))