In this notebook, we will be going through a tutorial on how to use the typegraph functionality to extract the relationships between types and functions within programs.
+
Typegraphs: teaching the compiler to reason about models¶
In this notebook, we will be going through a tutorial on how to use the typegraph functionality to extract the relationships between types and functions within programs.
We can apply our model augmentation framework to models that are compositions of component models.
+
+
+
+
+
+
+
+
+
We are going to use the model augmentation presented in examples/agentgraft.jl as a baseline simulation and build a workflow to compose that model with the example in examples/polynomial_regression.jl. It is strongly recommended that you understand those examples before following this notebook.
+
+
+
+
+
+
+
+
+
This example combines an agent based model of SIR diseases with a statistical model of polynomial regression to quantify the response of the agent based model with respect to one of its parameters. The input models have to be composed carefully in order to make the software work.
+
As taught by the scientific computing education group Software Carpentry, the best practice for composing scientific models is to have each component write files to disk and then use a workflow tool such as Make to orchestrate the execution of the modeling scripts.
+
An alternative approach is to design modeling frameworks for representing the models. The problem with this avenue becomes apparent when models are composed. The frameworks must be interoperable in order to make combined models. ModelTools avoids this problem by representing the models as code and manipulating the codes. The interoperation of two models is defined by user supplied functions in a fully featured programming language.
+
+
+
+
+
+
+
+
+
Let $m_1,m_2$ be models, and $t_1,t_2$ be tranformations and define $M_i = t_i(m_i)$. If we denote the creation of pipelines with the function composition symbol $g\circ f$ then we want to implement everything such that the following diagram commutes.
+
This example shows how you can use a pipeline to represent the combination of models and then apply combinations of transformations to that pipeline. Transforming models and composing them into pipelines are two operations that commute, you can transform then compose or compose and then transform.
Here is the baseline model, which is read in from a text file. You could instead of using parsefile use a quote/end block to code up the baseline model in this script.
The following expression defines a univariate polynomial regression model of degree 0, which just computes the average of target variable. This model can be augmented to an polynomial regression model using transformations
+$T_1,T_x$ which will be defined later.
+
+
+
+
+
+
+
In [9]:
+
+
+
expr=quote
+ moduleRegression
+ usingRandom
+ usingLsqFit
+ usingLinearAlgebra
+
+ functionf(x,β)
+ # This .+ node is added so that we have something to grab onto
+ # in the metaprogramming. It is the ∀a .+(a) == a.
+ return.+(β[1].*x.^0)
+ end
+
+ functionsample(g::Function,n)
+ x=randn(Float64,n)
+ target=g(x).+randn(Float64,n[1])./1600
+ returnx,target
+ end
+
+ functiondescribe(fit)
+ if!fit.converged
+ error("Did not converge")
+ end
+ return(β=fit.param,r=norm(fit.resid,2),n=length(fit.resid))
+ end
+ #setup
+ a₀=[1.0]
+ functionmain(X,target)
+ #solving
+ fit=curve_fit(f,X,target,a₀)#; autodiff=:forwarddiff)
+ result=describe(fit)
+ returnfit,result
+ end
+end
+end
+Regression=eval(expr.args[2])
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
┌ Info: Recompiling stale cache file /Users/jfairbanks6/.julia/compiled/v1.0/LsqFit/GCdY9.ji for LsqFit [2fda8390-95c7-5789-9bda-21331edee243]
+└ @ Base loading.jl:1190
+
The following code is the implementation details for representing the models as an AbstractProblem and representing the transformations as Product{Tuple{Pow{Int}, Pow{Int}}} and applying the transformations onto the models.
+
See the examples/polynomial_regression.jl example for details of what this code does.
Given our transformations f(x) -> xf(x) and f(x) -> f(x) + beta we are able to generate all possible polynomial regression using composition of these transformations.
+
+
+
+
+
+
+
In [17]:
+
+
+
# Let's build an instance of the model object from the code snippet expr
+m=model(Lsq,deepcopy(expr))
+mstats=deepcopy(m)
+@showm
+poly(m)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
m = Lsq(
+ f=:f,
+ coefficient=:β,
+ p₀=:a₀
+)
+
+
+
+
+
+
+
Out[17]:
+
+
+
+
+
+
:((.+)(β[1] .* x .^ 0))
+
+
+
+
+
+
+
+
+
+
+
+
+
Some generator elements will come in handy for building elements of the transformation group.
+$T_x,T_1$ are generators for our group of transformations $T = \langle T_x, T_1 \rangle$. $T_1$ adds a constant to our polynomial and $T_x$ increments all the powers of the terms by 1. Any polynomial can be generated by these two operations. The proof of Horner's rule for evaluating $p(x)$ gives a construction for how to create $f(x,\beta) = p(x)$ from these two operations.
Models can be chained together into workflows, the most basic type is a pipeline where the outputs from model $m_i$ are passed to model $m_{i+1}$. One area where traditional modeling frameworks get in trouble is the fact that the connections between the models can be arbitrarily complex. Thus any modeling framework that supports worflows, must embed a programming language for describing the connectors between the steps of the workflow.
+
Since we are already embedded in Julia, we will use regular Julia functions for the connectors.
+
Mathematically, a pipeline is defined as $r_n = P(m_1,\dots,m_n, c_1,\dots,c_n)$ based on the recurrence,
+
$r_0 = m_1(c)$ where $c$ is a constant value, and
+
$r_i = m_i(c_i(r_{i-1}))$
+
We store the values of $r_i$ in the field results so that they can be accessed later by visualization and analysis programs.
We are going to add an additional state to the model to represent the infectious disease fatalities. The user must specify what that concept means in terms of the name for the new state and the behavior of that state. D is a terminal state for a finite automata.
+
+
+
+
+
+
+
In [29]:
+
+
+
functionaddstate!(m::ExpStateModel)
+ println("\nThe system states are $(m.states.args)")
+ println("\nAdding un estado de los muertos")
+
+ put!(m,ExpStateTransition(:D,:((x...)->:D)))
+
+ println("\nThe system states are $(m.states.args)")
+ # once you are dead, you are dead forever
+ println("\nThere is no resurrection in this model")
+ println("\nInfected individuals recover or die in one step")
+
+ # replace!(m, ExpStateTransition(:I, :((x...)->rand(Bool) ? :D : :I)))
+ m[:I]=:((x...)->begin
+ roll=rand()
+ ifroll<ρ
+ return:R
+ elseifrand(Bool)
+ return:D
+ else
+ return:I
+ end
+ end
+ )
+ @showm[:I]
+ returnm
+end
+
+
+
+
+
+
+
+
+
+
+
+
+
Out[29]:
+
+
+
+
+
+
addstate! (generic function with 1 method)
+
+
+
+
+
+
+
+
+
+
+
+
+
Some utilities for manipulating functions at a higher level than expressions.
Another change we can make to our model is the introduction of population growth. Our model for population is that on each timestep, one new suceptible person will be added to the list of agents. We use the tick! function as an anchor point for this transformation.
+
+
+
+
+
+
+
+
In [31]:
+
+
+
functionaddgrowth!(m::ExpStateModel)
+ println("\nAdding population growth to this model")
+ stepr=filter(x->isa(x,Expr),findfunc(m.expr,:tick!))[1]
+ @showstepr
+ push!(Func(),stepr,:(push!(sm.agents,:S)))
+ println("------------------------")
+ @showstepr;
+ returnm
+end
+
We can apply the tranformations that we defined onto the pipeline. Remember that $T_1\times T_2$ acts on a pipeline by creating $P(T_1(m_1),T_2(m_2), c_1,c_2)$. The current implementation does not support transforming the connectors, but that would be straightforward to add.
+
+
+
+
+
+
+
In [32]:
+
+
+
functionconnector(finalcounts,i,j)
+ n=length(finalcounts)
+ Data=zeros(n,length(finalcounts[1].counts))
+ params=zeros(n,length(finalcounts[1].params))
+ @showsize(Data)
+ foriin1:n
+ c=finalcounts[i].counts
+ Data[i,:]=map(last,c)
+ params[i,:]=collect(map(float,finalcounts[i].params))
+ end
+ # multivariate regression not yet supported
+ # X = Data[:, 1:end-2]
+ # Y = Data[:, end]
+ # @assert(size(X) == (n,size(Data,2)-2))
+ # @assert(size(Y) == (n,))
+ X=params[:,i]
+ Y=Data[:,j]
+ # normalization
+ #Y = Y ./ [sum(Data[i, :]) for i in 1:n]
+ @assertsize(X,1)==size(Y,1)
+ returnX,Y
+end
+P=Pipelines.Pipeline(deepcopy.([magents,mstats]),
+ [(m,args...)->begin
+ Random.seed!(42)
+ results=Any[]
+ Mod=eval(m.expr)
+ @showMod
+ foriin1:samples
+ r=Base.invokelatest(Mod.main,args...)
+ push!(results,(model=:basic,counts=r[2],params=r[3]))
+ #push!(results, r)
+ end
+ return[results]
+ end,
+ (m,results...)->begin
+ data=connector(results...,1,4)
+ Mod=eval(m.expr)
+ Base.invokelatest(Mod.main,data...)
+ end
+ ],
+ Any[(10)]
+ )
+println("\nInitial Pipeline")
+println("----------------")
+@showP.steps[1].states
+@showpoly(P.steps[2])
+println("\n\nApplying the first pair of transformations")
+println("------------------------------------------")
+Product((addstate!,one(Pow)))(P)
+@showP.steps[1].states
+@showpoly(P.steps[2])
+println("\n\nApplying the second pair of transformations")
+println("-------------------------------------------")
+Product((x->x,AddConst()))(P)
+Product((addgrowth!,one(Pow)))(P)
+Product((x->x,AddConst()))(P)
+Product((x->x,one(Pow)))(P)
+Product((x->x,AddConst()))(P)
+println("\n\nThe final model state")
+println("---------------------")
+println(filter(isexpr,findfunc(P.steps[1].expr,:tick!))[end])
+@showpoly(P.steps[2]);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Initial Pipeline
+----------------
+(P.steps[1]).states = :([:S, :I, :R])
+poly(P.steps[2]) = :((.+)(β[1] .* x .^ 0))
+
+
+Applying the first pair of transformations
+------------------------------------------
+
+The system states are Any[:(:S), :(:I), :(:R)]
+
+Adding un estado de los muertos
+
+The system states are Any[:(:S), :(:I), :(:R), :(:D)]
+
+There is no resurrection in this model
+
+Infected individuals recover or die in one step
+m[:I] = :(x...->begin
+ #= In[29]:13 =#
+ #= In[29]:14 =#
+ roll = rand()
+ #= In[29]:15 =#
+ if roll < ρ
+ #= In[29]:16 =#
+ return :R
+ elseif #= In[29]:17 =# rand(Bool)
+ #= In[29]:18 =#
+ return :D
+ else
+ #= In[29]:20 =#
+ return :I
+ end
+ end)
+(P.steps[1]).states = :([:S, :I, :R, :D])
+poly(P.steps[2]) = :((.+)(β[1] .* x .^ 1))
+
+
+Applying the second pair of transformations
+-------------------------------------------
+p = :((.+)(β[1] .* x .^ 1))
+assigns = Expr[:(a₀ = [1.0])]
+
+Adding population growth to this model
+stepr = :(function tick!(sm::StateModel)
+ #= none:59 =#
+ sm.loads = map((s->begin
+ #= none:59 =#
+ stateload(sm, s)
+ end), sm.states)
+ #= none:60 =#
+ return sm.loads
+ end)
+------------------------
+stepr = :(function tick!(sm::StateModel)
+ #= none:59 =#
+ sm.loads = map((s->begin
+ #= none:59 =#
+ stateload(sm, s)
+ end), sm.states)
+ #= none:60 =#
+ return sm.loads
+ push!(sm.agents, :S)
+ end)
+p = :(β[1] .* x .^ 2 .+ β[2] .* x .^ 1)
+assigns = Expr[:(a₀ = [1.0, 1])]
+p = :(.+(β[1] .* x .^ 3, β[2] .* x .^ 2, β[3] .* x .^ 1))
+assigns = Expr[:(a₀ = [1.0, 1, 1])]
+
+
+The final model state
+---------------------
+function tick!(sm::StateModel)
+ #= none:59 =#
+ sm.loads = map((s->begin
+ #= none:59 =#
+ stateload(sm, s)
+ end), sm.states)
+ #= none:60 =#
+ return sm.loads
+ push!(sm.agents, :S)
+end
+poly(P.steps[2]) = :(.+(β[1] .* x .^ 3, β[2] .* x .^ 2, β[3] .* x .^ 1, β[4] .* x .^ 0))
+
(β = [32.4032, -43.9778, -0.99626, 13.2007], r = 25.657138767118166, n = 100)
+
+
+
+
+
+
+
+
+
+
+
+
+
Here is the data we observed when running the first stage of the pipeline, stage two fits a polynomial to these observations
+
+
+
+
+
+
+
In [ ]:
+
+
+
table=map(x->(round(x.params.ρ,digits=4),last(x.counts[end])),P.results[2][1])|>sort
+try
+ usingPlots
+catch
+ @warn"Plotting is not available, make a table"
+ fortintable
+ println(join(t,"\t"))
+ end
+end
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
┌ Info: Recompiling stale cache file /Users/jfairbanks6/.julia/compiled/v1.0/Plots/ld3vC.ji for Plots [91a5bcdd-55d7-5caf-9e0b-520d859cae80]
+└ @ Base loading.jl:1190
+
The regression model that we have trained based on the simulated data from the SIRD model with population growth can be presented as a polynomial sampled over the domain. We construct this table to show the nonlinear dependence of the model on the recovery parameter $\rho$. The best fitting polynomial is shown below.
@info"Loading Plots, this may take a while"
+usingPlots
+
+
+
+
+
+
+
+
+
+
In [ ]:
+
+
+
@info"Making plots, this may take a while"
+p=scatter(first.(table),last.(table),label="obs")
+plot!(first.(xŷ),last.(xŷ),label="fit")
+xlabel!(p,"Probability of Recovery")
+ylabel!(p,"Deaths")
+println("β: ",P.results[end][2].β,"\n",string(poly(P.steps[2])))
+p
+
This example shows that the SemanticModels approach to post-hoc modeling frameworks can enable metamodeling which is the combinations of model composed using different technologies into a coherent modeling workflow. Our ModelTools provides the basic building blocks for representing models and transformations in such a way that they transformations can be composed and models can be combined. Composition of transformations respects the combination of models. In this case the Product of transformation respects the Pipeline of models. Such that you can transform the models and then pipeline them, or pipeline them and then transform them.
+
This example combined an agent based model of SIR diseases with a statistical model of polynomial regression to quantify the response of the agent based model with respect to one of its parameters. The input models have to be composed carefully in order to make the software work.
+
As taught by the scientific computing education group Software Carpentry, the best practice for composing scientific models is to have each component write files to disk and then use a workflow tool such as Make to orchestrate the execution of the modeling scripts.
+
An alternative approach is to design modeling frameworks for representing the models. The problem with this avenue becomes apparent when models are composed. The frameworks must be interoperable in order to make combined models. ModelTools avoids this problem by representing the models as code and manipulating the codes. The interoperation of two models is defined by user supplied functions in a fully featured programming language.
+
SemanticModels.jl also provides transformations on these models that are grounded in category theory and abstract algebra. The concepts of category theory such as Functors and Product Categories allow us to build a general framework fit for any modeling task. In the language of category theory, the Pipelining functor on models commutes with the Product functor on transformations.
+
This examples shows that metamodeling is feasible with SemanticModels and that the algebras of model transformations can be preserved when acting on metamodel workflows.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/agenttypes.jl b/examples/agenttypes.jl
index 17cff47b..a54c2e91 100644
--- a/examples/agenttypes.jl
+++ b/examples/agenttypes.jl
@@ -162,9 +162,11 @@ function main(nsteps)
return newsam, counts
end
+# +
# # An example of how to run this thing.
-main(10)
-
+# main(10)
+# -
+
end
diff --git a/examples/agenttypes2.jl b/examples/agenttypes2.jl
index 04f53116..0f3e8d6b 100644
--- a/examples/agenttypes2.jl
+++ b/examples/agenttypes2.jl
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# # @typegraph example
+# # Typegraphs: teaching the compiler to reason about models
#
# In this notebook, we will be going through a tutorial on how to use the `typegraph` functionality to extract the relationships between types and functions within programs.
@@ -71,7 +71,7 @@ for e in E_typed
end
-# ## visualizing the edges
+# ## Visualizing the edges
#
# Now that we have extracted the relevant type information, we want to visualize these transformations in a knowledge graph.
@@ -170,7 +170,7 @@ color(v) = "#$(hex(cm[v + floor(Int, nv(h)/2)]))"
#
# In this drawing each vertex has its own color. These colors will be used again when drawing the next graph.
-for v in vertices(g)
+for v in vertices(h)
h.vprops[v][:color] = color(v)
h.vprops[v][:style] = "filled"
end