Sigmoid-Logit Mapping


Download

sigmoid-logit-main.maxpat
tim.sigmoid.map.maxpat
tim.sigmoid.shift.map.maxpat
README
LICENSE

Github repo

Do you also find yourself often looking for elegant parameter mapping solutions to complex input behaviours? At least I have had this issue numerous times before; enough to warrant keeping a module at hand for most purposes. In this article I want to present some of these tools and explain a bit what is meant by the term sigmoid mapping, a type of "S-curve" mapping that provides greater flexibility to model more complex behaviours.

Mapping a value is understood as the action of modifying a range of input values to a target range. Most commonly the operations are linear, meaning you only shift (addition) and scale (multiplication) the input range to fit the target. For example, MIDI only sends integer values from 0 to 127. So, in order to use the velocity information coming from a MIDI keyboard, you might want to scale those values down in the range 0.0 to 1.0 and use that as a scaling factor to control a sound's amplitude.

However, in some cases, linear mapping is just not enough. For example, mapping the MIDI values to frequency values requires the MIDI range to be mapping to the logarithmic frequency space, where each octave is a doubling in Hz. MaxMSP provides objects for these common use cases, such as [mtof], or providing the addition of an exponent argument to the [scale] object. But, as things become even more complex, more complex mappings suddenly become more necessary to achieve more obscure tasks. In particular, when composing procedurally or aleatorically, values are often mapped to different distribution curves of various shapes to achieve a number of outcomes.

My journey towards discovering the class of sigmoid functions begins with the exponential curve. While the classic exponential function may seem like the obvious choice – and it may be the most accurate one in some use cases – it offers little flexibility out of the box. In order to map it easily to any target range, it would first have to be fitted to a [0,1] range. You do this by shifting the function by the value it has at 0, then dividing it by it value at 1. Then, you may want to alter the curvature, so you parameterise the base. The resulting function might look something like this:

ax1a1

However, this is just a power function with extra steps. You can achieve a similar result by simply switching x from the exponent into the base and do the opposite to the parameter a. The resulting function xᵃ is already fixed to [0,0] and [1,1], so no shifting nor scaling is necessary! Sure, the parameter a does not map the same way, being much more dramatic in the xᵃ case, and the curvature is slightly different... but it usually does the job in most cases... and, especially in MaxMSP, it is plopped down so much more quickly! Below you can play around with the comparison between the two and have the difference between them visualised:

This is all well and good, but the values are merely either distorted towards the lower bound (if a>1) or towards the upper bound (if 0<a<1). While more complex that a linear mapping, it is still pretty limiting. Surely it should be possible to describe more complex curves within the [0,1] range mathematically? As long as we approach the problem pragmatically, the above excursion also shows us that there are usually more than one road to Rome.

I always wondered if it would be possible to make some curve that has a kind of plateau, or saddle point, in the middle, instead, where most values would be distorted against or pushed away from. Initial tries combined the square root (for lower input values) and power function (for higher input values). Alas, the ending curvature of the square root does not align well with the start of the power function, creating and uneven curve that is both ugly to program and ugly mathematically.

Then I came across the signed power function: sgn(xb)|xb|a It is an elegant solution for negative bases, all real-numbered exponents and produces a smooth transition at the [0,0] point. More importantly, it basically mirrors the power function at the [0,0] point, creating the "S-curve" type sigmoid I was looking for. It even smoothly transitions into a logit function if a>1. Granted, like my sqrt/pow solution above, it maps an input from [-1,-1] to [1,1], but a bit of input scaling and output scaling is fine. However, I got curious, what if we not only confine it to [0,1], but also make the saddle point shiftable. For this, we can use the remapping formula: g(x)g(0)g(0)g(1) ...which gives us the shiftable signed power function: sgn(xb)|xb|a+ba(1b)a+ba

You can play around with the function and the parameters in the widget below. a controls the curve's curvature, while b control the position of the saddle point.

In case you have not tried the following, I would like you to set a>1 and then play around with b. At first you might not notice anything... however, while having b≠0.5, change a instead and see how the saddle point moves – which ideally it shouldn't, since a should change the curvature and b the saddle point alone! While in "logit mode" (i.e. a>1), the saddle point shifts up and down when changing a and makes b much more sensitive... whereas in "sigmoid mode" (i.e. 0<a<1) the vertical saddle remains fixed in place. The asymmetry comes from the fact that the signed power function is not symmetric around b on the [0,1] domain. The distances from b to the two anchors 0 and 1 are b and 1-b respectively. Raising those to the power a doesn't preserve the midpoint relationship unless b=0.5.

The solution to this is take the complement function to the shiftable signed power function, which means solving it for y and use one of the two functions for the sigmoid case, while its complement is used for the logit case (ideally whichever has either case in the 0<a<1 range, respectively). This also requires shifting a around a bit in the background to make the input "feel" the same, as previously in the pure signed power solution. It may show a>1 in the bottom right corner of the widget below, but in actuality it is 0<a≤1 internally. Play around with the parameters and see how the entire curve is much more stable, particularly in the logit case (the sideways "S"-curve). You will also see the first derivative plotted for the function. Have a look at the derivative if you spot anything strange...

Now, if you set, say, b=0.75, the saddle point also stays at 0.75 no matter which value you give for a – for both the sigmoid and logit cases! Pretty good. However, the whole function became much more complex. Granted, the condition is in the parameter a and not the variable x, as was the case in the sqrt/pow solution, but we do require two functions to map the entire behaviour as compared to a single one using pure signed power approach. The whole lot is also pretty ugly to program in MaxMSP! Moreover, you might have noticed the discontinuity, as demonstrated by the derivative that was added. While not problematic from a practical sense, still a bit of a thorn in my eye. Lastly, while the saddle point is fixed along x-axis in the sigmoid case and along the y-axis in the logit case, you may have noticed that it wanders along the other respective axis in each case as you modify b. This can be a good or a bad feature, depending on your use case.

So, I poked around some more. One function that I did not consider before and which has a natural sigmoid shape is the hyperbolic tangent, tanh. We can constrain the tanh function in the [-1, 1] interval by dividing it by its value at 1. By scaling the argument by a we can modify the curvature of the function: tanh(xa)tanh(a) To get the logit version, we, once again, have to compute the complement to this function. Have a look below how this complex of functions behaves for different values of a and have a look at the first derivative as well:

Now, I do not know if this matters, not only is the function continuous for all values of a, but the maximum of the first derivative is nearly equal the value of a! Doing the math, it is obvious, of course: f(0)=a(1tanh(2(0))tanh(a)=a1tanh(a)=atanh(a) For large values of a, the term tanh(a) tends towards 1 and, thus, we are pretty much left with a (or 1/a in the logit case).

Moreover, though, this also implies we always have a slope around the saddle point. While the signed power function's discontinuity guarantees a point where the function is truly horizontal or vertical, this new tanh solution will give you something that is never exactly horizontal nor vertical, no matter how high you set a to. Again, this can either be a bug or a feature, depending on your use case.

Of course, I then had to see what happens if we were to shift the [0,0] across the x or the y axis, respectively. The simplest approach was to use the same remapping formula as above and plug our new tanh into it. Then we end up with this very neat and tidy formula for the shiftable hyperbolic tangent solution (sigmoid version): tanh(a(xb))tanh(ab)tanh(a(1b))tanh(ab) Arguably, its complement function to get the logit version is not as neat, but by shifting everything over on the other side of the equation and solving it for y (also, we switch out x for y once we're done), we get the following: atanh(x[tanh(a[1b])tanh(ab)]+tanh(ab))a+b The good news, concerning MaxMSP, is that we have most of the elements already computed for the sigmoid case! The bit that is tanh(a(1−b))−tanh(−ab) is just a constant that needs re-computation on parameter change, as is tanh(−ab). So, the only trigonometric function left to compute on every new value of x is the somewhat daunting atanh... But, it's not so bad, since you do have the [atanh], which should make it simple enough...

So, below, you have the final trigonometric solution using tanh and atanh in each the sigmoid and logit case respectively. You can play around with the a and b and see how the curve behaves. Personally, it is just so satisfying how shifts across the respective axis while modifying b, has it's inflection points right on the set value for b and does not changealong the other axis... but maybe that is just pure curve aesthetics and has no real use in real world applications...

Also, you can toggle the signed power function solution from before to compare the two. Have a look how they dance around each other, when one is higher or lower than the other and how their curvature differs. Consider that a is modified to be 10(a/10) instead, in the background, in this case. Additionally, you can include the difference curve between the trigonometric and arithmetic solutions to analyse the distance between them.

To wrap this up, I want to leave you with a few other alternatives that I still have pending looking into. These can also be found in a list on wikipedia, for the interested reader:

  • Algebraic approach: No transcendental functions at all, just powers and, thus, very fast to compute. Additionally, the inverse is also algebraic.
  • Arctangent: uses atan instead of tanh to achieve the sigmoid shape.
  • Logistic function: actually has most of the properties that I want and is a promising candidate to replace the tanh solution.
  • Gudermannian function: I do not understand this function and the plot in my intent to visualise it currently is botched.
  • Error function: can only be approximated here and its computation is rather complex. The curve is similar to that of the tanh solution.
  • Smoothstep polynomial: relies on the Hermite interpolation to transition from the low to the high boundary.

Feel free to explore my current implementations in my javascript widget below: