diff --git a/moptipy/examples/jssp/evaluation.py b/moptipy/examples/jssp/evaluation.py index 88fd0f3dd..29113eb19 100644 --- a/moptipy/examples/jssp/evaluation.py +++ b/moptipy/examples/jssp/evaluation.py @@ -7,12 +7,10 @@ from statistics import median from typing import Callable, Final, Iterable, cast -from moptipy.api.logging import KEY_LAST_IMPROVEMENT_TIME_MILLIS, KEY_TOTAL_FES from moptipy.evaluation.axis_ranger import AxisRanger from moptipy.evaluation.base import TIME_UNIT_FES, TIME_UNIT_MILLIS from moptipy.evaluation.end_results import EndResult from moptipy.evaluation.end_statistics import EndStatistics -from moptipy.evaluation.statistics import KEY_MEAN_ARITH from moptipy.evaluation.tabulate_end_results import ( DEFAULT_ALGORITHM_INSTANCE_STATISTICS, DEFAULT_ALGORITHM_SUMMARY_STATISTICS, @@ -32,7 +30,7 @@ from moptipy.utils.console import logger from moptipy.utils.help import argparser from moptipy.utils.lang import EN -from moptipy.utils.logger import SCOPE_SEPARATOR, sanitize_name +from moptipy.utils.logger import sanitize_name from moptipy.utils.path import Path from moptipy.utils.strings import ( beautify_float_str, @@ -183,18 +181,8 @@ def __ma(mtch: Match, suffix: str) -> str: # fix some names using regular expressions namer: dict[str, str] = {} used_names: set[str] = set(names) - for pattern, repl in [("hc_swap2", "hc"), ("hcr_32768_swap2", "hcr"), - ("hc_swapn", "hcn"), ("hcr_65536_swapn", "hcrn"), - ("rls_swap2", "rls"), ("rls_swapn", "rlsn"), - ("ea_([0-9]+)_([0-9]+)_swap2", "\\1+\\2_ea"), - ("ea_([0-9]+)_([0-9]+)_(0d[0-9]+)_gap_swap2", - __eacr), - ("sa_exp([0-9]+)_([0-9])em([0-9])_swap2", - __sa), - ("marls_([0-9]+)_([0-9]+)_([0-9]+)_gap_swap2", - lambda mm: __ma(mm, "rls")), - ("ma_([0-9]+)_([0-9]+)_([0-9]+)_gap_sa_exp.*", - lambda mm: __ma(mm, "sa"))]: + for pattern, repl in [("hc_swap2", "hc"), ("hc_swapn", "hcn"), + ("rls_swap2", "rls"), ("rls_swapn", "rlsn")]: re: Pattern = _compile(pattern) found = False for s in names: @@ -218,80 +206,6 @@ def __ma(mtch: Match, suffix: str) -> str: if not found: raise ValueError(f"did not find {pattern!r}.") - # fix the basic EA families - ea1p1: Final[int] = names_new.index("ea_1_1_swap2") - ea_families: dict[str, int] = { - NAME_EA_MU_PLUS_MU: ea1p1, - NAME_EA_MU_PLUS_SQRT_MU: ea1p1, - NAME_EA_MU_PLUS_LOG_MU: ea1p1, - NAME_EA_MU_PLUS_1: ea1p1} - for n in names: - if n.startswith("ea_"): - try: - fam = ea_family(n) - except ValueError: - continue - if fam not in ea_families: - if fam in used_names: - raise ValueError(f"duplicated ea family {fam!r}.") - used_names.add(fam) - ea_families[fam] = names_new.index(n) - else: - ea_families[fam] = min(ea_families[fam], names_new.index(n)) - for i, n in sorted([(n[1], n[0]) for n in ea_families.items()], - reverse=True, key=lambda a: a[0]): - names_new.insert(i, n) - - # fix the general EA names - for name in names: - if name.startswith("generalEa_"): - n = name[9:] - has_fitness: bool = False - for fl, fs in (("_direct", "D"), ("_rank", "R")): - if fl in n: - n = n.replace(fl, fs) - has_fitness = True - if not has_fitness: - n = f"RI{n}" - n = n.replace("_tour", "t").replace("_best", "b")\ - .replace("_rndNoRep", "n").replace("_fpsus", "f")\ - .replace("_0d125_gap_swap2", "") - if "_4_4" in n: - n = "ea4" + n.replace("_4_4", "") - elif "_32_32" in n: - n = "ea32" + n.replace("_32_32", "") - else: - raise ValueError(f"invalid general EA: {name!r}.") - os = namer.get(name, None) - if os is not None: - if os == n: - continue - raise ValueError(f"{name!r} -> {n!r}, {os!r}?") - if n in used_names: - raise ValueError(f"Already got {n!r}.") - namer[name] = n - names_new.insert(names_new.index(name), n) - - # do sa families - sa_done: set[str] = set() - for name in names: - if name.startswith("sa_"): - n = sa_family(name) - if n in sa_done: - continue - sa_done.add(n) - names_new.insert(names_new.index(name), n) - - # do ma families - ma_done: set[str] = set() - for name in names: - if name.startswith(("ma_", "marls_")): - n = ma_family(name) - if n in ma_done: - continue - ma_done.add(n) - names_new.insert(names_new.index(name), n) - return {__n: __i for __i, __n in enumerate(names_new)}, namer @@ -674,175 +588,6 @@ def evaluate_experiment(results_dir: str = pp.join(".", "results"), progress(["hc_swap2", "rs"], dest, source) progress(["hc_swap2", "rs"], dest, source, millis=False) - logger("Now evaluating the hill climbing algorithm with " - "restarts 'hcr' on 'swap2'.") - makespans_over_param( - end_results, - lambda an: an.startswith("hcr_") and an.endswith("_swap2"), - lambda es: int(es.algorithm.split("_")[1]), - "hcr_L_swap2", dest, - lambda: AxisRanger(log_scale=True, log_base=2.0), "L") - table(end_results, ["hcr_32768_swap2", "hc_swap2", "rs"], dest) - makespans(end_results, ["hcr_32768_swap2", "hc_swap2", "rs"], dest) - gantt(end_results, "hcr_32768_swap2", dest, source) - progress(["hcr_32768_swap2", "hc_swap2", "rs"], dest, source) - - logger("Now evaluating the hill climbing algorithm with 'swapn'.") - table(end_results, ["hc_swapn", "hcr_32768_swap2", "hc_swap2"], dest) - makespans(end_results, ["hc_swapn", "hcr_32768_swap2", "hc_swap2"], dest) - progress(["hc_swapn", "hcr_32768_swap2", "hc_swap2"], dest, source) - progress(["hc_swapn", "hcr_32768_swap2", "hc_swap2"], dest, source, - millis=False) - - logger("Now evaluating the hill climbing algorithm with " - "restarts 'hcr' on 'swapn'.") - makespans_over_param( - end_results, - lambda an: an.startswith("hcr_") and an.endswith("_swapn"), - lambda es: int(es.algorithm.split("_")[1]), - "hcr_L_swapn", dest, - lambda: AxisRanger(log_scale=True, log_base=2.0), "L") - table(end_results, ["hcr_65536_swapn", "hc_swapn", - "hcr_32768_swap2"], dest) - makespans(end_results, ["hcr_65536_swapn", "hc_swapn", - "hcr_32768_swap2"], dest) - progress(["hcr_65536_swapn", "hc_swapn", - "hcr_32768_swap2"], dest, source) - tests(end_results, ["hcr_65536_swapn", "hcr_32768_swap2", - "hc_swapn"], dest) - - logger("Now evaluating the RLS algorithm with 'swap2' and 'swapn'.") - table(end_results, ["rls_swapn", "rls_swap2", - "hcr_32768_swap2", "hcr_65536_swapn"], dest) - tests(end_results, ["rls_swapn", "rls_swap2", "hcr_32768_swap2"], dest) - makespans(end_results, ["rls_swapn", "rls_swap2", "hcr_32768_swap2", - "hcr_65536_swapn"], dest) - gantt(end_results, "rls_swap2", dest, source) - progress(["rls_swapn", "rls_swap2", "hcr_32768_swap2", - "hcr_65536_swapn"], dest, source) - gantt(end_results, "rls_swap2", dest, source, True, ["ta70"]) - - logger("Now evaluating EA without crossover.") - - makespans_over_param( - end_results=end_results, - selector=lambda n: n.startswith("ea_") and n.split("_")[3] == "swap2", - x_getter=lambda es: int(es.algorithm.split("_")[1]), - name_base="ea_no_cr", - algo_getter=ea_family, - title=f"{LETTER_M}+{LETTER_L}_ea", x_label=LETTER_M, - title_x=0.8, - x_axis=AxisRanger(log_scale=True, log_base=2.0), - legend_pos="upper center", - dest_dir=dest) - lims: Final[str] = (f"{KEY_LAST_IMPROVEMENT_TIME_MILLIS}{SCOPE_SEPARATOR}" - f"{KEY_MEAN_ARITH}") - totfes: Final[str] = f"{KEY_TOTAL_FES}{SCOPE_SEPARATOR}{KEY_MEAN_ARITH}" - table(end_results, ["ea_1_2_swap2", "ea_2_2_swap2", "ea_2_4_swap2", - "ea_512_512_swap2", "rls_swap2"], dest, - swap_stats=[(lims, totfes)]) - tests(end_results, ["ea_1_2_swap2", "ea_2_2_swap2", "rls_swap2"], dest) - makespans(end_results, ["ea_1_2_swap2", "ea_2_2_swap2", - "ea_512_512_swap2", "rls_swap2"], dest, 0.6) - progress(["ea_1_2_swap2", "ea_2_2_swap2", "ea_2_4_swap2", - "ea_64_1_swap2", "ea_1024_1024_swap2", "ea_8192_65536_swap2", - "rls_swap2"], - dest, source) - - logger("Now evaluating EA with crossover.") - makespans_over_param( - end_results=end_results, - selector=lambda n: n.startswith("ea_") and n.endswith("_gap_swap2"), - x_getter=lambda er: name_str_to_num(er.algorithm.split("_")[3]), - name_base="ea_cr", - algo_getter=ea_family, - title=f"{LETTER_M}+{LETTER_M}_ea_br", x_label="br", - x_axis=AxisRanger(log_scale=True, log_base=2.0), - legend_pos="upper right", - dest_dir=dest) - table(end_results, ["ea_2_2_0d001953125_gap_swap2", "ea_2_2_swap2", - "rls_swap2"], dest) - tests(end_results, ["ea_2_2_0d001953125_gap_swap2", "ea_2_2_swap2", - "rls_swap2"], dest) - progress(["ea_2_2_0d001953125_gap_swap2", "ea_2_2_swap2", "rls_swap2", - "ea_256_256_swap2", "ea_256_256_0d25_gap_swap2", - "ea_32_32_swap2", "ea_32_32_0d125_gap_swap2"], - dest, source) - - logger("Now evaluating the general EA.") - for s in ["_4_4", "_32_32"]: - selected = [y for y in ALL_NAMES if (s in y) - and y.startswith("generalEa_")] - selected.append("rls_swap2") - selected.append(f"ea{s}_0d125_gap_swap2") - progress(selected, dest, source, millis=False) - - selected = [y for y in ALL_NAMES if y.startswith("generalEa_")] - selected.append("rls_swap2") - selected.append("ea_4_4_0d125_gap_swap2") - selected.append("ea_32_32_0d125_gap_swap2") - table(end_results, selected, dest, swap_stats=[(lims, totfes)]) - - logger("now preparing to evaluate simulated annealing") - EN.set_current() - stats = ["bestF.sd", "lastImprovementFE.med", "totalFEs.med"] - tabulate_end_results( - end_results=get_end_results(end_results, algos={"rls_swap2"}), - file_name="end_stats_rls_2", dir_name=dest, - instance_sort_key=instance_sort_key, - algorithm_sort_key=algorithm_sort_key, - col_namer=command_column_namer, - algorithm_namer=algorithm_namer, - algorithm_instance_statistics=stats, - algorithm_summary_statistics=stats, - put_lower_bound=False, - use_lang=False) - - logger("now evaluating simulated annealing") - makespans_over_param( - end_results=end_results, - selector=lambda n: n.startswith("sa_") and n.endswith("_swap2") and ( - "1em7" not in n), - x_getter=lambda er: name_str_to_num(er.algorithm.split("_")[1][3:]), - name_base="sa_exp", - algo_getter=sa_family, - title="sa_T\u2080_\u03b5", x_label="T\u2080", - x_axis=AxisRanger(log_scale=True, log_base=2.0), - legend_pos="upper center", - title_x=0.8, - dest_dir=dest) - table(end_results, ["sa_exp16_1em6_swap2", "rls_swap2"], dest) - tests(end_results, ["sa_exp16_1em6_swap2", "rls_swap2"], dest) - makespans(end_results, ["sa_exp16_1em6_swap2", "rls_swap2"], dest, 0.72) - gantt(end_results, "sa_exp16_1em6_swap2", dest, source) - progress(["sa_exp16_1em6_swap2", "sa_exp16_1em7_swap2", - "sa_exp16_1em5_swap2", "rls_swap2"], dest, source) - gantt(end_results, "sa_exp16_1em6_swap2", dest, source, True, ["orb06"]) - - logger("now evaluating memetic algorithm") - table(end_results, ["ma_2_2_1048576_gap_sa_exp16_5d1em6_swap2", - "marls_8_8_16_gap_swap2", - "sa_exp16_1em6_swap2"], dest, - swap_stats=[(lims, totfes)]) - makespans_over_param( - end_results=end_results, - selector=lambda n: n.startswith("ma"), - x_getter=lambda er: name_str_to_num(er.algorithm.split("_")[3]), - name_base="ma_", - algo_getter=ma_family, - title=f"{LETTER_M}+{LETTER_L}_ma{{rls|sa}}", x_label="ls_fes", - x_axis=AxisRanger(log_scale=True, log_base=2.0), - legend_pos="lower center", - title_x=0.8, - y_label_location=0.1, - dest_dir=dest) - progress(["marls_8_8_16_gap_swap2", - "ma_2_2_1048576_gap_sa_exp16_5d1em6_swap2", - "sa_exp16_1em6_swap2", "ea_2_2_swap2", - "ea_8_8_swap2"], dest, source) - makespans(end_results, ["ma_2_2_1048576_gap_sa_exp16_5d1em6_swap2", - "sa_exp16_1em6_swap2"], dest, 0.72) - logger(f"Finished evaluation from {source!r} to {dest!r}.") diff --git a/moptipy/examples/jssp/experiment.py b/moptipy/examples/jssp/experiment.py index f2c30fd19..f1f6a40d5 100644 --- a/moptipy/examples/jssp/experiment.py +++ b/moptipy/examples/jssp/experiment.py @@ -4,46 +4,21 @@ from typing import Any, Callable, Final, Iterable, cast import moptipy.api.experiment as ex -from moptipy.algorithms.modules.selections.fitness_proportionate_sus import ( - FitnessProportionateSUS, -) -from moptipy.algorithms.modules.selections.tournament_with_repl import ( - TournamentWithReplacement, -) -from moptipy.algorithms.modules.selections.tournament_without_repl import ( - TournamentWithoutReplacement, -) -from moptipy.algorithms.modules.temperature_schedule import ExponentialSchedule from moptipy.algorithms.random_sampling import RandomSampling from moptipy.algorithms.random_walk import RandomWalk from moptipy.algorithms.single_random_sample import SingleRandomSample -from moptipy.algorithms.so.ea import EA -from moptipy.algorithms.so.fitnesses.direct import Direct -from moptipy.algorithms.so.fitnesses.rank import Rank -from moptipy.algorithms.so.general_ea import GeneralEA from moptipy.algorithms.so.hill_climber import HillClimber -from moptipy.algorithms.so.hill_climber_with_restarts import ( - HillClimberWithRestarts, -) -from moptipy.algorithms.so.ma import MA -from moptipy.algorithms.so.marls import MARLS -from moptipy.algorithms.so.ppa import PPA from moptipy.algorithms.so.rls import RLS -from moptipy.algorithms.so.simulated_annealing import SimulatedAnnealing from moptipy.api.algorithm import Algorithm from moptipy.api.execution import Execution from moptipy.examples.jssp.gantt_space import GanttSpace from moptipy.examples.jssp.instance import Instance from moptipy.examples.jssp.makespan import Makespan from moptipy.examples.jssp.ob_encoding import OperationBasedEncoding -from moptipy.operators.op0_forward import Op0Forward from moptipy.operators.permutations.op0_shuffle import Op0Shuffle +from moptipy.operators.permutations.op1_insert1 import Op1Insert1 from moptipy.operators.permutations.op1_swap2 import Op1Swap2 -from moptipy.operators.permutations.op1_swap_try_n import Op1SwapTryN from moptipy.operators.permutations.op1_swapn import Op1SwapN -from moptipy.operators.permutations.op2_gap import ( - Op2GeneralizedAlternatingPosition, -) from moptipy.spaces.permutations import Permutations from moptipy.utils.help import argparser from moptipy.utils.path import Path @@ -71,8 +46,8 @@ #: perform exactly this many runs. EXPERIMENT_RUNS: Final[int] = 23 -#: We will perform two minutes per run. -EXPERIMENT_RUNTIME_MS: Final[int] = 2 * 60 * 1000 +#: We will perform five minutes per run. +EXPERIMENT_RUNTIME_MS: Final[int] = 5 * 60 * 1000 #: The default set of algorithms for our experiments. #: Each of them is a Callable that receives two parameters, the instance @@ -82,120 +57,15 @@ lambda inst, pwr: SingleRandomSample(Op0Shuffle(pwr)), # single sample lambda inst, pwr: RandomSampling(Op0Shuffle(pwr)), # random sampling lambda inst, pwr: HillClimber(Op0Shuffle(pwr), Op1Swap2()), # hill climb. - lambda inst, pwr: RLS(Op0Shuffle(pwr), Op1Swap2()), # RLS + lambda inst, pwr: RLS(Op0Shuffle(pwr), Op1Swap2()), # RLS with swap-2 lambda inst, pwr: RandomWalk(Op0Shuffle(pwr), Op1Swap2()), # random walk lambda inst, pwr: HillClimber(Op0Shuffle(pwr), Op1SwapN()), # hill climb. lambda inst, pwr: RLS(Op0Shuffle(pwr), Op1SwapN()), # RLS + lambda inst, pwr: RandomWalk(Op0Shuffle(pwr), Op1SwapN()), # random walk + lambda inst, pwr: HillClimber(Op0Shuffle(pwr), Op1Insert1()), # HC + lambda inst, pwr: RLS(Op0Shuffle(pwr), Op1Insert1()), # RLS + lambda inst, pwr: RandomWalk(Op0Shuffle(pwr), Op1Insert1()), # random walk ] -for scale in range(7, 21): # add the hill climbers with restarts - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, i=scale: HillClimberWithRestarts( - Op0Shuffle(pwr), Op1Swap2(), 2 ** i))) # hill clim. with restarts - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, i=scale: HillClimberWithRestarts( - Op0Shuffle(pwr), Op1SwapN(), 2 ** i))) # hill clim. with restarts -for log2_mu in range(14): - mu: int = 2 ** log2_mu - for lambda_ in sorted({1, max(1, log2_mu), round(mu ** 0.5), mu, mu + mu, - 4 * mu, 8 * mu}): - if lambda_ > 65536: - continue - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, mm=mu, ll=lambda_: EA( - Op0Shuffle(pwr), Op1Swap2(), None, mm, ll, 0.0))) - if (mu == lambda_) and (mu in {2, 4, 32, 256, 4096}): - for br_exp in range(1, 11): - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, ml=mu, br=(2 ** -br_exp): EA( - Op0Shuffle(pwr), Op1Swap2(), - Op2GeneralizedAlternatingPosition(pwr), ml, ml, br))) - for br_exp in range(2, 11): - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, ml=mu, br=1.0 - (2 ** -br_exp): EA( - Op0Shuffle(pwr), Op1Swap2(), - Op2GeneralizedAlternatingPosition(pwr), ml, ml, br))) - -for mu_lambda in [4, 32]: - for ts in [2, 4]: - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, ml=mu_lambda, sze=ts: GeneralEA( - Op0Shuffle(pwr), Op1Swap2(), - Op2GeneralizedAlternatingPosition(pwr), ml, ml, 2 ** -3, - survival=TournamentWithReplacement(sze)))) - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, ml=mu_lambda, sze=ts: GeneralEA( - Op0Shuffle(pwr), Op1Swap2(), - Op2GeneralizedAlternatingPosition(pwr), ml, ml, 2 ** -3, - survival=TournamentWithoutReplacement(sze)))) - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, ml=mu_lambda: GeneralEA( - Op0Shuffle(pwr), Op1Swap2(), - Op2GeneralizedAlternatingPosition(pwr), ml, ml, 2 ** -3, - mating=TournamentWithoutReplacement(2)))) - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, ml=mu_lambda: GeneralEA( - Op0Shuffle(pwr), Op1Swap2(), - Op2GeneralizedAlternatingPosition(pwr), ml, ml, 2 ** -3, - fitness=Direct(), survival=FitnessProportionateSUS()))) - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, ml=mu_lambda: GeneralEA( - Op0Shuffle(pwr), Op1Swap2(), - Op2GeneralizedAlternatingPosition(pwr), ml, ml, 2 ** -3, - fitness=Direct(), survival=FitnessProportionateSUS(), - mating=TournamentWithoutReplacement(2)))) - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, ml=mu_lambda: GeneralEA( - Op0Shuffle(pwr), Op1Swap2(), - Op2GeneralizedAlternatingPosition(pwr), ml, ml, 2 ** -3, - fitness=Rank(), survival=FitnessProportionateSUS(), - mating=TournamentWithoutReplacement(2)))) - -for t0 in [2.0, 4.0, 8.0, 13.0, 16.0, 32.0, 44.0, 64.0, 128.0, 148.0, 256.0]: - for epsilon in [1e-7, 5e-7, 1e-6, 5e-6, 1e-5]: - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, t=t0, e=epsilon: SimulatedAnnealing( - Op0Shuffle(pwr), Op1Swap2(), - ExponentialSchedule(t, e)))) - -for mu_lambda in [2, 8, 32]: - for ls_steps in [2 ** i for i in range(24)]: - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, ml=mu_lambda, lss=ls_steps: - MARLS(Op0Shuffle(pwr), Op1Swap2(), - Op2GeneralizedAlternatingPosition(pwr), - ml, ml, lss))) - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, ml=mu_lambda, lss=ls_steps: - MA(Op0Shuffle(pwr), - Op2GeneralizedAlternatingPosition(pwr), - SimulatedAnnealing( - Op0Forward(), Op1Swap2(), - ExponentialSchedule(16.0, max(1e-7, round( - 1e7 * (1 - ((0.22 / 16.0) ** ( - 1.0 / (0.8 * lss))))) / 1e7))), - ml, ml, lss))) - -for m in [1, 2, 4, 8, 16, 32]: - for nmax in [1, 2, 4, 8, 16, 32]: - for max_step in [1.0, 0.5, 0.3, 0.0]: - ALGORITHMS.append(cast( - Callable[[Instance, Permutations], Algorithm], - lambda inst, pwr, mm=m, ll=nmax, ms=max_step: PPA( - Op0Shuffle(pwr), Op1SwapTryN(pwr), mm, ll, ms))) def run_experiment(base_dir: str = pp.join(".", "results"), diff --git a/moptipy/operators/permutations/op1_insert1.py b/moptipy/operators/permutations/op1_insert1.py index 729b86385..5fdc10059 100644 --- a/moptipy/operators/permutations/op1_insert1.py +++ b/moptipy/operators/permutations/op1_insert1.py @@ -59,7 +59,7 @@ 2016, Denver, CO, USA, pages 57-58, New York, NY, USA: ACM. ISBN: 978-1-4503-4323-7. https://doi.org/10.1145/2908961.2909001 """ -from typing import Final +from typing import Callable, Final import numba # type: ignore import numpy as np @@ -69,7 +69,7 @@ @numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False) -def _rotate(arr: np.ndarray, i1: int, i2: int) -> bool: +def try_single_rotate(arr: np.ndarray, i1: int, i2: int) -> bool: # +book """ Rotate a portion of an array to the left or right in place. @@ -100,107 +100,110 @@ def _rotate(arr: np.ndarray, i1: int, i2: int) -> bool: >>> dest = npx.array(range(10)) >>> print(dest) [0 1 2 3 4 5 6 7 8 9] - >>> _rotate(dest, 3, 4) + >>> try_single_rotate(dest, 3, 4) False >>> print(dest) [0 1 2 4 3 5 6 7 8 9] - >>> _rotate(dest, 3, 4) + >>> try_single_rotate(dest, 3, 4) False >>> print(dest) [0 1 2 3 4 5 6 7 8 9] - >>> _rotate(dest, 4, 3) + >>> try_single_rotate(dest, 4, 3) False >>> print(dest) [0 1 2 4 3 5 6 7 8 9] - >>> _rotate(dest, 4, 3) + >>> try_single_rotate(dest, 4, 3) False >>> print(dest) [0 1 2 3 4 5 6 7 8 9] - >>> _rotate(dest, 3, 6) + >>> try_single_rotate(dest, 3, 6) False >>> print(dest) [0 1 2 4 5 6 3 7 8 9] - >>> _rotate(dest, 6, 3) + >>> try_single_rotate(dest, 6, 3) False >>> print(dest) [0 1 2 3 4 5 6 7 8 9] - >>> _rotate(dest, 0, len(dest) - 1) + >>> try_single_rotate(dest, 0, len(dest) - 1) False >>> print(dest) [1 2 3 4 5 6 7 8 9 0] - >>> _rotate(dest, len(dest) - 1, 0) + >>> try_single_rotate(dest, len(dest) - 1, 0) False >>> print(dest) [0 1 2 3 4 5 6 7 8 9] - >>> _rotate(dest, 7, 7) + >>> try_single_rotate(dest, 7, 7) True >>> dest = np.array([0, 1, 2, 3, 3, 3, 3, 3, 8, 9]) - >>> _rotate(dest, 7, 7) + >>> try_single_rotate(dest, 7, 7) True - >>> _rotate(dest, 4, 6) + >>> try_single_rotate(dest, 4, 6) True >>> print(dest) [0 1 2 3 3 3 3 3 8 9] - >>> _rotate(dest, 6, 4) + >>> try_single_rotate(dest, 6, 4) True >>> print(dest) [0 1 2 3 3 3 3 3 8 9] - >>> _rotate(dest, 4, 7) + >>> try_single_rotate(dest, 4, 7) True >>> print(dest) [0 1 2 3 3 3 3 3 8 9] - >>> _rotate(dest, 6, 7) + >>> try_single_rotate(dest, 6, 7) True >>> print(dest) [0 1 2 3 3 3 3 3 8 9] - >>> _rotate(dest, 4, 8) + >>> try_single_rotate(dest, 4, 8) False >>> print(dest) [0 1 2 3 3 3 3 8 3 9] - >>> _rotate(dest, 8, 4) + >>> try_single_rotate(dest, 8, 4) False >>> print(dest) [0 1 2 3 3 3 3 3 8 9] - >>> _rotate(dest, 9, 4) + >>> try_single_rotate(dest, 9, 4) False >>> print(dest) [0 1 2 3 9 3 3 3 3 8] - >>> _rotate(dest, 4, 9) + >>> try_single_rotate(dest, 4, 9) False >>> print(dest) [0 1 2 3 3 3 3 3 8 9] """ + # start book if i1 == i2: # nothing to be done return True # array will not be changed - unchanged: bool = True # was the array unchanged? + unchanged: bool = True # initially, assume that there is no change - if i1 < i2: # rotate left - first = arr[i1] - while i1 < i2: - i3 = i1 + 1 - cpy = arr[i3] - unchanged = unchanged and (cpy == arr[i1]) - arr[i1] = cpy - i1 = i3 - unchanged = unchanged and (first == arr[i2]) - arr[i2] = first - return unchanged + if i1 < i2: # rotate to the left: move elements to lower indices? + first = arr[i1] # get the element to be removed + while i1 < i2: # iterate the indices + i3 = i1 + 1 # get next higher index + cpy = arr[i3] # get next element at that higher index + unchanged = unchanged and (cpy == arr[i1]) # is a change? + arr[i1] = cpy # store next element at the lower index + i1 = i3 # move to next higher index + unchanged = unchanged and (first == arr[i2]) # check if change + arr[i2] = first # store removed element at highest index + return unchanged # return True if something changed, else False - last = arr[i1] - while i2 < i1: # rotate right - i3 = i1 - 1 - cpy = arr[i3] - unchanged = unchanged and (cpy == arr[i1]) - arr[i1] = cpy - i1 = i3 - unchanged = unchanged and (last == arr[i2]) - arr[i2] = last - return unchanged + last = arr[i1] # last element; rotate right: move elements up + while i2 < i1: # iterate over indices + i3 = i1 - 1 # get next lower index + cpy = arr[i3] # get element at that lower index + unchanged = unchanged and (cpy == arr[i1]) # is a change? + arr[i1] = cpy # store element at higher index + i1 = i3 # move to next lower index + unchanged = unchanged and (last == arr[i2]) # check if change + arr[i2] = last # store removed element at lowest index + return unchanged # return True if something changed, else False +# end book # Temporary fix for https://github.com/numba/numba/issues/9103 -def rotate(random: Generator, dest: np.ndarray, x: np.ndarray) -> None: +def rotate(random: Generator, dest: np.ndarray, # +book + x: np.ndarray) -> None: # +book """ Copy `x` into `dest` and then rotate a subsequence by one step. @@ -224,13 +227,14 @@ def rotate(random: Generator, dest: np.ndarray, x: np.ndarray) -> None: >>> print(out) [0 1 2 3 4 8 5 6 7 9] """ + # start book dest[:] = x[:] length: Final[int] = len(dest) # Get the length of `dest`. + rint: Callable[[int, int], int] = random.integers # fast call # try to rotate the dest array until something changes - while _rotate(dest, random.integers(0, length), - random.integers(0, length)): - pass + while try_single_rotate(dest, rint(0, length), rint(0, length)): + pass # do nothing in the loop, but try rotating again class Op1Insert1(Op1): @@ -250,6 +254,7 @@ def __init__(self) -> None: """Initialize the object.""" super().__init__() self.op1 = rotate # type: ignore # use function directly +# end book def __str__(self) -> str: """ diff --git a/moptipy/operators/permutations/op1_swap2.py b/moptipy/operators/permutations/op1_swap2.py index dde12a52a..e12609d42 100644 --- a/moptipy/operators/permutations/op1_swap2.py +++ b/moptipy/operators/permutations/op1_swap2.py @@ -46,7 +46,7 @@ This operator performs one swap. It is similar to :class:`~moptipy.operators.\ permutations.op1_swapn.Op1SwapN`, which performs a random number of swaps. """ -from typing import Final +from typing import Callable, Final import numpy as np from numpy.random import Generator @@ -86,11 +86,12 @@ def swap_2(random: Generator, dest: np.ndarray, # +book # start book dest[:] = x[:] # First, we copy `x` to `dest`. length: Final[int] = len(dest) # Get the length of `dest`. + rint: Callable[[int, int], int] = random.integers # fast call - i1: Final[int] = random.integers(0, length) # first random index + i1: Final[int] = rint(0, length) # first random index v1: Final = dest[i1] # Get the value at the first index. while True: # Repeat until we find a different value. - i2: int = random.integers(0, length) # second random index + i2: int = rint(0, length) # second random index v2 = dest[i2] # Get the value at the second index. if v1 != v2: # If both values different... dest[i2] = v1 # store v1 where v2 was diff --git a/moptipy/operators/permutations/op1_swapn.py b/moptipy/operators/permutations/op1_swapn.py index 6922f4818..c9d01568c 100644 --- a/moptipy/operators/permutations/op1_swapn.py +++ b/moptipy/operators/permutations/op1_swapn.py @@ -47,7 +47,7 @@ Institute of Applied Optimization (IAO), School of Artificial Intelligence and Big Data, Hefei University. http://thomasweise.github.io/oa/ """ -from typing import Final +from typing import Callable, Final import numpy as np from numpy.random import Generator @@ -81,14 +81,15 @@ def swap_n(random: Generator, dest: np.ndarray, # +book # start book dest[:] = x[:] # First, we copy `x` to `dest`. length: Final[int] = len(dest) # Get the length of `dest`. + rint: Callable[[int, int], int] = random.integers # fast call i1: int = random.integers(0, length) # Get the first random index. last = first = dest[i1] # Get the value at the first index. continue_after: bool = True # True -> loop at least once. while continue_after: # Repeat until we should stop - continue_after = random.integers(0, 2) <= 0 # 50/50 chance + continue_after = rint(0, 2) <= 0 # 50/50 chance while True: # Loop forever until eligible element found. - i2: int = random.integers(0, length) # new random index. + i2: int = rint(0, length) # new random index. current = dest[i2] # Get the value at the new index. if current == last: # If it is the same as the continue # previous value, continue. diff --git a/moptipy/operators/permutations/op2_ox2.py b/moptipy/operators/permutations/op2_ox2.py index ab13de730..4a7e5f573 100644 --- a/moptipy/operators/permutations/op2_ox2.py +++ b/moptipy/operators/permutations/op2_ox2.py @@ -68,7 +68,7 @@ def op2(self, random: Generator, dest: np.ndarray, indices: Final[np.ndarray] = self.__indices x1_done: Final[np.ndarray] = self.__x1_done x1_done.fill(False) # all values in x1 are available - ri: Final[Callable[[int], int]] = random.integers + rint: Final[Callable[[int], int]] = random.integers rbin: Final[Callable[[int, float], int]] = random.binomial length: Final[int] = len(indices) # get length of string copy_from_x0: int # the end index of copying from x0 @@ -85,7 +85,7 @@ def op2(self, random: Generator, dest: np.ndarray, mode: bool = True # mode: True = copy from x0, False = from x1 x1i: int = 0 # the index of the next unused value from x1 while True: # loop until we are finished - index_i: int = ri(i) # pick a random index-index + index_i: int = rint(i) # pick a random index-index index: int = indices[index_i] # load the actual index i = i - 1 # reduce the number of values indices[i], indices[index_i] = index, indices[i] # swap diff --git a/moptipy/operators/signed_permutations/op1_swap_2_or_flip.py b/moptipy/operators/signed_permutations/op1_swap_2_or_flip.py index 1fd6a5c98..4a1ab4315 100644 --- a/moptipy/operators/signed_permutations/op1_swap_2_or_flip.py +++ b/moptipy/operators/signed_permutations/op1_swap_2_or_flip.py @@ -5,7 +5,7 @@ operator which *only* swaps elements (for :mod:`~moptipy.spaces.permutations`) is defined in :mod:`~moptipy.operators.permutations.op1_swap2`. """ -from typing import Final +from typing import Callable, Final # = import numba # type: ignore import numpy as np @@ -48,11 +48,12 @@ def swap_2_or_flip(random: Generator, dest: np.ndarray, """ dest[:] = x[:] # First, we copy `x` to `dest`. length: Final[int] = len(dest) # Get the length of `dest`. + rint: Callable[[int, int], int] = random.integers # fast call - i1: Final[int] = random.integers(0, length) # first random index. + i1: Final[int] = rint(0, length) # first random index. v1: Final = dest[i1] # Get the value at the first index. - if random.integers(0, 2) == 0: # With p=0.5, we flip the sign of v1. + if rint(0, 2) == 0: # With p=0.5, we flip the sign of v1. dest[i1] = -v1 # Flip the sign of v1 and store it back at i1. return # Quit. @@ -61,7 +62,7 @@ def swap_2_or_flip(random: Generator, dest: np.ndarray, # possible that all values currently are the same. To avoid an endless loop, # we therefore use a sufficiently large range. for _ in range(10 + length): - i2: int = random.integers(0, length) # Get the second random index. + i2: int = rint(0, length) # Get the second random index. v2 = dest[i2] # Get the value at the second index. if v1 != v2: # If both values different... dest[i2] = v1 # store v1 where v2 was diff --git a/moptipy/version.py b/moptipy/version.py index 1414912b8..979f68e71 100644 --- a/moptipy/version.py +++ b/moptipy/version.py @@ -2,4 +2,4 @@ from typing import Final #: the version string of `moptipy` -__version__: Final[str] = "0.9.101" +__version__: Final[str] = "0.9.102" diff --git a/tests/operators/permutations/test_op1_insert1.py b/tests/operators/permutations/test_op1_insert1.py index 2a3498695..bc875af14 100644 --- a/tests/operators/permutations/test_op1_insert1.py +++ b/tests/operators/permutations/test_op1_insert1.py @@ -5,7 +5,10 @@ from numpy.random import default_rng # noinspection PyProtectedMember -from moptipy.operators.permutations.op1_insert1 import Op1Insert1, _rotate +from moptipy.operators.permutations.op1_insert1 import ( + Op1Insert1, + try_single_rotate, +) from moptipy.tests.on_permutations import validate_op1_on_permutations @@ -38,7 +41,7 @@ def test_rotate() -> None: dst1[k + 1:j + 1] = dst1[k:j] dst1[k] = v - res = _rotate(dst2, j, k) + res = try_single_rotate(dst2, j, k) assert res == all(dst1 == x) if j == k: assert res