Coverage for moptipy / operators / intspace / op1_mnormal.py: 60%
68 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-15 11:18 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-15 11:18 +0000
1"""
2A multi-normal distribution.
4>>> from moptipy.utils.nputils import rand_generator
5>>> gen = rand_generator(12)
7>>> from moptipy.operators.intspace.op0_random import Op0Random
8>>> space = IntSpace(4, -2, 2)
9>>> op0 = Op0Random(space)
10>>> x1 = space.create()
11>>> op0.op0(gen, x1)
12>>> x1
13array([-1, 2, 2, 1], dtype=int8)
15>>> op1 = Op1MNormal(space, 1, True, 1.5)
16>>> op1.initialize()
17>>> x2 = space.create()
18>>> op1.op1(gen, x2, x1)
19>>> x2
20array([-1, 0, 0, 1], dtype=int8)
21>>> op1.op1(gen, x2, x1)
22>>> x2
23array([-1, 2, 1, -2], dtype=int8)
24>>> op1.op1(gen, x2, x1)
25>>> x2
26array([-1, 2, 2, 0], dtype=int8)
28>>> space = IntSpace(12, 0, 20)
29>>> op0 = Op0Random(space)
30>>> x1 = space.create()
31>>> op0.op0(gen, x1)
32>>> x1
33array([ 6, 1, 18, 11, 13, 11, 2, 3, 13, 7, 10, 14], dtype=int8)
35>>> op1 = Op1MNormal(space, 1, True, 1.5)
36>>> op1.initialize()
37>>> x2 = space.create()
38>>> op1.op1(gen, x2, x1)
39>>> x2
40array([ 6, 1, 18, 11, 13, 11, 2, 3, 12, 7, 9, 13], dtype=int8)
41>>> op1.op1(gen, x2, x1)
42>>> x2
43array([ 6, 1, 20, 11, 13, 11, 2, 3, 13, 7, 10, 14], dtype=int8)
44>>> op1.op1(gen, x2, x1)
45>>> x2
46array([ 3, 1, 18, 11, 13, 11, 2, 3, 13, 7, 9, 14], dtype=int8)
48>>> str(op1)
49'normB1_1d5'
50"""
51from math import ceil, floor, isfinite
52from typing import Final
54import numba # type: ignore
55import numpy as np
56from numpy.random import Generator
57from pycommons.types import check_int_range, type_error
59from moptipy.api.operators import Op1
60from moptipy.spaces.intspace import IntSpace
61from moptipy.utils.logger import KeyValueLogSection
62from moptipy.utils.nputils import (
63 fill_in_canonical_permutation,
64 int_range_to_dtype,
65)
66from moptipy.utils.strings import num_to_str_for_name
69@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False)
70def _mnormal(m: int, none_is_ok: bool, permutation: np.ndarray,
71 random: Generator, sd: float, min_val: int, max_val: int,
72 dest: np.ndarray, x: np.ndarray) -> None:
73 """
74 Copy `x` into `dest` and sample normal distribution for each element n/m.
76 This method will first copy `x` to `dest`. Then it will decide for each
77 value in `dest` whether it should be changed: The change happens with
78 probability `m/n`, where `n` is the length of `dest`.
79 Regardless of the probability, at least one element will always be
80 changed if self.at_least_1 is True.
82 :param m: the value of m
83 :param none_is_ok: is it OK to flip nothing?
84 :param permutation: the internal permutation
85 :param random: the random number generator
86 :param sd: the standard deviation to be used for the normal distribution
87 :param min_val: the minimal permissible value
88 :param max_val: the maximal permissible value
89 :param dest: the destination array to receive the new point
90 :param x: the existing point in the search space
91 """
92 dest[:] = x[:] # copy source to destination
93 length: Final[int] = len(dest) # get n
94 p: Final[float] = m / length # probability to change values
96 flips: int # the number of values to flip
97 while True:
98 flips = random.binomial(length, p) # get number of values to change
99 if flips > 0:
100 break # we will change some values
101 if none_is_ok:
102 return # we will change no values
104 i: int = length
105 end: Final[int] = length - flips
106 while i > end: # we iterate from i=length down to end=length-change
107 k = random.integers(0, i) # index of next value index in permutation
108 i -= 1 # decrease i
109 idx = permutation[k] # get index of bit to value and move to end
110 permutation[i], permutation[k] = idx, permutation[i]
112 # put a normal distribution around old value and sample
113 # a new value
114 old_value = dest[idx]
115 while True:
116 rnd = random.normal(scale=sd)
117 new_value = old_value + (ceil(rnd) if rnd > 0 else floor(rnd))
118 if (new_value != old_value) and (min_val <= new_value <= max_val):
119 break
120 dest[idx] = new_value
123class Op1MNormal(Op1):
124 """Randomly choose a number of ints to change with normal distribution."""
126 def __init__(self, space: IntSpace, m: int = 1, at_least_1: bool = True,
127 sd: float = 1.0):
128 """
129 Initialize the operator.
131 :param n: the length of the bit strings
132 :param m: the factor for computing the probability of flipping
133 the bits
134 :param at_least_1: should at least one bit be flipped?
135 """
136 super().__init__()
137 if not isinstance(space, IntSpace):
138 raise type_error(space, "space", IntSpace)
139 #: the internal dimension
140 self.__n: Final[int] = space.dimension
141 #: the minimum permissible value
142 self.__min: Final[int] = space.min_value
143 #: the maximum permissible value
144 self.__max: Final[int] = space.max_value
145 #: the value of m in p=m/n
146 self.__m: Final[int] = check_int_range(m, "m", 1, self.__n)
147 if not isinstance(at_least_1, bool):
148 raise type_error(at_least_1, "at_least_1", bool)
149 #: is it OK to not flip any bit?
150 self.__none_is_ok: Final[bool] = not at_least_1
151 #: the internal permutation
152 self.__permutation: Final[np.ndarray] = np.empty(
153 self.__n, dtype=int_range_to_dtype(0, self.__n - 1))
154 if not isinstance(sd, float):
155 raise type_error(sd, "sd", float)
156 if not (isfinite(sd) and (0 < sd <= (self.__max - self.__min + 1))):
157 raise ValueError(
158 f"Invalid value {sd} for sd with {self.__min}..{self.__max}.")
159 #: the internal standard deviation
160 self.__sd: Final[float] = sd
162 def initialize(self) -> None:
163 """Initialize this operator."""
164 super().initialize()
165 fill_in_canonical_permutation(self.__permutation)
167 def op1(self, random: Generator, dest: np.ndarray, x: np.ndarray) -> None:
168 """
169 Copy `x` into `dest` and change each value with probability m/n.
171 This method will first copy `x` to `dest`. Then it will change each
172 value in `dest` with probability `m/n`, where `n` is the length of
173 `dest`. Regardless of the probability, at least one value will always
174 be changed if self.at_least_1 is True.
176 If a value is changed, we will put a normal distribution around it and
177 sample it from that. Of course, we only accept values within the
178 limits of the integer space and that are different from the original
179 value.
181 :param self: the self pointer
182 :param random: the random number generator
183 :param dest: the destination array to receive the new point
184 :param x: the existing point in the search space
185 """
186 _mnormal(self.__m, self.__none_is_ok, self.__permutation, random,
187 self.__sd, self.__min, self.__max, dest, x)
189 def __str__(self) -> str:
190 """
191 Get the name of this unary operator.
193 :return: "fileB" + m + "n" if none-is-ok else ""
194 """
195 return (f"normB{self.__m}{'n' if self.__none_is_ok else ''}_"
196 f"{num_to_str_for_name(self.__sd)}")
198 def log_parameters_to(self, logger: KeyValueLogSection) -> None:
199 """
200 Log the parameters of this operator to the given logger.
202 :param logger: the logger for the parameters
203 """
204 super().log_parameters_to(logger)
205 logger.key_value("m", self.__m)
206 logger.key_value("n", self.__n)
207 logger.key_value("sd", self.__sd)
208 logger.key_value("min", self.__min)
209 logger.key_value("max", self.__max)