Coverage for moptipy / operators / permutations / op1_swapn.py: 42%

31 statements  

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

1""" 

2An operator swapping several elements in a permutation. 

3 

4This operator works similarly to 

5:class:`~moptipy.operators.permutations.op1_swap2.Op1Swap2`, but instead of 

6swapping two elements (i.e., performing 1 swap), it will perform a random 

7number of swaps. 

8 

9First, the operator copies the source string `x` into the destination `dest`. 

10It then chooses a random index `i1`. It remembers the element at that index 

11in two variables, `last` and `first`. (`last` will always be the value of the 

12element will next be overwritten and `first` will never change and remember 

13the first overwritten element, i.e., the value that we need to store again 

14into the array in the end.) We also initialize a variable `continue_after` 

15with `True`. This variable will always tell us if we need to continue and 

16draw another random index. 

17 

18We then begin with the main loop, which is iterated as long as 

19`continue_after` is `True`. Directly as first action in this loop, we set 

20`continue_after = ri(2) <= 0`. `ri(2)` will return a random integer in 

21`{0, 1}`. `ri(2) <= 0` will therefore be `True` with probability 0.5. 

22 

23We now will draw a new random index `i2`. We load the element at index `i2` 

24into variable `current`. If `current == last`, we immediately go back and 

25draw another random `i2`. The reason is that we want to store `current` at 

26`i1`, i.e., where `last` is currently stored. If `current == last`, this 

27would change nothing. Furthermore, if `continue_after` is `False`, then 

28`current` must also be different from `first`: If we exit the main loop, 

29then we will store `first` into the place where we found `current`. Remember, 

30we will overwrite the element at index `i1` with `current`, so the very first 

31element we overwrite must eventually be stored back into the array. 

32 

33OK, if we have obtained an index `i2` whose corresponding element `current` 

34fulfills all requirements, we can set `dest[i1] = last = current`, i.e., 

35remember `current` in `last` and also store it at index `i1`. Next, we will 

36overwrite the element at index `i2`, so we set `i1 = i2`. If `continue_after` 

37is `True`, the loop will continue. Otherwise, it will stop. 

38 

39In the latter case, we store `first` back into the array at the last index 

40`i1` and do `dest[i1] = first`. 

41 

42As a result, the operator will swap exactly 2 elements with probability 0.5. 

43With probability 0.25, it will swap three elements, with 0.125 probability, it 

44will swap 4 elements, and so on. 

45 

461. Thomas Weise. *Optimization Algorithms.* 2021. Hefei, Anhui, China: 

47 Institute of Applied Optimization (IAO), School of Artificial Intelligence 

48 and Big Data, Hefei University. http://thomasweise.github.io/oa/ 

49""" 

50from typing import Callable, Final 

51 

52import numba # type: ignore 

53import numpy as np 

54from numpy.random import Generator 

55 

56from moptipy.api.operators import Op1 

57 

58 

59@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False) 

60def swap_n(random: Generator, dest: np.ndarray, # +book 

61 x: np.ndarray) -> None: # +book 

62 """ 

63 Copy `x` into `dest` and then swap several different values. 

64 

65 :param random: the random number generator 

66 :param dest: the array to receive the modified copy of `x` 

67 :param x: the existing point in the search space 

68 

69 >>> rand = np.random.default_rng(10) 

70 >>> xx = np.array(range(10), int) 

71 >>> out = np.empty(len(xx), int) 

72 >>> swap_n(rand, out, xx) 

73 >>> print(out) 

74 [0 1 7 3 4 5 6 2 8 9] 

75 >>> swap_n(rand, out, xx) 

76 >>> print(out) 

77 [0 1 8 3 4 5 6 7 2 9] 

78 >>> swap_n(rand, out, xx) 

79 >>> print(out) 

80 [0 5 2 3 4 8 6 7 1 9] 

81 """ 

82 # start book 

83 dest[:] = x[:] # First, we copy `x` to `dest`. 

84 length: Final[int] = len(dest) # Get the length of `dest`. 

85 rint: Callable[[int, int], int] = random.integers # fast call 

86 

87 i1 = rint(0, length) # Get the first random index. 

88 last = first = dest[i1] # Get the value at the first index. 

89 continue_after: bool = True # True -> loop at least once. 

90 while continue_after: # Repeat until we should stop 

91 continue_after = rint(0, 2) <= 0 # 50/50 chance 

92 while True: # Loop forever until eligible element found. 

93 i2 = rint(0, length) # new random index. 

94 current = dest[i2] # Get the value at the new index. 

95 if current == last: # If it is the same as the 

96 continue # previous value, continue. 

97 if continue_after or (current != first): # If we want 

98 break # to stop, then it must be != first value. 

99 dest[i1] = last = current # Store value for from i2 at i1. 

100 i1 = i2 # Update i1 to now point to cell of i2. 

101 dest[i1] = first # Finally, store first element back at end. 

102 

103 

104class Op1SwapN(Op1): 

105 """ 

106 A unary search operation that swaps several (different) elements. 

107 

108 It is similar to `swap2`, but instead may perform a random number 

109 of swaps. After each swap, it decides with probability 0.5 whether 

110 or not to perform another swap. 

111 """ 

112 

113 def __init__(self) -> None: 

114 """Initialize the object.""" 

115 super().__init__() # -book 

116 self.op1 = swap_n # type: ignore # use function directly 

117 # end book 

118 

119 def __str__(self) -> str: 

120 """ 

121 Get the name of this unary operator. 

122 

123 :returns: "swapn", the name of this operator 

124 :retval "swapn": always 

125 """ 

126 return "swapn"