Reflect values between a minimum and maximum. I have written both an iterative and recursive version of the algorithm.
import math
def reflect(value, min_value, max_value):
span = max_value - min_value
if value < min_value:
length = min_value - value
times = math.floor(length / span)
delta = length - span * times
return min_value + delta if times % 2 == 0 else max_value - delta
elif value > max_value:
length = value - max_value
times = math.floor(length / span)
delta = length - span * times
return max_value - delta if times % 2 == 0 else min_value + delta
else:
return value
def reflect_recursive(value, min_value, max_value):
if value < min_value:
return reflect_recursive(min_value + (min_value - value), min_value, max_value)
elif value > max_value:
return reflect_recursive(max_value - (value - max_value), min_value, max_value)
else:
return value
This algorithm is useful for situations where one or multiple values need to be constrained within a predefined space.
While a simple clamp-function will do this just fine, the values might end up residing along the minimum or maximum, which leads to less variance in the data.
This algorithm may be useful in constrained search space exploration in multiple dimensions and was originally created for that purpose. Such searches are common in global optimization tasks, or hyper-parameter optimization algorithms for black-box functions.
The recursive function is more compact, but it also requires more operations; the number of operations scale linearly with how many times the value exceeds the span between the minimum and maximum. It is therefore recommended to use the iterative function as it requires the same amount of operations regardless of how large the value is.
import random
min_value = 0
max_value = 100
for value in [random.uniform(-250., 250.) for _ in range(10)]:
print(value, "-->", reflect(value, min_value, max_value))
Output:
35.76567245051814 --> 35.76567245051814
-65.8385756123771 --> 65.8385756123771
-44.858018802589896 --> 44.858018802589896
-243.09102707381268 --> 43.09102707381268
9.921358638726929 --> 9.921358638726929
-7.940068972499944 --> 7.940068972499944
151.39851126754178 --> 48.60148873245822
-177.72874419074253 --> 22.271255809257468
89.64493033145743 --> 89.64493033145743
-40.733802405349905 --> 40.733802405349905
import matplotlib.pyplot as plt
# define constraint space
min_value = -50
max_value = 50
# generate points
n_points = 20
points = [(random.randint(-250, 250), random.randint(-250, 250)) for _ in range(n_points)]
# define figure
plt.figure(figsize=(8, 8))
plt.xlabel("x")
plt.ylabel("y")
plt.axvline(x=min_value, color='r', linestyle='-', linewidth=0.4)
plt.axvline(x=max_value, color='r', linestyle='-', linewidth=0.4)
plt.axhline(y=min_value, color='r', linestyle='-', linewidth=0.4)
plt.axhline(y=max_value, color='r', linestyle='-', linewidth=0.4)
for i, (x, y) in enumerate(points):
new_x = reflect(x, min_value, max_value)
new_y = reflect(y, min_value, max_value)
plt.scatter(x, y, marker="x", color='g', label="original" if i == 0 else "")
plt.scatter(new_x, new_y, marker="x", color='b', label="reflected" if i == 0 else "")
plt.arrow(x=x, y=y, dx=new_x-x, dy=new_y-y)
plt.legend()
plt.savefig(fname="test/plot.png", format='png', transparent=False)
plt.show()