dfdx / Espresso.jl

Expression transformation package

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ExGraph related example

ivborissov opened this issue · comments

Hi, thanks for this package!
I would like to use Espresso to deal with systems of ODE and general NL equations but I wasn't able to find an example in the docs or tests. Could you please give a more detailed explanation how to solve the following problem using Espresso?

I parse systems of equations from JSON files and before evaluating them as Julia functions I need to:

  1. Build a graph of computations based on input variables, Expr block and output variables
  2. Sort the nodes (equations) to check correct execution order (at the same time check if all variables are initialized)
  3. Remove unused equations (defuse)
  4. Simplify equations (remove 1.0*, etc)
  5. Transform the graph back to Expr block for future eval as Julia function

An example of my ODE system is:

 ex = quote
    comp2 = p[1]
    k1 = p[2]
    PS12 = p[3]
    k2 = p[4]
    EC50 = p[5]
    x1 = u[1] / comp1
    x2 = u[2] / comp2
    comp1 = 1 + x2 / (EC50 + x2)
    v1 = comp1 * k1
    v2 = PS12 * (x1 - x2)
    v3 = comp2 * k2 * x2
    du__1 = 1v1 - 1v2
    du__2 = 1v2 - 1v3
end  

Now I am trying to do smth like:
fuse_assigned(topsort(ExGraph(simplify(ex),p=rand(5),u=rand(2))), outvars=[:du__1,:du__2]) |> to_expr

In this example I can get rid of p[i] (they shouldn't be sorted) and provide them as input Dict. So basicaly my goal is to perform 1-5 having an Expr block, a list of input variables and a list of output variables. I will be grateful for your guidance on how to solve this problem.

I'm not sure I understand what is the question - the example you provided seems to work as expected:

fuse_assigned(topsort(ExGraph(simplify(ex),p=rand(5),u=rand(2))), outvars=[:du__1,:du__2]) |> to_expr
quote
    tmp477 = 1
    tmp478 = u[tmp477]
    tmp467 = 1
    tmp475 = 5
    tmp473 = 4
    tmp471 = 3
    EC50 = p[tmp475]
    k2 = p[tmp473]
    PS12 = p[tmp471]
    comp2 = p[tmp467]
    tmp480 = 2
    tmp481 = u[tmp480]
    x2 = tmp481 / comp2
    v3 = comp2 * k2 * x2
    tmp484 = EC50 + x2
    tmp485 = x2 / tmp484
    tmp483 = 1
    comp1 = tmp483 + tmp485
    x1 = tmp478 / comp1
    tmp488 = x1 - x2
    v2 = PS12 * tmp488
    du__2 = v2 - v3
end

If you want to move p outside of expression into an input dict, you can use combination of findex(pat, ex) and rewrite_all(ex, pat, rpat). E.g. to extract all occurrences of p[i] try:

julia> findex(:(p[_i]), ex)
5-element Array{Any,1}:
 :(p[1])
 :(p[2])
 :(p[3])
 :(p[4])
 :(p[5])

and to rewrite each p[1] to, say, a use:

rewrite_all(ex, :(p[1]), :a)
quote
    comp2 = a
    k1 = p[2]
    PS12 = p[3]
    k2 = p[4]
    EC50 = p[5]
    x1 = u[1] / comp1
    x2 = u[2] / comp2
    comp1 = 1 + x2 / (EC50 + x2)
    v1 = comp1 * k1
    v2 = PS12 * (x1 - x2)
    v3 = comp2 * k2 * x2
    du__1 = 1v1 - 1v2
    du__2 = 1v2 - 1v3
end

Sorry if this is not what you were asking, maybe if you show example input and example output I will be able to give a more useful answer.

Thanks for your response! In general I wanted to check if I understand the ideas correctly )) I am exploring the package and using it in the following chain:

  1. parse JSON with equations
  2. build Expr block with equations
  3. simplify expressions
  4. build ExGraph from expression block
  5. topsort graph
  6. fuse_assigned to remove equations not necessary for outvars
  7. get new Expr block with to_expr
  8. rewrite_all to substitute vectors u[i] on the place of parameters
  9. @eval func(u,p,t) = $expr

For this case Espresso is very helpful! In general I wanted to assure that I am not making simple things difficult and it is a proper way to use Espresso.

Yes, it's pretty much the intended usage. A couple of notes that might be useful:

  • fuse_assigned is designed to remove unnecessary assignments, in particular created from the parse!() method call; if you only want to remove unused variables, you can use remove_unused() directly
  • Espresso also contains a few utilities useful for function expression parsing and generation, e.g. make_func_expr()

Thanks for clarification! make_func_expr could also be useful in my case. And one more question. What is a good practice to eval functions created, for example, with make_func_expr? Simple @eval leads to world age error so after eval function I have use some construction with invokelatest like (args...) -> Base.invokelastest(func, args...)which is said to affect the performance

In statics settings, you can generate new function in a global scope during package construction, either using @eval / eval() or returning expression from a macro. However, in dynamic settings like yours when you build expression in runtime there isn't much choice but to use Base.invokelatest(). Good news is that invokelatest() usually isn't that bad, so better measure performance on your use case.

I see, thanks!