
It’s time to revisit this
Previously, I took a look at how to constrain a number to be between two values and two ways to do that. The second method which gives a smooth transition between the input x and output q only applies when x is within ±5 or so. This would be an issue if you’re like “I know my value is between 120 and 180, so I’ll vary x between 90 and 270 and apply the constraint” – because x is never between -5 and 5, you would only ever get the upper bound of 180 from it and nothing else. Here’s an animation of what I mean:
The solid blue line is the input verses the output. The gray vertical lines are the bounds along the input and the yellow dashed lines are the bounds along the output. The green dashed lines are where the values at the bounds come out as (I.E. if the upper bound is 10 and x is at 25, the output q is 9.9991).
As you can see, if the input and bounds slide away from zero, any output just ends up at one of the bounds; and if the bounds grow wide, most input values end up near the bounds.
What I wanted for the constraint function to do is adjust where it applies the constraints so it’s centered around the bounds along x. I also wanted to have another parameter that could control the ‘tightness’ of the function. This took some time to fiddle and figure out – there’s a reason a whole year passed between posts!
The idea for the tightness parameter was to have whatever the input is when it is at the bounds, the output is some percentage of the way there. That is, if my input is 10 and my upper bound is 10 and my tightness is 95%, the output should be 9.5. And if my lower bound is -10 and my input is -10, the output would be -9.5. Or in other words, the tightness defines how quickly the constraint is applied.
So if my tightness value is 50%, values at the bounds will come out at only 50% of the way to the bounds and if it was at 99.99%, those values would be very very close to the bounds.
After many iterations and various attempts, as well as plugging in the values I would get into wolfram-alpha to see if there was a closed form for my scaling factor or function that generated the scaling factor, I finally came up with this:
scl = np.arctanh(p) * (4 / (upper - lower))
p here is the percentage, between 0 and 1. Arctanh is the inverse hyperbolic tangent function.
When the upper and lower bounds are centered around zero (like -4 and 4 or -13 and 13), it becomes apparent: constraint(x=13, lower= -13, upper=+ 13, p=0.95) divided by x=13 results in .95, and the same is true when x=-13, the result is still 0.95. But the moment the bounds aren’t centered around zero, this division test doesn’t work. This confused me for a long time before I wondered if I was looking at the wrong metric. After making an animation of it, it became clearer that what I was doing was what I was after:
So here’s the full code for it:
import numpy as np
def constraint(x, lower, upper, p=0.95):
"""constrain x to between lower and upper bounds such that
x is p percent of the way to the respective limit at either bound
from Orthallelous"""
if not 0 < abs(p) < 1:
err = ValueError('p must be between (and not equal to) 0 and 1')
raise err
# get midpoint and width of bounds
avg = 0.5 * (upper + lower)
wdh = (upper - lower)
# this scale factor found by trial and error and wolfram-alpha
scl = np.arctanh(p) * (4 / wdh)
# you can hardcode the number at 95% if you want:
#scl = 7.3271232922592928548974653569756886189 / wdh
y = x - avg
if y > 0:
# done this way so there's never an overflow
ex = np.exp(-y * scl)
ftr = 1. / (1 + ex)
else:
ex = np.exp( y * scl)
ftr = ex / (1 + ex)
return lower + wdh * ftr
I doubt I’ve discovered a secret math trick here – what’s far more likely is that I stumbled onto something that was originally published ages ago and then forgotten about. Either that, or it’s published in every standard textbook for a specific math course that I just didn’t have – like the discrete math class; only math majors were allowed to take it (I was a physics major), and even then, friends that did take it refused to talk about it.


Leave a comment