Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lerp to the standard math library #54

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

FunnyGlaggle
Copy link

@witchiestwitchery
Copy link
Contributor

Could we get this merged? Doesn't seem like theres any objections to it.

@vegorov-rbx
Copy link
Contributor

Suggested wording change has never been applied. I would say there's no rush in getting it in.
Team is also focused on delivering other projects, so no need to rush things.

Additionally, the implementation is hard to follow with respect to the guarantees presented and whether or not we need them.
It doesn't match any of the popular implementations mentioned in the Rust issue.
It is also not as precise as the one from C++ (which I don't think is valuable to have).
We currently don't have time to do an analysis on numerical claims of that implementation, maybe the author can give a link to a paper that does that? I think that while special-casing at 3 points makes it exact, but might actually remove monotonicity.

Feels like that the developers will be better off implementing their usual custom implementation of a simple x + (y - x) * alpha.
Current implementation is complex enough with 3 condition statements to not be implemented in native codegen, so a custom function will win on performance.
My suggestion would be that developers who need precise numerical properties can have their own implementation with them, but that the one in Luau can have some properties missing, but be equal to the general expectation of what lerp does.

Here's the objection you wanted :)


## Drawbacks

As mentioned in the [Design](##design) section, the naïve implementation of `lerp` may introduce precision error, and all of the edge-cases will need to be accounted for.
Copy link

@AxisAngles AxisAngles Nov 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lerp(a0, a1, t):

  • t can be much higher precision when near 0
    • so when t is near 0, we don't ever want to have a function of the form A + (1 - t)*B
  • we want exactness, lerp(a0, a1, 0) == a0 and lerp(a0, a1, 1) == a1
    • when t is closer to 0, we want higher accuracy around a0
    • when t is closer to 1, we want higher accuracy around a1
  • we want functions of the form A + t*B, as this guarantees monotonic behavior

Some things to consider for building intuition:

-- (100000000 samples)

-- given x, y = sortabs(random()*randomsign(), random()*randomsign()):
--   x - y + y ~= x: ~49.2%
--   y - x + x ~= y: ~20.6%

-- given x, y = sortabs(random(), random()):
--   x - y + y ~= x: ~30.1%
--   y - x + x ~= y: ~3.98%

-- given x, y = sortabs(random()*1.5, random()*1.5):
--   x - y + y ~= x: ~25.4%
--   y - x + x ~= y: ~1.63%

-- given floorlog2(x) == floorlog2(y) >= floorlog2(y - x)
--   x - y + y ~= x: 0%
--   y - x + x ~= y: 0%

Taking all this into account, we build our first example:

  • Obvious solution is to split when t < 1/2
    • at the interchange point, t = 1/2, we want to guarantee monotonic behavior
    • so we bound the result with respect to (a0 + a1)/2
--[[
guarantees exactness because
	a0 + 0*(a1 - a0) = a0
	a1 + (1 - 1)*(a1 - a0) = a1 + 0*(a1 - a0) = a1
guarantees consistency because
	a + (a - a)*t = a + 0*t = a
guarantees monotonicity because
	we bound the results such that
	lerp(a0, a1, 1/2 - 2^-54) <= lerp(a0, a1, 1/2)
]]
local function lerp1(a0, a1, t)
	local m = (a0 + a1)/2
	if t < 1/2 then
		local a = a0 + (a1 - a0)*t
		if a0 < a1 then
			return math.min(m, a)
		else
			return math.max(m, a)
		end
	else
		local a = a1 + (a1 - a0)*(t - 1)
		if a0 < a1 then
			return math.max(m, a)
		else
			return math.min(m, a)
		end
	end
end

Then we build a simpler case, to make sure what we are doing is necessary:

  • The relevant question is
    • can a0 + (1/2 - 2^-54)*(a1 - a0) > a1 - 1/2*(a1 - a0) ever be true?
    • From a lot of random sampling, it appears to be impossible, or at least very rare
-- this guarantees exactness and consistency
-- this appears to guarantee monotonicity intrinsically
local function lerp2(a0, a1, t)
	if t < 1/2 then
		return a0 + (a1 - a0)*t
	else
		return a1 + (a1 - a0)*(t - 1)
	end
end

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so from that, seems like at most all we'd need is to conditionally swap a <-> b and invert t to reach most/all the guarantees we wanted? aka no reason to go more complex than lerp2?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blending in @/vegorov-rbx's comments, would it even be worth special casing anything? would the only issue with using a0 + (a1 - a0) * t be that t has less precision around 1?

Copy link

@AxisAngles AxisAngles Nov 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

t has less precision around 1, but there's no way to extract more precision from t in the first place, what they give is what we get to work with.

Edit:
using only a0 + (a1 - a0)*t is problematic because often times, when t = 1, it does not return a1

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a user perspective, if I want more accuracy around a1 than a0, I would need to find some accurate way of generating s = 1 - t directly, and then use math.lerp(a1, a0, s)

Copy link

@dphblox dphblox Nov 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right this is about the precision of (a1 - a0), right? We want to take advantage of the higher density of floating point numbers near 0 so we would rather "multiply down". e.g.: x * 0.01 will be more precise than x - (x * 0.99).

All makes sense - lerp2 seems like the nicest "correct" solution. I suppose it just depends on whether we would want to use a conditional or whether that's deemed too heavy - will let the Luau folk speak to that.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my experience as a user, the most important guarantees lerp can have is for
lerp(a0, a1, 0) == a0 and lerp(a0, a1, 1) == a1

If conditionals are not allowed, then the optimal solution seems to be
(1 - t)*a0 + t*a1
but this is neither monotonic nor consistent.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh this is actually really clever, so the only "inaccuracy" we get is near 0.5

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this new insight, I will try and push this forward :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

6 participants