Coverage for moptipy / examples / jssp / experiment.py: 85%

78 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-24 08:49 +0000

1"""Run the moptipy example experiment.""" 

2import argparse 

3import os.path as pp 

4from typing import Any, Callable, Final, Iterable, cast 

5 

6from pycommons.io.path import Path 

7from pycommons.types import check_int_range, type_error 

8 

9import moptipy.api.experiment as ex 

10from moptipy.algorithms.random_sampling import RandomSampling 

11from moptipy.algorithms.random_walk import RandomWalk 

12from moptipy.algorithms.single_random_sample import SingleRandomSample 

13from moptipy.algorithms.so.hill_climber import HillClimber 

14from moptipy.algorithms.so.rls import RLS 

15from moptipy.api.algorithm import Algorithm 

16from moptipy.api.execution import Execution 

17from moptipy.examples.jssp.gantt_space import GanttSpace 

18from moptipy.examples.jssp.instance import Instance 

19from moptipy.examples.jssp.makespan import Makespan 

20from moptipy.examples.jssp.ob_encoding import OperationBasedEncoding 

21from moptipy.operators.permutations.op0_shuffle import Op0Shuffle 

22from moptipy.operators.permutations.op1_insert1 import Op1Insert1 

23from moptipy.operators.permutations.op1_swap2 import Op1Swap2 

24from moptipy.operators.permutations.op1_swapn import Op1SwapN 

25from moptipy.spaces.permutations import Permutations 

26from moptipy.utils.help import moptipy_argparser 

27 

28#: The default instances to be used in our experiment. These have been 

29#: computed via instance_selector.propose_instances. 

30#: The instances in this tuple are sorted by the scale of the search space, 

31#: if that search space is the order-based encoding, i.e., permutations with 

32#: repetitions. This is almost the same as an ordering by the number of 

33#: possible (feasible or infeasible) Gantt charts than can be constructed, with 

34#: one exception: For `dmu67`, the search space size is `2.768*(10**1241)` and 

35#: for `dmu72`, it is `3.862*(10**1226)`, i.e., `dmu67` has a larger search 

36#: order-based encoding search space than `dmu72`. However, for `dmu72`, we can 

37#: construct `1.762*(10**967)` different (feasible or infeasible) Gantt charts, 

38#: whereas for `dmu67`, we can only construct `1.710*(10**958)`, i.e., fewer. 

39#: Therefore, `dmu67` in this ordering here comes after `dmu72`, but it would 

40#: come before if we were sorting instances by the solution space size. 

41INSTANCES: \ 

42 Final[tuple[str, str, str, str, str, str, str, str]] = \ 

43 ("orb06", "la38", "abz8", "yn4", "swv14", "dmu72", "dmu67", "ta70") 

44 

45#: The number of runs per instance in our JSSP experiment. 

46#: For each combination of algorithm setup and JSSP instance, we will 

47#: perform exactly this many runs. 

48EXPERIMENT_RUNS: Final[int] = 23 

49 

50#: We will perform five minutes per run. 

51EXPERIMENT_RUNTIME_MS: Final[int] = 5 * 60 * 1000 

52 

53#: The default set of algorithms for our experiments. 

54#: Each of them is a Callable that receives two parameters, the instance 

55#: `inst` and the permutation with repetitions-space `pwr`. 

56ALGORITHMS: Final[list[ 

57 Callable[[Instance, Permutations], Algorithm]]] = [ 

58 lambda _, pwr: SingleRandomSample(Op0Shuffle(pwr)), # single sample 

59 lambda _, pwr: RandomSampling(Op0Shuffle(pwr)), # random sampling 

60 lambda _, pwr: HillClimber(Op0Shuffle(pwr), Op1Swap2()), # hill climb. 

61 lambda _, pwr: RLS(Op0Shuffle(pwr), Op1Swap2()), # RLS with swap-2 

62 lambda _, pwr: RandomWalk(Op0Shuffle(pwr), Op1Swap2()), # random walk 

63 lambda _, pwr: HillClimber(Op0Shuffle(pwr), Op1SwapN()), # hill climb. 

64 lambda _, pwr: RLS(Op0Shuffle(pwr), Op1SwapN()), # RLS 

65 lambda _, pwr: RandomWalk(Op0Shuffle(pwr), Op1SwapN()), # random walk 

66 lambda _, pwr: HillClimber(Op0Shuffle(pwr), Op1Insert1()), # HC 

67 lambda _, pwr: RLS(Op0Shuffle(pwr), Op1Insert1()), # RLS 

68 lambda _, pwr: RandomWalk(Op0Shuffle(pwr), Op1Insert1()), # random walk 

69] 

70 

71 

72def run_experiment(base_dir: str = pp.join(".", "results"), 

73 algorithms: Iterable[ 

74 Callable[[Instance, Permutations], 

75 Algorithm]] = tuple(ALGORITHMS), 

76 instances: Iterable[str] = INSTANCES, 

77 n_runs: int = EXPERIMENT_RUNS, 

78 max_time: int | None = EXPERIMENT_RUNTIME_MS, 

79 max_fes: int | None = None) -> None: 

80 """ 

81 Run the experiment. 

82 

83 :param algorithms: the algorithm factories. 

84 Each factory receives as input one JSSP `Instance` and one instance 

85 of `PermutationWithRepetition`. It returns either an instance of 

86 `Algorithm` or of `Execution`. 

87 :param instances: the JSSP instance names 

88 :param base_dir: the base directory 

89 :param n_runs: the number of runs 

90 :param max_time: the maximum runtime in milliseconds 

91 :param max_fes: the maximum runtime in FEs 

92 """ 

93 # The initial parameter validity checks. 

94 if not isinstance(base_dir, str): 

95 raise type_error(base_dir, "base_dir", str) 

96 if not isinstance(algorithms, Iterable): 

97 raise type_error(algorithms, "algorithms", Iterable) 

98 if not isinstance(instances, Iterable): 

99 raise type_error(instances, "instances", Iterable) 

100 check_int_range(n_runs, "n_runs", 1, 10_000_000) 

101 if max_time is not None: 

102 check_int_range(max_time, "max_time", 1, 100_000_000_000) 

103 if max_fes is not None: 

104 check_int_range(max_fes, "max_fes", 1, 1_000_000_000_000) 

105 if (max_fes is None) and (max_time is None): 

106 raise ValueError("Either max_fes or max_time must be provided.") 

107 

108 inst_gens = [(lambda s=s: Instance.from_resource(s)) for s in instances] 

109 if len(inst_gens) <= 0: 

110 raise ValueError("There must be at least one instance.") 

111 del instances 

112 

113 # In this loop, we convert the simple algorithm lambdas to execution 

114 # creators. 

115 algo_gens = [] 

116 for algo in algorithms: 

117 # we create one constructor for each algorithm factory 

118 def creator(inst: Instance, algor: Callable = algo) -> Execution: 

119 """ 

120 Create the algorithm for a given instance. 

121 

122 :param inst: the JSSP instance 

123 :param algor: the algorithm creator 

124 :return: an Execution for the experiment 

125 """ 

126 pwr: Permutations = Permutations.with_repetitions( 

127 inst.jobs, inst.machines) 

128 

129 val: Execution | Algorithm = algor(inst, pwr) 

130 experiment: Execution 

131 if isinstance(val, Execution): 

132 experiment = cast("Execution", val) 

133 else: 

134 if not isinstance(val, Algorithm): 

135 raise type_error(val, "result of factory function", 

136 Algorithm) 

137 experiment = Execution() 

138 experiment.set_algorithm(cast("Algorithm", val)) 

139 

140 if max_time is not None: 

141 experiment.set_max_time_millis(max_time) 

142 if max_fes is not None: 

143 experiment.set_max_fes(max_fes) 

144 experiment.set_objective(Makespan(inst)) 

145 experiment.set_solution_space(GanttSpace(inst)) 

146 experiment.set_search_space(pwr) 

147 experiment.set_encoding(OperationBasedEncoding(inst)) 

148 experiment.set_log_improvements() 

149 return experiment 

150 

151 algo_gens.append(creator) # add constructor to generator list 

152 del creator # the creator is no longer needed 

153 del algorithms 

154 

155 if len(algo_gens) <= 0: 

156 raise ValueError("There must be at least one algorithm.") 

157 

158 ikwargs: dict[str, Any] = {"base_dir": base_dir, 

159 "instances": inst_gens, 

160 "setups": algo_gens, 

161 "n_runs": n_runs, 

162 "perform_warmup": True, 

163 "warmup_fes": 20, 

164 "perform_pre_warmup": True, 

165 "pre_warmup_fes": 20} 

166 

167 ex.run_experiment(**ikwargs) # invoke the actual experiment 

168 

169 

170# Execute experiment if run as script 

171if __name__ == "__main__": 

172 parser: Final[argparse.ArgumentParser] = moptipy_argparser( 

173 __file__, "Execute the JSSP example experiment.", 

174 "Execute an example experiment on the Job " 

175 "Shop Scheduling Problem (JSSP).") 

176 parser.add_argument( 

177 "dest", help="the directory where the results should be stored", 

178 type=Path, default="./results", nargs="?") 

179 args: Final[argparse.Namespace] = parser.parse_args() 

180 run_experiment(base_dir=args.dest)