Limitations

FD does not support expressions with conditionals on FD variables. For example, you can do this:

julia> f(a,b,c) = a< 1.0 ? cos(b) : sin(c)
f (generic function with 2 methods)

julia> f(0.0,x,y)
cos(x)

julia> f(1.0,x,y)
sin(y)

but you can't do this:

julia> f(a,b) = a < b ? cos(a) : sin(b)
f (generic function with 2 methods)

julia> f(x,y)
ERROR: MethodError: no method matching isless(::FastDifferentiation.Node{Symbol, 0}, ::FastDifferentiation.Node{Symbol, 0})

Closest candidates are:
  isless(::Any, ::DataValues.DataValue{Union{}})
   @ DataValues ~/.julia/packages/DataValues/N7oeL/src/scalar/core.jl:291
  isless(::S, ::DataValues.DataValue{T}) where {S, T}
   @ DataValues ~/.julia/packages/DataValues/N7oeL/src/scalar/core.jl:285
  isless(::DataValues.DataValue{Union{}}, ::Any)
   @ DataValues ~/.julia/packages/DataValues/N7oeL/src/scalar/core.jl:293
  ...

This is because the call f(x,y) creates an expression graph. At graph creation time the FD variables x,y are unevaluated variables with no specific value so they cannot be compared with any other value.

The algorithm can be extended to work with conditionals applied to FD variables but the processing time and graph size may grow exponentially with conditional nesting depth. A future version may allow for limited conditional nesting. See Future Work for a potential long term solution to this problem.

FD does not support looping internally. All operations with loops, such as matrix vector multiplication, are unrolled into scalar operations. The corresponding executable functions generated by make_function have size proportional to the number of operations.

Expressions with ≈10⁵ scalar operations have reasonable symbolic preprocessing and compilation times. Beyond this size LLVM compilation time can become extremely long and eventually the executables become so large that their caching behavior is not good and performance declines.

A possible solution to this problem is to do what is called rerolling: detecting repreating indexing patterns in the FD expressions and automatically generating loops to replace inlined code. This rerolling step would be performed on the FD expressions graphs before function compliation.

It is not necessary to completely undo the unrolling back to the original expresssion, just to reduce code size enough to get reasonable compilation times and better caching behavior.

For example, in this matrix vector multiplication


julia> a = make_variables(:a,3,3)
3×3 Matrix{FastDifferentiation.Node}:
 a1_1  a1_2  a1_3
 a2_1  a2_2  a2_3
 a3_1  a3_2  a3_3

julia> b = make_variables(:b,3)
3-element Vector{FastDifferentiation.Node}:
 b1
 b2
 b3

julia> a*b
3-element Vector{Any}:
 (((a1_1 * b1) + (a1_2 * b2)) + (a1_3 * b3))
 (((a2_1 * b1) + (a2_2 * b2)) + (a2_3 * b3))
 (((a3_1 * b1) + (a3_2 * b2)) + (a3_3 * b3))

the goal is to replace concrete index numbers with symbolic index variables that represent offsets rather than absolute indices


[
  a[i,j]*b[j] + a[i, j+1]*b[j+1] + a[i,j+2]*b[j+2]
  a[i+1,j]*b[j] + a[i+1, j+1]*b[j+1] + a[i+1,j+2]*b[j+2]
  a[i+2,j]*b[j] + a[i+2, j+1]*b[j+1] + a[i+2,j+2]*b[j+2]
]

and then to extract looping structure from these indices.