Some remarks on linexp and lincurve

I posted this a while ago on the mailing list and can’t find it anymore. As it’s also one of those questions coming up again and again (and as help for my weak memory also) I’m reposting it here.

In contrast to the lincurve mapping which - with same conditions, but not necessarily positive range - solves

g(x) = k * (a ** (x - x0)) + b

with a given base ‘a’, with linexp mapping the base is determined by the conditions, moreover there’s no offset term b (thus no non-positive out values allowed)

As a solution for the linexp mapping we get

a = (y1 / y0) ** (1 / (x1 - x0))
k = y0 / (a ** x0)

and f, slightly transformed:

f(x) = y0 * ((y1 / y0) ** ((x - x0) / (x1 - x0)))

The solutions k, b of the lincurve mapping are

b = y1 - (y0 * (a ** (x1 - x0))) / (1 - (a ** (x1 - x0)))
k = y0 - b

The offset term b equals 0 for

a = (y1 / y0) ** (1 / (x1 - x0))

or

a = exp(ln(y1 / y0) / (x1 - x0))

And, by convention, not ‘a’ is passed as curvature in linexp, but its natural log, multiplied by (x1 - x0).
So to simulate linexp with lincurve, just take log(y1 / y0) as curvature.

(
~expMapping = { |x, inMin = 0, inMax = 1, outMin = 0, outMax = 1, a = 4|
	var p = pow(a, inMax - inMin);
	var b = outMax - (outMin * p) / (1 - p);
	var k = outMin - b;
	k * pow(a, (x - inMin)) + b
}
)

// all the same

{ |x| ~expMapping.(x, 0, 2, 0.01, 3, exp(log(300)/2)) }.plotGraph(from: 0, to: 2)
{ |x| x.lincurve(0, 2, 0.01, 3, log(300)) }.plotGraph(from: 0, to: 2)
{ |x| x.linexp(0, 2, 0.01, 3) }.plotGraph(from: 0, to: 2)

// difference is from floating point inaccuracy, not math

{ |x| x.linexp(0, 2, 0.01, 3) - x.lincurve(0, 2, 0.01, 3, log(300)) }.plotGraph(from: 0, to: 2)
1 Like

Have to wait for coffee to kick in before I think of the math, but I’ve been manually replacing exp Specs with high-degree polynomial ones for some time now, so that I can hit the base value too (e.g. 0) My “algorithm” for doing this: gawk at the (exp) slider in EnvirGui. Pick a value that I like to be the “default” to be as close to center as possible. Adjust the polynomial degree of a 2nd slider (same rage and the exp) until the “default value” is roughly in the same spot. This can probably be turned into math-solving equation (for the degree of the polynomial as the variable), but I haven’t tried that more formally.

A practical example

// Starting spec, the range is fixed by the application
Spec.add(\lrfreq, [40, 4000, \exp, 0.01, 600]);

e = (lrfreq: 600);
EnvirGui(e);

// add a 2nd slider, same default val
e[\lrfreqP] = 600;

Spec.add(\lrfreqP, [40, 4000, 5, 0.01, 600]); // try 5th degree poly; it's a bit "to the right"
Spec.add(\lrfreqP, [40, 4000, 4.6, 0.01, 600]); // try 4.6; look "ok" for matching lrfreq
//... if that's what want

// Sometimes I want the default value centered.
Spec.add(\lrfreqP, [40, 4000, 3.7, 0.01, 600]); // this more centered (on 600) though

The fact that EnvirGui picks up the changes as soon as you make them (to the global specs) is quite helpful here…

Even if what you want as actual default is sometimes one of the end-point values of the range, it’s still useful to think/consider what should the “center value” on the slider be.

And of course you can hit 0 with the “poly” one, e.g. if what you have is a difference/offest between two frequencies, that makes sense.

Spec.add(\cfd, [0, 4000, 3.7, 0.1, 600]);
e[\cfd] = 1500;

I vaguely recall someone did an S-shaped Warp, which would be (more) useful for 0-centered value that can be negative but you want it “going exponential” (or nearly) towards both endpoints. But I can’t find it right now. (SegWarp in wslib maybe, but that looks more complicated. Also it looks like polynomial curves aren’t actually implemented in it at the moment. Maybe it was one of the spline quarks.)

Thought about that too some while ago, reposting it here for further reference

https://www.listarc.bham.ac.uk/lists/sc-dev-2017/msg57562.html

A power mapping would be the solution of

f(x) = k * ((x - x0) ** r) + b

compared to exponential mapping (lincurve and curve params in several contexts)

f(x) = k * (a ** (x - x0)) + b

with conditions

f(x0) = y0
f(x1) = y1

(
~powMapping = { |x, inMin = 0, inMax = 1, outMin = 0, outMax = 1, r = 4|
	var b = outMin;
	var k = (outMax - outMin) / ((inMax - inMin) ** r);
	k * pow((x - inMin), r) + b
}
)


// for positive r:

{ |x| ~powMapping.(x, 0, 2, 2, 5, 3) }.plotGraph(from: 0, to: 2)

{ |x| ~powMapping.(x, 0, 2, 2, 5, 0.3) }.plotGraph(from: 0, to: 2)


// as with lincurve it can be shifted across zero

{ |x| ~powMapping.(x, -1, 1, 2, 5, 0.3) }.plotGraph(from: -1, to: 1)

Anyway I didn’t really use it in practical work.

… though general polynomials of course would be a bigger chapter …