User-dependent meta-programming and recompilation

So if you can’t tell by my username, I’m a horrendous programmer. That said, I’m hoping to get opinions on the following:

1. What would actually be a decent way to achieve this
2. Is there any hope for user-dependent meta-programming in compiled code?

Suppose I would like to develop a package to numerically integrate through some hefty dynamics models, dependent on a user’s desires: [:modelA, :alternative_to_model_C], where each entry turned on a simulated physical effect: gravity, wind model 1, wind model 2, etc. One could then define the function and proceed, assuming the contents of the latter array were appropriately mapped to the Boolean sub-array, opts[1]:

function somethingLessEgregious(state0, opts)
    function dynamics!(dS, state, options, Δt)
        options[1][1] && A_dynamics!(dS, state, options[2])
        options[1][2] && B_dynamics!(dS, state, options[3])
        options[1][3] ? C_dynamics!(dS, state) : alternative_C_dynamics!(dS, state)
    end

    commonSense = ODEProblem(dynamics!, state0, (0.0, 3.14159), opts)

    return solve(commonSense)
end

The numerical integration itself could take on the order of several minutes (high fidelity models are…lovely), but would only need to be performed on occasion, after which the data could be saved for a rainy day. Even if there were 10+ effects the user could specify within the array of symbols, I realize that the logic implemented in dynamics! is computationally very light, whereas defining multiple methods could get rather unwieldy.

The following solution is akin to chopping off ones leg in order to prevent a singular mosquito bite, but the “what if?” question got the best of me:

models = [:modelA, :alternativeC]

function absolutelyAwful(state0, opts, models)
    lunacy = "function metaDynamics!(dS, state, options, Δt)\n"

    :modelA in models && (lunacy *= "A_dynamics!(dS, state, options[1])\n")
    :modelB in models && (lunacy *= "B_dynamics!(dS, state, options[2])\n")
    :modelC in models && (lunacy *= "C_dynamics!(dS, state)\n")
    :alternativeC in models && (lunacy *= "alternative_C_dynamics!(dS, state)\n")

    lunacy *= "return dS\n end"
    eval(Meta.parse(lunacy))

    updated(arg1, arg2, arg3, arg4) = Base.invokelatest(metaDynamics!, arg1, arg2, arg3, arg4)
    someonesNuts = ODEproblem(updated, state0, (0.0, 3.14159), opts)

    return solve(someonesNuts)
end

Not surprisingly, the performance is many thousand times worse. But hey, if I chop off one leg, I have a backup, right?

The meta-programming question ties in very closely with the “automatic recompilation of dependent functions” discussion. Is there any hope for being able to re-compile a section of code whose structure depends on user inputs without slowing things down with Base.invokelatest (although I’m appreciative that such a function exists!)? Are there any suggestions for developing a package where such a dynamics! function does not have to unnecessarily check so much logic at every step of the numerical integration scheme while still being able to account for potentially many tens of user-dependent options? A much better solution may very readily be apparent, but it evades me.

Thanks for the input and forgive me for the excessively long post. I wasn’t sure how best to capture certain complexities.

The easiest variant is probably to encode the models into a type, e.g.

const optionA = 1
const optionB = 1<<1
const optionC = 1<<2

function dynamics!(dS,state,options{optFlag}, deltaT)
(optFlag & optionA)!=0 && A_dynamics!(dS, state, options[2])
...

That way, you get rid of the dynamic typechecks without delving into @generated messes.

If you know how many models (i.e., top-level models, the A, B, and C, not including alternatives) then you can just use the type system rather than any meta-programming. Try

struct ModelA end;
struct ModelB end;
struct ModelC end;

struct AlternativeModelC end;

dynamics!(ds, state, ::ModelA) = nothing # something
dynamics!(ds, state, ::ModelB) = nothing # something
dynamics!(ds, state, ::ModelC) = nothing # something
dynamics!(ds, state, ::AlternativeModelC) = nothing # something

function myproblem(state0, opts, model::Tuple) # use a Tuple so that the model types are specialised on
    function odedynamics!(ds, state, options, Δt)
        # You need to know how many models (A, B, and Cs) to unpack the Tuple.
        # You cannot use a for-loop without losing type stability.
        dynamics!(ds, state, model[1])
        dynamics!(ds, state, model[2])
        dynamics!(ds, state, model[3])
        # If there are a small number of possible numbers of models you can hand-write the possibilities.
        # If there are a large number of possible numbers of models, you can use an @generated function.
    end
    prob = ODEProblem(odedynamics!, state0, (0.0, 3.14159), opts)
    return solve(prob)
end

myproblem(somestate, opts, (ModelA(), ModelB(), AlternativeModelC()))

Since the types propagate through the Tuple, Julia will automatically specialise the function code on the types you have provided and so, after the initial compilation time, this will be fast.

And if you really did want to use @generated functions just for the fun of it, try something along the lines of

@generated function dynamics!(ds, state, model::M) where {M <: Tuple}
    quote
        $((:(dynamics!(ds, state, model[$i])) for i in eachindex(M.parameters))...)
        # Generates a function body of the form (dynamics!(ds, state, model[1]); dynamics!(ds, state, model[2]); ...)
    end
end

function myproblem(state0, models::Tuple) # use a Tuple so that the model types are specialised on
    prob = ODEProblem((ds, state) -> dynamics!(ds, state, models), state0, (0.0, 3.14159))
    return solve(prob)
end

I have a few of these sorts of definitions in my own dynamical models where I want to switch in and out different behaviours.

Parameters are required in that derivative function, and you can just make it dispatch on parameters here.

Thanks @foobar_lv2! I’ve never thought of doing something like this with a bit pattern before. Genius! This is definitely a very sensible thing to do and I would be wise to use it - a very powerful framework for many problems indeed.

@dawbarton, this seems like it would be horribly complicated and perhaps even unstable throughout Julia releases…I LOVE IT! :stuck_out_tongue:

I won’t have any a priori knowledge of the number of dynamical effects to be included, some of which require shared time/step-dependent calculations (the position vector of the sun is a common one), so the added flexibility is most welcome. I look forward to giving it a try!

Thank you! I’m afraid I don’t readily see how to accomplish this without defining A) a prohibatively large number of methods, B) sacrificing the type stability in a for loop (thanks @dawbarton!), or C) knowing the number of models to include during compilation, but I’m sure there’s a good way to pull this off. I’ll keep chewing on this one.