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:
- Build a graph of computations based on input variables,
Expr
block and output variables - Sort the nodes (equations) to check correct execution order (at the same time check if all variables are initialized)
- Remove unused equations (defuse)
- Simplify equations (remove
1.0*
, etc) - Transform the graph back to
Expr
block for futureeval
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:
- parse
JSON
with equations - build
Expr
block with equations simplify
expressions- build
ExGraph
from expression block topsort
graph- fuse_assigned to remove equations not necessary for
outvars
- get new
Expr
block withto_expr
rewrite_all
to substitute vectorsu[i]
on the place of parameters@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 theparse!()
method call; if you only want to remove unused variables, you can useremove_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!