How to implement a smooth clamp function in python?
What you are looking for is something like the Smoothstep function, which has a free parameter N
, giving the "smoothness", i.e. how many derivatives should be continuous. It is defined as such:
This is used in several libraries and can be implemented in numpy as
import numpy as np
from scipy.special import comb
def smoothstep(x, x_min=0, x_max=1, N=1):
x = np.clip((x - x_min) / (x_max - x_min), 0, 1)
result = 0
for n in range(0, N + 1):
result += comb(N + n, n) * comb(2 * N + 1, N - n) * (-x) ** n
result *= x ** (N + 1)
return result
It reduces to the regular clamp function given N=0
(0 times differentiable), and gives increasing smoothness as you increase N. You can visualize it like this:
import matplotlib.pyplot as plt
x = np.linspace(-0.5, 1.5, 1000)
for N in range(0, 5):
y = smoothstep(x, N=N)
plt.plot(x, y, label=str(N))
plt.legend()
which gives this result:
Normal clamp:
np.clip(x, mi, mx)
Smoothclamp (guaranteed to agree with normal clamp for x < min and x > max):
def smoothclamp(x, mi, mx): return mi + (mx-mi)*(lambda t: np.where(t < 0 , 0, np.where( t <= 1 , 3*t**2-2*t**3, 1 ) ) )( (x-mi)/(mx-mi) )
Sigmoid (Approximates clamp, never smaller than min, never larger than max)
def sigmoid(x,mi, mx): return mi + (mx-mi)*(lambda t: (1+200**(-t+0.5))**(-1) )( (x-mi)/(mx-mi) )
For some purposes Sigmoid will be better than Smoothclamp because Sigmoid is an invertible function - no information is lost.
For other purposes, you may need to be certain that f(x) = xmax for all x > xmax - in that case Smoothclamp is better. Also, as mentioned in another answer, there is a whole family of Smoothclamp functions, though the one given here is adequate for my purposes (no special properties other than a smooth derivative needed)
Plot them:
import numpy as np
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 1)
x = np.linspace(-4,7,1000)
ax.plot(x, np.clip(x, -1, 4),'k-', lw=2, alpha=0.8, label='clamp')
ax.plot(x, smoothclamp(x, -1, 4),'g-', lw=3, alpha=0.5, label='smoothclamp')
ax.plot(x, sigmoid(x, -1, 4),'b-', lw=3, alpha=0.5, label='sigmoid')
plt.legend(loc='upper left')
plt.show()
Also of potential use is the arithmetic mean of these two:
def clampoid(x, mi, mx): return mi + (mx-mi)*(lambda t: 0.5*(1+200**(-t+0.5))**(-1) + 0.5*np.where(t < 0 , 0, np.where( t <= 1 , 3*t**2-2*t**3, 1 ) ) )( (x-mi)/(mx-mi) )