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
« 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
6from pycommons.io.path import Path
7from pycommons.types import check_int_range, type_error
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
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")
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
50#: We will perform five minutes per run.
51EXPERIMENT_RUNTIME_MS: Final[int] = 5 * 60 * 1000
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]
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.
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.")
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
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.
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)
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))
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
151 algo_gens.append(creator) # add constructor to generator list
152 del creator # the creator is no longer needed
153 del algorithms
155 if len(algo_gens) <= 0:
156 raise ValueError("There must be at least one algorithm.")
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}
167 ex.run_experiment(**ikwargs) # invoke the actual experiment
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)