Skip to content

Commit

Permalink
now using numba jit again for many operators
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasWeise committed Jan 30, 2025
1 parent cb41a40 commit b81bd99
Show file tree
Hide file tree
Showing 11 changed files with 151 additions and 245 deletions.
5 changes: 2 additions & 3 deletions moptipy/operators/bitstrings/op1_flip_m.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,8 @@ def op1(self, random: Generator, dest: np.ndarray, x: np.ndarray,
"""
np.copyto(dest, x) # copy source to destination
n: Final[int] = len(dest) # get the number of bits
flips: Final[np.ndarray] = random.choice( # choose the bits
n, exponential_step_size(step_size, 1, n), False)
dest[flips] ^= True # flip the selected bits via xor
dest[random.choice(n, exponential_step_size(
step_size, 1, n), False)] ^= True # flip the selected bits via xor

def __str__(self) -> str:
"""
Expand Down
69 changes: 44 additions & 25 deletions moptipy/operators/bitstrings/op1_m_over_n_flip.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""A unary operator flipping each bit with probability m/n."""
from typing import Callable, Final
from typing import Final

import numba # type: ignore
import numpy as np
from numpy.random import Generator
from pycommons.types import check_int_range, type_error
Expand All @@ -12,6 +13,46 @@
)


@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False)
def _op1_movern(m: int, none_is_ok: bool, permutation: np.ndarray,
random: Generator, dest: np.ndarray, x: np.ndarray) -> None:
"""
Copy `x` into `dest` and flip each bit with probability m/n.
This method will first copy `x` to `dest`. Then it will flip each bit
in `dest` with probability `m/n`, where `n` is the length of `dest`.
Regardless of the probability, at least one bit will always be
flipped if self.at_least_1 is True.
:param m: the value of m
:param none_is_ok: is it OK to flip nothing?
:param permutation: the internal permutation
:param random: the random number generator
:param dest: the destination array to receive the new point
:param x: the existing point in the search space
"""
dest[:] = x[:] # copy source to destination
length: Final[int] = len(dest) # get n
p: Final[float] = m / length # probability to flip bit

flips: int # the number of bits to flip
while True:
flips = random.binomial(length, p) # get the number of bits to flip
if flips > 0:
break # we will flip some bit
if none_is_ok:
return # we will flip no bit

i: int = length
end: Final[int] = length - flips
while i > end: # we iterate from i=length down to end=length-flips
k = random.integers(0, i) # index of next bit index in permutation
i -= 1 # decrease i
idx = permutation[k] # get index of bit to flip and move to end
permutation[i], permutation[k] = idx, permutation[i]
dest[idx] = not dest[idx] # flip bit


class Op1MoverNflip(Op1):
"""
A unary search operation that flips each bit with probability of `m/n`.
Expand Down Expand Up @@ -61,30 +102,8 @@ def op1(self, random: Generator, dest: np.ndarray, x: np.ndarray) -> None:
:param dest: the destination array to receive the new point
:param x: the existing point in the search space
"""
np.copyto(dest, x) # copy source to destination
length: Final[int] = len(dest) # get n
p: Final[float] = self.__m / length # probability to flip bit
none_is_ok: Final[bool] = self.__none_is_ok

flips: int # the number of bits to flip
rbi: Final[Callable[[int, float], int]] = random.binomial
while True:
flips = rbi(length, p) # compute the number of bits to flip
if flips > 0:
break # we will flip some bit
if none_is_ok:
return # we will flip no bit

permutation: Final[np.ndarray] = self.__permutation
i: int = length
end: Final[int] = length - flips
ri: Final[Callable[[int], int]] = random.integers
while i > end: # we iterate from i=length down to end=length-flips
k = ri(i) # get index of next bit index in permutation
i -= 1 # decrease i
idx = permutation[k] # get index of bit to flip and move to end
permutation[i], permutation[k] = idx, permutation[i]
dest[idx] = not dest[idx] # flip bit
_op1_movern(self.__m, self.__none_is_ok, self.__permutation,
random, dest, x)

def __str__(self) -> str:
"""
Expand Down
166 changes: 37 additions & 129 deletions moptipy/operators/permutations/op1_insert1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 Callable, Final
from typing import Final

import numba # type: ignore
import numpy as np
Expand All @@ -69,144 +69,30 @@


@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False)
def try_single_rotate(arr: np.ndarray, i1: int, i2: int) -> bool: # +book
def rotate(random: Generator, dest: np.ndarray, x: np.ndarray) -> None:
"""
Rotate a portion of an array to the left or right in place.
Copy `x` into `dest` and then rotate a subsequence by one step.
The function repeatedly tries to rotate a portion of an array to the left
or right in place. It will continue trying until something changed.
In each step, it draws two indices `i1` and `i2`.
If `i1 < i2`, then a left rotation by one step is performed. In other
words, the element at index `i1 + 1` goes to index `i1`, the element at
index `i1 + 2` goes to index `i1 + 1`, and so on. The lst element, i.e.,
the one at index `i2` goes to index `i2 - 1`. Finally, the element that
originally was at index `i1` goes to index `i2`. If any element in the
array has changed, this function returns `False`, otherwise `True`.
array has changed, this function is done, otherwise it tries again.
If `i1 > i2`, then a right rotation by one step is performed. In other
words, the element at index `i1 - 1` goes to index `i1`, the element at
index `i1 - 2` goes to index `i1 - 1`, and so on. Finally, the element
that originally was at index `i1` goes to index `i2`. If any element in
the array has changed, this function returns `False`, otherwise `True`.
the array has changed, this function tries again, otherwise it stops.
This corresponds to extracting the element at index `i1` and re-inserting
it at index `i2`.
:param arr: the array to rotate
:param i1: the start index, in `0..len(arr)-1`
:param i2: the end index, in `0..len(arr)-1`
:returns: whether the array was *unchanged*
:retval False: if the array `arr` is now different from before
:retval True: if the array `arr` has not changed
>>> import numpy as npx
>>> dest = npx.array(range(10))
>>> print(dest)
[0 1 2 3 4 5 6 7 8 9]
>>> try_single_rotate(dest, 3, 4)
False
>>> print(dest)
[0 1 2 4 3 5 6 7 8 9]
>>> try_single_rotate(dest, 3, 4)
False
>>> print(dest)
[0 1 2 3 4 5 6 7 8 9]
>>> try_single_rotate(dest, 4, 3)
False
>>> print(dest)
[0 1 2 4 3 5 6 7 8 9]
>>> try_single_rotate(dest, 4, 3)
False
>>> print(dest)
[0 1 2 3 4 5 6 7 8 9]
>>> try_single_rotate(dest, 3, 6)
False
>>> print(dest)
[0 1 2 4 5 6 3 7 8 9]
>>> try_single_rotate(dest, 6, 3)
False
>>> print(dest)
[0 1 2 3 4 5 6 7 8 9]
>>> try_single_rotate(dest, 0, len(dest) - 1)
False
>>> print(dest)
[1 2 3 4 5 6 7 8 9 0]
>>> try_single_rotate(dest, len(dest) - 1, 0)
False
>>> print(dest)
[0 1 2 3 4 5 6 7 8 9]
>>> try_single_rotate(dest, 7, 7)
True
>>> dest = np.array([0, 1, 2, 3, 3, 3, 3, 3, 8, 9])
>>> try_single_rotate(dest, 7, 7)
True
>>> try_single_rotate(dest, 4, 6)
True
>>> print(dest)
[0 1 2 3 3 3 3 3 8 9]
>>> try_single_rotate(dest, 6, 4)
True
>>> print(dest)
[0 1 2 3 3 3 3 3 8 9]
>>> try_single_rotate(dest, 4, 7)
True
>>> print(dest)
[0 1 2 3 3 3 3 3 8 9]
>>> try_single_rotate(dest, 6, 7)
True
>>> print(dest)
[0 1 2 3 3 3 3 3 8 9]
>>> try_single_rotate(dest, 4, 8)
False
>>> print(dest)
[0 1 2 3 3 3 3 8 3 9]
>>> try_single_rotate(dest, 8, 4)
False
>>> print(dest)
[0 1 2 3 3 3 3 3 8 9]
>>> try_single_rotate(dest, 9, 4)
False
>>> print(dest)
[0 1 2 3 9 3 3 3 3 8]
>>> 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 # initially, assume that there is no change

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] # 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, # +book
x: np.ndarray) -> None: # +book
"""
Copy `x` into `dest` and then rotate a subsequence by one step.
:param random: the random number generator
:param dest: the array to receive the modified copy of `x`
:param x: the existing point in the search space
Expand All @@ -227,14 +113,37 @@ def rotate(random: Generator, dest: np.ndarray, # +book
>>> 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

unchanged: bool = True
# try to rotate the dest array until something changes
while try_single_rotate(dest, rint(0, length), rint(0, length)):
pass # do nothing in the loop, but try rotating again
while unchanged:
i1: int = random.integers(0, length)
i2: int = random.integers(0, length)
if i1 == i2: # nothing to be done
continue # array will not be changed

if i1 < i2: # rotate to the left: move elements to lower indices?
first = dest[i1] # get the element to be removed
while i1 < i2: # iterate the indices
i3 = i1 + 1 # get next higher index
cpy = dest[i3] # get next element at that higher index
unchanged &= (cpy == dest[i1]) # is a change?
dest[i1] = cpy # store next element at the lower index
i1 = i3 # move to next higher index
unchanged &= (first == dest[i2]) # check if change
dest[i2] = first # store removed element at highest index
continue

last = dest[i1] # last element; rotate right: move elements up
while i2 < i1: # iterate over indices
i3 = i1 - 1 # get next lower index
cpy = dest[i3] # get element at that lower index
unchanged &= (cpy == dest[i1]) # is a change?
dest[i1] = cpy # store element at higher index
i1 = i3 # move to next lower index
unchanged &= (last == dest[i2]) # check if change
dest[i2] = last # store removed element at lowest index


class Op1Insert1(Op1):
Expand All @@ -254,7 +163,6 @@ def __init__(self) -> None:
"""Initialize the object."""
super().__init__()
self.op1 = rotate # type: ignore # use function directly
# end book

def __str__(self) -> str:
"""
Expand Down
3 changes: 2 additions & 1 deletion moptipy/operators/permutations/op1_swap2.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,14 @@
"""
from typing import Callable, Final

import numba # type: ignore
import numpy as np
from numpy.random import Generator

from moptipy.api.operators import Op1


# Temporary fix for https://github.com/numba/numba/issues/9103
@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False)
def swap_2(random: Generator, dest: np.ndarray, # +book
x: np.ndarray) -> None: # +book
"""
Expand Down
5 changes: 3 additions & 2 deletions moptipy/operators/permutations/op1_swap_exactly_n.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"""
from typing import Counter, Final, Iterable

import numba # type: ignore
import numpy as np
from numpy.random import Generator
from pycommons.types import check_int_range, type_error
Expand Down Expand Up @@ -239,7 +240,7 @@ def get_max_changes(blueprint: Iterable[int]) -> int:
return changes


# Temporary fix for https://github.com/numba/numba/issues/9103
@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False)
def find_move(x: np.ndarray, indices: np.ndarray, step_size: int,
random: Generator, max_trials: int,
temp: np.ndarray) -> np.ndarray:
Expand Down Expand Up @@ -402,7 +403,7 @@ def find_move(x: np.ndarray, indices: np.ndarray, step_size: int,
return temp[0:best_size]


# Temporary fix for https://github.com/numba/numba/issues/9103
@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False)
def apply_move(x: np.ndarray, dest: np.ndarray, move: np.ndarray,
random: Generator, max_trials: int) -> None:
"""
Expand Down
3 changes: 2 additions & 1 deletion moptipy/operators/permutations/op1_swap_try_n.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"""
from typing import Final

import numba # type: ignore
import numpy as np
from numpy.random import Generator
from pycommons.types import type_error
Expand All @@ -28,7 +29,7 @@
from moptipy.utils.nputils import DEFAULT_INT, fill_in_canonical_permutation


# Temporary fix for https://github.com/numba/numba/issues/9103
@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False)
def swap_try_n(random: Generator, dest: np.ndarray, x: np.ndarray,
step_size: float, indices: np.ndarray) -> None:
"""
Expand Down
3 changes: 2 additions & 1 deletion moptipy/operators/permutations/op1_swapn.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@
"""
from typing import Callable, Final

import numba # type: ignore
import numpy as np
from numpy.random import Generator

from moptipy.api.operators import Op1


# Temporary fix for https://github.com/numba/numba/issues/9103
@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False)
def swap_n(random: Generator, dest: np.ndarray, # +book
x: np.ndarray) -> None: # +book
"""
Expand Down
Loading

0 comments on commit b81bd99

Please sign in to comment.