Trig functions
johnbchron opened this issue · 14 comments
Hey Matt! Once again thanks for your work on this incredible library.
From my perspective, the greatest inhibitor for this library at the moment is the lack of operations that can be included in a context/tape. I understand that some operations are purposefully excluded to keep complexity down and performance up, but the ones that I think would be most helpful are trig functions and modulo (if there's a way to fake modulo with min
and div
I couldn't find it).
I've written a naive implementation of sin
/cos
in a fork. I haven't written any test suites for them, I skipped out on the interval stuff (set it to [-1, 1]
for now), and left the JIT implementation alone with a todo!
since I can't write the assembly. For these reasons I decided not to make a PR, but the rest is implemented and the math is correct as far as I can tell. Honestly it's above me to make this level of modifications and I'm not sure how I managed to succeed, but it works. If you use this work I'd recommend thoroughly double-checking everything.
Anyways I just wanted to offer this in case you did plan on implementing trig functions at some point. Hope you're well!
Your intuition is correct – the hard part about supporting trig functions is hand-writing the assembly for them, and didn't want to introduce any operations without JIT parity.
A reasonable stop-gap would be to implement them in terms of calling an extern "C"
function, written in Rust and passed to the JIT function by address. This would have greater overhead (since it wouldn't be inlined), but would at least work.
Anyways, I appreciate the pointer to your branch, and will get to this... eventually 😄
I would like to add "parity with libfive" to this feature request; I use some of the less known functions in libfive for particularly unusual geometries. For instance, I have found this abomination quite useful:
def heaviside_step_function(a):
return Shape.min(0.0, Shape.compare(a, 0.0)) + 1.0
def if_nonnegative_else(a, f_zero, f_one):
res = heaviside_step_function(a)
return res * f_zero + (1.0 - res) * f_one
I've added a bunch of transcendental functions in #23, take a look!
Things that are still missing:
- Modulo / remainder
compare
and other non-linear functions
compare
is coming? :ooo I've avoided compare
because I thought it was something that was seen as bad or hidden in libfive, but damn, I should reconsider! Otherwise I've been using mixes of min
and max
to achieve the same effect.
I have mixed feelings about compare
.
The downside is that it easily creates expressions that have C0 discontinuities, which were a persistent problem in libfive
's meshing. If x < 0
but x + ε > 0
, then we have to be very careful handling floating-point values to avoid defining a vertex as both inside and outside the model (depending on the cell).
The upside is that it's a useful expression, and I'm thinking more and more about making fidget
useful for arbitrary evaluation (i.e. not just implicit surfaces).
My current plan is not to implement compare
directly, but to implement
- "Normal" comparison operators (e.g.
<
, in theless-than
branch) - Logical operators (and / or), designed play nice with tape simplification (
and([1, 1], v)
should simplify tov
)
The latter allows for efficient conditionals, which have been missing up until now: if a { b } else { c }
can be written as (a && b) || (!a && c)
.
@mkeeter this might be a bad question, but what is the current risk of abusing trig functions to define if_nonnegative_else
? That is:
round(x) = x - atan(tan(pi * x)) / pi
heaviside_step_function(x) = round(atan(x) / pi - 1 / 2) + 1
if_nonnegative_else(a, f, g) = heaviside_step_function(a) * f + (1 - heaviside_step_function(a)) * g
This isn't adhering to any standard notation or fidget syntax, but should make the idea clear.
@Wulfsta I changed my mind and implemented Context::compare
, which returns [-1, 0, 1]
(or NAN
if either side is NAN
), so you could build the step function with something like max(compare(lhs, 0), 0)
.
Oh, nice! Any chance of getting nearest integer and floor, so I don’t have to make up hacks like above?
Also, is there an equivalent to libfive’s nanfill
?
Oh, nice! Any chance of getting nearest integer and floor, so I don’t have to make up hacks like above?
Anything is possible, but I'm balancing writing new opcodes with new features / general crate development.
Also, is there an equivalent to libfive’s
nanfill
?
Can you say more about how you use nanfill
? It's a bit of a hack, and (more importantly) discards information that would be used in interval evaluation; it would basically always be better to clamp to a valid domain first, rather than using nanfill
to fix things up.
That is fair, I tend to use it as a convenience in surfaces that are not defined on some domain - for instance, I make use of it here rather than checking the norm and using an if_nonnegative_else
. I can’t think of a time I couldn’t get around this somehow, though when there are NaNs at a single point (such as zero) I have seen benefit from it.
Oh, there is one situation in which nanfill
is unavoidable. When using a function like if_nonnegative_else
and constructing a sum that evaluates to 0 * f(x,y,z) + 1 * g(x,y,z)
, if f(x,y,z)
returns Inf
then you produce NaN
as a result. There is no getting around needing a nanfill
function here as a result of how floats work.
Glad to hear it, I like that solution much more! I’ll take a look later today.