diff --git a/src/CausalLoop.jl b/src/CausalLoop.jl index a9746af0..9d572cff 100644 --- a/src/CausalLoop.jl +++ b/src/CausalLoop.jl @@ -30,18 +30,18 @@ add_edges!(c::AbstractCausalLoop,n,s,t;kw...) = add_parts!(c,:E,n,s=s,t=t;kw...) Create causal loop diagram from collection of nodes and collection of edges. """ CausalLoop(ns,es) = begin - c = CausalLoop() - ns = vectorify(ns) - es = vectorify(es) - - ns_idx=state_dict(ns) - add_nodes!(c, length(ns), nname=ns) - - s=map(first,es) - t=map(last,es) - add_edges!(c, length(es), map(x->ns_idx[x], s), map(x->ns_idx[x], t)) - - c + c = CausalLoop() + ns = vectorify(ns) + es = vectorify(es) + + ns_idx=state_dict(ns) + add_nodes!(c, length(ns), nname=ns) + + s=map(first,es) + t=map(last,es) + add_edges!(c, length(es), map(x->ns_idx[x], s), map(x->ns_idx[x], t)) + + c end # return the count of each components @@ -62,25 +62,25 @@ nnames(c::AbstractCausalLoop) = [nname(c, n) for n in 1:nn(c)] function convertToCausalLoop(p::AbstractStockAndFlowStructure) - - sns=snames(p) - fns=fnames(p) - svns=svnames(p) - flowVariableIndexs=[flowVariableIndex(p,f) for f in 1:nf(p)] - vNotf=setdiff(1:nvb(p),flowVariableIndexs) - vNotfns=[vname(p,v) for v in vNotf] - - ns=vcat(sns,fns,svns,vNotfns) - - lses=[sname(p,subpart(p,ls,:lss))=>svname(p,subpart(p,ls,:lssv)) for ls in 1:nls(p)] - lsvfes=[svname(p,subpart(p,lsv,:lsvsv))=>subpart(p,lsv,:lsvv) in flowVariableIndexs ? fname(p,only(incident(p,subpart(p,lsv,:lsvv),:fv))) : vname(p,subpart(p,lsv,:lsvv)) for lsv in 1:nlsv(p)] - lfves=[sname(p,subpart(p,lv,:lvs))=>subpart(p,lv,:lvv) in flowVariableIndexs ? fname(p,only(incident(p,subpart(p,lv,:lvv),:fv))) : vname(p,subpart(p,lv,:lvv)) for lv in 1:nlv(p)] - fies=[fname(p,subpart(p,i,:ifn))=>sname(p,subpart(p,i,:is)) for i in 1:ni(p)] - foes=[fname(p,subpart(p,o,:ofn))=>sname(p,subpart(p,o,:os)) for o in 1:no(p)] - - es=vcat(lses,lsvfes,lfves,fies,foes) - - return CausalLoop(ns,es) + + sns=snames(p) + fns=fnames(p) + svns=svnames(p) + flowVariableIndexs=[flowVariableIndex(p,f) for f in 1:nf(p)] + vNotf=setdiff(1:nvb(p),flowVariableIndexs) + vNotfns=[vname(p,v) for v in vNotf] + + ns=vcat(sns,fns,svns,vNotfns) + + lses=[sname(p,subpart(p,ls,:lss))=>svname(p,subpart(p,ls,:lssv)) for ls in 1:nls(p)] + lsvfes=[svname(p,subpart(p,lsv,:lsvsv))=>subpart(p,lsv,:lsvv) in flowVariableIndexs ? fname(p,only(incident(p,subpart(p,lsv,:lsvv),:fv))) : vname(p,subpart(p,lsv,:lsvv)) for lsv in 1:nlsv(p)] + lfves=[sname(p,subpart(p,lv,:lvs))=>subpart(p,lv,:lvv) in flowVariableIndexs ? fname(p,only(incident(p,subpart(p,lv,:lvv),:fv))) : vname(p,subpart(p,lv,:lvv)) for lv in 1:nlv(p)] + fies=[fname(p,subpart(p,i,:ifn))=>sname(p,subpart(p,i,:is)) for i in 1:ni(p)] + foes=[fname(p,subpart(p,o,:ofn))=>sname(p,subpart(p,o,:os)) for o in 1:no(p)] + + es=vcat(lses,lsvfes,lfves,fies,foes) + + return CausalLoop(ns,es) end """ @@ -89,27 +89,27 @@ Nodes: stocks, flows, sum variables, parameters, nonflow dynamic variables Edges: morphisms in stock flow """ function convertToCausalLoop(p::AbstractStockAndFlowStructureF) - - sns=snames(p) - fns=fnames(p) - svns=svnames(p) - pns=pnames(p) - flowVariableIndexs=[flowVariableIndex(p,f) for f in 1:nf(p)] - vNotf=setdiff(1:nvb(p),flowVariableIndexs) - vNotfns=[vname(p,v) for v in vNotf] - - ns=vcat(sns,fns,svns,vNotfns,pns) - - lses=[sname(p,subpart(p,ls,:lss))=>svname(p,subpart(p,ls,:lssv)) for ls in 1:nls(p)] - lsvfes=[svname(p,subpart(p,lsv,:lsvsv))=>subpart(p,lsv,:lsvv) in flowVariableIndexs ? fname(p,only(incident(p,subpart(p,lsv,:lsvv),:fv))) : vname(p,subpart(p,lsv,:lsvv)) for lsv in 1:nlsv(p)] - lfves=[sname(p,subpart(p,lv,:lvs))=>subpart(p,lv,:lvv) in flowVariableIndexs ? fname(p,only(incident(p,subpart(p,lv,:lvv),:fv))) : vname(p,subpart(p,lv,:lvv)) for lv in 1:nlv(p)] - fies=[fname(p,subpart(p,i,:ifn))=>sname(p,subpart(p,i,:is)) for i in 1:ni(p)] - foes=[fname(p,subpart(p,o,:ofn))=>sname(p,subpart(p,o,:os)) for o in 1:no(p)] - lpvs=[pname(p,subpart(p,lp,:lpvp))=>subpart(p,lp,:lpvv) in flowVariableIndexs ? fname(p,only(incident(p,subpart(p,lp,:lpvv),:fv))) : vname(p,subpart(p,lp,:lpvv)) for lp in 1:nlpv(p)] - lvvs=[subpart(p,lv,:lvsrc) in flowVariableIndexs ? fname(p,only(incident(p,subpart(p,lv,:lvsrc),:fv))) : vname(p,subpart(p,lv,:lvsrc))=>subpart(p,lv,:lvtgt) in flowVariableIndexs ? fname(p,only(incident(p,subpart(p,lv,:lvtgt),:fv))) : vname(p,subpart(p,lv,:lvtgt)) for lv in 1:nlvv(p)] - - - es=vcat(lses,lsvfes,lfves,fies,foes,lpvs,lvvs) - - return CausalLoop(ns,es) + + sns=snames(p) + fns=fnames(p) + svns=svnames(p) + pns=pnames(p) + flowVariableIndexs=[flowVariableIndex(p,f) for f in 1:nf(p)] + vNotf=setdiff(1:nvb(p),flowVariableIndexs) + vNotfns=[vname(p,v) for v in vNotf] + + ns=vcat(sns,fns,svns,vNotfns,pns) + + lses=[sname(p,subpart(p,ls,:lss))=>svname(p,subpart(p,ls,:lssv)) for ls in 1:nls(p)] + lsvfes=[svname(p,subpart(p,lsv,:lsvsv))=>subpart(p,lsv,:lsvv) in flowVariableIndexs ? fname(p,only(incident(p,subpart(p,lsv,:lsvv),:fv))) : vname(p,subpart(p,lsv,:lsvv)) for lsv in 1:nlsv(p)] + lfves=[sname(p,subpart(p,lv,:lvs))=>subpart(p,lv,:lvv) in flowVariableIndexs ? fname(p,only(incident(p,subpart(p,lv,:lvv),:fv))) : vname(p,subpart(p,lv,:lvv)) for lv in 1:nlv(p)] + fies=[fname(p,subpart(p,i,:ifn))=>sname(p,subpart(p,i,:is)) for i in 1:ni(p)] + foes=[fname(p,subpart(p,o,:ofn))=>sname(p,subpart(p,o,:os)) for o in 1:no(p)] + lpvs=[pname(p,subpart(p,lp,:lpvp))=>subpart(p,lp,:lpvv) in flowVariableIndexs ? fname(p,only(incident(p,subpart(p,lp,:lpvv),:fv))) : vname(p,subpart(p,lp,:lpvv)) for lp in 1:nlpv(p)] + lvvs=[subpart(p,lv,:lvsrc) in flowVariableIndexs ? fname(p,only(incident(p,subpart(p,lv,:lvsrc),:fv))) : vname(p,subpart(p,lv,:lvsrc))=>subpart(p,lv,:lvtgt) in flowVariableIndexs ? fname(p,only(incident(p,subpart(p,lv,:lvtgt),:fv))) : vname(p,subpart(p,lv,:lvtgt)) for lv in 1:nlvv(p)] + + + es=vcat(lses,lsvfes,lfves,fies,foes,lpvs,lvvs) + + return CausalLoop(ns,es) end \ No newline at end of file diff --git a/src/PremadeModels.jl b/src/PremadeModels.jl index b6f9f8e3..a6ccb8ef 100644 --- a/src/PremadeModels.jl +++ b/src/PremadeModels.jl @@ -8,154 +8,154 @@ export seir, sis, sir, svi Return a new SEIR model """ function seir() - return deepcopy(seir_model) + return deepcopy(seir_model) end """ Return a new SIS model """ function sis() - return deepcopy(sis_model) + return deepcopy(sis_model) end """ Return a new SIR model """ function sir() - return deepcopy(sir_model) + return deepcopy(sir_model) end """ Return a new SVI model """ function svi() - return deepcopy(svi_model) + return deepcopy(svi_model) end seir_model = @stock_and_flow begin - :stocks - S - E - I - R + :stocks + S + E + I + R - :parameters - μ - β - tlatent - trecovery - δ - c + :parameters + μ + β + tlatent + trecovery + δ + c - :dynamic_variables - v_prevalence = NI / NS - v_meanInfectiousContactsPerS = c * v_prevalence # where c doesn't matter, can just make it 1 - v_perSIncidenceRate = β * v_meanInfectiousContactsPerS - v_newIncidence = S * v_perSIncidenceRate + :dynamic_variables + v_prevalence = NI / NS + v_meanInfectiousContactsPerS = c * v_prevalence # where c doesn't matter, can just make it 1 + v_perSIncidenceRate = β * v_meanInfectiousContactsPerS + v_newIncidence = S * v_perSIncidenceRate - v_birth = μ * N + v_birth = μ * N - v_inf = E / tlatent + v_inf = E / tlatent - v_rec = I / trecovery + v_rec = I / trecovery - v_deathS = δ * S - v_deathE = δ * E - v_deathI = δ * I - v_deathR = δ * R + v_deathS = δ * S + v_deathE = δ * E + v_deathI = δ * I + v_deathR = δ * R - :flows - CLOUD => f_birth(v_birth) => S - S => f_incid(v_newIncidence) => E - S => f_deathS(v_deathS) => CLOUD - E => f_inf(v_inf) => I - E => f_deathE(v_deathE) => CLOUD - I => f_rec(v_rec) => R - I => f_deathI(v_deathI) => CLOUD - R => f_deathR(v_deathR) => CLOUD + :flows + CLOUD => f_birth(v_birth) => S + S => f_incid(v_newIncidence) => E + S => f_deathS(v_deathS) => CLOUD + E => f_inf(v_inf) => I + E => f_deathE(v_deathE) => CLOUD + I => f_rec(v_rec) => R + I => f_deathI(v_deathI) => CLOUD + R => f_deathR(v_deathR) => CLOUD - :sums - N = [S, E, I, R] - NI = [I] - NS = [S, E, I, R] + :sums + N = [S, E, I, R] + NI = [I] + NS = [S, E, I, R] end sis_model = @stock_and_flow begin - :stocks - S - I + :stocks + S + I - :parameters - μ - β - trec # 1 / trecovery. This corresponds to σ. - δ - c + :parameters + μ + β + trec # 1 / trecovery. This corresponds to σ. + δ + c - :dynamic_variables - v_deathsX = δ * S - v_births = μ * N + :dynamic_variables + v_deathsX = δ * S + v_births = μ * N - v_prevalence = NI / NS - v_meanInfectiousContactsPerS = c * v_prevalence - v_perSIncidenceRate = β * v_meanInfectiousContactsPerS - v_newIncidence = S * v_perSIncidenceRate + v_prevalence = NI / NS + v_meanInfectiousContactsPerS = c * v_prevalence + v_perSIncidenceRate = β * v_meanInfectiousContactsPerS + v_newIncidence = S * v_perSIncidenceRate - v_newRecovery = I * trec - v_deathsI = I * δ + v_newRecovery = I * trec + v_deathsI = I * δ - :flows + :flows - S => f_deathsX(v_deathsX) => CLOUD - CLOUD => f_births(v_births) => S - S => f_newInfectious(v_newIncidence) => I - I => f_newRecovery(v_newRecovery) => S + S => f_deathsX(v_deathsX) => CLOUD + CLOUD => f_births(v_births) => S + S => f_newInfectious(v_newIncidence) => I + I => f_newRecovery(v_newRecovery) => S - I => f_deathsI(v_deathsI) => CLOUD + I => f_deathsI(v_deathsI) => CLOUD - :sums - N = [S, I] - NI = [I] - NS = [S, I] + :sums + N = [S, I] + NI = [I] + NS = [S, I] end sir_model = @stock_and_flow begin - :stocks - S - I - R + :stocks + S + I + R - :parameters - c - β - rRec + :parameters + c + β + rRec - :dynamic_variables - v_prevalence = NI / NS - v_meanInfectiousContactsPerS = c * v_prevalence - v_perSIncidenceRate = β * v_meanInfectiousContactsPerS - v_newInfections = S * v_perSIncidenceRate - v_newRecovery = I * rRec + :dynamic_variables + v_prevalence = NI / NS + v_meanInfectiousContactsPerS = c * v_prevalence + v_perSIncidenceRate = β * v_meanInfectiousContactsPerS + v_newInfections = S * v_perSIncidenceRate + v_newRecovery = I * rRec - :flows - S => f_inf(v_newInfections) => I - I => f_rec(v_newRecovery) => R + :flows + S => f_inf(v_newInfections) => I + I => f_rec(v_newRecovery) => R - :sums - N = [S, I, R] - NI = [I] - NS = [S,I,R] + :sums + N = [S, I, R] + NI = [I] + NS = [S,I,R] end @@ -164,39 +164,39 @@ end svi_model = @stock_and_flow begin - :stocks - S - V - I + :stocks + S + V + I - :parameters - rvaccine - δ - evaccine - c - β + :parameters + rvaccine + δ + evaccine + c + β - :dynamic_variables - v_vacc = S * rvaccine - v_deathV = δ * V + :dynamic_variables + v_vacc = S * rvaccine + v_deathV = δ * V - v_prevalence = NI / NS - v_meanInfectiousContactsPerS = c * v_prevalence - v_perSIncidenceRate = β * v_meanInfectiousContactsPerS - v_vaccineInfectionRate = V / evaccine # same thing as multiplying by complement - v_perSIncidenceVaccinated = v_vaccineInfectionRate * v_perSIncidenceRate + v_prevalence = NI / NS + v_meanInfectiousContactsPerS = c * v_prevalence + v_perSIncidenceRate = β * v_meanInfectiousContactsPerS + v_vaccineInfectionRate = V / evaccine # same thing as multiplying by complement + v_perSIncidenceVaccinated = v_vaccineInfectionRate * v_perSIncidenceRate - :flows - S => f_vacc(v_vacc) => V - V => f_deathV(v_deathV) => CLOUD - V => f_infV(v_perSIncidenceVaccinated) => I + :flows + S => f_vacc(v_vacc) => V + V => f_deathV(v_deathV) => CLOUD + V => f_infV(v_perSIncidenceVaccinated) => I - :sums - N = [S, V, I] - NI = [I] - NS = [S, V, I] + :sums + N = [S, V, I] + NI = [I] + NS = [S, V, I] end diff --git a/src/StockFlow.jl b/src/StockFlow.jl index f4683506..73a11234 100644 --- a/src/StockFlow.jl +++ b/src/StockFlow.jl @@ -292,62 +292,62 @@ get_lvtgt(sf::AbstractStockAndFlowF) = collect(values(sf.subparts[:lvtgt].m)) StockAndFlowStructure(s,f,sv) = begin - p = StockAndFlowStructure() - - s = vectorify(s) - f = vectorify(f) - sv = vectorify(sv) - - sname = map(first,s) - fname = map(first, f) - vname = map(last, f) - svname = map(first, sv) - s_idx = state_dict(sname) - f_idx = state_dict(fname) - v_idx = state_dict(vname) - sv_idx = state_dict(svname) - - # adding the objects that do not have out-morphisms firstly - add_variables!(p, length(vname), vname=vname) # add objects :V (auxiliary variables) - add_svariables!(p, length(svname), svname=svname) # add objects :SV (sum auxiliary variables) - add_flows!(p,map(x->v_idx[x], map(last,f)),length(fname),fname=fname) # add objects :F (flows) - - # Parse the elements included in "s" -- stocks - for (i, (name,(ins,outs,vs,svs))) in enumerate(s) - i = add_stock!(p,sname=name) # add objects :S (stocks) - ins=vectorify(ins) # inflows of each stock - outs=vectorify(outs) # outflows of each stock - vs=vectorify(vs) # auxiliary variables depends on the stock - svs=vectorify(svs) # sum auxiliary variables depends on the stock - # filter out the fake (empty) elements - ins = ins[ins .!= FK_FLOW_NAME] - outs = outs[outs .!= FK_FLOW_NAME] - vs = vs[vs .!= FK_VARIABLE_NAME] - svs = svs[svs .!= FK_SVARIABLE_NAME] - - if length(ins)>0 - add_inflows!(p, length(ins), repeat([i], length(ins)), map(x->f_idx[x], ins)) # add objects :I (inflows) - end - if length(outs)>0 - add_outflows!(p, length(outs), repeat([i], length(outs)), map(x->f_idx[x], outs)) # add objects :O (outflows) - end - if length(vs)>0 - add_Vlinks!(p, length(vs), repeat([i], length(vs)), map(x->v_idx[x], vs)) # add objects :LV (links from Stock to dynamic variable) - end - if length(svs)>0 - add_Slinks!(p, length(svs), repeat([i], length(svs)), map(x->sv_idx[x], svs)) # add objects :LS (links from Stock to sum dynamic variable) - end + p = StockAndFlowStructure() + + s = vectorify(s) + f = vectorify(f) + sv = vectorify(sv) + + sname = map(first,s) + fname = map(first, f) + vname = map(last, f) + svname = map(first, sv) + s_idx = state_dict(sname) + f_idx = state_dict(fname) + v_idx = state_dict(vname) + sv_idx = state_dict(svname) + + # adding the objects that do not have out-morphisms firstly + add_variables!(p, length(vname), vname=vname) # add objects :V (auxiliary variables) + add_svariables!(p, length(svname), svname=svname) # add objects :SV (sum auxiliary variables) + add_flows!(p,map(x->v_idx[x], map(last,f)),length(fname),fname=fname) # add objects :F (flows) + + # Parse the elements included in "s" -- stocks + for (i, (name,(ins,outs,vs,svs))) in enumerate(s) + i = add_stock!(p,sname=name) # add objects :S (stocks) + ins=vectorify(ins) # inflows of each stock + outs=vectorify(outs) # outflows of each stock + vs=vectorify(vs) # auxiliary variables depends on the stock + svs=vectorify(svs) # sum auxiliary variables depends on the stock + # filter out the fake (empty) elements + ins = ins[ins .!= FK_FLOW_NAME] + outs = outs[outs .!= FK_FLOW_NAME] + vs = vs[vs .!= FK_VARIABLE_NAME] + svs = svs[svs .!= FK_SVARIABLE_NAME] + + if length(ins)>0 + add_inflows!(p, length(ins), repeat([i], length(ins)), map(x->f_idx[x], ins)) # add objects :I (inflows) + end + if length(outs)>0 + add_outflows!(p, length(outs), repeat([i], length(outs)), map(x->f_idx[x], outs)) # add objects :O (outflows) + end + if length(vs)>0 + add_Vlinks!(p, length(vs), repeat([i], length(vs)), map(x->v_idx[x], vs)) # add objects :LV (links from Stock to dynamic variable) + end + if length(svs)>0 + add_Slinks!(p, length(svs), repeat([i], length(svs)), map(x->sv_idx[x], svs)) # add objects :LS (links from Stock to sum dynamic variable) end + end - # Parse the elements included in "sv" -- sum auxiliary vairables - for (i, (svname,vs)) in enumerate(sv) - vs=vectorify(vs) - vs = vs[vs .!= FK_SVVARIABLE_NAME] - if length(vs)>0 - add_SVlinks!(p, length(vs), repeat(collect(sv_idx[svname]), length(vs)), map(x->v_idx[x], collect(vs))) - end + # Parse the elements included in "sv" -- sum auxiliary vairables + for (i, (svname,vs)) in enumerate(sv) + vs=vectorify(vs) + vs = vs[vs .!= FK_SVVARIABLE_NAME] + if length(vs)>0 + add_SVlinks!(p, length(vs), repeat(collect(sv_idx[svname]), length(vs)), map(x->v_idx[x], collect(vs))) end - p + end + p end ###### TODO #### delete?? @@ -361,62 +361,62 @@ end StockAndFlow(s,f,v,sv) = begin - p = StockAndFlow() - - s = vectorify(s) - f = vectorify(f) - v = vectorify(v) - sv = vectorify(sv) - - sname = map(first,s) - fname = map(first, f) - vname = map(first, v) - svname = map(first, sv) - s_idx = state_dict(sname) - f_idx = state_dict(fname) - v_idx = state_dict(vname) - sv_idx = state_dict(svname) - - # adding the objects that do not have out-morphisms firstly - add_variables!(p, length(vname), vname=vname, funcDynam=map(last, v)) # add objects :V (auxiliary variables) - add_svariables!(p, length(svname), svname=svname) # add objects :SV (sum auxiliary variables) - add_flows!(p,map(x->v_idx[x], map(last,f)),length(fname),fname=fname) # add objects :F (flows) - - # Parse the elements included in "s" -- stocks - for (i, (name,(ins,outs,vs,svs))) in enumerate(s) - i = add_stock!(p,sname=name) # add objects :S (stocks) - ins=vectorify(ins) # inflows of each stock - outs=vectorify(outs) # outflows of each stock - vs=vectorify(vs) # auxiliary variables depends on the stock - svs=vectorify(svs) # sum auxiliary variables depends on the stock - # filter out the fake (empty) elements - ins = ins[ins .!= FK_FLOW_NAME] - outs = outs[outs .!= FK_FLOW_NAME] - vs = vs[vs .!= FK_VARIABLE_NAME] - svs = svs[svs .!= FK_SVARIABLE_NAME] - if length(ins)>0 - add_inflows!(p, length(ins), repeat([i], length(ins)), map(x->f_idx[x], ins)) # add objects :I (inflows) - end - if length(outs)>0 - add_outflows!(p, length(outs), repeat([i], length(outs)), map(x->f_idx[x], outs)) # add objects :O (outflows) - end - if length(vs)>0 - add_Vlinks!(p, length(vs), repeat([i], length(vs)), map(x->v_idx[x], vs)) # add objects :LV (links from Stock to dynamic variable) - end - if length(svs)>0 - add_Slinks!(p, length(svs), repeat([i], length(svs)), map(x->sv_idx[x], svs)) # add objects :LS (links from Stock to sum dynamic variable) - end + p = StockAndFlow() + + s = vectorify(s) + f = vectorify(f) + v = vectorify(v) + sv = vectorify(sv) + + sname = map(first,s) + fname = map(first, f) + vname = map(first, v) + svname = map(first, sv) + s_idx = state_dict(sname) + f_idx = state_dict(fname) + v_idx = state_dict(vname) + sv_idx = state_dict(svname) + + # adding the objects that do not have out-morphisms firstly + add_variables!(p, length(vname), vname=vname, funcDynam=map(last, v)) # add objects :V (auxiliary variables) + add_svariables!(p, length(svname), svname=svname) # add objects :SV (sum auxiliary variables) + add_flows!(p,map(x->v_idx[x], map(last,f)),length(fname),fname=fname) # add objects :F (flows) + + # Parse the elements included in "s" -- stocks + for (i, (name,(ins,outs,vs,svs))) in enumerate(s) + i = add_stock!(p,sname=name) # add objects :S (stocks) + ins=vectorify(ins) # inflows of each stock + outs=vectorify(outs) # outflows of each stock + vs=vectorify(vs) # auxiliary variables depends on the stock + svs=vectorify(svs) # sum auxiliary variables depends on the stock + # filter out the fake (empty) elements + ins = ins[ins .!= FK_FLOW_NAME] + outs = outs[outs .!= FK_FLOW_NAME] + vs = vs[vs .!= FK_VARIABLE_NAME] + svs = svs[svs .!= FK_SVARIABLE_NAME] + if length(ins)>0 + add_inflows!(p, length(ins), repeat([i], length(ins)), map(x->f_idx[x], ins)) # add objects :I (inflows) end + if length(outs)>0 + add_outflows!(p, length(outs), repeat([i], length(outs)), map(x->f_idx[x], outs)) # add objects :O (outflows) + end + if length(vs)>0 + add_Vlinks!(p, length(vs), repeat([i], length(vs)), map(x->v_idx[x], vs)) # add objects :LV (links from Stock to dynamic variable) + end + if length(svs)>0 + add_Slinks!(p, length(svs), repeat([i], length(svs)), map(x->sv_idx[x], svs)) # add objects :LS (links from Stock to sum dynamic variable) + end + end - # Parse the elements included in "sv" -- sum auxiliary vairables - for (i, (svname,vs)) in enumerate(sv) - vs=vectorify(vs) - vs = vs[vs .!= FK_SVVARIABLE_NAME] - if length(vs)>0 - add_SVlinks!(p, length(vs), repeat(collect(sv_idx[svname]), length(vs)), map(x->v_idx[x], collect(vs))) - end + # Parse the elements included in "sv" -- sum auxiliary vairables + for (i, (svname,vs)) in enumerate(sv) + vs=vectorify(vs) + vs = vs[vs .!= FK_SVVARIABLE_NAME] + if length(vs)>0 + add_SVlinks!(p, length(vs), repeat(collect(sv_idx[svname]), length(vs)), map(x->v_idx[x], collect(vs))) end - p + end + p end add_parameter!(p::AbstractStockAndFlowStructureF;kw...) = add_part!(p,:P;kw...) @@ -638,11 +638,11 @@ fvs(p::AbstractStockAndFlowStructure)=[fv(p,f) for f in 1:nf(p)] """ return the pair of names of (stock, sum-auxiliary-variable) for all linkages between them """ lsnames(p::AbstractStockAndFlow0) = begin - s = map(x->subpart(p,x,:lss),collect(1:nls(p))) - sv = map(x->subpart(p,x,:lssv),collect(1:nls(p))) - sn = map(x->sname(p,x),s) - svn = map(x->svname(p,x),sv) - pssv = collect(zip(sn, svn)) + s = map(x->subpart(p,x,:lss),collect(1:nls(p))) + sv = map(x->subpart(p,x,:lssv),collect(1:nls(p))) + sn = map(x->sname(p,x),s) + svn = map(x->svname(p,x),sv) + pssv = collect(zip(sn, svn)) end """ return inflows of stock index s """ @@ -694,56 +694,56 @@ lpvvposition(p::AbstractStockAndFlowF,v) = subpart(p,incident(p,v,:lpvv),:lpvppo # create a dictionary make_dict(ks, vs) = begin - @assert length(ks)==length(vs) - dic=() - for (k,v) in zip(ks,vs) - dic=(dic...,(k,v)) - end - return Dict(dic) + @assert length(ks)==length(vs) + dic=() + for (k,v) in zip(ks,vs) + dic=(dic...,(k,v)) + end + return Dict(dic) end """ create expresision of an auxiliary variable v """ function make_v_expr(p::AbstractStockAndFlowF,v) - op = vop(p,v) - srcsv=map(i->sname(p,i),stocksv(p,v)) - srcsvv=map(i->svname(p,i),svsv(p,v)) - srcpv=map(i->pname(p,i),vpsrc(p,v)) - srcvv=map(i->vname(p,i),vsrc(p,v)) + op = vop(p,v) + srcsv=map(i->sname(p,i),stocksv(p,v)) + srcsvv=map(i->svname(p,i),svsv(p,v)) + srcpv=map(i->pname(p,i),vpsrc(p,v)) + srcvv=map(i->vname(p,i),vsrc(p,v)) - lvvp=lvvposition(p,v) - lvtgtp=lvtgtposition(p,v) - lsvvp=lsvvposition(p,v) - lpvvp=lpvvposition(p,v) + lvvp=lvvposition(p,v) + lvtgtp=lvtgtposition(p,v) + lsvvp=lsvvposition(p,v) + lpvvp=lpvvposition(p,v) - if length(srcvv)>0 - srcvv=map(x->make_v_expr(p,vsrc(p,v)[x]),1:length(vsrc(p,v))) - end + if length(srcvv)>0 + srcvv=map(x->make_v_expr(p,vsrc(p,v)[x]),1:length(vsrc(p,v))) + end - # create dictionary of (key=position, value=symbole of source argument) - position_src=merge(make_dict(lvvp,srcsv),make_dict(lsvvp,srcsvv),make_dict(lpvvp,srcpv),make_dict(lvtgtp,srcvv)) - ordered_position_src=sort(collect(position_src), by = x->x[1]) - srcs=map(x->last(x),ordered_position_src) + # create dictionary of (key=position, value=symbole of source argument) + position_src=merge(make_dict(lvvp,srcsv),make_dict(lsvvp,srcsvv),make_dict(lpvvp,srcpv),make_dict(lvtgtp,srcvv)) + ordered_position_src=sort(collect(position_src), by = x->x[1]) + srcs=map(x->last(x),ordered_position_src) - return math_expr(op,srcs...) + return math_expr(op,srcs...) end # genreate an array of all arguments of an expression generate_expr_args(expr) = begin - args=expr.args - argsarray=[] - for arg in args - if arg isa Expr - argsarray=vcat(argsarray,generate_expr_args(arg)) - else - argsarray=vcat(argsarray,arg) - end - end - ops=vcat(collect(values(Operators))...) - return setdiff(unique(argsarray),ops) + args=expr.args + argsarray=[] + for arg in args + if arg isa Expr + argsarray=vcat(argsarray,generate_expr_args(arg)) + else + argsarray=vcat(argsarray,arg) + end + end + ops=vcat(collect(values(Operators))...) + return setdiff(unique(argsarray),ops) end # evaluate an expression to a function eval_function(expr,s,sv,p,us,uNs,ps) = begin - f=eval(Expr(:->, Expr(:tuple, s..., sv..., p...), Expr(:block,:(()),expr))) - return @eval $f($(us...), $(uNs...), $(ps...)) + f=eval(Expr(:->, Expr(:tuple, s..., sv..., p...), Expr(:block,:(()),expr))) + return @eval $f($(us...), $(uNs...), $(ps...)) end @@ -770,20 +770,20 @@ funcDynam(p::AbstractStockAndFlow,v) = subpart(p,v,:funcDynam) funcDynam(sf::AbstractStockAndFlowF,v) return the functions of variables give index v """ funcDynam(sf::AbstractStockAndFlowF,v) = begin - expr=make_v_expr(sf,v) - args=generate_expr_args(expr) - - args_s=args[findall(in(snames(sf)),args)] - args_sv=args[findall(in(svnames(sf)),args)] - args_p=args[findall(in(pnames(sf)),args)] - - f(u,uN,p,t)=begin - us=map(i->u[i],args_s) - uNs=map(i->uN[i](u,t),args_sv) - ps=map(i->p[i],args_p) - return eval_function(expr,args_s,args_sv,args_p,us,uNs,ps) - end - return f + expr=make_v_expr(sf,v) + args=generate_expr_args(expr) + + args_s=args[findall(in(snames(sf)),args)] + args_sv=args[findall(in(svnames(sf)),args)] + args_p=args[findall(in(pnames(sf)),args)] + + f(u,uN,p,t)=begin + us=map(i->u[i],args_s) + uNs=map(i->uN[i](u,t),args_sv) + ps=map(i->p[i],args_p) + return eval_function(expr,args_s,args_sv,args_p,us,uNs,ps) + end + return f end """ return the auxiliary variable's index that related to the flow with index of f """ @@ -797,29 +797,29 @@ funcFlowsRaw(p::Union{AbstractStockAndFlow,AbstractStockAndFlowF}) = begin end """ generate the function substituting sum variables in with flow index fn """ funcFlow(pn::Union{AbstractStockAndFlow,AbstractStockAndFlowF}, fn) = begin - func=funcFlowRaw(pn,fn) - f(u,p,t) = begin - uN=funcSVs(pn) - return valueat(func,u,uN,p,t) - end + func=funcFlowRaw(pn,fn) + f(u,p,t) = begin + uN=funcSVs(pn) + return valueat(func,u,uN,p,t) + end end """ return the LVector of pairs: fname => function (with function of sum variables substitue in) """ funcFlows(p::Union{AbstractStockAndFlow,AbstractStockAndFlowF})=begin - fnames = [fname(p, f) for f in 1:nf(p)] - LVector(;[(fnames[f]=>funcFlow(p, f)) for f in 1:nf(p)]...) + fnames = [fname(p, f) for f in 1:nf(p)] + LVector(;[(fnames[f]=>funcFlow(p, f)) for f in 1:nf(p)]...) end """ generate the function of a sum auxiliary variable (index sv) with the sum of all stocks links to it """ funcSV(p::AbstractStockAndFlow0,sv) = begin - uN(u,t) = begin - sumS = 0 - for i in stockssv(p,sv) - sumS=sumS+u[sname(p,i)] - end - return sumS - end - return uN + uN(u,t) = begin + sumS = 0 + for i in stockssv(p,sv) + sumS=sumS+u[sname(p,i)] + end + return sumS + end + return uN end """ return the LVector of pairs: svname => function """ funcSVs(p::AbstractStockAndFlow0) = begin @@ -833,11 +833,11 @@ end # given a stock and flow diagram in schema "StockAndFlow", return a stock and flow diagram in schema "StockAndFlow0" object_shift_right(p::StockAndFlowStructure) = begin - s = snames(p) - sv = svnames(p) - ssv = map(y->map(x->(sname(p,y),x),svname(p,svsstock(p,y))),collect(1:ns(p))) - ssv = vcat(ssv...) - StockAndFlow0(s,sv,ssv) + s = snames(p) + sv = svnames(p) + ssv = map(y->map(x->(sname(p,y),x),svname(p,svsstock(p,y))),collect(1:ns(p))) + ssv = vcat(ssv...) + StockAndFlow0(s,sv,ssv) end # create open acset, as the structured cospan @@ -852,27 +852,27 @@ foot(s, sv, ssv) = StockAndFlow0(s, sv, ssv) ntcomponent(a, x0) = map(x->state_dict(x0)[x], a) leg(a::StockAndFlow0, x::Union{StockAndFlowStructure,StockAndFlow,StockAndFlowStructureF,StockAndFlowF}) = begin - if ns(a)>0 # if have stocks - ϕs = ntcomponent(snames(a), snames(x)) - else - ϕs = Int[] - end + if ns(a)>0 # if have stocks + ϕs = ntcomponent(snames(a), snames(x)) + else + ϕs = Int[] + end - if nsv(a) > 0 # if have sum-auxiliary-variable - ϕsv = ntcomponent(svnames(a), svnames(x)) - else - ϕsv = Int[] - end + if nsv(a) > 0 # if have sum-auxiliary-variable + ϕsv = ntcomponent(svnames(a), svnames(x)) + else + ϕsv = Int[] + end - if nls(a)>0 # if have links between stocks and sum-auxiliary-variables - ϕls = ntcomponent(lsnames(a), lsnames(x)) - else - ϕls = Int[] - end + if nls(a)>0 # if have links between stocks and sum-auxiliary-variables + ϕls = ntcomponent(lsnames(a), lsnames(x)) + else + ϕls = Int[] + end - result = OpenACSetLeg(a, S=ϕs, LS=ϕls, SV=ϕsv) + result = OpenACSetLeg(a, S=ϕs, LS=ϕls, SV=ϕsv) - result + result end Open(p::StockAndFlow, feet...) = begin diff --git a/src/Syntax.jl b/src/Syntax.jl index de64137f..8bfe3240 100644 --- a/src/Syntax.jl +++ b/src/Syntax.jl @@ -4,101 +4,101 @@ ```julia # An S-I-R model of infection SIR = @stock_and_flow begin - :stocks - S - I - R - - :parameters - c - beta - tRec - - :dynamic_variables - v_prevalence = I / N - v_meanInfectiousContactsPerS = c * v_prevalence - v_perSIncidenceRate = beta * v_meanInfectiousContactsPerS - v_newInfections = S * v_perSIncidenceRate - v_newRecovery = I / tRec - - :flows - S => inf(v_newInfections) => I - I => rec(v_newRecovery) => R - - :sums - N = [S, I, R] + :stocks + S + I + R + + :parameters + c + beta + tRec + + :dynamic_variables + v_prevalence = I / N + v_meanInfectiousContactsPerS = c * v_prevalence + v_perSIncidenceRate = beta * v_meanInfectiousContactsPerS + v_newInfections = S * v_perSIncidenceRate + v_newRecovery = I / tRec + + :flows + S => inf(v_newInfections) => I + I => rec(v_newRecovery) => R + + :sums + N = [S, I, R] end # Generates: # SIR = StockAndFlowF( -# # stocks -# (:S => (:F_NONE, :inf, :N), :I => (:inf, :rec, :N), :R => (:rec, :F_NONE, :N)), -# # parameters -# (:c, :beta, :tRec), -# # dynamical variables -# ( :v_prevalence => ((:I, :N) => :/), -# :v_meanInfectiousContactsPerS => ((:c, :v_prevalence) => :*), -# :v_perSIncidenceRate => ((:beta, :v_meanInfectiousContactsPerS) => :*), -# :v_newInfections => ((:S, :v_perSIncidenceRate) => :*), -# :v_newRecovery => ((:I, :tRec) => :/), -# ), -# # flows -# (:inf => :v_newInfections, :rec => :v_newRecovery), -# # sum dynamical variables -# (:N), +# # stocks +# (:S => (:F_NONE, :inf, :N), :I => (:inf, :rec, :N), :R => (:rec, :F_NONE, :N)), +# # parameters +# (:c, :beta, :tRec), +# # dynamical variables +# ( :v_prevalence => ((:I, :N) => :/), +# :v_meanInfectiousContactsPerS => ((:c, :v_prevalence) => :*), +# :v_perSIncidenceRate => ((:beta, :v_meanInfectiousContactsPerS) => :*), +# :v_newInfections => ((:S, :v_perSIncidenceRate) => :*), +# :v_newRecovery => ((:I, :tRec) => :/), +# ), +# # flows +# (:inf => :v_newInfections, :rec => :v_newRecovery), +# # sum dynamical variables +# (:N), # ) # The same model as before, but with the dynamic variables inferred SIR_2 = @stock_and_flow begin - :stocks - S - I - R + :stocks + S + I + R - :parameters - c - beta - tRec + :parameters + c + beta + tRec - # We can leave out dynamic variables and let them be inferred from flows entirely! + # We can leave out dynamic variables and let them be inferred from flows entirely! - :flows - S => inf(S * beta * (c * (I / N))) => I - I => rec(I / tRec) => R + :flows + S => inf(S * beta * (c * (I / N))) => I + I => rec(I / tRec) => R - :sums - N = [S, I, R] + :sums + N = [S, I, R] end # Another possible S-I-R model definition SIR_3 = @stock_and_flow begin - :stocks - S - I - R - - :parameters - c - beta - tRec - omega - alpha - - :dynamic_variables - v_prevalence = I / totalPopulation - v_forceOfInfection = c * v_prevalence * beta - - :flows - S => inf(S * v_forceOfInfection) => I - ☁ => births(totalPopulation * alpha) => S - S => deathsS(S * omega) => ☁ - I => rec(I / tRec) => R - I => deathsI(I * omega) => ☁ - R => deathsR(R * omega) => ☁ - - - :sums - totalPopulation = [S, I, R] + :stocks + S + I + R + + :parameters + c + beta + tRec + omega + alpha + + :dynamic_variables + v_prevalence = I / totalPopulation + v_forceOfInfection = c * v_prevalence * beta + + :flows + S => inf(S * v_forceOfInfection) => I + ☁ => births(totalPopulation * alpha) => S + S => deathsS(S * omega) => ☁ + I => rec(I / tRec) => R + I => deathsI(I * omega) => ☁ + R => deathsR(R * omega) => ☁ + + + :sums + totalPopulation = [S, I, R] end ``` """ @@ -107,8 +107,9 @@ export @stock_and_flow, @foot, @feet, infer_links using ..StockFlow using MLStyle +using Catlab.CategoricalAlgebra -import Base: ==, Iterators.flatmap +import Base: ==, Iterators.flatmap, get """ stock_and_flow(block :: Expr) @@ -116,46 +117,46 @@ import Base: ==, Iterators.flatmap Compiles stock and flow syntax of the line-based block form ```julia :stocks - symbol_1 - symbol_2 - ... - symbol_n + symbol_1 + symbol_2 + ... + symbol_n :parameters - param_1 - param_2 - ... - param_n + param_1 + param_2 + ... + param_n :dynamic_variables - dyvar_1 = symbol_h * param_g ... - symbol_x / param_y - ... - dyvar_n = symbol_k * param_j - dyvar_a ... - symbol_p / param_q + dyvar_1 = symbol_h * param_g ... - symbol_x / param_y + ... + dyvar_n = symbol_k * param_j - dyvar_a ... - symbol_p / param_q :flows - symbol_r => flow_name_1(dyvar_k) => symbol_q - symbol_z => flow_name_2(dyvar_g * param_v) => symbol_p - ☁ => flow_name_3(symbol_c + dyvar_b) => symbol_r - symbol_j => flow_name_4(param_l + symbol_m) => CLOUD - ... - symbol_y => flow_name_n(dyvar_f) => ☁ + symbol_r => flow_name_1(dyvar_k) => symbol_q + symbol_z => flow_name_2(dyvar_g * param_v) => symbol_p + ☁ => flow_name_3(symbol_c + dyvar_b) => symbol_r + symbol_j => flow_name_4(param_l + symbol_m) => CLOUD + ... + symbol_y => flow_name_n(dyvar_f) => ☁ ``` into a StockAndFlowF data type for use with the StockFlow.jl modelling system. """ macro stock_and_flow(block) - Base.remove_linenums!(block) - block_args = block.args - return quote - local syntax_lines = parse_stock_and_flow_syntax($block_args) - local saff_args = stock_and_flow_syntax_to_arguments(syntax_lines) - StockAndFlowF( - saff_args.stocks, - saff_args.params, - map(kv -> kv.first => get(kv.second), saff_args.dyvars), - saff_args.flows, - saff_args.sums, - ) - end + Base.remove_linenums!(block) + block_args = block.args + return quote + local syntax_lines = parse_stock_and_flow_syntax($block_args) + local saff_args = stock_and_flow_syntax_to_arguments(syntax_lines) + StockAndFlowF( + saff_args.stocks, + saff_args.params, + map(kv -> kv.first => get(kv.second), saff_args.dyvars), + saff_args.flows, + saff_args.sums, + ) + end end """ @@ -164,21 +165,23 @@ end Contains the elements that make up the Stock and Flow block syntax. ### Fields -- `stocks` -- Each stock is defined by a single valid Julia variable name on a line. -- `params` -- Each parameter is defined by a single valid Julia variable name on a line. -- `dyvars` -- Each dynamic variable is defined by a valid Julia assignment statement of the - form `dyvar = expr` +- `stocks` -- Each stock is defined by a single valid Julia variable name on a + line. +- `params` -- Each parameter is defined by a single valid Julia variable name on + a line. +- `dyvars` -- Each dynamic variable is defined by a valid Julia assignment + statement of the form `dyvar = expr` - `flows` -- Each flow is defined by a valid Julia pair expression of the form - `variable_name => flow_name(flow_expression) => variable_name` -- `sums` -- Each sum is defined by a valid Julia assignment statement of the form - `sum_name = [a, b, c, ...]` + `variable_name => flow_name(flow_expression) => variable_name` +- `sums` -- Each sum is defined by a valid Julia assignment statement of the + form `sum_name = [a, b, c, ...]` """ struct StockAndFlowBlock - stocks::Vector{Symbol} - params::Vector{Symbol} - dyvars::Vector{Tuple{Symbol,Expr}} - flows::Vector{Tuple{Symbol,Expr,Symbol}} - sums::Vector{Tuple{Symbol,Vector{Symbol}}} + stocks::Vector{Symbol} + params::Vector{Symbol} + dyvars::Vector{Tuple{Symbol,Expr}} + flows::Vector{Tuple{Symbol,Expr,Symbol}} + sums::Vector{Tuple{Symbol,Vector{Symbol}}} end """ @@ -189,103 +192,104 @@ This can be used to directly instantiate StockAndFlowF. """ abstract type DyvarExprT end struct Binop{P1,P2} <: DyvarExprT - binop::Pair{Tuple{P1,P2},Symbol} + binop::Pair{Tuple{P1,P2},Symbol} end struct Ref <: DyvarExprT - ref::Pair{Symbol,Symbol} + ref::Pair{Symbol,Symbol} end get(r::Ref) = r.ref get(b::Binop) = b.binop struct StockAndFlowArguments - stocks::Vector{ - Pair{ - Symbol, - Tuple{ - Union{Symbol,Vector{Symbol}}, - Union{Symbol,Vector{Symbol}}, - Union{Symbol,Vector{Symbol}}, - }, - }, - } - params::Vector{Symbol} - dyvars::Vector{Pair{Symbol, DyvarExprT}} - flows::Vector{Pair{Symbol,Symbol}} - sums::Vector{Symbol} + stocks::Vector{ + Pair{ + Symbol, + Tuple{ + Union{Symbol,Vector{Symbol}}, + Union{Symbol,Vector{Symbol}}, + Union{Symbol,Vector{Symbol}}, + }, + }, + } + params::Vector{Symbol} + dyvars::Vector{Pair{Symbol, DyvarExprT}} + flows::Vector{Pair{Symbol,Symbol}} + sums::Vector{Symbol} end """ parse_stock_and_flow_syntax(statements :: Vector{Any}) -Given a vector of Julia expressions, attempt to interpret them using the block syntax -defined for Stock and Flow diagrams. +Given a vector of Julia expressions, attempt to interpret them using the block +syntax defined for Stock and Flow diagrams. ### Input -- `statements` -- A series of Julia expressions, each a line of code in - a block of statements. +- `statements` -- A series of Julia expressions, each a line of code in a block + of statements. ### Output -A StockAndFlowSyntax data type which contains the syntax pieces required to define -a Stock and Flow model: stocks, parameters, dynamic variables, flows, and sums. +A StockAndFlowSyntax data type which contains the syntax pieces required to +define a Stock and Flow model: stocks, parameters, dynamic variables, flows, and +sums. """ function parse_stock_and_flow_syntax(statements::Vector{Any}) - stocks::Vector{Symbol} = [] - params::Vector{Symbol} = [] - dyvars::Vector{Tuple{Symbol,Expr}} = [] - flows::Vector{Tuple{Symbol,Expr,Symbol}} = [] - sums::Vector{Tuple{Symbol,Vector{Symbol}}} = [] - current_phase = (_, _) -> () - for statement in statements - @match statement begin - QuoteNode(:stocks) => begin - current_phase = s -> parse_stock!(stocks, s) - end - QuoteNode(:parameters) => begin - current_phase = p -> parse_param!(params, p) - end - QuoteNode(:dynamic_variables) => begin - current_phase = d -> parse_dyvar!(dyvars, d) - end - QuoteNode(:flows) => begin - current_phase = f -> parse_flow!(flows, f) - end - QuoteNode(:sums) => begin - current_phase = s -> parse_sum!(sums, s) - end - QuoteNode(kw) => - error("Unknown block type for Stock and Flow syntax: " * String(kw)) - _ => current_phase(statement) - end + stocks::Vector{Symbol} = [] + params::Vector{Symbol} = [] + dyvars::Vector{Tuple{Symbol,Expr}} = [] + flows::Vector{Tuple{Symbol,Expr,Symbol}} = [] + sums::Vector{Tuple{Symbol,Vector{Symbol}}} = [] + current_phase = (_, _) -> () + for statement in statements + @match statement begin + QuoteNode(:stocks) => begin + current_phase = s -> parse_stock!(stocks, s) + end + QuoteNode(:parameters) => begin + current_phase = p -> parse_param!(params, p) + end + QuoteNode(:dynamic_variables) => begin + current_phase = d -> parse_dyvar!(dyvars, d) + end + QuoteNode(:flows) => begin + current_phase = f -> parse_flow!(flows, f) + end + QuoteNode(:sums) => begin + current_phase = s -> parse_sum!(sums, s) + end + QuoteNode(kw) => + error("Unknown block type for Stock and Flow syntax: " * String(kw)) + _ => current_phase(statement) end + end - s = StockAndFlowBlock(stocks, params, dyvars, flows, sums) - return s + s = StockAndFlowBlock(stocks, params, dyvars, flows, sums) + return s end """ stock_and_flow_syntax_to_arguments(syntax_elements::StockAndFlowSyntax) -Convert the Stock and Flow Syntax elements to parameters suitable for StockAndFlowF -data type instantiation +Convert the Stock and Flow Syntax elements to parameters suitable for +StockAndFlowF data type instantiation ### Input -- `syntax_elements` -- The output from `parse_stock_and_flow_syntax`, - containing the definition of a stock and flow model +- `syntax_elements` -- The output from `parse_stock_and_flow_syntax`, containing + the definition of a stock and flow model ### Output Parameters for instantiation of a StockAndFlowF data type. """ function stock_and_flow_syntax_to_arguments(syntax_elements::StockAndFlowBlock) - stocks = assemble_stock_definitions( - syntax_elements.stocks, - syntax_elements.flows, - syntax_elements.sums, - ) - params = syntax_elements.params - dyvars = dyvar_exprs_to_symbolic_repr(syntax_elements.dyvars) - dyvar_names = [dyvar_name for (dyvar_name, _dyvar_def) in dyvars] - (flows, flow_dyvars) = create_flow_definitions(syntax_elements.flows, dyvar_names) - sums = sum_variables(syntax_elements.sums) - return StockAndFlowArguments(stocks, params, vcat(dyvars, flow_dyvars), flows, sums) + stocks = assemble_stock_definitions( + syntax_elements.stocks, + syntax_elements.flows, + syntax_elements.sums, + ) + params = syntax_elements.params + dyvars = dyvar_exprs_to_symbolic_repr(syntax_elements.dyvars) + dyvar_names = [dyvar_name for (dyvar_name, _dyvar_def) in dyvars] + (flows, flow_dyvars) = create_flow_definitions(syntax_elements.flows, dyvar_names) + sums = sum_variables(syntax_elements.sums) + return StockAndFlowArguments(stocks, params, vcat(dyvars, flow_dyvars), flows, sums) end @@ -294,8 +298,8 @@ end Add a stock to the list of stocks. -Stocks in the Stock and Flow syntax are just a single symbol on a line, so no work -is currently done by this function. +Stocks in the Stock and Flow syntax are just a single symbol on a line, so no +work is currently done by this function. ### Input - `stocks` -- A list of stocks already parsed from the block @@ -305,7 +309,7 @@ is currently done by this function. None. This mutates the given stocks vector. """ function parse_stock!(stocks::Vector{Symbol}, stock::Symbol) - push!(stocks, stock) + push!(stocks, stock) end """ @@ -313,8 +317,8 @@ end Add a param to the list of params. -Params in the Stock and Flow syntax are just a single symbol on a line, -so no work is currently done by this function. +Params in the Stock and Flow syntax are just a single symbol on a line, so no +work is currently done by this function. ### Input - `params` -- A list of params already parsed from the block @@ -324,11 +328,11 @@ so no work is currently done by this function. None. This mutates the given params vector. """ function parse_param!(params::Vector{Symbol}, param::Symbol) - push!(params, param) + push!(params, param) end """ - is_recursive_dyvar(dyvar_name :: Symbol, dyvar_def :: Expr) + is_recursive_dyvar(dyvar_name :: Symbol, dyvar_def :: Expr) Check that the dyvar_name is not used somewhere in the dyvar_def @@ -340,55 +344,58 @@ Check that the dyvar_name is not used somewhere in the dyvar_def True if the dyvar name is used in the expression; false elsewise. """ function is_recursive_dyvar(dyvar_name, dyvar_def) - @match dyvar_def begin - ::Symbol => dyvar_def == dyvar_name - :($f()) => f == dyvar_name - Expr(:call, args...) => true in map(arg -> is_recursive_dyvar(dyvar_name, arg), args) - end + @match dyvar_def begin + ::Symbol => dyvar_def == dyvar_name + :($f()) => f == dyvar_name + Expr(:call, args...) => true in map(arg -> is_recursive_dyvar(dyvar_name, arg), args) + end end """ parse_dyvar!(dyvars :: Vector{Tuple{Symbol, Expr}}, dyvar :: Expr) -Extract the dynamic variable name and defining expression from a Julia expression of form -`dyvar = a + b * c ...`, and add it to the vector of already parsed dynamic variables. +Extract the dynamic variable name and defining expression from a Julia +expression of form `dyvar = a + b * c ...`, and add it to the vector of already +parsed dynamic variables. ### Input - `dyvars` -- A list of dynamic variables and their defining Julia expressions - already parsed from the block -- `dyvar` -- A dynamic variable definition of the form `dyvar = defining_expression` + already parsed from the block +- `dyvar` -- A dynamic variable definition of the form `dyvar = + defining_expression` ### Output None. This mutates the given dyvars vector. """ function parse_dyvar!(dyvars::Vector{Tuple{Symbol,Expr}}, dyvar::Expr) - @match dyvar begin - :($dyvar_name = $dyvar_def) => - if !is_recursive_dyvar(dyvar_name, dyvar_def) - push!(dyvars, (dyvar_name, dyvar_def)) - else - error("Recursive dyvar detected in Symbol: " * String(dyvar_name)) - end - Expr(c, _, _) || Expr(c, _, _, _) => - error("Unhandled expression in dynamic variable definition " * String(c)) - end + @match dyvar begin + :($dyvar_name = $dyvar_def) => + if !is_recursive_dyvar(dyvar_name, dyvar_def) + push!(dyvars, (dyvar_name, dyvar_def)) + else + error("Recursive dyvar detected in Symbol: " * String(dyvar_name)) + end + Expr(c, _, _) || Expr(c, _, _, _) => + error("Unhandled expression in dynamic variable definition " * String(c)) + end end """ parse_flow_io(flow_definition :: Expr) -Given a flow definition of the form `SYMBOL => flow_name(flow_equation) => SYMBOL`, -return a 3-tuple of its constituent parts: the start symbol, the end symbol, -and the flow equations's definition as an expression. +Given a flow definition of the form `SYMBOL => flow_name(flow_equation) => +SYMBOL`, return a 3-tuple of its constituent parts: the start symbol, the end +symbol, and the flow equations's definition as an expression. ### Input -- `flow_definition` -- A flow definition of the form - `SYMBOL => flow_name(flow_equation) => SYMBOL`, - where SYMBOL can be an arbitrary name or special cases of ☁ or CLOUD, - which corresponds to a flow from nowhere. +- `flow_definition` -- A flow definition of the form `SYMBOL => + flow_name(flow_equation) => SYMBOL`, where SYMBOL can be an + arbitrary name or special cases of ☁ or CLOUD, which corresponds to + a flow from nowhere. ### Output -A 3-tuple (input, expression, output) of a an input and output symbol (either of which -may be :F_NONE for a flow from nowhere) and the flow equation as a julia expression. +A 3-tuple (input, expression, output) of a an input and output symbol (either of +which may be :F_NONE for a flow from nowhere) and the flow equation as a julia +expression. ### Examples ```julia-repl @@ -397,73 +404,75 @@ julia> Syntax.parse_flow_io(:(CLOUD => birthRate(a * b * c) => S)) ``` """ function parse_flow(flow_definition::Expr) - @match flow_definition begin - :(CLOUD => $flow => $stock_out) || :(☁ => $flow => $stock_out) => - (:F_NONE, flow, stock_out) - :($stock_in => $flow => CLOUD) || :($stock_in => $flow => ☁) => - (stock_in, flow, :F_NONE) - :($stock_in => $flow => $stock_out) => (stock_in, flow, stock_out) - Expr(en, _, _, _) || Expr(en, _, _) => - error("Unhandled expression in flow definition " * String(en)) - end + @match flow_definition begin + :(CLOUD => $flow => $stock_out) || :(☁ => $flow => $stock_out) => + (:F_NONE, flow, stock_out) + :($stock_in => $flow => CLOUD) || :($stock_in => $flow => ☁) => + (stock_in, flow, :F_NONE) + :($stock_in => $flow => $stock_out) => (stock_in, flow, stock_out) + Expr(en, _, _, _) || Expr(en, _, _) => + error("Unhandled expression in flow definition " * String(en)) + end end """ parse_flow!(flows :: Vector{Tuple{Symbol, Expr, Symbol}}, flow :: Expr) -Extract the flow input, output, and defining expression from a Julia expression of -the form `IN_SYMBOL => FLOW_NAME(FLOW_EQUATION) => OUT_SYMBOL`, -and add it to the vector of already parsed flows. +Extract the flow input, output, and defining expression from a Julia expression +of the form `IN_SYMBOL => FLOW_NAME(FLOW_EQUATION) => OUT_SYMBOL`, and add it to +the vector of already parsed flows. ### Input - `flows` -- A list of parsed flows -- `flow` -- A flow definition of the form `in => flow_name(flow_equation) => out` +- `flow` -- A flow definition of the form `in => flow_name(flow_equation) => + out` ### Output None. This mutates the given flows vector. """ function parse_flow!(flows::Vector{Tuple{Symbol,Expr,Symbol}}, flow::Expr) - parsed_flow = parse_flow(flow) - push!(flows, parsed_flow) + parsed_flow = parse_flow(flow) + push!(flows, parsed_flow) end """ parse_sum!(sums :: Vector{Tuple{Symbol, Vector{Symbol}}}, sum :: Expr) -Extract the sum name and the stocks that flow into it from a stock expression of the form -`SUM_NAME = [STOCK_1, STOCK_2, STOCK_3, ...]` +Extract the sum name and the stocks that flow into it from a stock expression of +the form `SUM_NAME = [STOCK_1, STOCK_2, STOCK_3, ...]` ### Input - `sums` -- A list of parsed sum names and their incoming stocks -- `sum` -- A sum definition as a Julia expression of the form - `sum_name = [stock_1, stock_2, stock_3, ...]` +- `sum` -- A sum definition as a Julia expression of the form `sum_name = + [stock_1, stock_2, stock_3, ...]` ### Output None. This mutates the given sums vector. """ function parse_sum!(sums::Vector{Tuple{Symbol,Vector{Symbol}}}, sum::Expr) - @match sum begin - :($sum_name = $equation) => push!(sums, (sum_name, equation.args)) - Expr(c, _, _) || Expr(c, _, _, _) => - error("Unhandled expression in sum defintion " * String(c)) - end + @match sum begin + :($sum_name = $equation) => push!(sums, (sum_name, equation.args)) + Expr(c, _, _) || Expr(c, _, _, _) => + error("Unhandled expression in sum defintion " * String(c)) + end end """ assemble_stock_definitions( stocks::Vector{Symbol} - , flows::Vector{Tuple{Symbol,Expr,Symbol}} - , sum_variables::Vector{Tuple{Symbol,Vector{Symbol}}} - ) + , flows::Vector{Tuple{Symbol,Expr,Symbol}} + , sum_variables::Vector{Tuple{Symbol,Vector{Symbol}}} + ) -Convert the raw syntax of a Stock and Flow block definition into a series of stock -definitions suitable for input into the StockAndFlowF data type, which is +Convert the raw syntax of a Stock and Flow block definition into a series of +stock definitions suitable for input into the StockAndFlowF data type, which is (:STOCK_NAME => ((INPUT_ARROWS,...), (OUTPUT_ARROWS,...), (SUM_ARROWS,...))). -The input, output, and sum arrows are calculated from the sum and flow definitions. +The input, output, and sum arrows are calculated from the sum and flow +definitions. ### Input - `stocks` -- A list of stock names as symbols - `flows` -- A list of flow definitions, which may use any of the stock names as - inputs or outputs. + inputs or outputs. - `sum_variables` -- A list of sum definitions. ### Output @@ -471,58 +480,59 @@ The raw syntax definitions of a Stock and Flow block rearranged into a StockAndFlowF stock definition vector. """ function assemble_stock_definitions( - stocks::Vector{Symbol}, - flows::Vector{Tuple{Symbol,Expr,Symbol}}, - sum_variables::Vector{Tuple{Symbol,Vector{Symbol}}}, + stocks::Vector{Symbol}, + flows::Vector{Tuple{Symbol,Expr,Symbol}}, + sum_variables::Vector{Tuple{Symbol,Vector{Symbol}}}, ) - # Check that all of the stocks involved in flow definitions exist - stock_set = Set(stocks) - push!(stock_set, :F_NONE) # for error handling step: any 'clouds' should be F_NONE by now. - for (start_object, flow, end_object) in flows - if !(start_object in stock_set) - error(String(start_object) * " is not a known stock.") - elseif !(end_object in stock_set) - error(String(end_object) * " is not a known stock.") - end + # Check that all of the stocks involved in flow definitions exist + stock_set = Set(stocks) + push!(stock_set, :F_NONE) # for error handling step: any 'clouds' should be F_NONE by now. + for (start_object, flow, end_object) in flows + if !(start_object in stock_set) + error(String(start_object) * " is not a known stock.") + elseif !(end_object in stock_set) + error(String(end_object) * " is not a known stock.") end + end - formatted_stocks = [] - for stock in stocks - input_arrows::Vector{Symbol} = [] - output_arrows::Vector{Symbol} = [] - sum_arrows::Vector{Symbol} = [] - for (start_object, flow, end_object) in flows - (flow_name, _) = extract_function_name_and_args_expr(flow) - if start_object == stock - push!(output_arrows, flow_name) - end - if end_object == stock - push!(input_arrows, flow_name) - end - end - for (sum_variable_name, inputs) in sum_variables - if stock in inputs - push!(sum_arrows, sum_variable_name) - end - end - push!( - formatted_stocks, - ( - stock => ( - fnone_value_or_vector(input_arrows), - fnone_value_or_vector(output_arrows), - default_or_value_or_vector(sum_arrows; default=:SV_NONE), - ) - ), - ) + formatted_stocks = [] + for stock in stocks + input_arrows::Vector{Symbol} = [] + output_arrows::Vector{Symbol} = [] + sum_arrows::Vector{Symbol} = [] + for (start_object, flow, end_object) in flows + (flow_name, _) = extract_function_name_and_args_expr(flow) + if start_object == stock + push!(output_arrows, flow_name) + end + if end_object == stock + push!(input_arrows, flow_name) + end + end + for (sum_variable_name, inputs) in sum_variables + if stock in inputs + push!(sum_arrows, sum_variable_name) + end end - return formatted_stocks + push!( + formatted_stocks, + ( + stock => ( + fnone_value_or_vector(input_arrows), + fnone_value_or_vector(output_arrows), + default_or_value_or_vector(sum_arrows; default=:SV_NONE), + ) + ), + ) + end + return formatted_stocks end """ generate_dyvar_name(name :: Symbol) -Generate a dynamic variable name using the given symbol as a prefix. All dynamic variables are prefixed with 'v_' +Generate a dynamic variable name using the given symbol as a prefix. All dynamic +variables are prefixed with 'v_' ### Input - `name` -- A symbol base for the dyvar @@ -538,140 +548,142 @@ julia> gensym(sym) Symbol("##v_my_var#718") """ function generate_dyvar_name(name::Symbol) - name_str = String(name) - if cmp("v_", name_str) == -1 - name_str - else - "v_" * name_str - end + name_str = String(name) + if cmp("v_", name_str) == -1 + name_str + else + "v_" * name_str + end end """ dyvar_exprs_to_symbolic_repr(dyvars::Vector{Tuple{Symbol,Expr}}) -Converts a series of dynamic variable definitions of the form `dyvar = dyvar_expression` -into a form suitable for input into the StockAndFlowF data type: -(:dyvar => ((:arg1, :arg2) => :function_name)) +Converts a series of dynamic variable definitions of the form `dyvar = +dyvar_expression` into a form suitable for input into the StockAndFlowF data +type: (:dyvar => ((:arg1, :arg2) => :function_name)) ### Input -- `dyvars` -- A vector of pairs of dynamic variable names (as symbols) - and Julia expressions defining them. +- `dyvars` -- A vector of pairs of dynamic variable names (as symbols) and Julia + expressions defining them. ### Output A vector of dynamic variable definitions suitable for input to StockAndFlowF. """ function dyvar_exprs_to_symbolic_repr(dyvars::Vector{Tuple{Symbol,Expr}}) - syms::Vector{Pair{Symbol,DyvarExprT}} = [] - for (dyvar_name, dyvar_definition) in dyvars - if is_binop_or_unary(dyvar_definition) - @match dyvar_definition begin - Expr(:call, op, a) => push!(syms, (dyvar_name => Ref(a => op))) - Expr(:call, op, a, b) => begin - push!(syms, (dyvar_name => Binop((a, b) => op))) - end - Expr(c, _, _) || Expr(c, _, _, _) => error( - "Unhandled expression in dynamic variable definition " * String(c), - ) - end - else - (binops, _) = - infix_expression_to_binops(dyvar_definition, finalsym=dyvar_name, gensymbase=generate_dyvar_name(dyvar_name)) - binops_syms = dyvar_exprs_to_symbolic_repr(binops) - syms = vcat(syms, binops_syms) + syms::Vector{Pair{Symbol,DyvarExprT}} = [] + for (dyvar_name, dyvar_definition) in dyvars + if is_binop_or_unary(dyvar_definition) + @match dyvar_definition begin + Expr(:call, op, a) => push!(syms, (dyvar_name => Ref(a => op))) + Expr(:call, op, a, b) => begin + push!(syms, (dyvar_name => Binop((a, b) => op))) end + Expr(c, _, _) || Expr(c, _, _, _) => error( + "Unhandled expression in dynamic variable definition " * String(c), + ) + end + else + (binops, _) = + infix_expression_to_binops(dyvar_definition, finalsym=dyvar_name, gensymbase=generate_dyvar_name(dyvar_name)) + binops_syms = dyvar_exprs_to_symbolic_repr(binops) + syms = vcat(syms, binops_syms) end - return syms + end + return syms end """ create_flow_definitions( flows::Vector{Tuple{Symbol,Expr,Symbol}} - , dyvar_names :: Vector{Symbol} - ) + , dyvar_names :: Vector{Symbol} + ) -Assemble the flow parameters for a StockAndFlowF data type from definitions of the form -`input_stock => flow_name(flow_equation) => output_stock`. +Assemble the flow parameters for a StockAndFlowF data type from definitions of +the form `input_stock => flow_name(flow_equation) => output_stock`. ### Input -- `flows` -- A vector of flow definitions in the form of 3-tuple - `(input_stock, flow_expr, output_stock) +- `flows` -- A vector of flow definitions in the form of 3-tuple `(input_stock, + flow_expr, output_stock) - `dyvar_names` -- A vector of known dynamic variable names ### Output A 2-tuple containing in the first cell the flow definitions of the form -`flow_name => dynamic_variable_name`, and in the second cell any dynamic variables -generated in generating the flow definitions. +`flow_name => dynamic_variable_name`, and in the second cell any dynamic +variables generated in generating the flow definitions. """ function create_flow_definitions(flows::Vector{Tuple{Symbol,Expr,Symbol}}, dyvar_names) - flow_definitions = [] - updated_dyvars = [] - # Start and end objects here can be ignored: - # In StockAndFlowF, they are factored into the stocks parameter, not here. - for (_start_object, flow, _end_object) in flows - (additional_dyvars, flow_definition) = flow_expr_to_symbolic_repr(flow, dyvar_names) - push!(flow_definitions, flow_definition) - updated_dyvars = vcat(updated_dyvars, additional_dyvars) - end - return (flow_definitions, updated_dyvars) + flow_definitions = [] + updated_dyvars = [] + # Start and end objects here can be ignored: In StockAndFlowF, they are + # factored into the stocks parameter, not here. + for (_start_object, flow, _end_object) in flows + (additional_dyvars, flow_definition) = flow_expr_to_symbolic_repr(flow, dyvar_names) + push!(flow_definitions, flow_definition) + updated_dyvars = vcat(updated_dyvars, additional_dyvars) + end + return (flow_definitions, updated_dyvars) end """ flow_expr_to_symbolic_repr(flow_expression :: Expr, dyvar_names :: Vector{Symbol}) -Converts a flow definition from one of three forms into a format suitable -for input to StockAndFlowF data types: +Converts a flow definition from one of three forms into a format suitable for +input to StockAndFlowF data types: - `flow_name(name)` -- uses the dynamic variable `name` as the flow equation -- `flow_name(expr)` -- hoists the expr out as a dynamic variable and replaces - it with the generated name -- `flow_name(expr, name=name)` -- hoists the expr out as a dynamic variable and replaces - it with the given name +- `flow_name(expr)` -- hoists the expr out as a dynamic variable and replaces it + with the generated name +- `flow_name(expr, name=name)` -- hoists the expr out as a dynamic variable and + replaces it with the given name ### Input - `flow_expression` -- a flow definition equation from Stock and Flow syntax -- `dyvar_names` -- a vector of symbols that the flow_expression may be referring to +- `dyvar_names` -- a vector of symbols that the flow_expression may be referring + to ### Output -The flow_expression's representation StockAndFlowF parameter format: -`flow_name => dynamic_variable` +The flow_expression's representation StockAndFlowF parameter format: `flow_name +=> dynamic_variable` """ function flow_expr_to_symbolic_repr(flow_expression, dyvar_names) - @match flow_expression begin - :($flow_name($expr)) => begin - if typeof(expr) <: Symbol - if expr in dyvar_names - return ([], flow_name => expr) - else - error("Unknown dynamic variable referenced " * String(expr)) - end - else - (additional_dyvars, var_name) = infix_expression_to_binops(expr, gensymbase=generate_dyvar_name(flow_name)) - dyvs = dyvar_exprs_to_symbolic_repr(additional_dyvars) - return (dyvs, flow_name => var_name) - end - end - :($flow_name($expr, name=$sym)) => begin - (additional_dyvars, var_name) = infix_expression_to_binops(expr, finalsym=sym) - dyvs = dyvar_exprs_to_symbolic_repr(additional_dyvars) - return (dyvs, flow_name => sym) - end - Expr(c, _, _) || Expr(c, _, _, _) => begin - error("Unhandled expression in flow equation definition " * String(c)) + @match flow_expression begin + :($flow_name($expr)) => begin + if typeof(expr) <: Symbol + if expr in dyvar_names + return ([], flow_name => expr) + else + error("Unknown dynamic variable referenced " * String(expr)) end + else + (additional_dyvars, var_name) = infix_expression_to_binops(expr, gensymbase=generate_dyvar_name(flow_name)) + dyvs = dyvar_exprs_to_symbolic_repr(additional_dyvars) + return (dyvs, flow_name => var_name) + end end + :($flow_name($expr, name=$sym)) => begin + (additional_dyvars, var_name) = infix_expression_to_binops(expr, finalsym=sym) + dyvs = dyvar_exprs_to_symbolic_repr(additional_dyvars) + return (dyvs, flow_name => sym) + end + Expr(c, _, _) || Expr(c, _, _, _) => begin + error("Unhandled expression in flow equation definition " * String(c)) + end + end end """ - extract_function_name_and_args_expr(flow::Expr) + extract_function_name_and_args_expr(flow::Expr) -Given a Julia expression of the form f(a), return the symbol :f and the expression, a, -the function is being called with. +Given a Julia expression of the form f(a), return the symbol :f and the +expression, a, the function is being called with. ### Input - `flow` -- a julia expression of the form f(a) ### Output -The name of the function as a symbol, and the expression being passed to it as an argument. +The name of the function as a symbol, and the expression being passed to it as +an argument. ### Examples ```julia-repl @@ -680,22 +692,23 @@ julia> Syntax.extract_flow_name_and_equation(:(infectionRate(a + b + c))) ``` """ function extract_function_name_and_args_expr(flow_equation::Expr) - @match flow_equation begin - :($flow_name($expr)) => (flow_name, expr) - :($flow_name($expr, name=$_)) => (flow_name, expr) - Expr(en, _, _, _) || Expr(en, _, _) => - error("Unhandled expression in flow name definition " * String(en)) - end + @match flow_equation begin + :($flow_name($expr)) => (flow_name, expr) + :($flow_name($expr, name=$_)) => (flow_name, expr) + Expr(en, _, _, _) || Expr(en, _, _) => + error("Unhandled expression in flow name definition " * String(en)) + end end """ default_or_value_or_tuple(arrows :: Vector{Symbol}) -Given a vector of arrow names, modify it into suitable input for a StockAndFlowF data type: -default for an empty vector, the value alone for a singleton vector, +Given a vector of arrow names, modify it into suitable input for a StockAndFlowF +data type: default for an empty vector, the value alone for a singleton vector, and the vector itself if there are multiple arrows given. ### Input -- `arrows` -- A vector of symbols which represents some of the arrows for a stock. +- `arrows` -- A vector of symbols which represents some of the arrows for a + stock. - `default` -- A default symbol if an empty vector is passed in ### Output @@ -706,25 +719,26 @@ given default, a single symbol, or a list of symbols. ` """ function default_or_value_or_vector(arrows::Vector{Symbol}; default=:F_NONE) - if isempty(arrows) - default - elseif length(arrows) == 1 - arrows[1] - else - arrows - end + if isempty(arrows) + default + elseif length(arrows) == 1 + arrows[1] + else + arrows + end end """ fnone_or_tuple(arrows :: Vector{Symbol}) -Given a vector of arrow names, modify it into suitable input for a StockAndFlowF data type: -:F_NONE for an empty vector, the value alone for a singleton vector, +Given a vector of arrow names, modify it into suitable input for a StockAndFlowF +data type: :F_NONE for an empty vector, the value alone for a singleton vector, and the vector itself if there are multiple arrows given. ### Input -- `arrows` -- A vector of symbols which represents some of the arrows for a stock. +- `arrows` -- A vector of symbols which represents some of the arrows for a + stock. ### Output :F_NONE, a single symbol, or a list of symbols. @@ -734,23 +748,23 @@ and the vector itself if there are multiple arrows given. ` """ function fnone_value_or_vector(arrows::Vector{Symbol}) - default_or_value_or_vector(arrows) + default_or_value_or_vector(arrows) end """ - infix_expression_to_binops( expression :: Expr; gensymbase :: String = "" - , finalsym :: Union{Nothing, Symbol} = nothing) + infix_expression_to_binops( expression :: Expr; gensymbase :: String = "" + , finalsym :: Union{Nothing, Symbol} = nothing) -Convert a nested expression of the form (a * b + c - d / e ...) into a series of binary -operations with named values (sym1 = a * b; sym2 = sym1 + c; ...; symn = symn-1 + somevar). -If finalsym is given, symn is replaced with the one given. +Convert a nested expression of the form (a * b + c - d / e ...) into a series of +binary operations with named values (sym1 = a * b; sym2 = sym1 + c; ...; symn = +symn-1 + somevar). If finalsym is given, symn is replaced with the one given. ### Input - `expression` -- a single expression containing nested function calls. - `gensymbase` -- the base name of the generated interim symbols for each binop -- `sym` -- (optional, default: `nothing`) a name for the final result to replace - one of the generated symbols. If not given, it will be of the form - `Symbol("###")` e.g. `Symbol("###418")`. +- `sym` -- (optional, default: `nothing`) a name for the final result to + replace one of the generated symbols. If not given, it will be of the + form `Symbol("###")` e.g. `Symbol("###418")`. ### Output A vector of tuples of variable definitions as symbols and the corresponding @@ -761,8 +775,8 @@ Julia expression that calculates the variable. julia> infix_expression_to_binops(:(a + b + c)) julia> Syntax.infix_expression_to_binops(:(a + b + c)) ( Tuple{Symbol, Expr}[ (Symbol("###495"), :(a + b)) - , (Symbol("###496"), :(var"###495" + c)) - ] + , (Symbol("###496"), :(var"###495" + c)) + ] , Symbol("###496") ) ``` @@ -774,11 +788,11 @@ That is, this converts the expression `a + b + c` into two expressions: ``` julia> Syntax.infix_expression_to_binops( :(a + b + c) - , gensymbase="generated_variable" - , lastsym=:infectionRate) + , gensymbase="generated_variable" + , lastsym=:infectionRate) ( Tuple{Symbol, Expr}[ (Symbol("##generated_variable#1045"), :(a + b)) - , (:infectionRate, :(var"##generated_variable#1045" + c)) - ] + , (:infectionRate, :(var"##generated_variable#1045" + c)) + ] , :infectionRate) ``` That is, this converts the expression `a + b + c` into the expressions: @@ -786,61 +800,62 @@ That is, this converts the expression `a + b + c` into the expressions: generated_variable#1045 = a + b infectionRate = generatedVariable#1045 + c ``` -This would be used in the case the original expression was `infectionRate = a + b + c`. +This would be used in the case the original expression was `infectionRate = a + +b + c`. """ function infix_expression_to_binops( - expression::Expr; - gensymbase::String="", - finalsym::Union{Nothing,Symbol}=nothing + expression::Expr; + gensymbase::String="", + finalsym::Union{Nothing,Symbol}=nothing ) - exprs::Vector{Tuple{Symbol,Expr}} = [] - function loop(e) - @match e begin - ::Symbol || ::Float32 || ::Float64 || ::Int || ::String => e - Expr(:call, f, a) => begin - asym = loop(a) - varname = gensym(gensymbase) - push!(exprs, (varname, :($f($asym)))) - varname - end - Expr(:call, f, a, b) => begin - asym = loop(a) - bsym = loop(b) - varname = gensym(gensymbase) - push!(exprs, (varname, :($f($asym, $bsym)))) - varname - end - Expr(:call, f, args...) => begin - if (isempty(args)) - error("Expression f() cannot be converted into form f(a, b)") - end - argsyms = map(loop, args) - lastsym = gensym(gensymbase) - a = popfirst!(argsyms) - b = popfirst!(argsyms) - symexpr = :($f($a, $b)) - push!(exprs, (lastsym, symexpr)) - for argsym in argsyms - currsym = gensym(gensymbase) - push!(exprs, (currsym, :($f($lastsym, $argsym)))) - lastsym = currsym - end - lastsym - end - Expr(en, _, _, _) || Expr(en, _, _) => begin - error( - "Unhandled expression type " * String(en) * " cannot be converted into form f(a, b)", - ) - end + exprs::Vector{Tuple{Symbol,Expr}} = [] + function loop(e) + @match e begin + ::Symbol || ::Float32 || ::Float64 || ::Int || ::String => e + Expr(:call, f, a) => begin + asym = loop(a) + varname = gensym(gensymbase) + push!(exprs, (varname, :($f($asym)))) + varname + end + Expr(:call, f, a, b) => begin + asym = loop(a) + bsym = loop(b) + varname = gensym(gensymbase) + push!(exprs, (varname, :($f($asym, $bsym)))) + varname + end + Expr(:call, f, args...) => begin + if (isempty(args)) + error("Expression f() cannot be converted into form f(a, b)") end + argsyms = map(loop, args) + lastsym = gensym(gensymbase) + a = popfirst!(argsyms) + b = popfirst!(argsyms) + symexpr = :($f($a, $b)) + push!(exprs, (lastsym, symexpr)) + for argsym in argsyms + currsym = gensym(gensymbase) + push!(exprs, (currsym, :($f($lastsym, $argsym)))) + lastsym = currsym + end + lastsym + end + Expr(en, _, _, _) || Expr(en, _, _) => begin + error( + "Unhandled expression type " * String(en) * " cannot be converted into form f(a, b)", + ) + end end - last_generated_sym = loop(expression) - if finalsym !== nothing - set_final_binop_varname!(exprs, finalsym) - return (exprs, finalsym) - else - return (exprs, last_generated_sym) - end + end + last_generated_sym = loop(expression) + if finalsym !== nothing + set_final_binop_varname!(exprs, finalsym) + return (exprs, finalsym) + else + return (exprs, last_generated_sym) + end end """ @@ -849,14 +864,14 @@ end Extract the names of the sum variables from its syntax elements. ### Input -`sum_syntax_elements` -- A vector of 2-tuples of the sum variable's name and - its constituent stocks. +`sum_syntax_elements` -- A vector of 2-tuples of the sum variable's name and its + constituent stocks. ### Output A vector of sum variable names. """ sum_variables(sum_syntax_elements) = - [sum_name for (sum_name, _sum_definition) in sum_syntax_elements] + [sum_name for (sum_name, _sum_definition) in sum_syntax_elements] """ @@ -868,7 +883,8 @@ Check if a Julia expression is a call of the form `op(a, b)` or `a op b` - `e` -- a Julia expression ### Output -A boolean indicating if the given julia expression is a function call of two parameters. +A boolean indicating if the given julia expression is a function call of two +parameters. ### Examples ```julia-repl @@ -885,34 +901,35 @@ false ``` """ function is_binop_or_unary(e::Expr) - @match e begin - Expr(:call, f::Symbol, a) => true - Expr(:call, f::Symbol, a, b) => true - _ => false - end + @match e begin + Expr(:call, f::Symbol, a) => true + Expr(:call, f::Symbol, a, b) => true + _ => false + end end """ set_finaL_binop_varname!(exprs::Vector{Tuple{Symbol, Expr}}, targetsym::Symbol) -Given an expression that is in the form of a series of binary operations -(e.g. from infix_expression_to_binops), replace the final generated symbol with a given one. +Given an expression that is in the form of a series of binary operations (e.g. +from infix_expression_to_binops), replace the final generated symbol with a +given one. ### Input -- `exprs` -- A vector of tuples of variable name symbols and their corresponding expression - definitions. +- `exprs` -- A vector of tuples of variable name symbols and their corresponding + expression definitions. - `varname` -- The final variable name to set. ### Output -The original collection, with the last element updated to have a new variable name -in its tuple. +The original collection, with the last element updated to have a new variable +name in its tuple. ### Examples ```julia-repl julia> (binops, _final_sym_name) = Syntax.infix_expression_to_binops(:(a + b + c)) ( Tuple{Symbol, Expr}[ (Symbol("###1415"), :(a + b)) - , (Symbol("###1416"), :(var"###1415" + c)) - ] + , (Symbol("###1416"), :(var"###1415" + c)) + ] , Symbol("###1416") ) @@ -930,9 +947,9 @@ julia> binops ``` """ function set_final_binop_varname!(exprs::Vector{Tuple{Symbol,Expr}}, varname::Symbol) - idx = lastindex(exprs) - (_oldvarname, expr) = last(exprs) - exprs[idx] = (varname, expr) + idx = lastindex(exprs) + (_oldvarname, expr) = last(exprs) + exprs[idx] = (varname, expr) end @@ -961,18 +978,17 @@ end """ feet(block :: Expr) -Create Vector of feet using same notation for foot macro. -Separated by newlines. +Create Vector of feet using same notation for foot macro. Separated by newlines. First argument is stock, second is sum variable. ```julia feetses = @feet begin - A => B - () => N - C => () - D => E - () => () - P => NI, R => NI, () => N + A => B + () => N + C => () + D => E + () => () + P => NI, R => NI, () => N end ``` """ @@ -999,9 +1015,12 @@ end """ create_foot(block :: Expr) -Create foot (StockAndFlow0) using format A => B, where A is a stock and B is a sum variable. Use () to represent no stock or sum variable. -To have multiple stocks or sum variables, chain together multiple pairs with commas. Repeated occurences of the same symbol will be treated as the same stock or sum variable. -You cannot create distinct stocks or sum variables with the same name using this format. +Create foot (StockAndFlow0) using format A => B, where A is a stock and B is a +sum variable. Use () to represent no stock or sum variable. To have multiple +stocks or sum variables, chain together multiple pairs with commas. Repeated +occurences of the same symbol will be treated as the same stock or sum variable. +You cannot create distinct stocks or sum variables with the same name using this +format. ```julia standard_foot = @foot A => N @@ -1014,77 +1033,80 @@ multiple_links = @foot A => B, A => B # will have two links from A to B. """ function create_foot(block::Expr) - @match block.head begin - :tuple => begin - if isempty(block.args) # case for create_foot(:()) - error("Cannot create foot with zero arguments.") - end - foot_s = Vector{Symbol}() - foot_sv = Vector{Symbol}() - foot_ssv = Vector{Pair{Symbol, Symbol}}() - - for (s, sv, ssv) ∈ map(match_foot_format, block.args) - if s != () push!(foot_s, s) end - if sv != () push!(foot_sv, sv) end - if ssv != () push!(foot_ssv, ssv) end - end - return foot(unique(foot_s), unique(foot_sv), foot_ssv) + @match block.head begin + :tuple => begin + if isempty(block.args) # case for create_foot(:()) + error("Cannot create foot with zero arguments.") + end + foot_s = Vector{Symbol}() + foot_sv = Vector{Symbol}() + foot_ssv = Vector{Pair{Symbol, Symbol}}() + + for (s, sv, ssv) ∈ map(match_foot_format, block.args) + if s != () push!(foot_s, s) end + if sv != () push!(foot_sv, sv) end + if ssv != () push!(foot_ssv, ssv) end end - :call => foot(match_foot_format(block)...) - _ => error("Invalid expression type $(block.head). Expecting tuple or call.") + return foot(unique(foot_s), unique(foot_sv), foot_ssv) end + :call => foot(match_foot_format(block)...) + _ => error("Invalid expression type $(block.head). Expecting tuple or call.") + end end """ match_foot_format(footblock :: Expr) -Takes as argument an expression of the form A => B and returns a tuple in a format acceptable as arguments to create a foot. -Return type is Tuple{Union{Tuple{}, Symbol}, Union{Tuple{}, Symbol}, Union{Tuple{}, Pair{Symbol, Symbol}}}. The empty tuple represents no stocks, no flows, or no links. +Takes as argument an expression of the form A => B and returns a tuple in a +format acceptable as arguments to create a foot. Return type is +Tuple{Union{Tuple{}, Symbol}, Union{Tuple{}, Symbol}, Union{Tuple{}, +Pair{Symbol, Symbol}}}. The empty tuple represents no stocks, no flows, or no +links. """ function match_foot_format(footblock::Expr) - @match footblock begin - :(() => ()) => ((), (), ()) - :($(s :: Symbol) => ()) => (s, (), ()) - :(() => $(sv :: Symbol)) => ((), sv, ()) - :($(s :: Symbol) => $(sv :: Symbol)) => (s, sv, s => sv) - :($(s :: Symbol) => sv) => error("Non-symbolic second argument of foot: $sv") - :($s => $(sv :: Symbol)) => error("Non-symbolic first argument of foot: $s") - :($s => $sv) => error("Foot definition requires symbolic names. Received: $s, $sv") - Expr(:call, name, args...) => error("Received: $name called with $args. Expected foot definition of form: A => B.") - _ => error("Invalid foot definition.") - end + @match footblock begin + :(() => ()) => ((), (), ()) + :($(s :: Symbol) => ()) => (s, (), ()) + :(() => $(sv :: Symbol)) => ((), sv, ()) + :($(s :: Symbol) => $(sv :: Symbol)) => (s, sv, s => sv) + :($(s :: Symbol) => sv) => error("Non-symbolic second argument of foot: $sv") + :($s => $(sv :: Symbol)) => error("Non-symbolic first argument of foot: $s") + :($s => $sv) => error("Foot definition requires symbolic names. Received: $s, $sv") + Expr(:call, name, args...) => error("Received: $name called with $args. Expected foot definition of form: A => B.") + _ => error("Invalid foot definition.") + end end ############################################# + """ infer_particular_link!(sfsrc, sftgt, f1, f2, map1, map2, destination_vector, posf=nothing) -infer_particular_link!(sfsrc, sftgt, get_lvs, get_lvv, stockmaps, dyvarmaps, lvmaps, get_lvvposition) # LV +infer_particular_link!(sfsrc, sftgt, get_lvs, get_lvv, stockmaps, dyvarmaps, +lvmaps, get_lvvposition) # LV -If we're mapping the same value to multiple positions, it doesn't matter which one goes where. -We have a few options, on how we want to distribute mappings. Way it's done here, always goes to the last position. +If we're mapping the same value to multiple positions, it doesn't matter which +one goes where. We have a few options, on how we want to distribute mappings. +Way it's done here, always goes to the last position. """ function infer_particular_link!(sfsrc, sftgt, f1, f2, map1, map2, destination_vector) - - hom1′_mappings = f1(sftgt) - hom2′_mappings = f2(sftgt) - - tgt::Dict{Tuple{Int, Int}, Int} = Dict((hom1′, hom2′) => i for (i, (hom1′, hom2′)) in enumerate(zip(hom1′_mappings, hom2′_mappings))) # ISSUE: If there are two matches, second one overwrites the first. - # SOLUTION: Who cares. Just map to the last. - for (i, (hom1, hom2)) in enumerate(zip(f1(sfsrc), f2(sfsrc))) - mapped_index1 = map1[hom1] - mapped_index2 = map2[hom2] + + hom1′_mappings = f1(sftgt) + hom2′_mappings = f2(sftgt) - linkmap = tgt[(mapped_index1, mapped_index2)] + tgt::Dict{Tuple{Int, Int}, Int} = Dict((hom1′, hom2′) => i for (i, (hom1′, hom2′)) in enumerate(zip(hom1′_mappings, hom2′_mappings))) # ISSUE: If there are two matches, second one overwrites the first. + # SOLUTION: Who cares. Just map to the last. + for (i, (hom1, hom2)) in enumerate(zip(f1(sfsrc), f2(sfsrc))) + mapped_index1 = map1[hom1] + mapped_index2 = map2[hom2] - - - destination_vector[i] = linkmap # updated - end - return destination_vector + linkmap = tgt[(mapped_index1, mapped_index2)] + destination_vector[i] = linkmap # updated + end + return destination_vector end @@ -1092,93 +1114,93 @@ end """ infer_links(sfsrc :: StockAndFlowF, sftgt :: StockAndFlowF, NecMaps :: Dict{Symbol, Vector{Int64}}) -Infer LS, I, O, LV, LSV, LVV, LPV mappings for an ACSetTransformation. -Returns dictionary of Symbols to lists of indices, corresponding to an ACSetTransformation argument. -If there exist no such mappings (eg, no LVV), that pairing will not be included in the returned dictionary. +Infer LS, I, O, LV, LSV, LVV, LPV mappings for an ACSetTransformation. Returns +dictionary of Symbols to lists of indices, corresponding to an +ACSetTransformation argument. If there exist no such mappings (eg, no LVV), that +pairing will not be included in the returned dictionary. -If A <- C -> B, and we have A -> A' and B -> B' and a unique C' such that A' <- C' -> B', we can assume C -> C'. +If A <- C -> B, and we have A -> A' and B -> B' and a unique C' such that A' <- +C' -> B', we can assume C -> C'. :S => [2,4,1,3], :F => [1,2,4,3], ... -NecMaps must contain keys S, F, SV, P, V, each pointing to a (possibly empty) array of indices +NecMaps must contain keys S, F, SV, P, V, each pointing to a (possibly empty) +array of indices """ function infer_links(sfsrc :: StockAndFlowF, sftgt :: StockAndFlowF, NecMaps :: Dict{Symbol, Vector{Int64}}) - - stockmaps = NecMaps[:S] - flowmaps = NecMaps[:F] - summaps = NecMaps[:SV] - parammaps = NecMaps[:P] - dyvarmaps = NecMaps[:V] - - lsmaps = zeros(Int, nls(sfsrc)) - imaps = zeros(Int, ni(sfsrc)) - omaps = zeros(Int, no(sfsrc)) - lvmaps = zeros(Int, nlv(sfsrc)) - lsvmaps = zeros(Int, nlsv(sfsrc)) - lvvmaps = zeros(Int, nlvv(sfsrc)) - lpvmaps = zeros(Int, nlpv(sfsrc)) - # After the following calls, there should be no zeroes. - - - infer_particular_link!(sfsrc, sftgt, get_lss, get_lssv, stockmaps, summaps, lsmaps) # LS - infer_particular_link!(sfsrc, sftgt, get_ifn, get_is, flowmaps, stockmaps, imaps) # I - infer_particular_link!(sfsrc, sftgt, get_ofn, get_os, flowmaps, stockmaps, omaps) # O - infer_particular_link!(sfsrc, sftgt, get_lvs, get_lvv, stockmaps, dyvarmaps, lvmaps) # LV - infer_particular_link!(sfsrc, sftgt, get_lsvsv, get_lsvv, summaps, dyvarmaps, lsvmaps) # LSV - infer_particular_link!(sfsrc, sftgt, get_lvsrc, get_lvtgt, dyvarmaps, dyvarmaps, lvvmaps) # LVV - infer_particular_link!(sfsrc, sftgt, get_lpvp, get_lpvv, parammaps, dyvarmaps, lpvmaps) # LPV - - return Dict(:LS => lsmaps, :LSV => lsvmaps, :LV => lvmaps, :I => imaps, :O => omaps, :LPV => lpvmaps, :LVV => lvvmaps) + stockmaps = NecMaps[:S] + flowmaps = NecMaps[:F] + summaps = NecMaps[:SV] + parammaps = NecMaps[:P] + dyvarmaps = NecMaps[:V] + + lsmaps = zeros(Int, nls(sfsrc)) + imaps = zeros(Int, ni(sfsrc)) + omaps = zeros(Int, no(sfsrc)) + lvmaps = zeros(Int, nlv(sfsrc)) + lsvmaps = zeros(Int, nlsv(sfsrc)) + lvvmaps = zeros(Int, nlvv(sfsrc)) + lpvmaps = zeros(Int, nlpv(sfsrc)) + # After the following calls, there should be no zeroes. + + + infer_particular_link!(sfsrc, sftgt, get_lss, get_lssv, stockmaps, summaps, lsmaps) # LS + infer_particular_link!(sfsrc, sftgt, get_ifn, get_is, flowmaps, stockmaps, imaps) # I + infer_particular_link!(sfsrc, sftgt, get_ofn, get_os, flowmaps, stockmaps, omaps) # O + infer_particular_link!(sfsrc, sftgt, get_lvs, get_lvv, stockmaps, dyvarmaps, lvmaps) # LV + infer_particular_link!(sfsrc, sftgt, get_lsvsv, get_lsvv, summaps, dyvarmaps, lsvmaps) # LSV + infer_particular_link!(sfsrc, sftgt, get_lvsrc, get_lvtgt, dyvarmaps, dyvarmaps, lvvmaps) # LVV + infer_particular_link!(sfsrc, sftgt, get_lpvp, get_lpvv, parammaps, dyvarmaps, lpvmaps) # LPV + + return Dict(:LS => lsmaps, :LSV => lsvmaps, :LV => lvmaps, :I => imaps, :O => omaps, :LPV => lpvmaps, :LVV => lvvmaps) end struct DSLArgument - key::Symbol - value::Symbol - flags::Set{Symbol} # At present, the only flag that exists is ~ - DSLArgument(kv::Pair{Union{Expr, Symbol}, Symbol}) = begin # this constructor seemed to fail... need to figure out why. Maybe it can't call other constructors. - key, flags = unwrap_expression(first(kv)) - new(key, second(kv), flags) - end - DSLArgument(k::Union{Expr, Symbol}, v::Symbol) = begin - key, flags = unwrap_expression(k) - new(key, v, flags) - end - DSLArgument(k::Symbol, v::Symbol, f::Set{Symbol}) = new(k, v, f) + key::Symbol + value::Symbol + flags::Set{Symbol} # At present, the only flag that exists is ~ + DSLArgument(kv::Pair{Union{Expr, Symbol}, Symbol}) = begin # this constructor seemed to fail... need to figure out why. Maybe it can't call other constructors. + key, flags = unwrap_expression(first(kv)) + new(key, second(kv), flags) + end + DSLArgument(k::Union{Expr, Symbol}, v::Symbol) = begin + key, flags = unwrap_expression(k) + new(key, v, flags) + end + DSLArgument(k::Symbol, v::Symbol, f::Set{Symbol}) = new(k, v, f) end ==(a::DSLArgument, b::DSLArgument) = a.key == b.key && a.value == b.value && a.flags == b.flags function unwrap_expression(x::Union{Symbol, Expr}, flags::Set{Symbol}=Set{Symbol}())::Tuple{Symbol, Set{Symbol}} # No mutable default arguments. - if typeof(x) == Symbol - return (x, flags) - else - return unwrap_expression(x.args[2], push!(flags, x.args[1])) - end + if typeof(x) == Symbol + return (x, flags) + else + return unwrap_expression(x.args[2], push!(flags, x.args[1])) + end end """ -S₁ => I₁ -S₂ => I₂ -S₁ => S₂ +S₁ => I₁ S₂ => I₂ S₁ => S₂ ⊢ I₁ => I₂ -Determine what index an element e maps to based upon what f we have in the mapping such that e -> f +Determine what index an element e maps to based upon what f we have in the +mapping such that e -> f """ function connect_by_value(; src::Dict{T,U}, mapping::Dict{T,T}, tgt::Dict{T,U})::Dict{U, U} where {T, U} - @assert allunique(values(src)) - - @assert all(x -> x ∈ keys(mapping), keys(src)) - @assert all(x -> x ∈ keys(tgt), values(mapping)) + @assert allunique(values(src)) - return Dict(src[key] => tgt[value] for (key, value) in mapping) + @assert all(x -> x ∈ keys(mapping), keys(src)) + @assert all(x -> x ∈ keys(tgt), values(mapping)) + return Dict(src[key] => tgt[value] for (key, value) in mapping) end @@ -1186,78 +1208,122 @@ end Filter a vector for all elements with substr as a substring. """ function substring_matches(v::Vector, substr::String)::Vector - return filter(x -> occursin(substr, string(x)), v) + return filter(x -> occursin(substr, string(x)), v) end """ -Takes a symbol 'key', applys flags, finds matches in s, and returns a vector of matching keys. -Currently, there are two options: no flags, in which case [key] is returned, or ~ is the only flag, in which case all Symbols with matching substrings are returned. +Takes a symbol 'key', applys flags, finds matches in s, and returns a vector of +matching keys. Currently, there are two options: no flags, in which case [key] +is returned, or ~ is the only flag, in which case all Symbols with matching +substrings are returned. """ function apply_flags(key::Symbol, flags::Set{Symbol}, s::Vector{Symbol})::Vector{Symbol} # Could make this a generator? - if isempty(flags) - @assert (key ∈ s) "$s does not contain key $key ! Did you forget to prefix ~?" - return [key] # potentially inefficient - elseif :~ ∈ flags + if isempty(flags) + @assert (key ∈ s) "$s does not contain key $key ! Did you forget to prefix ~?" + return [key] # potentially inefficient + elseif :~ ∈ flags - matches = collect(substring_matches(s, string(key))) + matches = collect(substring_matches(s, string(key))) - new_flags = copy(flags) # copy isn't necessary, probably - pop!(new_flags, :~) + new_flags = copy(flags) # copy isn't necessary, probably + pop!(new_flags, :~) - return collect(flatmap(x -> apply_flags(x, new_flags, s), matches)) # this is just in case we add additional flags. As is, the recursion is unnecessary. - else - error("Unknown flag found! $(flags)") - end + return collect(flatmap(x -> apply_flags(x, new_flags, s), matches)) # this is just in case we add additional flags. As is, the recursion is unnecessary. + else + error("Unknown flag found! $(flags)") + end end """ substitute_symbols(s::Dict{Symbol, Int}, t::Dict{Symbol, Int}, m::Vector{DSLArgument} ; use_flags::Bool=true)::Dict{Int, Int} -Convert Dict(SymA => IntA), Dict(SymB => IntB), Dict(SymA => SymB) into Dict{IntA => IntB} -Using original sf defintions, and the user defined mappings, transform user defined symbol mappings to index mappings. +Convert Dict(SymA => IntA), Dict(SymB => IntB), Dict(SymA => SymB) into +Dict{IntA => IntB} Using original sf defintions, and the user defined mappings, +transform user defined symbol mappings to index mappings. """ function substitute_symbols(s::Dict{Symbol, Int}, t::Dict{Symbol, Int}, m::Vector{DSLArgument} ; use_flags::Bool=true)::Dict{Int, Int} - if !use_flags - mapping = Dict(arg.key => arg.value for arg in m) - return connect_by_value(src=s, mapping=mapping, tgt=t) - else - master_dict::Dict{Int, Int} = Dict() - for statement in m - key_matches = apply_flags(statement.key, statement.flags, collect(keys(s))) # Vector of Symbol - if isempty(key_matches) - println("WARNING! No matches on $(statement.key) with flags $(statement.flags)") - else - mergewith!((x...) -> first(x), master_dict, Dict(s[match] => t[statement.value] for match ∈ key_matches)) - end - end - return master_dict + if !use_flags + mapping = Dict(arg.key => arg.value for arg in m) + return connect_by_value(src=s, mapping=mapping, tgt=t) + else + master_dict::Dict{Int, Int} = Dict() + for statement in m + key_matches = apply_flags(statement.key, statement.flags, collect(keys(s))) # Vector of Symbol + if isempty(key_matches) + println("WARNING! No matches on $(statement.key) with flags $(statement.flags)") + else + mergewith!((x...) -> first(x), master_dict, Dict(s[match] => t[statement.value] for match ∈ key_matches)) + end end + return master_dict + end end - """ -Convert a vector of unique elements to a dictionary with each element pointing to their original index. +Takes any arguments and returns nothing. Used so we can maintain equality when +making ACSetTransformations. """ -function invert_vector(v::Vector{K})::Dict{K, Int} where {K} # Elements of v must be hashable - new_dict = Dict(val => i for (i, val) ∈ enumerate(v)) - @assert length(new_dict) == length(v) "Nonunique key in vector v: $v" - return new_dict -end +NothingFunction(x...)::Nothing = nothing; -""" -Takes any arguments and returns nothing. -Used so we can maintain equality when making ACSetTransformations. -""" -NothingFunction(x...)::Nothing = nothing; + +function complete_mappings(sf1::K, sf2::K, all_index_mappings::Vector{Dict{Int, Int}}; strict_mappings = false)::Dict{Symbol, Vector{Int}} where {K <: AbstractStockAndFlowStructureF} + + default_index_stock = get(all_index_mappings[1], -1, 0) + default_index_sum = get(all_index_mappings[2], -1, 0) + default_index_dyvar = get(all_index_mappings[3], -1, 0) + default_index_flow = get(all_index_mappings[4], -1, 0) + default_index_param = get(all_index_mappings[5], -1, 0) + + + + # STEP 3 + if !strict_mappings # if there is only one stock, it needs to have index 1 + one_type_stock = length(snames(sf2)) == 1 ? 1 : 0 + one_type_flow = length(fnames(sf2)) == 1 ? 1 : 0 + one_type_dyvar = length(vnames(sf2)) == 1 ? 1 : 0 + one_type_param = length(pnames(sf2)) == 1 ? 1 : 0 + one_type_sum = length(svnames(sf2)) == 1 ? 1 : 0 + else + one_type_stock = one_type_flow = one_type_dyvar = one_type_param = one_type_sum = 0 + end + + # Convert back to vectors. If you find a zero, check if there's a default and + # use that. If there isn't a default, check if there's only one option and + # use that. Otherwise, there's an unassigned value which can't be inferred. + # Taking max because it's less verbose than ternary and accomplishes the same + # thing: + # - If both default_index and one_type are mapped, they must be mapped to the + # same thing, because one_type being mapped implies there's only one option. + # - If only one_type is mapped, then it will be positive, and default_infex + # will be 0 + # - If only default_index is mapped, it will be positive and one_type will be + # 0 + + stock_mappings::Vector{Int} = [get(all_index_mappings[1], i, + max(default_index_stock, one_type_stock)) for i ∈ 1:ns(sf1)] + sum_mappings::Vector{Int} = [get(all_index_mappings[2], i, + max(default_index_sum, one_type_sum)) for i ∈ 1:nsv(sf1)] + dyvar_mappings::Vector{Int} = [get(all_index_mappings[3], i, + max(default_index_dyvar, one_type_dyvar)) for i ∈ 1:nvb(sf1)] + flow_mappings::Vector{Int} = [get(all_index_mappings[4], i, + max(default_index_flow, one_type_flow)) for i ∈ 1:nf(sf1)] + param_mappings::Vector{Int} = [get(all_index_mappings[5], i, + max(default_index_param, one_type_param)) for i ∈ 1:np(sf1)] + + return Dict(:S => stock_mappings, :SV => sum_mappings, :V => dyvar_mappings, :F => flow_mappings, :P => param_mappings) + +end + include("syntax/Composition.jl") include("syntax/Stratification.jl") +include("syntax/Homomorphism.jl") end diff --git a/src/SystemStructure.jl b/src/SystemStructure.jl index 9e036f56..d9b3bea9 100644 --- a/src/SystemStructure.jl +++ b/src/SystemStructure.jl @@ -10,178 +10,178 @@ flattenTupleNames(sn::Vector)=[flattenTupleNames(x) for x in sn] function extracStocksStructureAndFlatten(p::AbstractStockAndFlowStructure) - s=[] + s=[] + + for is in 1:ns(p) + sn=sname(p,is) + sn=flattenTupleNames(sn) - for is in 1:ns(p) - sn=sname(p,is) - sn=flattenTupleNames(sn) - - ifs=inflows(p,is) - ofs=outflows(p,is) - vss=vsstock(p,is) - svss=svsstock(p,is) - - ifns=isempty(ifs) ? :F_NONE : fname(p,ifs) - ofns=isempty(ofs) ? :F_NONE : fname(p,ofs) - vsns=isempty(vss) ? :V_NONE : vname(p,vss) - svsns=isempty(svss) ? :SV_NONE : svname(p,svss) - - ifns=flattenTupleNames(ifns) - ofns=flattenTupleNames(ofns) - vsns=flattenTupleNames(vsns) - svsns=flattenTupleNames(svsns) - - ss=sn=>(ifns,ofns,vsns,svsns) - s=vcat(s,ss) - end + ifs=inflows(p,is) + ofs=outflows(p,is) + vss=vsstock(p,is) + svss=svsstock(p,is) + + ifns=isempty(ifs) ? :F_NONE : fname(p,ifs) + ofns=isempty(ofs) ? :F_NONE : fname(p,ofs) + vsns=isempty(vss) ? :V_NONE : vname(p,vss) + svsns=isempty(svss) ? :SV_NONE : svname(p,svss) - return s + ifns=flattenTupleNames(ifns) + ofns=flattenTupleNames(ofns) + vsns=flattenTupleNames(vsns) + svsns=flattenTupleNames(svsns) + + ss=sn=>(ifns,ofns,vsns,svsns) + s=vcat(s,ss) + end + + return s end """ Return stock names as Symbol, along with the linked flows and sum variables """ function extracStocksStructureAndFlatten(p::AbstractStockAndFlowStructureF) - s=[] + s=[] + + for is in 1:ns(p) + sn=sname(p,is) + sn=flattenTupleNames(sn) - for is in 1:ns(p) - sn=sname(p,is) - sn=flattenTupleNames(sn) - - ifs=inflows(p,is) - ofs=outflows(p,is) - svss=svsstock(p,is) - - ifns=isempty(ifs) ? :F_NONE : fname(p,ifs) - ofns=isempty(ofs) ? :F_NONE : fname(p,ofs) - svsns=isempty(svss) ? :SV_NONE : svname(p,svss) - - ifns=flattenTupleNames(ifns) - ofns=flattenTupleNames(ofns) - svsns=flattenTupleNames(svsns) - - ss=sn=>(ifns,ofns,svsns) - s=vcat(s,ss) - end + ifs=inflows(p,is) + ofs=outflows(p,is) + svss=svsstock(p,is) + + ifns=isempty(ifs) ? :F_NONE : fname(p,ifs) + ofns=isempty(ofs) ? :F_NONE : fname(p,ofs) + svsns=isempty(svss) ? :SV_NONE : svname(p,svss) - return s + ifns=flattenTupleNames(ifns) + ofns=flattenTupleNames(ofns) + svsns=flattenTupleNames(svsns) + + ss=sn=>(ifns,ofns,svsns) + s=vcat(s,ss) + end + + return s end """ Return flow names as Symbol, along with the linked flow variables """ function extracFlowsStructureAndFlatten(p::AbstractStockAndFlowStructure) - f=[] - - if nf(p)>0 - for ifl in 1:nf(p) - fn=fname(p,ifl) - vn=vname(p,fv(p,ifl)) - - fn=flattenTupleNames(fn) - vn=flattenTupleNames(vn) - - fvs=fn=>vn - f=vcat(f,fvs) - end + f=[] + + if nf(p)>0 + for ifl in 1:nf(p) + fn=fname(p,ifl) + vn=vname(p,fv(p,ifl)) + + fn=flattenTupleNames(fn) + vn=flattenTupleNames(vn) + + fvs=fn=>vn + f=vcat(f,fvs) end - - return f + end + + return f end """ Return parameter names as Symbol """ function extracPsStructureAndFlatten(p::AbstractStockAndFlowStructureF) - pns=[] - - if np(p)>0 - for pr in 1:np(p) - pn=pname(p,pr) - pn=flattenTupleNames(pn) - pns=vcat(pns,pn) - end + pns=[] + + if np(p)>0 + for pr in 1:np(p) + pn=pname(p,pr) + pn=flattenTupleNames(pn) + pns=vcat(pns,pn) end - - return pns + end + + return pns end function extracSumVStructureAndFlatten(p::AbstractStockAndFlowStructure) - sv=[] - - if nsv(p)>0 - for svi in 1:nsv(p) - svn=svname(p,svi) - vsvs=vssv(p,svi) - vsvns=isempty(vsvs) ? :SVV_NONE : vname(p,vsvs) - - svn=flattenTupleNames(svn) - vsvns=flattenTupleNames(vsvns) - - svs=svn=>vsvns - sv=vcat(sv,svs) - end - end - - return sv + sv=[] + + if nsv(p)>0 + for svi in 1:nsv(p) + svn=svname(p,svi) + vsvs=vssv(p,svi) + vsvns=isempty(vsvs) ? :SVV_NONE : vname(p,vsvs) + + svn=flattenTupleNames(svn) + vsvns=flattenTupleNames(vsvns) + + svs=svn=>vsvns + sv=vcat(sv,svs) + end + end + + return sv end """ Return sum variable names as Symbol, along with the linked dynamic variables """ function extracSumVStructureAndFlatten(p::AbstractStockAndFlowStructureF) - sv=[] - - if nsv(p)>0 - for svi in 1:nsv(p) - svn=svname(p,svi) - svn=flattenTupleNames(svn) - sv=vcat(sv,svn) - end - end - - return sv + sv=[] + + if nsv(p)>0 + for svi in 1:nsv(p) + svn=svname(p,svi) + svn=flattenTupleNames(svn) + sv=vcat(sv,svn) + end + end + + return sv end """ Return a Tuple of Vectors of Symbols of flattened stocks, sums, parameters and source dynamic variables a dynamic variable at index v links to. """ function args_vname(p::AbstractStockAndFlowStructureF,v) - srcsv=map(i->(flattenTupleNames(sname(p,i))),stocksv(p,v)) - srcsvv=map(i->(flattenTupleNames(svname(p,i))),svsv(p,v)) - srcpv=map(i->(flattenTupleNames(pname(p,i))),vpsrc(p,v)) - srcvv=map(i->(flattenTupleNames(vname(p,i))),vsrc(p,v)) + srcsv=map(i->(flattenTupleNames(sname(p,i))),stocksv(p,v)) + srcsvv=map(i->(flattenTupleNames(svname(p,i))),svsv(p,v)) + srcpv=map(i->(flattenTupleNames(pname(p,i))),vpsrc(p,v)) + srcvv=map(i->(flattenTupleNames(vname(p,i))),vsrc(p,v)) - return (srcsv,srcsvv,srcpv,srcvv) + return (srcsv,srcsvv,srcpv,srcvv) end """ - args(p::AbstractStockAndFlowStructureF,v) + args(p::AbstractStockAndFlowStructureF,v) Return a Vector of Symbols of flattened stocks, sums, parameters and source dynamic variables a dynamic variable at index v links to. """ function args(p::AbstractStockAndFlowStructureF,v) - (srcsv,srcsvv,srcpv,srcvv)=args_vname(p,v) - return vcat(srcsv,srcsvv,srcpv,srcvv) + (srcsv,srcsvv,srcpv,srcvv)=args_vname(p,v) + return vcat(srcsv,srcsvv,srcpv,srcvv) end """ - args(p::AbstractStockAndFlowF,v) + args(p::AbstractStockAndFlowF,v) Return a Vector of Symbols of flattened stocks, sums, parameters and source dynamic variables a dynamic variable at index v links to. """ function args(p::AbstractStockAndFlowF,v) - (srcsv,srcsvv,srcpv,srcvv)=args_vname(p,v) - - lvvp=lvvposition(p,v) - lvtgtp=lvtgtposition(p,v) - lsvvp=lsvvposition(p,v) - lpvvp=lpvvposition(p,v) - - # create dictionary of (key=position, value=symbole of source argument) - position_src=merge(make_dict(lvvp,srcsv),make_dict(lsvvp,srcsvv),make_dict(lpvvp,srcpv),make_dict(lvtgtp,srcvv)) - ordered_position_src=sort(collect(position_src), by = x->x[1]) - srcs=map(x->last(x),ordered_position_src) - - return srcs + (srcsv,srcsvv,srcpv,srcvv)=args_vname(p,v) + + lvvp=lvvposition(p,v) + lvtgtp=lvtgtposition(p,v) + lsvvp=lsvvposition(p,v) + lpvvp=lpvvposition(p,v) + + # create dictionary of (key=position, value=symbole of source argument) + position_src=merge(make_dict(lvvp,srcsv),make_dict(lsvvp,srcsvv),make_dict(lpvvp,srcpv),make_dict(lvtgtp,srcvv)) + ordered_position_src=sort(collect(position_src), by = x->x[1]) + srcs=map(x->last(x),ordered_position_src) + + return srcs end """ @@ -189,17 +189,17 @@ Return dynamic variable definitions as Vector with elements of form :dv => [:arg """ extracVStructureAndFlatten(p::AbstractStockAndFlowStructureF) = begin - vs=[] - - if nvb(p)>0 - for v in 1:nvb(p) - vn = flattenTupleNames(vname(p,v)) - vnp = vn=>args(p,v) - vs = vcat(vs,vnp) - end + vs=[] + + if nvb(p)>0 + for v in 1:nvb(p) + vn = flattenTupleNames(vname(p,v)) + vnp = vn=>args(p,v) + vs = vcat(vs,vnp) end - return vs - + end + return vs + end """ @@ -207,47 +207,47 @@ Convert dynamic variable names to Symbol, convert all operators to a single oper """ extracVAndAttrStructureAndFlatten(p::AbstractStockAndFlowF) = begin - vs=[] - - if nvb(p)>0 - for v in 1:nvb(p) - vn = flattenTupleNames(vname(p,v)) - v_op = allequal(vop(p,v)) ? vop(p,v)[1] : error("operators $(vop(p,v)) in the stratified model's auxiliary variable: $(join(vname(p,v))) should be the same!") - vnp = vn=>(args(p,v)=>v_op) - vs = vcat(vs,vnp) - end + vs=[] + + if nvb(p)>0 + for v in 1:nvb(p) + vn = flattenTupleNames(vname(p,v)) + v_op = allequal(vop(p,v)) ? vop(p,v)[1] : error("operators $(vop(p,v)) in the stratified model's auxiliary variable: $(join(vname(p,v))) should be the same!") + vnp = vn=>(args(p,v)=>v_op) + vs = vcat(vs,vnp) end - return vs + end + return vs end function rebuildStratifiedModelByFlattenSymbols(p::AbstractStockAndFlowStructure) - s=extracStocksStructureAndFlatten(p) - f=extracFlowsStructureAndFlatten(p) - sv=extracSumVStructureAndFlatten(p) - - return StockAndFlowStructure(s,f,sv) + s=extracStocksStructureAndFlatten(p) + f=extracFlowsStructureAndFlatten(p) + sv=extracSumVStructureAndFlatten(p) + + return StockAndFlowStructure(s,f,sv) end """ Return a new stock flow with flattened names, operators and positions from the old """ function rebuildStratifiedModelByFlattenSymbols(p::AbstractStockAndFlowF) - s=extracStocksStructureAndFlatten(p) - pr=extracPsStructureAndFlatten(p) - f=extracFlowsStructureAndFlatten(p) - sv=extracSumVStructureAndFlatten(p) - v=extracVAndAttrStructureAndFlatten(p) - - return StockAndFlowF(s,pr,v,f,sv) + s=extracStocksStructureAndFlatten(p) + pr=extracPsStructureAndFlatten(p) + f=extracFlowsStructureAndFlatten(p) + sv=extracSumVStructureAndFlatten(p) + v=extracVAndAttrStructureAndFlatten(p) + + return StockAndFlowF(s,pr,v,f,sv) end function convertSystemStructureToStockFlow(p::AbstractStockAndFlowStructure,v) - s=extracStocksStructureAndFlatten(p) - f=extracFlowsStructureAndFlatten(p) - sv=extracSumVStructureAndFlatten(p) - - return StockAndFlow(s,f,v,sv) + s=extracStocksStructureAndFlatten(p) + f=extracFlowsStructureAndFlatten(p) + sv=extracSumVStructureAndFlatten(p) + + return StockAndFlow(s,f,v,sv) end """ @@ -258,35 +258,35 @@ convertSystemStructureToStockFlow(MyStockFlowStructure, (:v_prevalence=>(:I,:N,: ``` """ function convertSystemStructureToStockFlow(p::AbstractStockAndFlowStructureF,v) - s=extracStocksStructureAndFlatten(p) - pr=extracPsStructureAndFlatten(p) - f=extracFlowsStructureAndFlatten(p) - sv=extracSumVStructureAndFlatten(p) - - return StockAndFlowF(s,pr,v,f,sv) + s=extracStocksStructureAndFlatten(p) + pr=extracPsStructureAndFlatten(p) + f=extracFlowsStructureAndFlatten(p) + sv=extracSumVStructureAndFlatten(p) + + return StockAndFlowF(s,pr,v,f,sv) end function convertStockFlowToSystemStructure(p::AbstractStockAndFlow) - - s=extracStocksStructureAndFlatten(p) - f=extracFlowsStructureAndFlatten(p) - sv=extracSumVStructureAndFlatten(p) - return StockAndFlowStructure(s,f,sv) + + s=extracStocksStructureAndFlatten(p) + f=extracFlowsStructureAndFlatten(p) + sv=extracSumVStructureAndFlatten(p) + return StockAndFlowStructure(s,f,sv) end """ Return a new StockAndFlowStructureF with flattened names, operators and positions from an AbstractStockAndFlowF. """ function convertStockFlowToSystemStructure(p::AbstractStockAndFlowF) - - s=extracStocksStructureAndFlatten(p) - pr=extracPsStructureAndFlatten(p) - v=extracVStructureAndFlatten(p) - f=extracFlowsStructureAndFlatten(p) - sv=extracSumVStructureAndFlatten(p) - - return StockAndFlowStructureF(s,pr,v,f,sv) + + s=extracStocksStructureAndFlatten(p) + pr=extracPsStructureAndFlatten(p) + v=extracVStructureAndFlatten(p) + f=extracFlowsStructureAndFlatten(p) + sv=extracSumVStructureAndFlatten(p) + + return StockAndFlowStructureF(s,pr,v,f,sv) end @@ -296,62 +296,62 @@ Concatenate Symbols. ++(a::Symbol,b::Symbol) = Symbol(string(a, b)) """ - add_suffix!(sf::AbstractStockAndFlow0, suffix) + add_suffix!(sf::AbstractStockAndFlow0, suffix) Modify a AbstractStockAndFlow0 so named elements end with suffix. Suffix can be anything which can be cast to a Symbol.""" function add_suffix!(sf::AbstractStockAndFlowStructureF, suffix) - suffix = Symbol(suffix) - set_snames!(sf, snames(sf) .++ suffix) - set_fnames!(sf, fnames(sf) .++ suffix) - set_svnames!(sf, svnames(sf) .++ suffix) - set_vnames!(sf, vnames(sf) .++ suffix) - set_pnames!(sf, pnames(sf) .++ suffix) - return sf + suffix = Symbol(suffix) + set_snames!(sf, snames(sf) .++ suffix) + set_fnames!(sf, fnames(sf) .++ suffix) + set_svnames!(sf, svnames(sf) .++ suffix) + set_vnames!(sf, vnames(sf) .++ suffix) + set_pnames!(sf, pnames(sf) .++ suffix) + return sf end """ - add_suffix!(sf::AbstractStockAndFlow0, suffix) + add_suffix!(sf::AbstractStockAndFlow0, suffix) Modify a AbstractStockAndFlow0 so named elements end with suffix. Suffix can be anything which can be cast to a Symbol. For feet. """ function add_suffix!(sf::AbstractStockAndFlow0, suffix) - suffix = Symbol(suffix) - set_snames!(sf, snames(sf) .++ suffix) - set_svnames!(sf, svnames(sf) .++ suffix) - return sf + suffix = Symbol(suffix) + set_snames!(sf, snames(sf) .++ suffix) + set_svnames!(sf, svnames(sf) .++ suffix) + return sf end """ - add_prefix!(sf::AbstractStockAndFlowStructureF, prefix) + add_prefix!(sf::AbstractStockAndFlowStructureF, prefix) Modify a AbstractStockAndFlowStructureF so named elements begin with prefix Prefix can be anything which can be cast to a Symbol. """ function add_prefix!(sf::AbstractStockAndFlowStructureF, prefix) - prefix = Symbol(prefix) - set_snames!(sf, prefix .++ snames(sf)) - set_fnames!(sf, prefix .++ fnames(sf)) - set_svnames!(sf, prefix .++ svnames(sf)) - set_vnames!(sf, prefix .++ vnames(sf)) - set_pnames!(sf, prefix .++ pnames(sf)) - return sf + prefix = Symbol(prefix) + set_snames!(sf, prefix .++ snames(sf)) + set_fnames!(sf, prefix .++ fnames(sf)) + set_svnames!(sf, prefix .++ svnames(sf)) + set_vnames!(sf, prefix .++ vnames(sf)) + set_pnames!(sf, prefix .++ pnames(sf)) + return sf end """ - add_prefix!(sf::AbstractStockAndFlowStructureF, prefix) + add_prefix!(sf::AbstractStockAndFlowStructureF, prefix) Modify a AbstractStockAndFlowStructureF so named elements begin with prefix Prefix can be anything which can be cast to a Symbol.For feet. """ function add_prefix!(sf::AbstractStockAndFlow0, prefix) - prefix = Symbol(prefix) - set_snames!(sf, prefix .++ snames(sf)) - set_svnames!(sf, prefix .++ svnames(sf)) - return sf + prefix = Symbol(prefix) + set_snames!(sf, prefix .++ snames(sf)) + set_svnames!(sf, prefix .++ svnames(sf)) + return sf end diff --git a/src/syntax/Composition.jl b/src/syntax/Composition.jl index 042de661..27ce6713 100755 --- a/src/syntax/Composition.jl +++ b/src/syntax/Composition.jl @@ -1,3 +1,7 @@ +""" +DSL for composition. +Macro @compose calls function sfcompose. +""" module Composition export sfcompose, @compose @@ -16,30 +20,35 @@ RETURN_UWD = false Construct a uwd to compose your open stockflows """ function create_uwd(; - Box::Vector{Symbol} = Vector{Symbol}(), # stockflows - Port::Vector{Tuple{Int, Int}} = Vector{Tuple{Int, Int}}(), # stockflow => foot number, for each foot on stockflow - OuterPort::Vector{Int} = Vector{Int}(), # unique feet number (1:n) - Junction::Vector{Symbol} = Vector{Symbol}() # A symbol for each (unique) foot - ) - - uwd = UntypedUnnamedRelationDiagram{Symbol, Symbol}(0) - add_parts!(uwd, :Box, length(Box), name=Box) - add_parts!(uwd, :Junction, length(Junction), variable=Junction) - add_parts!(uwd, :Port, length(Port), box=map(first, Port), junction=map(last, Port)) - add_parts!(uwd, :OuterPort, length(OuterPort), outer_junction=OuterPort) - return uwd + Box::Vector{Symbol} = Vector{Symbol}(), # stockflows + Port::Vector{Tuple{Int, Int}} = Vector{Tuple{Int, Int}}(), # stockflow => foot number, for each foot on stockflow + OuterPort::Vector{Int} = Vector{Int}(), # unique feet number (1:n) + Junction::Vector{Symbol} = Vector{Symbol}() # A symbol for each (unique) foot + ) + + uwd = UntypedUnnamedRelationDiagram{Symbol, Symbol}(0) + add_parts!(uwd, :Box, length(Box), name=Box) + add_parts!(uwd, :Junction, length(Junction), variable=Junction) + add_parts!(uwd, :Port, length(Port), box=map(first, Port), junction=map(last, Port)) + add_parts!(uwd, :OuterPort, length(OuterPort), outer_junction=OuterPort) + return uwd end """ Parse expression of form A ^ B => C, extract sf A and foot B => C """ function interpret_center_of_composition_statement(center::Expr)::Tuple{Symbol, Expr} # sf, foot defintion - @assert length(center.args) == 3 && center.args[1] == :(=>) && typeof(center.args[2]) == Expr "Invalid argument: expected A ^ B => C, A ^ () => C or A ^ B => (), got $center" - # third argument can be symbol or (), the latter of which is an Expr - center_caret_statement = center.args[2] - @assert length(center_caret_statement.args) == 3 && center_caret_statement.args[1] == :^ && typeof(center_caret_statement.args[2]) == Symbol "Invalid center argument: expected A ^ B or A ^ (), got $center" - # third argument here, too, can be symbol or () - return (center_caret_statement.args[2], Expr(:call, :(=>), center_caret_statement.args[3], center.args[3])) + @assert length(center.args) == 3 && center.args[1] == :(=>) && typeof(center.args[2]) == Expr + "Invalid argument: expected A ^ B => C, A ^ () => C or A ^ B => (), got $center" + # third argument can be symbol or (), the latter of which is an Expr + center_caret_statement = center.args[2] + @assert (length(center_caret_statement.args) == 3 + && center_caret_statement.args[1] == :^ + && typeof(center_caret_statement.args[2]) == Symbol) + "Invalid center argument: expected A ^ B or A ^ (), got $center" + + # third argument here, too, can be symbol or () + return (center_caret_statement.args[2], Expr(:call, :(=>), center_caret_statement.args[3], center.args[3])) end """ @@ -47,103 +56,103 @@ Go line by line and associate stockflows and feet """ function interpret_composition_notation(mapping_pair::Expr)::Tuple{Vector{Symbol}, StockAndFlow0} - if mapping_pair.head == :call # (A ^ B => C) case (incl where B or C are ()) - sf, foot_def = interpret_center_of_composition_statement(mapping_pair) - return [sf], create_foot(foot_def) - end + if mapping_pair.head == :call # (A ^ B => C) case (incl where B or C are ()) + sf, foot_def = interpret_center_of_composition_statement(mapping_pair) + return [sf], create_foot(foot_def) + end - expr_args = mapping_pair.args - stockflows = collect(Base.Iterators.takewhile(x -> typeof(x) == Symbol, expr_args)) - center_index = length(stockflows) + 1 - @assert center_index <= length(expr_args) "A tuple is an invalid expression for composition syntax. Expected argument of form sf1, sf2, ... ^ stock1 => sum1, stock2 => sum2, ..." - center = expr_args[center_index] + expr_args = mapping_pair.args + stockflows = collect(Base.Iterators.takewhile(x -> typeof(x) == Symbol, expr_args)) + center_index = length(stockflows) + 1 + @assert center_index <= length(expr_args) + "A tuple is an invalid expression for composition syntax. \ + Expected argument of form sf1, sf2, ... ^ stock1 => sum1, stock2 => sum2, ..." + center = expr_args[center_index] - foot_temp = Vector{Expr}() + foot_temp = Vector{Expr}() - sf, foot_def = interpret_center_of_composition_statement(center) - push!(foot_temp, foot_def) - push!(stockflows, sf) - append!(foot_temp, expr_args[center_index+1:end]) + sf, foot_def = interpret_center_of_composition_statement(center) + push!(foot_temp, foot_def) + push!(stockflows, sf) + append!(foot_temp, expr_args[center_index+1:end]) - return (stockflows, create_foot(Expr(:tuple, foot_temp...))) + return (stockflows, create_foot(Expr(:tuple, foot_temp...))) end """ -sirv = sfcompose(sir, svi, quote - (sr, sv) - sr, sv ^ S => N, I => N -end) - -Cannot use () => () as a foot, -the length of the first tuple must be the same as the number of stock flows given as argument, -and every foot can only be used once. +sirv = sfcompose(sir, svi, quote (sr, sv) sr, sv ^ S => N, I => N end) + +Cannot use () => () as a foot, the length of the first tuple must be the same as +the number of stock flows given as argument, and every foot can only be used +once. """ function sfcompose(sfs::Vector{K}, block::Expr) where {K <: AbstractStockAndFlowF}#(sf1, sf2, ..., block) - - - Base.remove_linenums!(block) - sf_names = block.args[1].args + Base.remove_linenums!(block) + sf_names = block.args[1].args - if length(sfs) == 0 # Composing 0 stock flows should give you an empty stock flow - return StockAndFlowF() - end - - @assert length(sf_names) == length(sfs) "The number of symbols on the first line is not the same as the number of stock flow arguments provided. Stockflow #: $(length(sfs)) Symbol #: $(length(sf_names))" + if length(sfs) == 0 # Composing 0 stock flows should give you an empty stock flow + return StockAndFlowF() + end + @assert length(sf_names) == length(sfs) "The number of symbols on the first \ + line is not the same as the number of stock flow arguments provided. \ + Stockflow #: $(length(sfs)) Symbol #: $(length(sf_names))" + @assert allunique(sf_names) "Not all choices of names for stock flows are unique!" - @assert allunique(sf_names) "Not all choices of names for stock flows are unique!" + empty_foot = (@foot () => ()) - empty_foot = (@foot () => ()) - - - # symbol representation of sf => (sf itself, sf's feet) - # Every sf has empty foot as first foot to get around being unable to create OpenStockAndFlowF without feet - sf_map::Dict{Symbol, Tuple{AbstractStockAndFlowF, Vector{StockAndFlow0}}} = Dict(sf_names[i] => (sfs[i], [empty_foot]) for i ∈ eachindex(sf_names)) # map the symbols to their corresponding stockflows - - # all feet - feet_index_dict::Dict{StockAndFlow0, Int} = Dict(empty_foot => 1) - for statement in block.args[2:end] - stockflows, foot = interpret_composition_notation(statement) - # adding new foot to list - @assert (foot ∉ keys(feet_index_dict)) "Foot has already been used, or you are using an empty foot!" - push!(feet_index_dict, foot => length(feet_index_dict) + 1) - for stockflow in stockflows - # adding this foot to each stock flow to its left - push!(sf_map[stockflow][2], foot) - end + # symbol representation of sf => (sf itself, sf's feet) Every sf has empty + # foot as first foot to get around being unable to create OpenStockAndFlowF + # without feet + (sf_map::Dict{Symbol, Tuple{AbstractStockAndFlowF, Vector{StockAndFlow0}}} + = Dict(sf_names[i] => (sfs[i], [empty_foot]) for i ∈ eachindex(sf_names))) + # map the symbols to their corresponding stockflows + + # all feet + feet_index_dict::Dict{StockAndFlow0, Int} = Dict(empty_foot => 1) + for statement in block.args[2:end] + stockflows, foot = interpret_composition_notation(statement) + # adding new foot to list + @assert (foot ∉ keys(feet_index_dict)) "Foot has already been used, or you are using an empty foot!" + push!(feet_index_dict, foot => length(feet_index_dict) + 1) + for stockflow in stockflows + # adding this foot to each stock flow to its left + push!(sf_map[stockflow][2], foot) end + end - Box::Vector{Symbol} = sf_names + Box::Vector{Symbol} = sf_names - Port = Vector{Tuple{Int, Int}}() + Port = Vector{Tuple{Int, Int}}() - for (k, v) ∈ sf_map # TODO: Just find a better way to do this. - for foot ∈ v[2] - push!(Port, (findfirst(x -> x == k, sf_names), feet_index_dict[foot])) - end + for (k, v) ∈ sf_map # TODO: Just find a better way to do this. + for foot ∈ v[2] + push!(Port, (findfirst(x -> x == k, sf_names), feet_index_dict[foot])) end + end - Junction::Vector{Symbol} = [gensym() for _ ∈ 1:length(feet_index_dict)] - OuterPort::Vector{Int} = collect(1:length(feet_index_dict)) + Junction::Vector{Symbol} = [gensym() for _ ∈ 1:length(feet_index_dict)] + OuterPort::Vector{Int} = collect(1:length(feet_index_dict)) - uwd = create_uwd(Box=Box, Port=Port, Junction=Junction, OuterPort=OuterPort) + uwd = create_uwd(Box=Box, Port=Port, Junction=Junction, OuterPort=OuterPort) - # I'd prefer this to be a vector, but oapply didn't like that - # I'd also prefer that I don't include the empty foot, but Open doesn't want to accept stockflows with no feet. - # open_stockflows::AbstractDict = Dict(sf_key => Open(sf_val, foot_dict[sf_val]...,) for (sf_key, sf_val) ∈ sf_map) + # I'd prefer this to be a vector, but oapply didn't like that I'd also prefer + # that I don't include the empty foot, but Open doesn't want to accept + # stockflows with no feet. open_stockflows::AbstractDict = Dict(sf_key => + # Open(sf_val, foot_dict[sf_val]...,) for (sf_key, sf_val) ∈ sf_map) - open_stockflows::AbstractDict = Dict(sf_key => Open(sf_val[1], sf_val[2]...) for (sf_key, sf_val) ∈ sf_map) + open_stockflows::AbstractDict = Dict(sf_key => Open(sf_val[1], sf_val[2]...) for (sf_key, sf_val) ∈ sf_map) - if RETURN_UWD # UWD might be a bit screwed up from the empty foot being first. - return apex(oapply(uwd, open_stockflows)), uwd - else - return apex(oapply(uwd, open_stockflows)) - end + if RETURN_UWD # UWD might be a bit screwed up from the empty foot being first. + return apex(oapply(uwd, open_stockflows)), uwd + else + return apex(oapply(uwd, open_stockflows)) + end end @@ -152,18 +161,18 @@ end Compose models. """ macro compose(args...) - if length(args) == 0 - return :(MethodError("No arguments provided! Please provide some number of stockflows, then a quote block.")) - end - escaped_block = Expr(:quote, args[end]) - sfs = esc.(args[1:end-1]) - quote - if length($sfs) == 0 - sfcompose(Vector{StockAndFlowF}(), $escaped_block) - else - sfcompose([$(sfs...)], $escaped_block) - end + if length(args) == 0 + return :(MethodError("No arguments provided! Please provide some number of stockflows, then a quote block.")) + end + escaped_block = Expr(:quote, args[end]) + sfs = esc.(args[1:end-1]) + quote + if length($sfs) == 0 + sfcompose(Vector{StockAndFlowF}(), $escaped_block) + else + sfcompose([$(sfs...)], $escaped_block) end + end end diff --git a/src/syntax/Homomorphism.jl b/src/syntax/Homomorphism.jl new file mode 100644 index 00000000..890403fc --- /dev/null +++ b/src/syntax/Homomorphism.jl @@ -0,0 +1,116 @@ +module Homomorphism + +using ...StockFlow +import ...StockFlow: state_dict + +using ..Syntax + +import ..Syntax: infer_links, substitute_symbols, DSLArgument, NothingFunction, + complete_mappings +using MLStyle +using Catlab.CategoricalAlgebra + +export @hom + +macro hom(sf1, sf2, block) + escaped_block = Expr(:quote, block) + quote + hom($(esc(sf1)), $(esc(sf2)), $escaped_block); + end +end + +function interpret_homomorphism_syntax(line :: Expr)::Vector{DSLArgument} + @match line begin + :($s => $t) => [DSLArgument(s,t)] + :($(shead...), $s => $t) => [[[DSLArgument(ss, t) for ss in shead] ; DSLArgument(s, t)]] + _ => error("Unknown symbol format. Must be A => B.") + end +end + + +function hom(sf1, sf2, block) + Base.remove_linenums!(block) + stocks::Vector{DSLArgument} = [] + params::Vector{DSLArgument} = [] + dyvars::Vector{DSLArgument} = [] + flows::Vector{DSLArgument} = [] + sums::Vector{DSLArgument} = [] + current_phase = (_, _) -> () + for statement in block.args + @match statement begin + QuoteNode(:stocks) => begin + current_phase = s -> append!(stocks, interpret_homomorphism_syntax(s)) + end + QuoteNode(:parameters) => begin + current_phase = p -> append!(params, interpret_homomorphism_syntax(p)) + end + QuoteNode(:dynamic_variables) => begin + current_phase = d -> append!(dyvars, interpret_homomorphism_syntax(d)) + end + QuoteNode(:flows) => begin + current_phase = f -> append!(flows, interpret_homomorphism_syntax(f)) + end + QuoteNode(:sums) => begin + current_phase = s -> append!(sums, interpret_homomorphism_syntax(s)) + end + QuoteNode(kw) => + error("Unknown block type for homomorphism syntax: " * String(kw)) + _ => current_phase(statement) + end + end + + sf1_snames = Dict{Symbol, Int}(state_dict(snames(sf1))) + sf1_svnames = Dict{Symbol, Int}(state_dict(svnames(sf1))) + sf1_vnames = Dict{Symbol, Int}(state_dict(vnames(sf1))) + sf1_fnames = Dict{Symbol, Int}(state_dict(fnames(sf1))) + sf1_pnames = Dict{Symbol, Int}(state_dict(pnames(sf1))) + + @assert (:_ ∉ keys(sf1_snames) && :_ ∉ keys(sf1_svnames) + && :_ ∉ keys(sf1_vnames) && :_ ∉ keys(sf1_fnames) && + :_ ∉ keys(sf1_pnames)) "A stockflow contains :_ ! \ + Please change temp_strat_default to a different symbol or \ + rename offending object." + + push!(sf1_snames, :_ => -1) + push!(sf1_svnames, :_ => -1) + push!(sf1_vnames, :_ => -1) + push!(sf1_fnames, :_ => -1) + push!(sf1_pnames, :_ => -1) + + + + master_vector = [substitute_symbols(sf1_snames, Dict{Symbol, Int}(state_dict(snames(sf2))), stocks), + substitute_symbols(sf1_svnames, Dict{Symbol, Int}(state_dict(svnames(sf2))), sums), + substitute_symbols(sf1_vnames, Dict{Symbol, Int}(state_dict(vnames(sf2))), dyvars), + substitute_symbols(sf1_fnames, Dict{Symbol, Int}(state_dict(fnames(sf2))), flows), + substitute_symbols(sf1_pnames, Dict{Symbol, Int}(state_dict(pnames(sf2))), params), + ]::Vector{Dict{Int, Int}} + + + new_master_dict = complete_mappings(sf1, sf2, master_vector) + + links = infer_links(sf1, sf2, new_master_dict) + + no_attribute_type = map(sf2, Name=NothingFunction, + Op=NothingFunction, Position=NothingFunction) + + transformation = ACSetTransformation(sf1, no_attribute_type, ; filter(kv -> !isempty(kv[2]), new_master_dict)..., filter(kv -> !isempty(kv[2]), links)..., :Op => NothingFunction, :Position => NothingFunction, :Name => NothingFunction) + @assert is_natural(transformation) + return transformation + + +end + +function all_names_to_index(sf) + Dict(:S => state_dict(snames(sf)) + :F => state_dict(fnames(sf)) + :V => state_dict(vnames(sf)) + :P => state_dict(pnames(sf)) + :SV => state_dict(svnames(sf)) + ) +end + + + + +end \ No newline at end of file diff --git a/src/syntax/Stratification.jl b/src/syntax/Stratification.jl index fad2b345..c9a4d8c1 100755 --- a/src/syntax/Stratification.jl +++ b/src/syntax/Stratification.jl @@ -1,130 +1,147 @@ +""" +DSL for stratification. +Takes two different forms. @stratify takes two stockflows and a type model +stockflow, @n_stratify takes an arbitrary number of stockflows and a type model +stockflow. + +Both call sfstratify. + +""" module Stratification export sfstratify, @stratify, @n_stratify using ...StockFlow +using ...StockFlow: state_dict + using ..Syntax using MLStyle import Base: get using Catlab.CategoricalAlgebra -import ..Syntax: infer_links, substitute_symbols, DSLArgument, NothingFunction, invert_vector +import ..Syntax: infer_links, substitute_symbols, DSLArgument, NothingFunction struct SFNames - sf::AbstractStockAndFlowF - - snames::Vector{Symbol} - svnames::Vector{Symbol} - vnames::Vector{Symbol} - fnames::Vector{Symbol} - pnames::Vector{Symbol} - - # name -> index - s::Dict{Symbol, Int} - sv::Dict{Symbol, Int} - v::Dict{Symbol, Int} - f::Dict{Symbol, Int} - p::Dict{Symbol, Int} - - # index -> new index - ms::Dict{Int, Int} - msv::Dict{Int, Int} - mv::Dict{Int, Int} - mf::Dict{Int, Int} - mp::Dict{Int, Int} - - # index -> new index, where the first index is the actual index of the vector, the second is the int at that location - mvs::Vector{Int} - mvsv::Vector{Int} - mvv::Vector{Int} - mvf::Vector{Int} - mvp::Vector{Int} - - - SFNames(sfarg::AbstractStockAndFlowF) = (new(sfarg, - snames(sfarg), svnames(sfarg), vnames(sfarg), fnames(sfarg), pnames(sfarg), - invert_vector(snames(sfarg)), invert_vector(svnames(sfarg)), invert_vector(vnames(sfarg)), invert_vector(fnames(sfarg)), invert_vector(pnames(sfarg)), - Dict{Int, Int}(), Dict{Int, Int}(), Dict{Int, Int}(), Dict{Int, Int}(), Dict{Int, Int}(), - Vector{Int}(),Vector{Int}(), Vector{Int}(), Vector{Int}(), Vector{Int}())) + sf::AbstractStockAndFlowF + + snames::Vector{Symbol} + svnames::Vector{Symbol} + vnames::Vector{Symbol} + fnames::Vector{Symbol} + pnames::Vector{Symbol} + + # name -> index + s::Dict{Symbol, Int} + sv::Dict{Symbol, Int} + v::Dict{Symbol, Int} + f::Dict{Symbol, Int} + p::Dict{Symbol, Int} + + # index -> new index + ms::Dict{Int, Int} + msv::Dict{Int, Int} + mv::Dict{Int, Int} + mf::Dict{Int, Int} + mp::Dict{Int, Int} + + # index -> new index, where the first index is the actual index of the vector, + # the second is the int at that location + mvs::Vector{Int} + mvsv::Vector{Int} + mvv::Vector{Int} + mvf::Vector{Int} + mvp::Vector{Int} + + + SFNames(sfarg::AbstractStockAndFlowF) = (new(sfarg, + snames(sfarg), svnames(sfarg), vnames(sfarg), fnames(sfarg), pnames(sfarg), + state_dict(snames(sfarg)), state_dict(svnames(sfarg)), + state_dict(vnames(sfarg)), state_dict(fnames(sfarg)), state_dict(pnames(sfarg)), + Dict{Int, Int}(), Dict{Int, Int}(), Dict{Int, Int}(), Dict{Int, Int}(), Dict{Int, Int}(), + Vector{Int}(),Vector{Int}(), Vector{Int}(), Vector{Int}(), Vector{Int}())) end function get_mappings(sfn::SFNames)::NTuple{5, Dict{Int, Int}} - return sfn.ms, sfn.msv, sfn.mv, sfn.mf, sfn.mp + return sfn.ms, sfn.msv, sfn.mv, sfn.mf, sfn.mp end function get_mapped_vectors(sfn::SFNames)::NTuple{5, Vector{Int}} - return sfn.mvs, sfn.mvsv, sfn.mvv, sfn.mvf, sfn.mvp + return sfn.mvs, sfn.mvsv, sfn.mvv, sfn.mvf, sfn.mvp end function get_mappings_infer_links_format(sfn::SFNames)::Dict{Symbol, Vector{Int}} - Dict(:S => sfn.mvs, :SV => sfn.mvsv, :V => sfn.mvv, :F => sfn.mvf, :P => sfn.mvp) + Dict(:S => sfn.mvs, :SV => sfn.mvsv, :V => sfn.mvv, :F => sfn.mvf, :P => sfn.mvp) end -function all_unique_names(sfn::SFNames)::Bool # Unnecessary, this is checked in invert_vector - return allunique(sfn.snames) && allunique(sfn.svnames) && allunique(vnames) && allunique(fnames) && allunique(pnames) +function all_unique_names(sfn::SFNames)::Bool + return (allunique(sfn.snames) && allunique(sfn.svnames) && allunique(vnames) + && allunique(fnames) && allunique(pnames)) end function no_temp_strat_default_in_names(sfn::SFNames, temp_strat_default)::Bool - return temp_strat_default ∉ keys(sfn.s) && temp_strat_default ∉ keys(sfn.sv) && temp_strat_default ∉ keys(sfn.v) && temp_strat_default ∉ keys(sfn.f) && temp_strat_default ∉ keys(sfn.p) + return (temp_strat_default ∉ keys(sfn.s) && temp_strat_default ∉ keys(sfn.sv) + && temp_strat_default ∉ keys(sfn.v) && temp_strat_default ∉ keys(sfn.f) + && temp_strat_default ∉ keys(sfn.p)) end function add_temp_strat_default!(sfn::SFNames, temp_strat_default) - push!(sfn.s, temp_strat_default => -1) - push!(sfn.sv, temp_strat_default => -1) - push!(sfn.v, temp_strat_default => -1) - push!(sfn.f, temp_strat_default => -1) - push!(sfn.p, temp_strat_default => -1) + push!(sfn.s, temp_strat_default => -1) + push!(sfn.sv, temp_strat_default => -1) + push!(sfn.v, temp_strat_default => -1) + push!(sfn.f, temp_strat_default => -1) + push!(sfn.p, temp_strat_default => -1) end function is_all_mapped(sfn::SFNames)::Bool - return all(vec -> 0 ∉ vec, get_mapped_vectors(sfn)) + return all(vec -> 0 ∉ vec, get_mapped_vectors(sfn)) end function get_names(sfn::SFNames)::NTuple{5, Vector{Symbol}} - return sfn.snames, sfn.svnames, sfn.vnames, sfn.fnames, sfn.pnames + return sfn.snames, sfn.svnames, sfn.vnames, sfn.fnames, sfn.pnames end """ - interpret_stratification_standard_notation(mapping_pair::Expr)::Tuple{Vector{DSLArgument}, Vector{DSLArgument}} -Take an expression of the form a1, ..., => t <= s1, ..., where every element is a symbol, and return a 2-tuple of form ((a1 => t, a2 => t, ...), (s1 => t, ...)) + interpret_stratification_standard_notation(mapping_pair::Expr)::Tuple{Vector{DSLArgument}, +Vector{DSLArgument}} Take an expression of the form a1, ..., => t <= s1, ..., +where every element is a symbol, and return a 2-tuple of form ((a1 => t, a2 => +t, ...), (s1 => t, ...)) """ function interpret_stratification_standard_notation(mapping_pair::Expr)::Vector{Vector{DSLArgument}} - @match mapping_pair begin - - - :($s => $t <= $a) => return [[DSLArgument(s,t)], [DSLArgument(a,t)]] - :($s => $t <= $a, $(atail...)) => [[DSLArgument(s,t)], [DSLArgument(a,t) ; [DSLArgument(as,t) for as in atail] ]] - :($(shead...), $s => $t <= $a) => [[[DSLArgument(ss, t) for ss in shead] ; DSLArgument(s, t)], [DSLArgument(a, t)]] - - if mapping_pair.head == :tuple end => begin - middle_index = findfirst(x -> typeof(x) == Expr && length(x.args) == 3, mapping_pair.args) # still isn't specific enough - if isnothing(middle_index) - error("Malformed line $mapping_pair, could not find center.") - end - @match mapping_pair.args[middle_index] begin - :($stail => $t <= $ahead) => begin - sdict = [[DSLArgument(ss, t) for ss in mapping_pair.args[1:middle_index-1]] ; DSLArgument(stail, t)] - adict = [DSLArgument(ahead, t) ; [DSLArgument(as, t) for as in mapping_pair.args[middle_index+1:end]]] - return [sdict, adict] - end - _ => "Unknown format found for match; middle three values formatted incorrectly." - end + @match mapping_pair begin + :($s => $t <= $a) => return [[DSLArgument(s,t)], [DSLArgument(a,t)]] + :($s => $t <= $a, $(atail...)) => [[DSLArgument(s,t)], [DSLArgument(a,t) ; [DSLArgument(as,t) for as in atail] ]] + :($(shead...), $s => $t <= $a) => [[[DSLArgument(ss, t) for ss in shead] ; DSLArgument(s, t)], [DSLArgument(a, t)]] + + if mapping_pair.head == :tuple end => begin + middle_index = findfirst(x -> typeof(x) == Expr && length(x.args) == 3, mapping_pair.args) # still isn't specific enough + if isnothing(middle_index) + error("Malformed line $mapping_pair, could not find center.") + end + @match mapping_pair.args[middle_index] begin + :($stail => $t <= $ahead) => begin + sdict = [[DSLArgument(ss, t) for ss in mapping_pair.args[1:middle_index-1]] ; DSLArgument(stail, t)] + adict = [DSLArgument(ahead, t) ; [DSLArgument(as, t) for as in mapping_pair.args[middle_index+1:end]]] + return [sdict, adict] end - _ => error("Unknown line format found in stratification notation.") + _ => "Unknown format found for match; middle three values formatted incorrectly." + end end + _ => error("Unknown line format found in stratification notation.") + end end function interpret_stratification_generalized_notation(mapping_pair::Expr)::Vector{Vector{DSLArgument}} - # asserts are covered before this function is called. - other = mapping_pair.args[2].args # needs to be a vector of tuples of symbols - type = mapping_pair.args[3] # needs to be a symbol - return [((typeof(tup) == Expr) && (tup.head == :tuple)) ? [DSLArgument(sym, type) for sym ∈ tup.args] : [DSLArgument(tup, type)] for tup ∈ other] + # asserts are covered before this function is called. + other = mapping_pair.args[2].args # needs to be a vector of tuples of symbols + type = mapping_pair.args[3] # needs to be a symbol + return ([((typeof(tup) == Expr) && (tup.head == :tuple)) ? + [DSLArgument(sym, type) for sym ∈ tup.args] : [DSLArgument(tup, type)] for tup ∈ other]) end @@ -132,309 +149,385 @@ end """ -Gets mapping information from each line and updates dictionaries. If a symbol already has a mapping and another is found, keep the first, or throw an error if strict_matches = true. +Gets mapping information from each line and updates dictionaries. If a symbol +already has a mapping and another is found, keep the first, or throw an error if +strict_matches = true. """ function read_stratification_line_and_update_dictionaries!(line::Expr, other_names::Vector{Dict{Symbol, Int}}, type_names::Dict{Symbol, Int}, other_mappings::Vector{Dict{Int, Int}} ; use_standard_stratification_syntax = true, strict_matches = false, use_flags = true) - if use_standard_stratification_syntax - interpret_stratification_notation_function = interpret_stratification_standard_notation - else - interpret_stratification_notation_function = interpret_stratification_generalized_notation - - # need to do this here, since we know the number of other_mappings at this point, but not in the interpret_stratification_notation - @assert length(line.args) == 3 - @assert typeof(line.args[3]) == Symbol - @assert line.args[1] == :(=>) - @assert length(line.args[2].args) == length(other_names) - @assert all(tup -> typeof(tup) == Symbol || tup.args[1] == :~ || tup.head == :tuple, line.args[2].args) # every element of the vector is an expression of tuple. - @assert all(tup -> typeof(tup) == Symbol || tup.args[1] == :~ || all(sym -> typeof(sym) <: Union{Symbol, Expr}, tup.args), line.args[2].args) # ensure all arguments in the tuples are expressions or symbols. - # In the future, if we have additional flags, may need to check for them as well. - # These asserts are a bit sloppy - end - - current_symbol_dict::Vector{Vector{DSLArgument}} = interpret_stratification_notation_function(line) - - current_mapping_dict::Vector{Dict{Int, Int}} = ((x, y) -> substitute_symbols(x,type_names, y; use_flags=use_flags)).(other_names, current_symbol_dict) - - ((cumulative_dict, new_dict) -> mergewith!((cv, nv) -> cv, cumulative_dict, new_dict)).(other_mappings, current_mapping_dict) + if use_standard_stratification_syntax + interpret_stratification_notation_function = interpret_stratification_standard_notation + else + interpret_stratification_notation_function = interpret_stratification_generalized_notation + + # need to do this here, since we know the number of other_mappings at this + # point, but not in the interpret_stratification_notation + @assert length(line.args) == 3 + @assert typeof(line.args[3]) == Symbol + @assert line.args[1] == :(=>) + @assert length(line.args[2].args) == length(other_names) + @assert all(tup -> typeof(tup) == Symbol || tup.args[1] == :~ + || tup.head == :tuple, line.args[2].args) # every element of the vector is an expression of tuple. + @assert all(tup -> typeof(tup) == Symbol || tup.args[1] == :~ + || all(sym -> typeof(sym) <: Union{Symbol, Expr}, tup.args), + line.args[2].args) # ensure all arguments in the tuples are expressions or symbols. + # In the future, if we have additional flags, may need to check for them as + # well. These asserts are a bit sloppy + end + + current_symbol_dict::Vector{Vector{DSLArgument}} = interpret_stratification_notation_function(line) + + current_mapping_dict::Vector{Dict{Int, Int}} = ((x, y) -> substitute_symbols(x,type_names, y; use_flags=use_flags)).(other_names, current_symbol_dict) + + ((cumulative_dict, new_dict) -> mergewith!((cv, nv) -> cv, cumulative_dict, new_dict)).(other_mappings, current_mapping_dict) end """ -Print all symbols such that the corresponding int is 0, representing an unmapped object. +Print all symbols such that the corresponding int is 0, representing an unmapped +object. """ function print_unmapped(SFNames, name="STOCKFLOW") - for (indices, names) ∈ zip(SFNames.get_mapped_vectors, SFNames.get_names) - for (i, val) ∈ enumerate(indices) - if val == 0 - println("UNMAPPED IN $(name):") - println(names[i]) - end - end + for (indices, names) ∈ zip(SFNames.get_mapped_vectors, SFNames.get_names) + for (i, val) ∈ enumerate(indices) + if val == 0 + println("UNMAPPED IN $(name):") + println(names[i]) + end end + end end """ -Iterates over each line in a stratification syntax block and updates the appropriate dictionaries. +Iterates over each line in a stratification syntax block and updates the +appropriate dictionaries. """ -function iterate_over_stratification_lines!(block, other_names::Vector{SFNames}, type_names::SFNames; use_standard_stratification_syntax=true, strict_matches=false, use_flags=true) - - current_phase = (_, _) -> () - for statement in block.args - @match statement begin - QuoteNode(:stocks) => begin - current_phase = s -> read_stratification_line_and_update_dictionaries!(s, (getfield.(other_names, :s))::Vector{Dict{Symbol, Int}}, type_names.s, (getfield.(other_names, :ms))::Vector{Dict{Int, Int}}; use_standard_stratification_syntax=use_standard_stratification_syntax, strict_matches=strict_matches, use_flags=use_flags) - end - QuoteNode(:sums) => begin - current_phase = sv -> read_stratification_line_and_update_dictionaries!(sv, (getfield.(other_names, :sv))::Vector{Dict{Symbol, Int}}, type_names.sv, (getfield.(other_names, :msv))::Vector{Dict{Int, Int}}; use_standard_stratification_syntax=use_standard_stratification_syntax, strict_matches=strict_matches, use_flags=use_flags) - end - QuoteNode(:dynamic_variables) => begin - current_phase = v -> read_stratification_line_and_update_dictionaries!(v, (getfield.(other_names, :v))::Vector{Dict{Symbol, Int}}, type_names.v, (getfield.(other_names, :mv))::Vector{Dict{Int, Int}}; use_standard_stratification_syntax=use_standard_stratification_syntax, strict_matches=strict_matches, use_flags=use_flags) - end - QuoteNode(:flows) => begin - current_phase = f -> read_stratification_line_and_update_dictionaries!(f, (getfield.(other_names, :f))::Vector{Dict{Symbol, Int}}, type_names.f, (getfield.(other_names, :mf))::Vector{Dict{Int, Int}}; use_standard_stratification_syntax=use_standard_stratification_syntax, strict_matches=strict_matches, use_flags=use_flags) - end - QuoteNode(:parameters) => begin - current_phase = p -> read_stratification_line_and_update_dictionaries!(p, (getfield.(other_names, :p))::Vector{Dict{Symbol, Int}}, type_names.p, (getfield.(other_names, :mp))::Vector{Dict{Int, Int}}; use_standard_stratification_syntax=use_standard_stratification_syntax, strict_matches=strict_matches, use_flags=use_flags) - end - QuoteNode(kw) => - error("Unknown block type for stratify syntax: " * String(kw)) - _ => current_phase(statement) - end +function iterate_over_stratification_lines!(block, other_names::Vector{SFNames}, + type_names::SFNames; use_standard_stratification_syntax=true, + strict_matches=false, use_flags=true) + + current_phase = (_, _) -> () + for statement in block.args + @match statement begin + QuoteNode(:stocks) => begin + current_phase = s -> read_stratification_line_and_update_dictionaries!(s, + (getfield.(other_names, :s))::Vector{Dict{Symbol, Int}}, type_names.s, + (getfield.(other_names, :ms))::Vector{Dict{Int, Int}}; + use_standard_stratification_syntax=use_standard_stratification_syntax, + strict_matches=strict_matches, use_flags=use_flags) + end + QuoteNode(:sums) => begin + current_phase = sv -> read_stratification_line_and_update_dictionaries!(sv, + (getfield.(other_names, :sv))::Vector{Dict{Symbol, Int}}, type_names.sv, + (getfield.(other_names, :msv))::Vector{Dict{Int, Int}}; + use_standard_stratification_syntax=use_standard_stratification_syntax, + strict_matches=strict_matches, use_flags=use_flags) + end + QuoteNode(:dynamic_variables) => begin + current_phase = v -> read_stratification_line_and_update_dictionaries!(v, + (getfield.(other_names, :v))::Vector{Dict{Symbol, Int}}, type_names.v, + (getfield.(other_names, :mv))::Vector{Dict{Int, Int}}; + use_standard_stratification_syntax=use_standard_stratification_syntax, + strict_matches=strict_matches, use_flags=use_flags) + end + QuoteNode(:flows) => begin + current_phase = f -> read_stratification_line_and_update_dictionaries!(f, + (getfield.(other_names, :f))::Vector{Dict{Symbol, Int}}, type_names.f, + (getfield.(other_names, :mf))::Vector{Dict{Int, Int}}; + use_standard_stratification_syntax=use_standard_stratification_syntax, + strict_matches=strict_matches, use_flags=use_flags) + end + QuoteNode(:parameters) => begin + current_phase = p -> read_stratification_line_and_update_dictionaries!(p, + (getfield.(other_names, :p))::Vector{Dict{Symbol, Int}}, type_names.p, + (getfield.(other_names, :mp))::Vector{Dict{Int, Int}}; + use_standard_stratification_syntax=use_standard_stratification_syntax, + strict_matches=strict_matches, use_flags=use_flags) + end + QuoteNode(kw) => + error("Unknown block type for stratify syntax: " * String(kw)) + _ => current_phase(statement) end + end end """ -Apply default mappings, infer mapping if there's only a single option, and convert from Dict{Int, Int} to Vector{Int} +Apply default mappings, infer mapping if there's only a single option, and +convert from Dict{Int, Int} to Vector{Int} """ function complete_mappings!(sfm::SFNames, sftype::SFNames; strict_mappings = false) - # get the default value, if it has been assigned. Use 0 if it hasn't. - all_index_mappings = get_mappings(sfm) - - default_index_stock = get(all_index_mappings[1], -1, 0) - default_index_sum = get(all_index_mappings[2], -1, 0) - default_index_dyvar = get(all_index_mappings[3], -1, 0) - default_index_flow = get(all_index_mappings[4], -1, 0) - default_index_param = get(all_index_mappings[5], -1, 0) - - - - # STEP 3 - if !strict_mappings - one_type_stock = length(sftype.snames) == 1 ? 1 : 0 # if there is only one stock, it needs to have index 1 - one_type_flow = length(sftype.fnames) == 1 ? 1 : 0 - one_type_dyvar = length(sftype.vnames) == 1 ? 1 : 0 - one_type_param = length(sftype.pnames) == 1 ? 1 : 0 - one_type_sum = length(sftype.svnames) == 1 ? 1 : 0 - else - one_type_stock = one_type_flow = one_type_dyvar = one_type_param = one_type_sum = 0 - end - - # Convert back to vectors. If you find a zero, check if there's a default and use that. If there isn't a default, check if there's only one option and use that. - # Otherwise, there's an unassigned value which can't be inferred. - # Taking max because it's less verbose than ternary and accomplishes the same thing: - # - If both default_index and one_type are mapped, they must be mapped to the same thing, because one_type being mapped implies there's only one option. - # - If only one_type is mapped, then it will be positive, and default_infex will be 0 - # - If only default_index is mapped, it will be positive and one_type will be 0 - - stock_mappings::Vector{Int} = [get(all_index_mappings[1], i, max(default_index_stock, one_type_stock)) for i ∈ eachindex(sfm.snames)] - sum_mappings::Vector{Int} = [get(all_index_mappings[2], i, max(default_index_sum, one_type_sum)) for i ∈ eachindex(sfm.svnames)] - dyvar_mappings::Vector{Int} = [get(all_index_mappings[3], i, max(default_index_dyvar, one_type_dyvar)) for i ∈ eachindex(sfm.vnames)] - flow_mappings::Vector{Int} = [get(all_index_mappings[4], i, max(default_index_flow, one_type_flow)) for i ∈ eachindex(sfm.fnames)] - param_mappings::Vector{Int} = [get(all_index_mappings[5], i, max(default_index_param, one_type_param)) for i ∈ eachindex(sfm.pnames)] - - append!(sfm.mvs, stock_mappings) - append!(sfm.mvsv, sum_mappings) - append!(sfm.mvv, dyvar_mappings) - append!(sfm.mvf, flow_mappings) - append!(sfm.mvp, param_mappings) + # get the default value, if it has been assigned. Use 0 if it hasn't. + all_index_mappings = get_mappings(sfm) + + default_index_stock = get(all_index_mappings[1], -1, 0) + default_index_sum = get(all_index_mappings[2], -1, 0) + default_index_dyvar = get(all_index_mappings[3], -1, 0) + default_index_flow = get(all_index_mappings[4], -1, 0) + default_index_param = get(all_index_mappings[5], -1, 0) + + + + # STEP 3 + if !strict_mappings # if there is only one stock, it needs to have index 1 + one_type_stock = length(sftype.snames) == 1 ? 1 : 0 + one_type_flow = length(sftype.fnames) == 1 ? 1 : 0 + one_type_dyvar = length(sftype.vnames) == 1 ? 1 : 0 + one_type_param = length(sftype.pnames) == 1 ? 1 : 0 + one_type_sum = length(sftype.svnames) == 1 ? 1 : 0 + else + one_type_stock = one_type_flow = one_type_dyvar = one_type_param = one_type_sum = 0 + end + + # Convert back to vectors. If you find a zero, check if there's a default and + # use that. If there isn't a default, check if there's only one option and + # use that. Otherwise, there's an unassigned value which can't be inferred. + # Taking max because it's less verbose than ternary and accomplishes the same + # thing: + # - If both default_index and one_type are mapped, they must be mapped to the + # same thing, because one_type being mapped implies there's only one option. + # - If only one_type is mapped, then it will be positive, and default_infex + # will be 0 + # - If only default_index is mapped, it will be positive and one_type will be + # 0 + + stock_mappings::Vector{Int} = [get(all_index_mappings[1], i, + max(default_index_stock, one_type_stock)) for i ∈ eachindex(sfm.snames)] + sum_mappings::Vector{Int} = [get(all_index_mappings[2], i, + max(default_index_sum, one_type_sum)) for i ∈ eachindex(sfm.svnames)] + dyvar_mappings::Vector{Int} = [get(all_index_mappings[3], i, + max(default_index_dyvar, one_type_dyvar)) for i ∈ eachindex(sfm.vnames)] + flow_mappings::Vector{Int} = [get(all_index_mappings[4], i, + max(default_index_flow, one_type_flow)) for i ∈ eachindex(sfm.fnames)] + param_mappings::Vector{Int} = [get(all_index_mappings[5], i, + max(default_index_param, one_type_param)) for i ∈ eachindex(sfm.pnames)] + + append!(sfm.mvs, stock_mappings) + append!(sfm.mvsv, sum_mappings) + append!(sfm.mvv, dyvar_mappings) + append!(sfm.mvf, flow_mappings) + append!(sfm.mvp, param_mappings) end """ - sfstratify(strata, type, aggregate, block ; kwargs) - - 1. Grab all names from strata, type and aggregate, and create dictionaries which map them to their indices - 2. Iterate over each line in the block - 2a. Split each line into a dictionary which maps all strata to that type and all aggregate to that type - 2b. Convert from two Symbol => Symbol dictionaries to two Int => Int dictionaries, using the dictionaries from step 1 - 2bα. If applicable, for symbols with ~ as a prefix, find all symbols with matching substrings in the symbol dictionaries, and map all those - 2c. Accumulate respective dictionaries (optionally, only allow first match vs throw an error (strict_matches = false vs true)) - 3. Create an array of 0s for stocks, flows, parameters, dyvars and sums for strata and aggregate. Insert into arrays all values from the two Int => Int dictionaries - 3a. If strict_mappings = false, if there only exists one option in type to map to, and it hasn't been explicitly specified, add it. If strict_mappings = true and it hasn't been specified, throw an error. - 4. Do a once-over of arrays and ensure there aren't any zeroes (unmapped values) remaining (helps with debugging when you screw up stratifying) - 5. Deal with attributes (create a copy of type sf with attributes mapped to nothing) - 6. Infer LS, LSV, etc. - 7. Construct strata -> type and aggregate -> type ACSetTransformations - 8. Return pullback (with flattened attributes) + sfstratify(strata, type, aggregate, block ; kwargs) + + 1. Grab all names from strata, type and aggregate, and create dictionaries + which map them to their indices + 2. Iterate over each line in the block 2a. Split each line into a dictionary + which maps all strata to that type and all aggregate to that type 2b. + Convert from two Symbol => Symbol dictionaries to two Int => Int + dictionaries, using the dictionaries from step 1 2bα. If applicable, for + symbols with ~ as a prefix, find all symbols with matching substrings in the + symbol dictionaries, and map all those 2c. Accumulate respective + dictionaries (optionally, only allow first match vs throw an error + (strict_matches = false vs true)) + 3. Create an array of 0s for stocks, flows, parameters, dyvars and sums for + strata and aggregate. Insert into arrays all values from the two Int => + Int dictionaries 3a. If strict_mappings = false, if there only exists one + option in type to map to, and it hasn't been explicitly specified, add it. + If strict_mappings = true and it hasn't been specified, throw an error. + 4. Do a once-over of arrays and ensure there aren't any zeroes (unmapped + values) remaining (helps with debugging when you screw up stratifying) + 5. Deal with attributes (create a copy of type sf with attributes mapped to + nothing) + 6. Infer LS, LSV, etc. + 7. Construct strata -> type and aggregate -> type ACSetTransformations + 8. Return pullback (with flattened attributes) """ -function sfstratify(others::Vector{K}, type::K, block::Expr ; use_standard_stratification_syntax = true, strict_mappings = false, strict_matches = false, temp_strat_default = :_, use_temp_strat_default = true, use_flags = true, return_homs = false) where {K <: AbstractStockAndFlowStructureF} +function sfstratify(others::Vector{K}, type::K, block::Expr + ; use_standard_stratification_syntax = true, strict_mappings = false, + strict_matches = false, temp_strat_default = :_, use_temp_strat_default = true, + use_flags = true, return_homs = false) where {K <: AbstractStockAndFlowStructureF} - Base.remove_linenums!(block) + Base.remove_linenums!(block) - # STEP 1 + # STEP 1 - other_names::Vector{SFNames} = [SFNames(sf) for sf ∈ others] - type_names::SFNames = SFNames(type) # has some unnecessary fields. + other_names::Vector{SFNames} = [SFNames(sf) for sf ∈ others] + type_names::SFNames = SFNames(type) # has some unnecessary fields. - if use_temp_strat_default - # Applies function to every element in vector. - @assert all((sfn -> no_temp_strat_default_in_names(sfn, temp_strat_default)).(other_names)) && no_temp_strat_default_in_names(type_names, temp_strat_default) "A stockflow contains $(temp_strat_default) ! Please change temp_strat_default to a different symbol or rename offending object." - (sfn -> add_temp_strat_default!(sfn, temp_strat_default)).(other_names) - end + if use_temp_strat_default + # Applies function to every element in vector. + @assert (all((sfn -> no_temp_strat_default_in_names(sfn, temp_strat_default)).(other_names)) + && no_temp_strat_default_in_names(type_names, temp_strat_default)) + "A stockflow contains $(temp_strat_default) ! \ + Please change temp_strat_default to a different symbol or rename offending object." + (sfn -> add_temp_strat_default!(sfn, temp_strat_default)).(other_names) + end - # STEP 2 - iterate_over_stratification_lines!(block, other_names, type_names ; use_standard_stratification_syntax=use_standard_stratification_syntax, strict_matches=strict_matches, use_flags=use_flags) + # STEP 2 + iterate_over_stratification_lines!(block, other_names, type_names ; + use_standard_stratification_syntax=use_standard_stratification_syntax, + strict_matches=strict_matches, use_flags=use_flags) - (sfn -> complete_mappings!(sfn, type_names ; strict_mappings=strict_mappings)).(other_names) + (sfn -> complete_mappings!(sfn, type_names ; strict_mappings=strict_mappings)).(other_names) - # STEP 4 + # STEP 4 - # This bit makes debugging when making a stratification easier. Tells you exactly which ones you forgot to map. + # This bit makes debugging when making a stratification easier. Tells you + # exactly which ones you forgot to map. - #unmapped: - if !(all(is_all_mapped.(other_names))) - for i ∈ eachindex(other_names) - print_unmapped(other_names[i], "STOCKFLOW $i") - end - error("There is an unmapped value!") + #unmapped: + if !(all(is_all_mapped.(other_names))) + for i ∈ eachindex(other_names) + print_unmapped(other_names[i], "STOCKFLOW $i") end - - - # STEP 5 - # NothingFunction(x...) = nothing; - no_attribute_type = map(type, Name=NothingFunction, Op=NothingFunction, Position=NothingFunction) - - # STEP 6/7 - # This is where we pull out the magic to infer links. - # - # A <- C -> B - # || || - # v v - # A'<- C'-> B' - # - # implies - # - # A <- C -> B - # || || || - # v v v - # A'<- C'-> B' - # - - generate_all_mappings_function = m -> Dict(infer_links(m.sf, type, get_mappings_infer_links_format(m))..., get_mappings_infer_links_format(m)..., :Op => NothingFunction, :Position => NothingFunction, :Name => NothingFunction) - all_mappings = generate_all_mappings_function.(other_names) + error("There is an unmapped value!") + end + + + # STEP 5 NothingFunction(x...) = nothing; + no_attribute_type = map(type, Name=NothingFunction, + Op=NothingFunction, Position=NothingFunction) + + # STEP 6/7 This is where we pull out the magic to infer links. + # + # A <- C -> B + # || || + # v v + # A'<- C'-> B' + # + # implies + # + # A <- C -> B + # || || || + # v v v + # A'<- C'-> B' + # + + generate_all_mappings_function = m -> Dict(infer_links(m.sf, type, + get_mappings_infer_links_format(m))..., + get_mappings_infer_links_format(m)..., + :Op => NothingFunction, :Position => NothingFunction, + :Name => NothingFunction) + + all_mappings = generate_all_mappings_function.(other_names) - all_transformations = [ACSetTransformation(sfn.sf, no_attribute_type ; mappings...) for (sfn, mappings) ∈ zip(other_names, all_mappings)] - - # STEP 8 - - pullback_model = pullback(all_transformations) |> apex |> rebuildStratifiedModelByFlattenSymbols; - - if return_homs - return pullback_model, all_transformations - else - return pullback_model - end - + all_transformations = [ + ACSetTransformation(sfn.sf, no_attribute_type ; mappings...) + for (sfn, mappings) ∈ zip(other_names, all_mappings) + ] + + # STEP 8 + + pullback_model = pullback(all_transformations) |> apex |> + rebuildStratifiedModelByFlattenSymbols; + + if return_homs + return pullback_model, all_transformations + else + return pullback_model + end + end """ - stratify(strata, type, aggregate, block) -Take three stockflows and a block describing where the first and third map on to the second, and get a new stratified stockflow. -Left side are strata objects, middle are type, right are aggregate. Each strata and aggregate object is associated with one type object. -The resultant stockflow contains objects which are the product of strata and aggregate objects which map to the same type object. -Use _ to match all objects in that category, ~ as a prefix to match all objects with the following string as a substring. Objects always go with their first match. -If the type model has a single object in a category, the mapping to it is automatically assumed. In the below example, we wouldn't need to specify :stocks or :sums. + stratify(strata, type, aggregate, block) Take three stockflows and a block +describing where the first and third map on to the second, and get a new +stratified stockflow. Left side are strata objects, middle are type, right are +aggregate. Each strata and aggregate object is associated with one type object. +The resultant stockflow contains objects which are the product of strata and +aggregate objects which map to the same type object. Use _ to match all objects +in that category, ~ as a prefix to match all objects with the following string +as a substring. Objects always go with their first match. If the type model has +a single object in a category, the mapping to it is automatically assumed. In +the below example, we wouldn't need to specify :stocks or :sums. ```julia @stratify WeightModel l_type ageWeightModel begin - :stocks - _ => pop <= _ - - :flows - ~Death => f_death <= ~Death - ~id => f_aging <= ~aging - ~Becoming => f_fstOrder <= ~id - _ => f_birth <= f_NB - - - :dynamic_variables - v_NewBorn => v_birth <= v_NB - ~Death => v_death <= ~Death - ~id => v_aging <= v_agingCA, v_agingAS - v_BecomingOverWeight, v_BecomingObese => v_fstOrder <= v_idC, v_idA, v_idS - - :parameters - μ => μ <= μ - δw, δo => δ <= δC, δA, δS - rw, ro => rFstOrder <= r - rage => rage <= rageCA, rageAS - - :sums - N => N <= N - + :stocks + _ => pop <= _ + + :flows + ~Death => f_death <= ~Death + ~id => f_aging <= ~aging + ~Becoming => f_fstOrder <= ~id + _ => f_birth <= f_NB + + + :dynamic_variables + v_NewBorn => v_birth <= v_NB + ~Death => v_death <= ~Death + ~id => v_aging <= v_agingCA, v_agingAS + v_BecomingOverWeight, v_BecomingObese => v_fstOrder <= v_idC, v_idA, v_idS + + :parameters + μ => μ <= μ + δw, δo => δ <= δC, δA, δS + rw, ro => rFstOrder <= r + rage => rage <= rageCA, rageAS + + :sums + N => N <= N + end ``` """ macro stratify(strata, type, aggregate, block) - escaped_block = Expr(:quote, block) - quote - sfstratify([$(esc(strata)), $(esc(aggregate))], $(esc(type)), $(esc(escaped_block))) - end + escaped_block = Expr(:quote, block) + quote + sfstratify([$(esc(strata)), $(esc(aggregate))], + $(esc(type)), $(esc(escaped_block))) + end end """ -Alternate syntax for stratification, allows for an arbitrary number of stockflows in a pullback. -Second last argument must be the type stockflow, last must be the block describing how the stratificaition is done. All arguments before that must be stockflows. +Alternate syntax for stratification, allows for an arbitrary number of +stockflows in a pullback. Second last argument must be the type stockflow, last +must be the block describing how the stratificaition is done. All arguments +before that must be stockflows. ```julia @n_stratify WeightModel ageWeightModel l_type begin - :stocks - [_, _] => pop - - :flows - [~Death, ~Death] => f_death - [~id, ~aging] => f_aging - [~Becoming, ~id] => f_fstOrder - [_, f_NB] => f_birth - - - :dynamic_variables - [v_NewBorn, v_NB] => v_birth - [~Death, ~Death] => v_death - [~id, (v_agingCA, v_agingAS)] => v_aging - [(v_BecomingOverWeight, v_BecomingObese), (v_idC, v_idA, v_idS)] => v_fstOrder - - :parameters - [μ, μ] => μ - [(δw, δo), (δC, δA, δS)] => δ - [(rw, ro), r] => rFstOrder - [rage, (rageCA, rageAS)] => rage - - :sums - [N,N] => N + :stocks + [_, _] => pop + + :flows + [~Death, ~Death] => f_death + [~id, ~aging] => f_aging + [~Becoming, ~id] => f_fstOrder + [_, f_NB] => f_birth + + + :dynamic_variables + [v_NewBorn, v_NB] => v_birth + [~Death, ~Death] => v_death + [~id, (v_agingCA, v_agingAS)] => v_aging + [(v_BecomingOverWeight, v_BecomingObese), (v_idC, v_idA, v_idS)] => v_fstOrder + + :parameters + [μ, μ] => μ + [(δw, δo), (δC, δA, δS)] => δ + [(rw, ro), r] => rFstOrder + [rage, (rageCA, rageAS)] => rage + + :sums + [N,N] => N end ``` """ macro n_stratify(args...) - if length(args) < 3 - return :(MethodError("Too few arguments provided! Please provide some number of stockflows, then the type stock flow, then a quote block.")) - else - escaped_block = Expr(:quote, args[end]) - other_sfs = esc.(args[1:end-2]) - type = (esc(args[end-1])) - quote - sfstratify([$(other_sfs...)], $type, $escaped_block ; use_standard_stratification_syntax = false) - end + if length(args) < 3 + return :(MethodError("Too few arguments provided! Please provide some \ + number of stockflows, then the type stock flow, then a quote block.")) + else + escaped_block = Expr(:quote, args[end]) + other_sfs = esc.(args[1:end-2]) + type = (esc(args[end-1])) + quote + sfstratify([$(other_sfs...)], $type, $escaped_block ; + use_standard_stratification_syntax = false) end + end end diff --git a/test/Syntax.jl b/test/Syntax.jl index 22c55e64..368db559 100755 --- a/test/Syntax.jl +++ b/test/Syntax.jl @@ -2,299 +2,309 @@ using Base: is_unary_and_binary_operator using Test using StockFlow using StockFlow.Syntax -using StockFlow.Syntax: is_binop_or_unary, sum_variables, infix_expression_to_binops, fnone_value_or_vector, extract_function_name_and_args_expr, is_recursive_dyvar, create_foot, apply_flags, substitute_symbols, DSLArgument +using StockFlow.Syntax: is_binop_or_unary, sum_variables, infix_expression_to_binops, +fnone_value_or_vector, extract_function_name_and_args_expr, is_recursive_dyvar, +create_foot, apply_flags, substitute_symbols @testset "Stratification DSL" begin - include("syntax/Stratification.jl") + include("syntax/Stratification.jl") end @testset "Composition DSL" begin - include("syntax/Composition.jl") + include("syntax/Composition.jl") end +@testset "Homomorphism DSL" begin + include("syntax/Homomorphism.jl") +end + + @testset "is_binop_or_unary recognises binops" begin - @test is_binop_or_unary(:(a + b)) - @test is_binop_or_unary(:(f(a, b))) - @test is_binop_or_unary(:(1.0 + x)) + @test is_binop_or_unary(:(a + b)) + @test is_binop_or_unary(:(f(a, b))) + @test is_binop_or_unary(:(1.0 + x)) end @testset "is_binop_or_unary recognises non-binops as non-binops" begin - @test !is_binop_or_unary(:(f())) - @test !is_binop_or_unary(:(a + b + c)) - @test !is_binop_or_unary(:(f(a, b, c))) + @test !is_binop_or_unary(:(f())) + @test !is_binop_or_unary(:(a + b + c)) + @test !is_binop_or_unary(:(f(a, b, c))) end @testset "sum_variables" begin - @test sum_variables([]) == [] - @test sum_variables([(:a, 1)]) == [:a] - @test sum_variables([(:a, 1), (:b, 2)]) == [:a, :b] + @test sum_variables([]) == [] + @test sum_variables([(:a, 1)]) == [:a] + @test sum_variables([(:a, 1), (:b, 2)]) == [:a, :b] end @testset "infix_expression_to_binops does nothing to binops and unary exprs" begin - @test infix_expression_to_binops(:(f(a)))[1][1][2] == :(f(a)) - @test infix_expression_to_binops(:(f(a, b)))[1][1][2] == :(f(a, b)) - @test infix_expression_to_binops(:(a + b))[1][1][2] == :(a + b) + @test infix_expression_to_binops(:(f(a)))[1][1][2] == :(f(a)) + @test infix_expression_to_binops(:(f(a, b)))[1][1][2] == :(f(a, b)) + @test infix_expression_to_binops(:(a + b))[1][1][2] == :(a + b) end @testset "infix_expression_to_binops creates right number of expressions" begin - @test length(infix_expression_to_binops(:(a + b + c))[1]) == 2 - @test length(infix_expression_to_binops(:(a + b + c + d))[1]) == 3 - @test length(infix_expression_to_binops(:(a + b + c + d + e))[1]) == 4 - @test length(infix_expression_to_binops(:(a + b + c + d + e + f))[1]) == 5 + @test length(infix_expression_to_binops(:(a + b + c))[1]) == 2 + @test length(infix_expression_to_binops(:(a + b + c + d))[1]) == 3 + @test length(infix_expression_to_binops(:(a + b + c + d + e))[1]) == 4 + @test length(infix_expression_to_binops(:(a + b + c + d + e + f))[1]) == 5 end @testset "infix_expression_to_binops throws exception when no binops provided" begin - @test_throws Exception infix_expression_to_binops(:(f())) + @test_throws Exception infix_expression_to_binops(:(f())) end @testset "infix_expression_to_binops uses final symbol" begin - @test infix_expression_to_binops(:(f(a, b)); finalsym=:testsym)[2] == :testsym - @test infix_expression_to_binops(:(a + b); finalsym=:testsym)[2] == :testsym + @test infix_expression_to_binops(:(f(a, b)); finalsym=:testsym)[2] == :testsym + @test infix_expression_to_binops(:(a + b); finalsym=:testsym)[2] == :testsym end @testset "fnone_value_or_vector" begin - empty_symbol_vector::Vector{Symbol} = [] - @test fnone_value_or_vector(empty_symbol_vector) == :F_NONE - @test fnone_value_or_vector([:a]) == :a - @test fnone_value_or_vector([:a, :b]) == [:a, :b] + empty_symbol_vector::Vector{Symbol} = [] + @test fnone_value_or_vector(empty_symbol_vector) == :F_NONE + @test fnone_value_or_vector([:a]) == :a + @test fnone_value_or_vector([:a, :b]) == [:a, :b] end @testset "extract_function_name_args_expr extracts the flow name and flow definition" begin - @test extract_function_name_and_args_expr(:(testf(a))) == (:testf, :a) - @test extract_function_name_and_args_expr(:(testf(a + b + c + d))) == (:testf, :(a + b + c + d)) + @test extract_function_name_and_args_expr(:(testf(a))) == (:testf, :a) + @test extract_function_name_and_args_expr(:(testf(a + b + c + d))) == (:testf, :(a + b + c + d)) end @testset "extract_function_name_args_expr rejects invalid flow expressions" begin - # Undefined flow equation - @test_throws Exception extract_function_name_and_args_expr(:(testf())) - # Multiparameter flows - @test_throws Exception extract_function_name_and_args_expr(:(testf(a, b))) + # Undefined flow equation + @test_throws Exception extract_function_name_and_args_expr(:(testf())) + # Multiparameter flows + @test_throws Exception extract_function_name_and_args_expr(:(testf(a, b))) end #@testset "non-variable parameters in functions" begin # @stock_and_flow begin -# :stocks -# A -# B -# C -# :parameters -# x -# y -# z -# :dynamic_variables +# :stocks +# A +# B +# C +# :parameters +# x +# y +# z +# :dynamic_variables # TODO? f = 1.0 - x -# :flows -# A => fname(f) => B +# :flows +# A => fname(f) => B # end #end @testset "model allows uses unary functions like log" begin - SIR_1_via_macro = @stock_and_flow begin - :stocks - S - I - R - - :parameters - c - beta - tRec - - :dynamic_variables - v_prevalence = exp(I) - v_meanInfectiousContactsPerS = c * v_prevalence - v_perSIncidenceRate = beta * v_meanInfectiousContactsPerS - v_newInfections = S * v_perSIncidenceRate - v_newRecovery = log(I) - - :flows - S => inf(v_newInfections) => I - I => rec(v_newRecovery) => R - - :sums - N = [S, I, R] - end - - SIR_1_canonical = StockAndFlowF( - # stocks - (:S => (:F_NONE, :inf, :N), :I => (:inf, :rec, :N), :R => (:rec, :F_NONE, :N)), - # parameters - (:c, :beta, :tRec), - # dynamical variables - (:v_prevalence => (:I => :exp), - :v_meanInfectiousContactsPerS => ((:c, :v_prevalence) => :*), - :v_perSIncidenceRate => ((:beta, :v_meanInfectiousContactsPerS) => :*), - :v_newInfections => ((:S, :v_perSIncidenceRate) => :*), - :v_newRecovery => (:I => :log), - ), - # flows - (:inf => :v_newInfections, :rec => :v_newRecovery), - # sum dynamical variables - (:N), - ) - @test SIR_1_via_macro == SIR_1_canonical + SIR_1_via_macro = @stock_and_flow begin + :stocks + S + I + R + + :parameters + c + beta + tRec + + :dynamic_variables + v_prevalence = exp(I) + v_meanInfectiousContactsPerS = c * v_prevalence + v_perSIncidenceRate = beta * v_meanInfectiousContactsPerS + v_newInfections = S * v_perSIncidenceRate + v_newRecovery = log(I) + + :flows + S => inf(v_newInfections) => I + I => rec(v_newRecovery) => R + + :sums + N = [S, I, R] + end + + SIR_1_canonical = StockAndFlowF( + # stocks + (:S => (:F_NONE, :inf, :N), :I => (:inf, :rec, :N), :R => (:rec, :F_NONE, :N)), + # parameters + (:c, :beta, :tRec), + # dynamical variables + (:v_prevalence => (:I => :exp), + :v_meanInfectiousContactsPerS => ((:c, :v_prevalence) => :*), + :v_perSIncidenceRate => ((:beta, :v_meanInfectiousContactsPerS) => :*), + :v_newInfections => ((:S, :v_perSIncidenceRate) => :*), + :v_newRecovery => (:I => :log), + ), + # flows + (:inf => :v_newInfections, :rec => :v_newRecovery), + # sum dynamical variables + (:N), + ) + @test SIR_1_via_macro == SIR_1_canonical end @testset "stock_and_flow macro generates the expected StockAndFlowF representations" begin - SIR_1_via_macro = @stock_and_flow begin - :stocks - S - I - R - - :parameters - c - beta - tRec - - :dynamic_variables - v_prevalence = I / N - v_meanInfectiousContactsPerS = c * v_prevalence - v_perSIncidenceRate = beta * v_meanInfectiousContactsPerS - v_newInfections = S * v_perSIncidenceRate - v_newRecovery = I / tRec - - :flows - S => inf(v_newInfections) => I - I => rec(v_newRecovery) => R - - :sums - N = [S, I, R] - end - - SIR_1_canonical = StockAndFlowF( - # stocks - (:S => (:F_NONE, :inf, :N), :I => (:inf, :rec, :N), :R => (:rec, :F_NONE, :N)), - # parameters - (:c, :beta, :tRec), - # dynamical variables - (:v_prevalence => ((:I, :N) => :/), - :v_meanInfectiousContactsPerS => ((:c, :v_prevalence) => :*), - :v_perSIncidenceRate => ((:beta, :v_meanInfectiousContactsPerS) => :*), - :v_newInfections => ((:S, :v_perSIncidenceRate) => :*), - :v_newRecovery => ((:I, :tRec) => :/), - ), - # flows - (:inf => :v_newInfections, :rec => :v_newRecovery), - # sum dynamical variables - (:N), - ) - @test SIR_1_via_macro == SIR_1_canonical - - SIR_2 = @stock_and_flow begin - :stocks - S - I - R - - :parameters - c - beta - tRec - - # We can leave out dynamic variables and let them be inferred from flows entirely! - - :flows - S => inf(S * beta * (c * (I / N))) => I - I => rec(I / tRec) => R - - :sums - N = [S, I, R] - end - - # Although the variable names are different - # due to gensym, the models should structurally be the same. - for part in keys(SIR_2.parts) - @test SIR_2.parts[part] == SIR_1_canonical.parts[part] - end + SIR_1_via_macro = @stock_and_flow begin + :stocks + S + I + R + + :parameters + c + beta + tRec + + :dynamic_variables + v_prevalence = I / N + v_meanInfectiousContactsPerS = c * v_prevalence + v_perSIncidenceRate = beta * v_meanInfectiousContactsPerS + v_newInfections = S * v_perSIncidenceRate + v_newRecovery = I / tRec + + :flows + S => inf(v_newInfections) => I + I => rec(v_newRecovery) => R + + :sums + N = [S, I, R] + end + + SIR_1_canonical = StockAndFlowF( + # stocks + (:S => (:F_NONE, :inf, :N), :I => (:inf, :rec, :N), :R => (:rec, :F_NONE, :N)), + # parameters + (:c, :beta, :tRec), + # dynamical variables + (:v_prevalence => ((:I, :N) => :/), + :v_meanInfectiousContactsPerS => ((:c, :v_prevalence) => :*), + :v_perSIncidenceRate => ((:beta, :v_meanInfectiousContactsPerS) => :*), + :v_newInfections => ((:S, :v_perSIncidenceRate) => :*), + :v_newRecovery => ((:I, :tRec) => :/), + ), + # flows + (:inf => :v_newInfections, :rec => :v_newRecovery), + # sum dynamical variables + (:N), + ) + @test SIR_1_via_macro == SIR_1_canonical + + SIR_2 = @stock_and_flow begin + :stocks + S + I + R + + :parameters + c + beta + tRec + + # We can leave out dynamic variables and let them be inferred from flows entirely! + + :flows + S => inf(S * beta * (c * (I / N))) => I + I => rec(I / tRec) => R + + :sums + N = [S, I, R] + end + + # Although the variable names are different + # due to gensym, the models should structurally be the same. + for part in keys(SIR_2.parts) + @test SIR_2.parts[part] == SIR_1_canonical.parts[part] + end end @testset "stock_and_flow macro base cases" begin - empty_via_macro = @stock_and_flow begin end - @test empty_via_macro == StockAndFlowF() - - no_sums = @stock_and_flow begin - :stocks - A - B - C - - :parameters - p - q - - :dynamic_variables - dyvar1 = A + B - dyvar2 = B * C - dyvar3 = sqrt(q) - dyvar4 = exp(p) - dyvar5 = log(dyvar3, dyvar4) - - :flows - A => f1(dyvar1) => B - B => f2(dyvar2) => C - C => f3(dyvar5) => A - end - no_sums_canonical = StockAndFlowF( - #stocks - (:A => (:f3, :f1, :SV_NONE), :B => (:f1, :f2, :SV_NONE), :C => (:f2, :f3, :SV_NONE)), - #params - (:p, :q), - # dyvars - (:dyvar1 => ((:A, :B) => :+), - :dyvar2 => ((:B, :C) => :*), - :dyvar3 => (:q => :sqrt), - :dyvar4 => (:p => :exp), - :dyvar5 => ((:dyvar3, :dyvar4) => :log)), - #flows - (:f1 => :dyvar1, - :f2 => :dyvar2, - :f3 => :dyvar5), - ()) - @test no_sums == no_sums_canonical + empty_via_macro = @stock_and_flow begin end + @test empty_via_macro == StockAndFlowF() + + no_sums = @stock_and_flow begin + :stocks + A + B + C + + :parameters + p + q + + :dynamic_variables + dyvar1 = A + B + dyvar2 = B * C + dyvar3 = sqrt(q) + dyvar4 = exp(p) + dyvar5 = log(dyvar3, dyvar4) + + :flows + A => f1(dyvar1) => B + B => f2(dyvar2) => C + C => f3(dyvar5) => A + end + no_sums_canonical = StockAndFlowF( + #stocks + (:A => (:f3, :f1, :SV_NONE), :B => (:f1, :f2, :SV_NONE), :C => (:f2, :f3, :SV_NONE)), + #params + (:p, :q), + # dyvars + (:dyvar1 => ((:A, :B) => :+), + :dyvar2 => ((:B, :C) => :*), + :dyvar3 => (:q => :sqrt), + :dyvar4 => (:p => :exp), + :dyvar5 => ((:dyvar3, :dyvar4) => :log)), + #flows + (:f1 => :dyvar1, + :f2 => :dyvar2, + :f3 => :dyvar5), + ()) + @test no_sums == no_sums_canonical end @testset "is_recursive_dyvar detects recursive dyvars" begin - @test is_recursive_dyvar(:v, :(v + v)) - @test is_recursive_dyvar(:a, :(b + c + d / (e + f + g / (h + i + j / (a - b * a))))) + @test is_recursive_dyvar(:v, :(v + v)) + @test is_recursive_dyvar(:a, :(b + c + d / (e + f + g / (h + i + j / (a - b * a))))) end @testset "is_recursive_dyvar does not flag non-recursive dyvars" begin - @test !is_recursive_dyvar(:w, :(v + v)) - @test !is_recursive_dyvar(:z, :(b + c + d / (e + f + g / (h + i + j / (a - b * a))))) + @test !is_recursive_dyvar(:w, :(v + v)) + @test !is_recursive_dyvar(:z, :(b + c + d / (e + f + g / (h + i + j / (a - b * a))))) end @testset "recursive definitions should be disallowed" begin - @test_throws Exception @stock_and_flow begin - :dynamic_variables - v = v + v - end + @test_throws Exception @stock_and_flow begin + :dynamic_variables + v = v + v + end end @testset "the existence of references is checked" begin - @test_throws Exception @stock_and_flow begin - :stocks - A - B - C - - :flows - a => f(A * B) => B - end + @test_throws Exception @stock_and_flow begin + :stocks + A + B + C + + :flows + a => f(A * B) => B + end end @testset "foot syntax can create all types of feet" begin - @test (@foot A => B) == foot(:A, :B, :A => :B) - @test (@foot P => ()) == foot(:P, (), ()) - @test (@foot () => Q) == foot((), :Q, ()) - @test (@foot () => ()) == foot((),(),()) - - @test (@foot =>((), SV)) == foot((),:SV,()) - @test (@foot A11 => B22) == foot(:A11, :B22, :A11 => :B22) - - @test (@foot () => B, A => ()) == foot(:A, :B, ()) - @test (@foot A => B, A => C) == foot(:A, (:B, :C), (:A => :B, :A => :C)) - @test (@foot A => B, A => B, A => B) == foot(:A, :B, (:A => :B, :A => :B, :A => :B)) # at present, it deduplicates stocks and sums, but not links. - @test (@foot P => Q, R => (), () => ()) == foot((:P, :R), (:Q), (:P => :Q)) - @test (@foot () => (), () => ()) == foot((), (), ()) + @test (@foot A => B) == foot(:A, :B, :A => :B) + @test (@foot P => ()) == foot(:P, (), ()) + @test (@foot () => Q) == foot((), :Q, ()) + @test (@foot () => ()) == foot((),(),()) + + @test (@foot =>((), SV)) == foot((),:SV,()) + @test (@foot A11 => B22) == foot(:A11, :B22, :A11 => :B22) + + @test (@foot () => B, A => ()) == foot(:A, :B, ()) + @test (@foot A => B, A => C) == foot(:A, (:B, :C), (:A => :B, :A => :C)) + + # at present, it deduplicates stocks and sums, but not links. + @test (@foot A => B, A => B, A => B) == foot(:A, :B, (:A => :B, :A => :B, :A => :B)) + @test (@foot P => Q, R => (), () => ()) == foot((:P, :R), (:Q), (:P => :Q)) + @test (@foot () => (), () => ()) == foot((), (), ()) end + @testset "foot syntax disallows invalid feet" begin # note, @feet calls create_foot for each line, so this should apply to both @foot and @feet @test_throws ErrorException @foot A => B => C # Invalid syntax for second argument of foot: B => C @test_throws ErrorException @foot oooo2 + f => C # Invalid syntax for first argument of foot: oooo2 + f @@ -309,39 +319,28 @@ end @testset "feet syntax can create feet" begin - @test (@feet begin + @test (@feet begin - A => B - C => D - () => () + A => B + C => D + () => () - P => () - () => Q + P => () + () => Q - end) == [foot(:A, :B, :A => :B), foot(:C, :D, :C => :D), foot((),(),()), foot(:P, (),()), foot((),:Q,())] + end) == [foot(:A, :B, :A => :B), foot(:C, :D, :C => :D), foot((),(),()), foot(:P, (),()), foot((),:Q,())] - @test (@feet P => Q) == [foot(:P, :Q, :P => :Q)] - @test (@feet begin end) == Vector{StockAndFlow0}() - @test (@feet begin P => Q; L => R end) == [foot(:P, :Q, :P => :Q), foot(:L, :R, :L => :R)] + @test (@feet P => Q) == [foot(:P, :Q, :P => :Q)] + @test (@feet begin end) == Vector{StockAndFlow0}() + @test (@feet begin P => Q; L => R end) == [foot(:P, :Q, :P => :Q), foot(:L, :R, :L => :R)] - @test (@feet P => P) == [foot(:P, :P, :P => :P)] + @test (@feet P => P) == [foot(:P, :P, :P => :P)] - @test (@feet begin A => NS, A => N; B => NS; C => (), D => () end) == [ - foot((:A), (:NS, :N), (:A => :NS, :A => :N)), - foot((:B), (:NS), (:B => :NS)), - foot((:C, :D), (), ()) - ] - - @test (@feet begin - P => Q, R => () - () => () - J => K, J => Q - end) == [ - foot((:P, :R), :Q, :P => :Q), - foot((), (), ()), - foot(:J, (:K, :Q), (:J => :K, :J => :Q)) + @test (@feet begin A => NS, A => N; B => NS; C => (), D => () end) == [ + foot((:A), (:NS, :N), (:A => :NS, :A => :N)), + foot((:B), (:NS), (:B => :NS)), + foot((:C, :D), (), ()) ] - @test (@feet ) == Vector{StockAndFlow0}(); end @@ -354,207 +353,201 @@ end ########################### @testset "infer_links works as expected" begin - # No prior mappings means no inferred mappings - @test (infer_links(StockAndFlowF(), StockAndFlowF(), Dict{Symbol, Vector{Int64}}(:S => [], :F => [], :SV => [], :P => [], :V => [])) - == Dict(:LS => [], :LSV => [], :LV => [], :I => [], :O => [], :LPV => [], :LVV => [])) - - # S: 1 -> 1 and SV: 1 -> 1 implies LS: 1 -> 1 - @test (infer_links( - (@stock_and_flow begin; :stocks; A; :sums; NA = [A]; end), - (@stock_and_flow begin; :stocks; B; :sums; NB = [B]; end), - Dict{Symbol, Vector{Int64}}(:S => [1], :F => [], :SV => [1], :P => [], :V => [])) - == Dict(:LS => [1], :LSV => [], :LV => [], :I => [], :O => [], :LPV => [], :LVV => [])) - - # annoying exanmple, required me to add code to disambiguate using position - # that is, vA = A + A, vA -> vB, A -> implies that the As in the vA definition map to the Bs in the vB definition - # But both As link to the same stock and dynamic variable so just looking at those isn't enough to figure out what it maps to. - # There will exist cases where it's impossible to tell - eg, when there exist multiple duplicate links, and some positions don't match up. + # No prior mappings means no inferred mappings + @test (infer_links(StockAndFlowF(), StockAndFlowF(), Dict{Symbol, Vector{Int64}}(:S => [], :F => [], :SV => [], :P => [], :V => [])) + == Dict(:LS => [], :LSV => [], :LV => [], :I => [], :O => [], :LPV => [], :LVV => [])) + + # S: 1 -> 1 and SV: 1 -> 1 implies LS: 1 -> 1 + @test (infer_links( + (@stock_and_flow begin; :stocks; A; :sums; NA = [A]; end), + (@stock_and_flow begin; :stocks; B; :sums; NB = [B]; end), + Dict{Symbol, Vector{Int64}}(:S => [1], :F => [], :SV => [1], :P => [], :V => [])) + == Dict(:LS => [1], :LSV => [], :LV => [], :I => [], :O => [], :LPV => [], :LVV => [])) + - # It does not currently look at the operator. You could therefore map vA = A + A -> vB = B * B - # I can see this being useful, actually, specifically when mapping between + and -, * and /, etc. Probably logs and powers too. - # Just need to be aware that it won't say it's invalid. - @test (infer_links( - (@stock_and_flow begin; :stocks; A; :dynamic_variables; vA = A + A; end), - (@stock_and_flow begin; :stocks; B; :dynamic_variables; vB = B + B; end), - Dict{Symbol, Vector{Int64}}(:S => [1], :F => [], :SV => [], :P => [], :V => [1])) - == Dict(:LS => [], :LSV => [], :LV => [2,2], :I => [], :O => [], :LPV => [], :LVV => [])) # If duplicate values, always map to end. - - @test (infer_links( - (@stock_and_flow begin; :stocks; A; :parameters; pA; :dynamic_variables; vA = A + pA; end), - (@stock_and_flow begin; :stocks; B; :parameters; pB; :dynamic_variables; vB = pB + B; end), - Dict{Symbol, Vector{Int64}}(:S => [1], :F => [], :SV => [], :P => [1], :V => [1])) - == Dict(:LS => [], :LSV => [], :LV => [1], :I => [], :O => [], :LPV => [1], :LVV => [])) - - @test (infer_links( - (@stock_and_flow begin - :stocks - S - I - R - - :parameters - p_inf - p_rec - - - :flows - S => f_StoI(p_inf * S) => I - I => f_ItoR(I * p_rec) => R - - :sums - N = [S,I,R] - NI = [I] - NS = [S,I,R] - end), - (@stock_and_flow begin - :stocks - pop - - :parameters - p_generic - - - :flows - pop => f_generic(p_generic * pop) => pop - - :sums - N = [pop] - NI = [pop] - NS = [pop] - end), - - Dict{Symbol, Vector{Int64}}(:S => [1,1,1], :F => [1,1], :SV => [1,2,3], :P => [1,1], :V => [1,1])) - == Dict(:LS => [1,3,1,2,3,1,3], :LSV => [], :LV => [1,1], :I => [1,1], :O => [1,1], :LPV => [1,1], :LVV => [])) + # It does not currently look at the operator. You could therefore map vA = A + A -> vB = B * B + # I can see this being useful, actually, specifically when mapping between + and -, * and /, etc. Probably logs and powers too. + # Just need to be aware that it won't say it's invalid. + @test (infer_links( + (@stock_and_flow begin; :stocks; A; :dynamic_variables; vA = A + A; end), + (@stock_and_flow begin; :stocks; B; :dynamic_variables; vB = B + B; end), + Dict{Symbol, Vector{Int64}}(:S => [1], :F => [], :SV => [], :P => [], :V => [1])) + == Dict(:LS => [], :LSV => [], :LV => [2,2], :I => [], :O => [], :LPV => [], :LVV => [])) # If duplicate values, always map to end. + + @test (infer_links( + (@stock_and_flow begin; :stocks; A; :parameters; pA; :dynamic_variables; vA = A + pA; end), + (@stock_and_flow begin; :stocks; B; :parameters; pB; :dynamic_variables; vB = pB + B; end), + Dict{Symbol, Vector{Int64}}(:S => [1], :F => [], :SV => [], :P => [1], :V => [1])) + == Dict(:LS => [], :LSV => [], :LV => [1], :I => [], :O => [], :LPV => [1], :LVV => [])) + + @test (infer_links( + (@stock_and_flow begin + :stocks + S + I + R + + :parameters + p_inf + p_rec + + + :flows + S => f_StoI(p_inf * S) => I + I => f_ItoR(I * p_rec) => R + + :sums + N = [S,I,R] + NI = [I] + NS = [S,I,R] + end), + (@stock_and_flow begin + :stocks + pop + + :parameters + p_generic + + + :flows + pop => f_generic(p_generic * pop) => pop + + :sums + N = [pop] + NI = [pop] + NS = [pop] + end), + + Dict{Symbol, Vector{Int64}}(:S => [1,1,1], :F => [1,1], :SV => [1,2,3], :P => [1,1], :V => [1,1])) + == Dict(:LS => [1,3,1,2,3,1,3], :LSV => [], :LV => [1,1], :I => [1,1], :O => [1,1], :LPV => [1,1], :LVV => [])) end @testset "Applying flags can correctly find substring matches" begin - @test apply_flags(:f_, Set([:~]), Vector{Symbol}()) == [] - @test apply_flags(:f_, Set([:~]), [:f_death, :f_birth]) == [:f_death, :f_birth] - @test apply_flags(:NOMATCH, Set([:~]), [:f_death, :f_birth]) == [] - @test apply_flags(:f_birth, Set([:~]), [:f_death, :f_birth]) == [:f_birth] - @test apply_flags(:f_birth, Set{Symbol}(), [:f_death, :f_birth]) == [:f_birth] - - # Note, apply_flags is specifically meant to work on vectors without duplicates; the vector which is input are the keys of a dictionary. - # Regardless, the following will hold: - @test apply_flags(:f_birth, Set{Symbol}(), [:f_death, :f_birth, :f_birth, :f_birth]) == [:f_birth] - @test apply_flags(:f_birth, Set{Symbol}([:~]), [:f_death, :f_birth, :f_birth, :f_birth]) == [:f_birth, :f_birth, :f_birth] + @test apply_flags(:f_, Set([:~]), Vector{Symbol}()) == [] + @test apply_flags(:f_, Set([:~]), [:f_death, :f_birth]) == [:f_death, :f_birth] + @test apply_flags(:NOMATCH, Set([:~]), [:f_death, :f_birth]) == [] + @test apply_flags(:f_birth, Set([:~]), [:f_death, :f_birth]) == [:f_birth] + @test apply_flags(:f_birth, Set{Symbol}(), [:f_death, :f_birth]) == [:f_birth] + + # Note, apply_flags is specifically meant to work on vectors without duplicates; the vector which is input are the keys of a dictionary. + # Regardless, the following will hold: + @test apply_flags(:f_birth, Set{Symbol}(), [:f_death, :f_birth, :f_birth, :f_birth]) == [:f_birth] + @test apply_flags(:f_birth, Set{Symbol}([:~]), [:f_death, :f_birth, :f_birth, :f_birth]) == [:f_birth, :f_birth, :f_birth] end @testset "substitute_symbols will correctly associate values of the two provided dictionaries based on user defined mappings" begin - # substitute_symbols(s::Dict{Symbol, Int}, t::Dict{Symbol, Int}, m::Vector{DSLArgument} ; use_flags::Bool=true)::Dict{Int, Int} - + # Note, these dictionaries represent a vector where all the entries are unique, and the values are the original indices. + # So, both keys and values should be unique. + # For stratification, first dictionary is strata or aggregate, second is type, and the vector of DSLArgument are the user-defined maps. + # For homomorphism, first argument is src, second is dest, vector are user-defined maps. + @test substitute_symbols(Dict{Symbol, Int}(), Dict{Symbol, Int}(), Vector{DSLArgument}()) == Dict{Int, Int}() + @test substitute_symbols(Dict{Symbol, Int}(), Dict(:B => 2), Vector{DSLArgument}()) == Dict{Int, Int}() - # Note, these dictionaries represent a vector where all the entries are unique, and the values are the original indices. - # So, both keys and values should be unique. - # For stratification, first dictionary is strata or aggregate, second is type, and the vector of DSLArgument are the user-defined maps. - # For homomorphism, first argument is src, second is dest, vector are user-defined maps. - @test substitute_symbols(Dict{Symbol, Int}(), Dict{Symbol, Int}(), Vector{DSLArgument}()) == Dict{Int, Int}() - @test substitute_symbols(Dict{Symbol, Int}(), Dict(:B => 2), Vector{DSLArgument}()) == Dict{Int, Int}() + @test substitute_symbols(Dict(:A => 1), Dict(:B => 1), [DSLArgument(:A, :B, Set{Symbol}())]) == Dict(1 => 1) + @test substitute_symbols(Dict(:A1 => 1, :A2 => 2), Dict(:B => 1), [DSLArgument(:A1, :B, Set{Symbol}()), DSLArgument(:A2, :B, Set{Symbol}())]) == Dict(1 => 1, 2 => 1) + @test substitute_symbols(Dict(:A1 => 1), Dict(:B1 => 1, :B2 => 2), [DSLArgument(:A1, :B2, Set{Symbol}())]) == Dict(1 => 2) - @test substitute_symbols(Dict(:A => 1), Dict(:B => 1), [DSLArgument(:A, :B, Set{Symbol}())]) == Dict(1 => 1) - @test substitute_symbols(Dict(:A1 => 1, :A2 => 2), Dict(:B => 1), [DSLArgument(:A1, :B, Set{Symbol}()), DSLArgument(:A2, :B, Set{Symbol}())]) == Dict(1 => 1, 2 => 1) - @test substitute_symbols(Dict(:A1 => 1), Dict(:B1 => 1, :B2 => 2), [DSLArgument(:A1, :B2, Set{Symbol}())]) == Dict(1 => 2) + @test substitute_symbols(Dict(:A1 => 1, :A2 => 2), Dict(:B1 => 1, :B2 => 2), [DSLArgument(:A, :B2, Set{Symbol}([:~]))]) == Dict(1 => 2, 2 => 2) - @test substitute_symbols(Dict(:A1 => 1, :A2 => 2), Dict(:B1 => 1, :B2 => 2), [DSLArgument(:A, :B2, Set{Symbol}([:~]))]) == Dict(1 => 2, 2 => 2) + # 1:100 + # 1:50 + # All multiples x of 14 below 100 go to x % 10 + 1 + @test (substitute_symbols(Dict(Symbol(i) => i for i ∈ 1:100), Dict(Symbol(-i) => i for i ∈ 1:50), [DSLArgument(Symbol(i), Symbol(-((i%10) + 1)), Set{Symbol}()) for i ∈ 1:100 if i % 14 == 0]) + == Dict(14 => 5, 28 => 9, 42 => 3, 56 => 7, 70 => 1, 84 => 5, 98 => 9)) - # 1:100 - # 1:50 - # All multiples x of 14 below 100 go to x % 10 + 1 - @test (substitute_symbols(Dict(Symbol(i) => i for i ∈ 1:100), Dict(Symbol(-i) => i for i ∈ 1:50), [DSLArgument(Symbol(i), Symbol(-((i%10) + 1)), Set{Symbol}()) for i ∈ 1:100 if i % 14 == 0]) - == Dict(14 => 5, 28 => 9, 42 => 3, 56 => 7, 70 => 1, 84 => 5, 98 => 9)) + # Captures everything with a 7 as a digit + @test (substitute_symbols(Dict(Symbol(i) => i for i ∈ 1:100), Dict(Symbol(-i) => i for i ∈ 1:50), [DSLArgument(Symbol(7), Symbol(-1), Set{Symbol}([:~]))]) + == Dict(7 => 1, 17 => 1, 27 => 1, 37 => 1, 47 => 1, 57 => 1, 67 => 1, 70 => 1, 71 => 1, 72 => 1, 73 => 1, 74 => 1, 75 => 1, 76 => 1, 77 => 1, 78 => 1, 79 => 1, 87 => 1, 97 => 1)) - # Captures everything with a 7 as a digit - @test (substitute_symbols(Dict(Symbol(i) => i for i ∈ 1:100), Dict(Symbol(-i) => i for i ∈ 1:50), [DSLArgument(Symbol(7), Symbol(-1), Set{Symbol}([:~]))]) - == Dict(7 => 1, 17 => 1, 27 => 1, 37 => 1, 47 => 1, 57 => 1, 67 => 1, 70 => 1, 71 => 1, 72 => 1, 73 => 1, 74 => 1, 75 => 1, 76 => 1, 77 => 1, 78 => 1, 79 => 1, 87 => 1, 97 => 1)) - - @test substitute_symbols(Dict(Symbol("~") => 1), Dict(:R => 1), [DSLArgument(Symbol("~"), :R, Set([:~]))], ; use_flags = false) == Dict(1 => 1) # Note, the Set([:~]) is ignored because use_flags is false + @test substitute_symbols(Dict(Symbol("~") => 1), Dict(:R => 1), [DSLArgument(Symbol("~"), :R, Set([:~]))], ; use_flags = false) == Dict(1 => 1) # Note, the Set([:~]) is ignored because use_flags is false end @testset "non-natural transformations fail infer_links" begin - # Map both dynamic variables to the same - # Obviously, this will fail, as the new dynamic variable needs a LVV and one LV, but instead has two LV - @test_throws KeyError (infer_links( - (@stock_and_flow begin - :stocks - A - - :dynamic_variables - v1 = A + A - v2 = v1 + A - end), + # Map both dynamic variables to the same + # Obviously, this will fail, as the new dynamic variable needs a LVV and one LV, but instead has two LV + @test_throws KeyError (infer_links( (@stock_and_flow begin - :stocks - A - - :dynamic_variables - v1 = A + A - end), - Dict{Symbol, Vector{Int64}}(:S => [1], :V => [1,1]))) - + :stocks + A + + :dynamic_variables + v1 = A + A + v2 = v1 + A + end), + (@stock_and_flow begin + :stocks + A + :dynamic_variables + v1 = A + A + end), + Dict{Symbol, Vector{Int64}}(:S => [1], :V => [1,1]))) - # Mapping it all to I + - # This one fails when trying to figure out the inflow. Stock maps to 2, and flow maps to 2, - # But inflows on the target have (1,2) and (2,3) + # Mapping it all to I - # This also wouldn't work if we tried mapping flow to 1 instead. Outflows expect 1,1 or 2,2, - # so it fails on (2,1). - @test_throws KeyError (infer_links( - (@stock_and_flow begin - :stocks - pop + # This one fails when trying to figure out the inflow. Stock maps to 2, and flow maps to 2, + # But inflows on the target have (1,2) and (2,3) - :parameters - p_generic + # This also wouldn't work if we tried mapping flow to 1 instead. Outflows expect 1,1 or 2,2, + # so it fails on (2,1). + @test_throws KeyError (infer_links( + (@stock_and_flow begin + :stocks + pop + :parameters + p_generic - :flows - pop => f_generic(p_generic * pop) => pop - :sums - N = [pop] - NI = [pop] - NS = [pop] - end), - (@stock_and_flow begin - :stocks - S - I - R + :flows + pop => f_generic(p_generic * pop) => pop - :parameters - p_inf - p_rec + :sums + N = [pop] + NI = [pop] + NS = [pop] + end), + (@stock_and_flow begin + :stocks + S + I + R + :parameters + p_inf + p_rec - :flows - S => f_StoI(p_inf * S) => I - I => f_ItoR(I * p_rec) => R - :sums - N = [S,I,R] - NI = [I] - NS = [S,I,R] - end), - Dict{Symbol, Vector{Int64}}(:S => [2], :F => [2], :SV => [1,2,3], :P => [2], :V => [2]))) + :flows + S => f_StoI(p_inf * S) => I + I => f_ItoR(I * p_rec) => R + + :sums + N = [S,I,R] + NI = [I] + NS = [S,I,R] + end), + Dict{Symbol, Vector{Int64}}(:S => [2], :F => [2], :SV => [1,2,3], :P => [2], :V => [2]))) end @testset "Applying flags throws on invalid inputs" begin - @test_throws ErrorException apply_flags(:f_, Set([:+]), [:f_death, :f_birth]) # fails because :+ is not a defined operation - @test_throws ErrorException apply_flags(:f_birth, Set([:~, :+]), [:f_death, :f_birth]) # also fails for same reason + @test_throws ErrorException apply_flags(:f_, Set([:+]), [:f_death, :f_birth]) # fails because :+ is not a defined operation + @test_throws ErrorException apply_flags(:f_birth, Set([:~, :+]), [:f_death, :f_birth]) # also fails for same reason - @test_throws AssertionError apply_flags(:NOMATCH, Set{Symbol}(), Vector{Symbol}()) # fails because it's not looking for substrings, and :NOMATCH isn't in the list of options. - @test_throws AssertionError apply_flags(:NOMATCH, Set{Symbol}(), [:nomatch]) # same reason + @test_throws AssertionError apply_flags(:NOMATCH, Set{Symbol}(), Vector{Symbol}()) # fails because it's not looking for substrings, and :NOMATCH isn't in the list of options. + @test_throws AssertionError apply_flags(:NOMATCH, Set{Symbol}(), [:nomatch]) # same reason + +end -end \ No newline at end of file diff --git a/test/SystemStructure.jl b/test/SystemStructure.jl index 79e08abc..2ac12f8d 100644 --- a/test/SystemStructure.jl +++ b/test/SystemStructure.jl @@ -5,63 +5,63 @@ using StockFlow.Syntax empty = @stock_and_flow begin end p = @stock_and_flow begin - :stocks - S - I + :stocks + S + I - :dynamic_variables - v = S + I + :dynamic_variables + v = S + I - :parameters - p1 - p2 + :parameters + p1 + p2 - :flows - S => f1(v) => CLOUD + :flows + S => f1(v) => CLOUD - :sums - N = [S] - NI = [I] + :sums + N = [S] + NI = [I] end p_prefixed = @stock_and_flow begin - :stocks - prefS - prefI + :stocks + prefS + prefI - :dynamic_variables - prefv = prefS + prefI + :dynamic_variables + prefv = prefS + prefI - :parameters - prefp1 - prefp2 + :parameters + prefp1 + prefp2 - :flows - prefS => preff1(prefv) => CLOUD + :flows + prefS => preff1(prefv) => CLOUD - :sums - prefN = [prefS] - prefNI = [prefI] + :sums + prefN = [prefS] + prefNI = [prefI] end p_suffixed = @stock_and_flow begin - :stocks - Ssuf - Isuf + :stocks + Ssuf + Isuf - :dynamic_variables - vsuf = Ssuf + Isuf + :dynamic_variables + vsuf = Ssuf + Isuf - :parameters - p1suf - p2suf + :parameters + p1suf + p2suf - :flows - Ssuf => f1suf(vsuf) => CLOUD + :flows + Ssuf => f1suf(vsuf) => CLOUD - :sums - Nsuf = [Ssuf] - NIsuf = [Isuf] + :sums + Nsuf = [Ssuf] + NIsuf = [Isuf] end empty_foot = @foot () => () @@ -72,12 +72,12 @@ footA_suf = @foot Ssuf => Nsuf @testset "changing names act the same as if the stock flow/foot/open was created with the changed name" begin - @test empty == add_suffix!(deepcopy(empty), "AAA") == add_prefix!(deepcopy(empty), "BBB") - @test add_prefix!(deepcopy(p), "pref") == p_prefixed - @test add_suffix!(deepcopy(p), "suf") == p_suffixed + @test empty == add_suffix!(deepcopy(empty), "AAA") == add_prefix!(deepcopy(empty), "BBB") + @test add_prefix!(deepcopy(p), "pref") == p_prefixed + @test add_suffix!(deepcopy(p), "suf") == p_suffixed - @test empty_foot == add_suffix!(deepcopy(empty_foot), "CCC") == add_prefix!(deepcopy(empty_foot), "DDD") - @test add_prefix!(deepcopy(footA), "pref") == footA_pref - @test add_suffix!(deepcopy(footA), "suf") == footA_suf + @test empty_foot == add_suffix!(deepcopy(empty_foot), "CCC") == add_prefix!(deepcopy(empty_foot), "DDD") + @test add_prefix!(deepcopy(footA), "pref") == footA_pref + @test add_suffix!(deepcopy(footA), "suf") == footA_suf end diff --git a/test/runtests.jl b/test/runtests.jl index db5631f2..949da5cc 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,9 +2,9 @@ using Test using StockFlow @testset "StockFlow DSL" begin - include("Syntax.jl") + include("Syntax.jl") end @testset "Attribute names prefixes and suffixes" begin - include("SystemStructure.jl") + include("SystemStructure.jl") end \ No newline at end of file diff --git a/test/syntax/Composition.jl b/test/syntax/Composition.jl index 34293096..05264c8c 100644 --- a/test/syntax/Composition.jl +++ b/test/syntax/Composition.jl @@ -5,105 +5,105 @@ using StockFlow.Syntax.Composition import StockFlow.Syntax.Composition: interpret_composition_notation @testset "Composition creates expected stock flows" begin - empty_sf = StockAndFlowF() - - - @test (@compose (begin # composing no stock flows returns an empty stock flow. - () - end)) == empty_sf - - @test (@compose empty_sf begin - (sf,) - end) == empty_sf - - @test (@compose (@stock_and_flow begin; :stocks; A; end;) (@stock_and_flow begin; :stocks; B; end;) (begin - (sf1, sf2) - end)) == (@stock_and_flow begin; :stocks; A; B; end;) # Combining without any composing - - @test (@compose (@stock_and_flow begin; :stocks; A; end;) (@stock_and_flow begin; :stocks; A; end;) (begin - (sf1, sf2) - end)) == (@stock_and_flow begin; :stocks; A; A; end;) - - @test (@compose (@stock_and_flow begin; :stocks; A; end;) (@stock_and_flow begin; :stocks; A; end;) (begin - (sf1, sf2) - sf1, sf2 ^ A => () - end)) == (@stock_and_flow begin; :stocks; A; end;) - - @test ((@compose (@stock_and_flow begin - :stocks - A - B - - :dynamic_variables - v1 = A + B - - :sums - N = [A,B] - end) (@stock_and_flow begin - :stocks - B - C - - :dynamic_variables - v2 = B + C - - :sums - N = [B,C] - end) (begin - (sfA, sfC) - sfA, sfC ^ B => N - end)) - == - (@stock_and_flow begin - :stocks - A - B - C - - :dynamic_variables - v1 = A + B - v2 = B + C - - :sums - N = [A, B, C] - end)) - + empty_sf = StockAndFlowF() + + + @test (@compose (begin # composing no stock flows returns an empty stock flow. + () + end)) == empty_sf + + @test (@compose empty_sf begin + (sf,) + end) == empty_sf + + @test (@compose (@stock_and_flow begin; :stocks; A; end;) (@stock_and_flow begin; :stocks; B; end;) (begin + (sf1, sf2) + end)) == (@stock_and_flow begin; :stocks; A; B; end;) # Combining without any composing + + @test (@compose (@stock_and_flow begin; :stocks; A; end;) (@stock_and_flow begin; :stocks; A; end;) (begin + (sf1, sf2) + end)) == (@stock_and_flow begin; :stocks; A; A; end;) + + @test (@compose (@stock_and_flow begin; :stocks; A; end;) (@stock_and_flow begin; :stocks; A; end;) (begin + (sf1, sf2) + sf1, sf2 ^ A => () + end)) == (@stock_and_flow begin; :stocks; A; end;) + + @test ((@compose (@stock_and_flow begin + :stocks + A + B + + :dynamic_variables + v1 = A + B + + :sums + N = [A,B] + end) (@stock_and_flow begin + :stocks + B + C + + :dynamic_variables + v2 = B + C + + :sums + N = [B,C] + end) (begin + (sfA, sfC) + sfA, sfC ^ B => N + end)) + == + (@stock_and_flow begin + :stocks + A + B + C + + :dynamic_variables + v1 = A + B + v2 = B + C + + :sums + N = [A, B, C] + end)) + end @testset "interpret_composition_notation interprets arguments correctly" begin - # @test interpret_composition_notation(:(() ^ A => N)) == (Vector{Symbol}(), (@foot A => N)) - @test interpret_composition_notation(:(sf ^ A => N)) == ([:sf], (@foot A => N)) - @test interpret_composition_notation(:(sf1, sf2 ^ A => N)) == ([:sf1,:sf2], (@foot A => N)) - @test interpret_composition_notation(:(sf1, sf2 ^ A => N, A => NI)) == ([:sf1,:sf2], (@foot A => N, A => NI)) - @test interpret_composition_notation(:(sf1, sf2, sf3, sf4 ^ () => NI)) == ([:sf1, :sf2, :sf3, :sf4], (@foot () => NI)) - @test interpret_composition_notation(:(sf1, sf2 ^ L => ())) == ([:sf1,:sf2], (@foot L => ())) + # @test interpret_composition_notation(:(() ^ A => N)) == (Vector{Symbol}(), (@foot A => N)) + @test interpret_composition_notation(:(sf ^ A => N)) == ([:sf], (@foot A => N)) + @test interpret_composition_notation(:(sf1, sf2 ^ A => N)) == ([:sf1,:sf2], (@foot A => N)) + @test interpret_composition_notation(:(sf1, sf2 ^ A => N, A => NI)) == ([:sf1,:sf2], (@foot A => N, A => NI)) + @test interpret_composition_notation(:(sf1, sf2, sf3, sf4 ^ () => NI)) == ([:sf1, :sf2, :sf3, :sf4], (@foot () => NI)) + @test interpret_composition_notation(:(sf1, sf2 ^ L => ())) == ([:sf1,:sf2], (@foot L => ())) - @test interpret_composition_notation(:(sf1, sf2 ^ () => ())) == ([:sf1,:sf2], (@foot () => ())) + @test interpret_composition_notation(:(sf1, sf2 ^ () => ())) == ([:sf1,:sf2], (@foot () => ())) end @testset "invalid composition expressions fail" begin - @test_throws AssertionError interpret_composition_notation(:(B => C)) - @test_throws AssertionError interpret_composition_notation(:(A, B, C)) - @test_throws AssertionError interpret_composition_notation(:(A ^ B ^ C)) - @test_throws AssertionError interpret_composition_notation(:(A => B => C)) - @test_throws ErrorException interpret_composition_notation(:(A ^ B => C => D)) # caught by create_foot + @test_throws AssertionError interpret_composition_notation(:(B => C)) + @test_throws AssertionError interpret_composition_notation(:(A, B, C)) + @test_throws AssertionError interpret_composition_notation(:(A ^ B ^ C)) + @test_throws AssertionError interpret_composition_notation(:(A => B => C)) + @test_throws ErrorException interpret_composition_notation(:(A ^ B => C => D)) # caught by create_foot end @testset "invalid sfcompose calls fail" begin - @test_throws AssertionError sfcompose([(@stock_and_flow begin; :stocks; A; end;), (@stock_and_flow begin; :stocks; A; end;)], quote - (sf1, sf2) - sf1, sf2 ^ () => () - end) # not allowed to map to empty - @test_throws AssertionError sfcompose([(@stock_and_flow begin; :stocks; A; end;), (@stock_and_flow begin; :stocks; A; end;)], quote - (sf1, sf2) - sf1 ^ A => () - sf2 ^ A => () - end) # not allowed to map to the same foot twice - @test_throws AssertionError sfcompose([(@stock_and_flow begin; :stocks; A; end;), (@stock_and_flow begin; :stocks; A; end;)], quote - (sf1,) - sf1 ^ A => () - end) # incorrect number of symbols on the first line in the quote + @test_throws AssertionError sfcompose([(@stock_and_flow begin; :stocks; A; end;), (@stock_and_flow begin; :stocks; A; end;)], quote + (sf1, sf2) + sf1, sf2 ^ () => () + end) # not allowed to map to empty + @test_throws AssertionError sfcompose([(@stock_and_flow begin; :stocks; A; end;), (@stock_and_flow begin; :stocks; A; end;)], quote + (sf1, sf2) + sf1 ^ A => () + sf2 ^ A => () + end) # not allowed to map to the same foot twice + @test_throws AssertionError sfcompose([(@stock_and_flow begin; :stocks; A; end;), (@stock_and_flow begin; :stocks; A; end;)], quote + (sf1,) + sf1 ^ A => () + end) # incorrect number of symbols on the first line in the quote end diff --git a/test/syntax/Homomorphism.jl b/test/syntax/Homomorphism.jl new file mode 100644 index 00000000..316e139a --- /dev/null +++ b/test/syntax/Homomorphism.jl @@ -0,0 +1,250 @@ +using StockFlow +using StockFlow.Syntax +using StockFlow.Syntax: NothingFunction +using StockFlow.Syntax.Homomorphism + + +using StockFlow.PremadeModels + + +using Catlab.CategoricalAlgebra + +@testset "hom macro creates correct homomorphisms" begin + empty = @stock_and_flow begin end + empty_hom = ACSetTransformation(empty, map(empty, Op = NothingFunction, Position = NothingFunction, Name = NothingFunction); :Op => NothingFunction, :Position => NothingFunction, :Name => NothingFunction ) + @test (@hom empty empty begin end) == empty_hom + + sfA = @stock_and_flow begin; :stocks; A; end; + sfB = @stock_and_flow begin; :stocks; B; end; + @test (@hom sfA sfB begin; :stocks; A => B; end;) == ACSetTransformation(sfA, map(sfB, Op = NothingFunction, Position = NothingFunction, Name = NothingFunction) ; :S => [1], :Op => NothingFunction, :Position => NothingFunction, :Name => NothingFunction) + + seir = PremadeModels.seir() + + @test (@hom seir seir begin + :stocks + S => S + E => E + I => I + R => R + + :parameters + μ => μ + β => β + tlatent => tlatent + trecovery => trecovery + δ => δ + c => c + + :dynamic_variables + v_prevalence => v_prevalence + v_meanInfectiousContactsPerS => v_meanInfectiousContactsPerS + v_perSIncidenceRate => v_perSIncidenceRate + v_newIncidence => v_newIncidence + v_birth => v_birth + v_inf => v_inf + v_rec => v_rec + v_deathS => v_deathS + v_deathE => v_deathE + v_deathI => v_deathI + v_deathR => v_deathR + + :flows + f_birth => f_birth + f_incid => f_incid + f_deathS => f_deathS + f_inf => f_inf + f_deathE => f_deathE + f_rec => f_rec + f_deathI => f_deathI + f_deathR => f_deathR + + :sums + N => N + NI => NI + NS => NS + end) == ACSetTransformation(seir,map(seir, Op = NothingFunction, Position = NothingFunction, Name = NothingFunction) ; :S => (1:4), :F => (1:8), :V => (1:11), + :SV => (1:3), :P => (1:6), :LS => (1:9), :I => (1:4), :O => (1:7), :LV => (1:7), + :LSV => (1:3), :LVV => (1:3), :LPV => (1:9), + :Op => NothingFunction, :Position => NothingFunction, :Name => NothingFunction) + + + + l_type = @stock_and_flow begin + :stocks + pop + + :parameters + μ + δ + rFstOrder + rage + + :dynamic_variables + v_aging = pop * rage + v_fstOrder = pop * rFstOrder + v_birth = N * μ + v_death = pop * δ + + :flows + pop => f_aging(v_aging) => pop + pop => f_fstOrder(v_fstOrder) => pop + CLOUD => f_birth(v_birth) => pop + pop => f_death(v_death) => CLOUD + + :sums + N = [pop] + + end; + + # l_type_noatts = map(l_type, Name=NothingFunction, Op=NothingFunction, Position=NothingFunction); + + + begin + s, = parts(l_type, :S) + N, = parts(l_type, :SV) + lsn, = parts(l_type, :LS) + f_aging, f_fstorder, f_birth, f_death = parts(l_type, :F) + i_aging, i_fstorder, i_birth = parts(l_type, :I) + o_aging, o_fstorder, o_death = parts(l_type, :O) + v_aging, v_fstorder, v_birth, v_death = parts(l_type, :V) + lv_aging1, lv_fstorder1, lv_death1 = parts(l_type, :LV) + lsv_birth1, = parts(l_type, :LSV) + p_μ, p_δ, p_rfstOrder, p_rage = parts(l_type, :P) + lpv_aging2, lpv_fstorder2, lpv_birth2, lpv_death2 = parts(l_type, :LPV) + end; + + + WeightModel = @stock_and_flow begin + :stocks + NormalWeight + OverWeight + Obese + + :parameters + μ + δw + rw + ro + δo + rage + + :dynamic_variables + v_NewBorn = N * μ + v_DeathNormalWeight = NormalWeight * δw + v_BecomingOverWeight = NormalWeight * rw + v_DeathOverWeight = OverWeight * δw + v_BecomingObese = OverWeight * ro + v_DeathObese = Obese * δo + v_idNW = NormalWeight * rage + v_idOW = OverWeight * rage + v_idOb = Obese * rage + + :flows + CLOUD => f_NewBorn(v_NewBorn) => NormalWeight + NormalWeight => f_DeathNormalWeight(v_DeathNormalWeight) => CLOUD + NormalWeight => f_BecomingOverWeight(v_BecomingOverWeight) => OverWeight + OverWeight => f_DeathOverWeight(v_DeathOverWeight) => CLOUD + + OverWeight => f_BecomingObese(v_BecomingObese) => Obese + Obese => f_DeathObese(v_DeathObese) => CLOUD + NormalWeight => f_idNW(v_idNW) => NormalWeight + OverWeight => f_idOW(v_idOW) => OverWeight + Obese => f_idOb(v_idOb) => Obese + + :sums + N = [NormalWeight, OverWeight, Obese] + + end; + + l_type_noatts = map(l_type, Name=NothingFunction, Op=NothingFunction, Position=NothingFunction); + + + typed_WeightModel=ACSetTransformation(WeightModel, l_type_noatts, + S = [s,s,s], + SV = [N], + LS = [lsn,lsn,lsn], + F = [f_birth, f_death, f_fstorder, f_death, f_fstorder, f_death, f_aging, f_aging, f_aging], + I = [i_birth, i_aging, i_fstorder, i_aging, i_fstorder, i_aging], + O = [o_death, o_fstorder, o_aging, o_death, o_fstorder, o_aging, o_death, o_aging], + V = [v_birth, v_death, v_fstorder, v_death, v_fstorder, v_death, v_aging, v_aging, v_aging], + LV = [lv_death1, lv_fstorder1, lv_death1, lv_fstorder1, lv_death1, lv_aging1, lv_aging1, lv_aging1], + LSV = [lsv_birth1], + P = [p_μ, p_δ, p_rfstOrder, p_rfstOrder, p_δ, p_rage], + LPV = [lpv_birth2, lpv_death2, lpv_fstorder2, lpv_death2, lpv_fstorder2, lpv_death2, lpv_aging2, lpv_aging2, lpv_aging2], + Name=NothingFunction, Op=NothingFunction, Position=NothingFunction +); + +@test (@hom WeightModel l_type begin + :stocks + NormalWeight => pop + OverWeight => pop + Obese => pop + + :parameters + μ => μ + δw => δ + rw => rFstOrder + ro => rFstOrder + δo => δ + rage => rage + + :dynamic_variables + v_NewBorn => v_birth + v_DeathNormalWeight => v_death + v_BecomingOverWeight => v_fstOrder + v_DeathOverWeight => v_death + v_BecomingObese => v_fstOrder + v_DeathObese => v_death + v_idNW => v_aging + v_idOW => v_aging + v_idOb => v_aging + + :flows + f_NewBorn => f_birth + f_DeathNormalWeight => f_death + f_BecomingOverWeight => f_fstOrder + f_DeathOverWeight => f_death + f_BecomingObese => f_fstOrder + f_DeathObese => f_death + f_idNW => f_aging + f_idOW => f_aging + f_idOb => f_aging + + :sums + N => N + +end) == typed_WeightModel + +@test (@hom WeightModel l_type begin + :stocks + NormalWeight => pop + OverWeight => pop + Obese => pop + + :parameters + μ => μ + ~δ => δ + rage => rage + _ => rFstOrder + + :dynamic_variables + v_NewBorn => v_birth + ~Becoming => v_fstOrder + ~Death => v_death + _ => v_aging + + :flows + f_NewBorn => f_birth + ~Becoming => f_fstOrder + ~Death => f_death + ~id => f_aging + _ => f_death + + + +end) == typed_WeightModel + + + + +end \ No newline at end of file diff --git a/test/syntax/Stratification.jl b/test/syntax/Stratification.jl index bfdea7d2..52070dff 100755 --- a/test/syntax/Stratification.jl +++ b/test/syntax/Stratification.jl @@ -13,442 +13,442 @@ using Catlab.CategoricalAlgebra @testset "Pullback computed in standard way is equal to DSL pullbacks" begin - l_type = @stock_and_flow begin - :stocks - pop - - :parameters - μ - δ - rFstOrder - rage - - :dynamic_variables - v_aging = pop * rage - v_fstOrder = pop * rFstOrder - v_birth = N * μ - v_death = pop * δ - - :flows - pop => f_aging(v_aging) => pop - pop => f_fstOrder(v_fstOrder) => pop - CLOUD => f_birth(v_birth) => pop - pop => f_death(v_death) => CLOUD - - :sums - N = [pop] - - end; - l_type_noatts = map(l_type, Name=NothingFunction, Op=NothingFunction, Position=NothingFunction); - - - WeightModel = @stock_and_flow begin - :stocks - NormalWeight - OverWeight - Obese - - :parameters - μ - δw - rw - ro - δo - rage - - :dynamic_variables - v_NewBorn = N * μ - v_DeathNormalWeight = NormalWeight * δw - v_BecomingOverWeight = NormalWeight * rw - v_DeathOverWeight = OverWeight * δw - v_BecomingObese = OverWeight * ro - v_DeathObese = Obese * δo - v_idNW = NormalWeight * rage - v_idOW = OverWeight * rage - v_idOb = Obese * rage - - :flows - CLOUD => f_NewBorn(v_NewBorn) => NormalWeight - NormalWeight => f_DeathNormalWeight(v_DeathNormalWeight) => CLOUD - NormalWeight => f_BecomingOverWeight(v_BecomingOverWeight) => OverWeight - OverWeight => f_DeathOverWeight(v_DeathOverWeight) => CLOUD - - OverWeight => f_BecomingObese(v_BecomingObese) => Obese - Obese => f_DeathObese(v_DeathObese) => CLOUD - NormalWeight => f_idNW(v_idNW) => NormalWeight - OverWeight => f_idOW(v_idOW) => OverWeight - Obese => f_idOb(v_idOb) => Obese - - :sums - N = [NormalWeight, OverWeight, Obese] - - end; - - - ageWeightModel = @stock_and_flow begin - :stocks - Child - Adult - Senior - - :parameters - μ - δC - δA - δS - rageCA - rageAS - r - - :dynamic_variables - v_NB = N * μ - v_DeathC = Child * δC - v_idC = Child * r - v_agingCA = Child * rageCA - v_DeathA = Adult * δA - v_idA = Adult * r - v_agingAS = Adult * rageAS - v_DeathS = Senior * δS - v_idS = Senior * r - - :flows - CLOUD => f_NB(v_NB) => Child - Child => f_idC(v_idC) => Child - Child => f_DeathC(v_DeathC) => CLOUD - Child => f_agingCA(v_agingCA) => Adult - Adult => f_idA(v_idA) => Adult - Adult => f_DeathA(v_DeathA) => CLOUD - Adult => f_agingAS(v_agingAS) => Senior - Senior => f_idS(v_idS) => Senior - Senior => f_DeathS(v_DeathS) => CLOUD - - :sums - N = [Child, Adult, Senior] - - end; - - begin - s, = parts(l_type, :S) - N, = parts(l_type, :SV) - lsn, = parts(l_type, :LS) - f_aging, f_fstorder, f_birth, f_death = parts(l_type, :F) - i_aging, i_fstorder, i_birth = parts(l_type, :I) - o_aging, o_fstorder, o_death = parts(l_type, :O) - v_aging, v_fstorder, v_birth, v_death = parts(l_type, :V) - lv_aging1, lv_fstorder1, lv_death1 = parts(l_type, :LV) - lsv_birth1, = parts(l_type, :LSV) - p_μ, p_δ, p_rfstOrder, p_rage = parts(l_type, :P) - lpv_aging2, lpv_fstorder2, lpv_birth2, lpv_death2 = parts(l_type, :LPV) - end; - - typed_WeightModel=ACSetTransformation(WeightModel, l_type_noatts, - S = [s,s,s], - SV = [N], - LS = [lsn,lsn,lsn], - F = [f_birth, f_death, f_fstorder, f_death, f_fstorder, f_death, f_aging, f_aging, f_aging], - I = [i_birth, i_aging, i_fstorder, i_aging, i_fstorder, i_aging], - O = [o_death, o_fstorder, o_aging, o_death, o_fstorder, o_aging, o_death, o_aging], - V = [v_birth, v_death, v_fstorder, v_death, v_fstorder, v_death, v_aging, v_aging, v_aging], - LV = [lv_death1, lv_fstorder1, lv_death1, lv_fstorder1, lv_death1, lv_aging1, lv_aging1, lv_aging1], - LSV = [lsv_birth1], - P = [p_μ, p_δ, p_rfstOrder, p_rfstOrder, p_δ, p_rage], - LPV = [lpv_birth2, lpv_death2, lpv_fstorder2, lpv_death2, lpv_fstorder2, lpv_death2, lpv_aging2, lpv_aging2, lpv_aging2], - Name=NothingFunction, Op=NothingFunction, Position=NothingFunction - ); - @assert is_natural(typed_WeightModel); - - - - typed_ageWeightModel=ACSetTransformation(ageWeightModel, l_type_noatts, - S = [s,s,s], - SV = [N], - LS = [lsn,lsn,lsn], - F = [f_birth, f_fstorder, f_death, f_aging, f_fstorder, f_death, f_aging, f_fstorder, f_death], - I = [i_birth, i_fstorder, i_aging, i_fstorder, i_aging, i_fstorder], - O = [o_fstorder, o_death, o_aging, o_fstorder, o_death, o_aging, o_fstorder, o_death], - V = [v_birth, v_death, v_fstorder, v_aging, v_death, v_fstorder, v_aging, v_death, v_fstorder], - LV = [lv_death1, lv_fstorder1, lv_aging1, lv_death1, lv_fstorder1, lv_aging1, lv_death1, lv_fstorder1], - LSV = [lsv_birth1], - P = [p_μ, p_δ, p_δ, p_δ, p_rage, p_rage, p_rfstOrder], - LPV = [lpv_birth2, lpv_death2, lpv_fstorder2, lpv_aging2, lpv_death2, lpv_fstorder2, lpv_aging2, lpv_death2, lpv_fstorder2], - Name =NothingFunction, Op=NothingFunction, Position=NothingFunction - ); - @assert is_natural(typed_ageWeightModel); - - aged_weight = pullback(typed_WeightModel, typed_ageWeightModel) |> apex |> rebuildStratifiedModelByFlattenSymbols; - - # ######################################### - - age_weight_2 = @stratify WeightModel l_type ageWeightModel begin - :stocks - NormalWeight, OverWeight, Obese => pop <= Child, Adult, Senior - - :flows - f_NewBorn => f_birth <= f_NB - f_DeathNormalWeight, f_DeathOverWeight, f_DeathObese => f_death <= f_DeathC, f_DeathA, f_DeathS - f_idNW, f_idOW, f_idOb => f_aging <= f_agingCA, f_agingAS - f_BecomingOverWeight, f_BecomingObese => f_fstOrder <= f_idC, f_idA, f_idS - - :dynamic_variables - v_NewBorn => v_birth <= v_NB - v_DeathNormalWeight, v_DeathOverWeight, v_DeathObese => v_death <= v_DeathC, v_DeathA, v_DeathS - v_idNW, v_idOW, v_idOb => v_aging <= v_agingCA, v_agingAS - v_BecomingOverWeight, v_BecomingObese => v_fstOrder <= v_idC, v_idA, v_idS - - :parameters - μ => μ <= μ - δw, δo => δ <= δC, δA, δS - rw, ro => rFstOrder <= r - rage => rage <= rageCA, rageAS - - :sums - N => N <= N - - end - ######################################### - - age_weight_3 = @stratify WeightModel l_type ageWeightModel begin - - :flows - f_NewBorn => f_birth <= f_NB - ~Death => f_death <= ~Death - ~id => f_aging <= ~aging - ~Becoming => f_fstOrder <= ~id - - :dynamic_variables - v_NewBorn => v_birth <= v_NB - ~Death => v_death <= ~Death - ~id => v_aging <= ~aging - ~Becoming => v_fstOrder <= ~id - - :parameters - μ => μ <= μ - ~δ => δ <= ~δ - rage => rage <= rageCA, rageAS - _ => rFstOrder <= _ - - end - - age_weight_4 = @stratify WeightModel l_type ageWeightModel begin - - :flows - ~NO_MATCHES => f_birth <= ~NO_MATCHES - f_NewBorn => f_birth <= f_NB - ~Death => f_death <= ~Death - ~id => f_aging <= ~aging - ~Becoming => f_fstOrder <= ~id - ~Becoming => f_aging <= ~id # Everything already matched; ignored - _ => f_aging <= _ # also ignored - - :dynamic_variables - v_NewBorn => v_birth <= v_NB - ~Death => v_death <= ~Death - ~id => v_aging <= ~aging - _ => v_fstOrder <= _ - - :parameters - μ => μ <= μ - ~δ => δ <= ~δ - rage => rage <= rageCA, rageAS - _ => rFstOrder <= _ - - end - - age_weight_5 = @n_stratify WeightModel ageWeightModel l_type begin - :stocks - [_, _] => pop - - :flows - [~Death, ~Death] => f_death - [~id, ~aging] => f_aging - [~Becoming, ~id] => f_fstOrder - [_, f_NB] => f_birth - - - :dynamic_variables - [v_NewBorn, v_NB] => v_birth - [~Death, ~Death] => v_death - [~id, (v_agingCA, v_agingAS)] => v_aging - [(v_BecomingOverWeight, v_BecomingObese), (v_idC, v_idA, v_idS)] => v_fstOrder - - :parameters - [μ, μ] => μ - [(δw, δo), (δC, δA, δS)] => δ - [(rw, ro), r] => rFstOrder - [rage, (rageCA, rageAS)] => rage - - :sums - [N,N] => N - end - - age_weight_6 = @n_stratify WeightModel ageWeightModel l_type begin - - :flows - [~Death, ~Death] => f_death - [~id, ~aging] => f_aging - [~Becoming, ~id] => f_fstOrder - [_, f_NB] => f_birth - - - :dynamic_variables - [v_NewBorn, v_NB] => v_birth - [~Death, ~Death] => v_death - [~id, (v_agingCA, v_agingAS)] => v_aging - [(v_BecomingOverWeight, v_BecomingObese), (v_idC, v_idA, v_idS)] => v_fstOrder - - :parameters - [μ, μ] => μ - [(δw, δo), (δC, δA, δS)] => δ - [(rw, ro), r] => rFstOrder - [rage, (rageCA, rageAS)] => rage - - end - - - - @test aged_weight == age_weight_2 - @test aged_weight == age_weight_3 - @test aged_weight == age_weight_4 - @test aged_weight == age_weight_5 - @test aged_weight == age_weight_6 + l_type = @stock_and_flow begin + :stocks + pop + + :parameters + μ + δ + rFstOrder + rage + + :dynamic_variables + v_aging = pop * rage + v_fstOrder = pop * rFstOrder + v_birth = N * μ + v_death = pop * δ + + :flows + pop => f_aging(v_aging) => pop + pop => f_fstOrder(v_fstOrder) => pop + CLOUD => f_birth(v_birth) => pop + pop => f_death(v_death) => CLOUD + + :sums + N = [pop] + + end; + l_type_noatts = map(l_type, Name=NothingFunction, Op=NothingFunction, Position=NothingFunction); + + + WeightModel = @stock_and_flow begin + :stocks + NormalWeight + OverWeight + Obese + + :parameters + μ + δw + rw + ro + δo + rage + + :dynamic_variables + v_NewBorn = N * μ + v_DeathNormalWeight = NormalWeight * δw + v_BecomingOverWeight = NormalWeight * rw + v_DeathOverWeight = OverWeight * δw + v_BecomingObese = OverWeight * ro + v_DeathObese = Obese * δo + v_idNW = NormalWeight * rage + v_idOW = OverWeight * rage + v_idOb = Obese * rage + + :flows + CLOUD => f_NewBorn(v_NewBorn) => NormalWeight + NormalWeight => f_DeathNormalWeight(v_DeathNormalWeight) => CLOUD + NormalWeight => f_BecomingOverWeight(v_BecomingOverWeight) => OverWeight + OverWeight => f_DeathOverWeight(v_DeathOverWeight) => CLOUD + + OverWeight => f_BecomingObese(v_BecomingObese) => Obese + Obese => f_DeathObese(v_DeathObese) => CLOUD + NormalWeight => f_idNW(v_idNW) => NormalWeight + OverWeight => f_idOW(v_idOW) => OverWeight + Obese => f_idOb(v_idOb) => Obese + + :sums + N = [NormalWeight, OverWeight, Obese] + + end; + + + ageWeightModel = @stock_and_flow begin + :stocks + Child + Adult + Senior + + :parameters + μ + δC + δA + δS + rageCA + rageAS + r + + :dynamic_variables + v_NB = N * μ + v_DeathC = Child * δC + v_idC = Child * r + v_agingCA = Child * rageCA + v_DeathA = Adult * δA + v_idA = Adult * r + v_agingAS = Adult * rageAS + v_DeathS = Senior * δS + v_idS = Senior * r + + :flows + CLOUD => f_NB(v_NB) => Child + Child => f_idC(v_idC) => Child + Child => f_DeathC(v_DeathC) => CLOUD + Child => f_agingCA(v_agingCA) => Adult + Adult => f_idA(v_idA) => Adult + Adult => f_DeathA(v_DeathA) => CLOUD + Adult => f_agingAS(v_agingAS) => Senior + Senior => f_idS(v_idS) => Senior + Senior => f_DeathS(v_DeathS) => CLOUD + + :sums + N = [Child, Adult, Senior] + + end; + + begin + s, = parts(l_type, :S) + N, = parts(l_type, :SV) + lsn, = parts(l_type, :LS) + f_aging, f_fstorder, f_birth, f_death = parts(l_type, :F) + i_aging, i_fstorder, i_birth = parts(l_type, :I) + o_aging, o_fstorder, o_death = parts(l_type, :O) + v_aging, v_fstorder, v_birth, v_death = parts(l_type, :V) + lv_aging1, lv_fstorder1, lv_death1 = parts(l_type, :LV) + lsv_birth1, = parts(l_type, :LSV) + p_μ, p_δ, p_rfstOrder, p_rage = parts(l_type, :P) + lpv_aging2, lpv_fstorder2, lpv_birth2, lpv_death2 = parts(l_type, :LPV) + end; + + typed_WeightModel=ACSetTransformation(WeightModel, l_type_noatts, + S = [s,s,s], + SV = [N], + LS = [lsn,lsn,lsn], + F = [f_birth, f_death, f_fstorder, f_death, f_fstorder, f_death, f_aging, f_aging, f_aging], + I = [i_birth, i_aging, i_fstorder, i_aging, i_fstorder, i_aging], + O = [o_death, o_fstorder, o_aging, o_death, o_fstorder, o_aging, o_death, o_aging], + V = [v_birth, v_death, v_fstorder, v_death, v_fstorder, v_death, v_aging, v_aging, v_aging], + LV = [lv_death1, lv_fstorder1, lv_death1, lv_fstorder1, lv_death1, lv_aging1, lv_aging1, lv_aging1], + LSV = [lsv_birth1], + P = [p_μ, p_δ, p_rfstOrder, p_rfstOrder, p_δ, p_rage], + LPV = [lpv_birth2, lpv_death2, lpv_fstorder2, lpv_death2, lpv_fstorder2, lpv_death2, lpv_aging2, lpv_aging2, lpv_aging2], + Name=NothingFunction, Op=NothingFunction, Position=NothingFunction + ); + @assert is_natural(typed_WeightModel); + + + + typed_ageWeightModel=ACSetTransformation(ageWeightModel, l_type_noatts, + S = [s,s,s], + SV = [N], + LS = [lsn,lsn,lsn], + F = [f_birth, f_fstorder, f_death, f_aging, f_fstorder, f_death, f_aging, f_fstorder, f_death], + I = [i_birth, i_fstorder, i_aging, i_fstorder, i_aging, i_fstorder], + O = [o_fstorder, o_death, o_aging, o_fstorder, o_death, o_aging, o_fstorder, o_death], + V = [v_birth, v_death, v_fstorder, v_aging, v_death, v_fstorder, v_aging, v_death, v_fstorder], + LV = [lv_death1, lv_fstorder1, lv_aging1, lv_death1, lv_fstorder1, lv_aging1, lv_death1, lv_fstorder1], + LSV = [lsv_birth1], + P = [p_μ, p_δ, p_δ, p_δ, p_rage, p_rage, p_rfstOrder], + LPV = [lpv_birth2, lpv_death2, lpv_fstorder2, lpv_aging2, lpv_death2, lpv_fstorder2, lpv_aging2, lpv_death2, lpv_fstorder2], + Name =NothingFunction, Op=NothingFunction, Position=NothingFunction + ); + @assert is_natural(typed_ageWeightModel); + + aged_weight = pullback(typed_WeightModel, typed_ageWeightModel) |> apex |> rebuildStratifiedModelByFlattenSymbols; + + # ######################################### + + age_weight_2 = @stratify WeightModel l_type ageWeightModel begin + :stocks + NormalWeight, OverWeight, Obese => pop <= Child, Adult, Senior + + :flows + f_NewBorn => f_birth <= f_NB + f_DeathNormalWeight, f_DeathOverWeight, f_DeathObese => f_death <= f_DeathC, f_DeathA, f_DeathS + f_idNW, f_idOW, f_idOb => f_aging <= f_agingCA, f_agingAS + f_BecomingOverWeight, f_BecomingObese => f_fstOrder <= f_idC, f_idA, f_idS + + :dynamic_variables + v_NewBorn => v_birth <= v_NB + v_DeathNormalWeight, v_DeathOverWeight, v_DeathObese => v_death <= v_DeathC, v_DeathA, v_DeathS + v_idNW, v_idOW, v_idOb => v_aging <= v_agingCA, v_agingAS + v_BecomingOverWeight, v_BecomingObese => v_fstOrder <= v_idC, v_idA, v_idS + + :parameters + μ => μ <= μ + δw, δo => δ <= δC, δA, δS + rw, ro => rFstOrder <= r + rage => rage <= rageCA, rageAS + + :sums + N => N <= N + + end + ######################################### + + age_weight_3 = @stratify WeightModel l_type ageWeightModel begin + + :flows + f_NewBorn => f_birth <= f_NB + ~Death => f_death <= ~Death + ~id => f_aging <= ~aging + ~Becoming => f_fstOrder <= ~id + + :dynamic_variables + v_NewBorn => v_birth <= v_NB + ~Death => v_death <= ~Death + ~id => v_aging <= ~aging + ~Becoming => v_fstOrder <= ~id + + :parameters + μ => μ <= μ + ~δ => δ <= ~δ + rage => rage <= rageCA, rageAS + _ => rFstOrder <= _ + + end + + age_weight_4 = @stratify WeightModel l_type ageWeightModel begin + + :flows + ~NO_MATCHES => f_birth <= ~NO_MATCHES + f_NewBorn => f_birth <= f_NB + ~Death => f_death <= ~Death + ~id => f_aging <= ~aging + ~Becoming => f_fstOrder <= ~id + ~Becoming => f_aging <= ~id # Everything already matched; ignored + _ => f_aging <= _ # also ignored + + :dynamic_variables + v_NewBorn => v_birth <= v_NB + ~Death => v_death <= ~Death + ~id => v_aging <= ~aging + _ => v_fstOrder <= _ + + :parameters + μ => μ <= μ + ~δ => δ <= ~δ + rage => rage <= rageCA, rageAS + _ => rFstOrder <= _ + + end + + age_weight_5 = @n_stratify WeightModel ageWeightModel l_type begin + :stocks + [_, _] => pop + + :flows + [~Death, ~Death] => f_death + [~id, ~aging] => f_aging + [~Becoming, ~id] => f_fstOrder + [_, f_NB] => f_birth + + + :dynamic_variables + [v_NewBorn, v_NB] => v_birth + [~Death, ~Death] => v_death + [~id, (v_agingCA, v_agingAS)] => v_aging + [(v_BecomingOverWeight, v_BecomingObese), (v_idC, v_idA, v_idS)] => v_fstOrder + + :parameters + [μ, μ] => μ + [(δw, δo), (δC, δA, δS)] => δ + [(rw, ro), r] => rFstOrder + [rage, (rageCA, rageAS)] => rage + + :sums + [N,N] => N + end + + age_weight_6 = @n_stratify WeightModel ageWeightModel l_type begin + + :flows + [~Death, ~Death] => f_death + [~id, ~aging] => f_aging + [~Becoming, ~id] => f_fstOrder + [_, f_NB] => f_birth + + + :dynamic_variables + [v_NewBorn, v_NB] => v_birth + [~Death, ~Death] => v_death + [~id, (v_agingCA, v_agingAS)] => v_aging + [(v_BecomingOverWeight, v_BecomingObese), (v_idC, v_idA, v_idS)] => v_fstOrder + + :parameters + [μ, μ] => μ + [(δw, δo), (δC, δA, δS)] => δ + [(rw, ro), r] => rFstOrder + [rage, (rageCA, rageAS)] => rage + + end + + + + @test aged_weight == age_weight_2 + @test aged_weight == age_weight_3 + @test aged_weight == age_weight_4 + @test aged_weight == age_weight_5 + @test aged_weight == age_weight_6 end @testset "Ensuring interpret_stratification_standard_notation correctly reads lines" begin # This should be all valid cases. There's always going to be at least one value on both sides. - # Note the orders. The lists produced go left to right. A1, A2 => B <= C1, C2 results in [A1 => B, A2 => B], [C1 => B. C2 => B] - - - @test interpret_stratification_standard_notation(:(A => B <= C)) == [[DSLArgument(:A, :B, Set{Symbol}())], [DSLArgument(:C, :B, Set{Symbol}())]] - - @test interpret_stratification_standard_notation(:(A1, A2 => B <= C)) == [ - [DSLArgument(:A1, :B, Set{Symbol}()), DSLArgument(:A2, :B, Set{Symbol}())], - [DSLArgument(:C, :B, Set{Symbol}())] - ] - @test interpret_stratification_standard_notation(:(A => B <= C1, C2)) == [ - [DSLArgument(:A, :B, Set{Symbol}())], - [DSLArgument(:C1, :B, Set{Symbol}()), DSLArgument(:C2, :B, Set{Symbol}())], - ] - @test interpret_stratification_standard_notation(:(_ => B <= _)) == [ - [DSLArgument(:_, :B, Set{Symbol}())], - [DSLArgument(:_, :B, Set{Symbol}())], - ] - @test interpret_stratification_standard_notation(:(~A => B <= ~C)) == [ - [DSLArgument(:A, :B, Set{Symbol}([:~]))], - [DSLArgument(:C, :B, Set{Symbol}([:~]))], - ] - @test interpret_stratification_standard_notation(:(~A1, A2 => B <= ~C)) == [ - [DSLArgument(:A1, :B, Set{Symbol}([:~])), DSLArgument(:A2, :B, Set{Symbol}())], - [DSLArgument(:C, :B, Set{Symbol}([:~]))], - ] - - @test interpret_stratification_standard_notation(:(~_ => B <= ~_, C)) == [ # Weird case. Matches everything with _ as a substring. - [DSLArgument(:_, :B, Set{Symbol}([:~]))], - [DSLArgument(:_, :B, Set{Symbol}([:~])), DSLArgument(:C, :B, Set{Symbol}())] - ] + # Note the orders. The lists produced go left to right. A1, A2 => B <= C1, C2 results in [A1 => B, A2 => B], [C1 => B. C2 => B] + + + @test interpret_stratification_standard_notation(:(A => B <= C)) == [[DSLArgument(:A, :B, Set{Symbol}())], [DSLArgument(:C, :B, Set{Symbol}())]] + + @test interpret_stratification_standard_notation(:(A1, A2 => B <= C)) == [ + [DSLArgument(:A1, :B, Set{Symbol}()), DSLArgument(:A2, :B, Set{Symbol}())], + [DSLArgument(:C, :B, Set{Symbol}())] + ] + @test interpret_stratification_standard_notation(:(A => B <= C1, C2)) == [ + [DSLArgument(:A, :B, Set{Symbol}())], + [DSLArgument(:C1, :B, Set{Symbol}()), DSLArgument(:C2, :B, Set{Symbol}())], + ] + @test interpret_stratification_standard_notation(:(_ => B <= _)) == [ + [DSLArgument(:_, :B, Set{Symbol}())], + [DSLArgument(:_, :B, Set{Symbol}())], + ] + @test interpret_stratification_standard_notation(:(~A => B <= ~C)) == [ + [DSLArgument(:A, :B, Set{Symbol}([:~]))], + [DSLArgument(:C, :B, Set{Symbol}([:~]))], + ] + @test interpret_stratification_standard_notation(:(~A1, A2 => B <= ~C)) == [ + [DSLArgument(:A1, :B, Set{Symbol}([:~])), DSLArgument(:A2, :B, Set{Symbol}())], + [DSLArgument(:C, :B, Set{Symbol}([:~]))], + ] + + @test interpret_stratification_standard_notation(:(~_ => B <= ~_, C)) == [ # Weird case. Matches everything with _ as a substring. + [DSLArgument(:_, :B, Set{Symbol}([:~]))], + [DSLArgument(:_, :B, Set{Symbol}([:~])), DSLArgument(:C, :B, Set{Symbol}())] + ] end @testset "Unwrapping expressions works correctly" begin - @test unwrap_expression(:S) == (:S, Set{Symbol}()) - @test unwrap_expression(:(~S)) == (:S, Set{Symbol}([:~])) - @test unwrap_expression(:(~_)) == (:_, Set{Symbol}([:~])) + @test unwrap_expression(:S) == (:S, Set{Symbol}()) + @test unwrap_expression(:(~S)) == (:S, Set{Symbol}([:~])) + @test unwrap_expression(:(~_)) == (:_, Set{Symbol}([:~])) end # function substitute_symbols(s::Dict{Symbol, Int}, t::Dict{Symbol, Int}, m::Vector{DSLArgument} ; use_flags::Bool=true)::Dict{Int, Int} @testset "Testing substituting symbols" begin # underscore matching occurs at the very end, after this step. - s1 = Dict(:A => 1) - t1 = Dict(:B => 2) - m1₁ = [DSLArgument(:A, :B, Set{Symbol}())] - m1₂ = [DSLArgument(:A, :B, Set{Symbol}([:~]))] + s1 = Dict(:A => 1) + t1 = Dict(:B => 2) + m1₁ = [DSLArgument(:A, :B, Set{Symbol}())] + m1₂ = [DSLArgument(:A, :B, Set{Symbol}([:~]))] - @test substitute_symbols(s1, t1, m1₁) == Dict(1 => 2) # A=>B -> 1=>2 - @test substitute_symbols(s1, t1, m1₂) == Dict(1 => 2) # A=>B -> 1=>2 - @test substitute_symbols(s1, t1, m1₂, use_flags=false) == Dict(1 => 2) # A=>B -> 1=>2 + @test substitute_symbols(s1, t1, m1₁) == Dict(1 => 2) # A=>B -> 1=>2 + @test substitute_symbols(s1, t1, m1₂) == Dict(1 => 2) # A=>B -> 1=>2 + @test substitute_symbols(s1, t1, m1₂, use_flags=false) == Dict(1 => 2) # A=>B -> 1=>2 - s2 = Dict(:A1 => 10, :A2 => 20, :A3 => 30) # Unfortunately, cannot do substring matches starting with numbers, since it would require a symbol starting with a number. Might need to add something for this... - t2 = Dict(:B1 => 1, :B2 => 2) - m2₁ = [DSLArgument(:A, :B1, Set{Symbol}([:~]))] + s2 = Dict(:A1 => 10, :A2 => 20, :A3 => 30) # Unfortunately, cannot do substring matches starting with numbers, since it would require a symbol starting with a number. Might need to add something for this... + t2 = Dict(:B1 => 1, :B2 => 2) + m2₁ = [DSLArgument(:A, :B1, Set{Symbol}([:~]))] - @test substitute_symbols(s2, t2, m2₁) == Dict(10 => 1, 20 => 1, 30 => 1) #~A=>B -> 10=>1, 20=>1, 30=>1 + @test substitute_symbols(s2, t2, m2₁) == Dict(10 => 1, 20 => 1, 30 => 1) #~A=>B -> 10=>1, 20=>1, 30=>1 - s3 = Dict{Symbol, Int}() - t3 = Dict{Symbol, Int}() - m3 = Vector{DSLArgument}() + s3 = Dict{Symbol, Int}() + t3 = Dict{Symbol, Int}() + m3 = Vector{DSLArgument}() - @test substitute_symbols(s3, t3, m3) == Dict() - @test substitute_symbols(s3, t3, m3, use_flags=false) == Dict() + @test substitute_symbols(s3, t3, m3) == Dict() + @test substitute_symbols(s3, t3, m3, use_flags=false) == Dict() - s4 = Dict(:A1 => 1, :A2 => 2, :AB3 => 3, :AB4 => 4, :A5 => 5) - t4 = Dict(:B1 => 1, :B2 => 2, :B3 => 3) - m4 = [DSLArgument(:A1, :B1, Set{Symbol}()), DSLArgument(:B, :B2, Set{Symbol}([:~])), DSLArgument(:A, :B3, Set{Symbol}([:~]))] + s4 = Dict(:A1 => 1, :A2 => 2, :AB3 => 3, :AB4 => 4, :A5 => 5) + t4 = Dict(:B1 => 1, :B2 => 2, :B3 => 3) + m4 = [DSLArgument(:A1, :B1, Set{Symbol}()), DSLArgument(:B, :B2, Set{Symbol}([:~])), DSLArgument(:A, :B3, Set{Symbol}([:~]))] - # always goes with first match. A1 is taken, B matches AB3 and AB4, then A matches A2 and A5 - @test substitute_symbols(s4, t4, m4) == Dict(1 => 1, 3 => 2, 4 => 2, 2 => 3, 5 => 3) + # always goes with first match. A1 is taken, B matches AB3 and AB4, then A matches A2 and A5 + @test substitute_symbols(s4, t4, m4) == Dict(1 => 1, 3 => 2, 4 => 2, 2 => 3, 5 => 3) end @testset "nondefault flags work as expected" begin - A_ = (@stock_and_flow begin - :stocks - A - _ - end) - - X_ = (@stock_and_flow begin - :stocks - X - _ - end) - - B_ = (@stock_and_flow begin - :stocks - B - _ - end) - - strat_AXB = (quote # Note, we use a quote when calling the function, begin when calling the macro. + A_ = (@stock_and_flow begin :stocks - _ => _ <= _ - A => X <= B - ~A => X <= ~B # everything is already assigned, so does nothing (or throws error if strict_matches is true) - end) + A + _ + end) + X_ = (@stock_and_flow begin + :stocks + X + _ + end) - sfA = (@stock_and_flow begin; :stocks; A; end;) + B_ = (@stock_and_flow begin + :stocks + B + _ + end) - @test (sfstratify([A_, B_], X_, strat_AXB, use_temp_strat_default=false) - == (@stock_and_flow begin - :stocks - AB - __ - end)) + strat_AXB = (quote # Note, we use a quote when calling the function, begin when calling the macro. + :stocks + _ => _ <= _ + A => X <= B + ~A => X <= ~B # everything is already assigned, so does nothing (or throws error if strict_matches is true) + end) - # doesn't show up anywhere, so doesn't affect anything. Could also set it to something untypable in the DSL, like Symbol("") - @test (sfstratify([A_, B_], X_, strat_AXB, temp_strat_default=:ABABABABA) - == (@stock_and_flow begin - :stocks - AB - __ - end)) - @test_throws AssertionError (sfstratify([A_, B_], X_, strat_AXB, strict_matches=true)) # A matches against A and ~A, which is disallowed with this flag. + sfA = (@stock_and_flow begin; :stocks; A; end;) - @test_throws ErrorException (sfstratify([sfA,sfA],sfA,(quote end) ; strict_mappings=true)) # strict_mappings=false wouldn't throw an error, and would infer strata and aggregate need to map to the only stock. + @test (sfstratify([A_, B_], X_, strat_AXB, use_temp_strat_default=false) + == (@stock_and_flow begin + :stocks + AB + __ + end)) + + # doesn't show up anywhere, so doesn't affect anything. Could also set it to something untypable in the DSL, like Symbol("") + @test (sfstratify([A_, B_], X_, strat_AXB, temp_strat_default=:ABABABABA) + == (@stock_and_flow begin + :stocks + AB + __ + end)) + @test_throws AssertionError (sfstratify([A_, B_], X_, strat_AXB, strict_matches=true)) # A matches against A and ~A, which is disallowed with this flag. - nothing_sfA = map(sfA, Position=NothingFunction, Op=NothingFunction, Name=NothingFunction) + @test_throws ErrorException (sfstratify([sfA,sfA],sfA,(quote end) ; strict_mappings=true)) # strict_mappings=false wouldn't throw an error, and would infer strata and aggregate need to map to the only stock. - @test (sfstratify([sfA,sfA],sfA,(quote end), return_homs=true) == ( - (@stock_and_flow begin - :stocks - AA - end), - [ACSetTransformation(sfA, nothing_sfA ; S=[1], F=Vector{Int}(),V =Vector{Int}(),SV=Vector{Int}(),P=Vector{Int}(),LS=Vector{Int}(),I=Vector{Int}(),O=Vector{Int}(),LV=Vector{Int}(),LSV=Vector{Int}(),LVV=Vector{Int}(),LPV=Vector{Int}(), Position=NothingFunction, Op=NothingFunction, Name=NothingFunction), # strata -> type - ACSetTransformation(sfA, nothing_sfA ; S=[1], F=Vector{Int}(),V =Vector{Int}(),SV=Vector{Int}(),P=Vector{Int}(),LS=Vector{Int}(),I=Vector{Int}(),O=Vector{Int}(),LV=Vector{Int}(),LSV=Vector{Int}(),LVV=Vector{Int}(),LPV=Vector{Int}(), Position=NothingFunction, Op=NothingFunction, Name=NothingFunction)] # aggregate -> type - )) # the empty lists are necessary for equality, but it'd still be an equivalent homomorphism if you didn't specify them. + + nothing_sfA = map(sfA, Position=NothingFunction, Op=NothingFunction, Name=NothingFunction) + + @test (sfstratify([sfA,sfA],sfA,(quote end), return_homs=true) == ( + (@stock_and_flow begin + :stocks + AA + end), + [ACSetTransformation(sfA, nothing_sfA ; S=[1], F=Vector{Int}(),V =Vector{Int}(),SV=Vector{Int}(),P=Vector{Int}(),LS=Vector{Int}(),I=Vector{Int}(),O=Vector{Int}(),LV=Vector{Int}(),LSV=Vector{Int}(),LVV=Vector{Int}(),LPV=Vector{Int}(), Position=NothingFunction, Op=NothingFunction, Name=NothingFunction), # strata -> type + ACSetTransformation(sfA, nothing_sfA ; S=[1], F=Vector{Int}(),V =Vector{Int}(),SV=Vector{Int}(),P=Vector{Int}(),LS=Vector{Int}(),I=Vector{Int}(),O=Vector{Int}(),LV=Vector{Int}(),LSV=Vector{Int}(),LVV=Vector{Int}(),LPV=Vector{Int}(), Position=NothingFunction, Op=NothingFunction, Name=NothingFunction)] # aggregate -> type + )) # the empty lists are necessary for equality, but it'd still be an equivalent homomorphism if you didn't specify them. end @@ -458,120 +458,120 @@ end @testset "n_stratify works as expected" begin - l_type = @stock_and_flow begin - :stocks - pop - - :parameters - μ - δ - rFstOrder - rage - - :dynamic_variables - v_aging = pop * rage - v_fstOrder = pop * rFstOrder - v_birth = N * μ - v_death = pop * δ - - :flows - pop => f_aging(v_aging) => pop - pop => f_fstOrder(v_fstOrder) => pop - CLOUD => f_birth(v_birth) => pop - pop => f_death(v_death) => CLOUD - - :sums - N = [pop] - - end - - chain_ltype = @stock_and_flow begin - :stocks - poppoppop - - :parameters - μμμ - δδδ - rFstOrderrFstOrderrFstOrder - rageragerage - - :dynamic_variables - v_agingv_agingv_aging = poppoppop * rageragerage - v_fstOrderv_fstOrderv_fstOrder = poppoppop * rFstOrderrFstOrderrFstOrder - v_birthv_birthv_birth = NNN * μμμ - v_deathv_deathv_death = poppoppop * δδδ - - :flows - poppoppop => f_agingf_agingf_aging(v_agingv_agingv_aging) => poppoppop - poppoppop => f_fstOrderf_fstOrderf_fstOrder(v_fstOrderv_fstOrderv_fstOrder) => poppoppop - CLOUD => f_birthf_birthf_birth(v_birthv_birthv_birth) => poppoppop - poppoppop => f_deathf_deathf_death(v_deathv_deathv_death) => CLOUD - - :sums - NNN = [poppoppop] - end - - chain_ltype_nstratify = @n_stratify l_type l_type l_type l_type begin - - :stocks - [pop, ~pop, _] => pop - - :parameters - [μ, μ, μ] => μ - [δ, δ, δ] => δ - [rFstOrder, rFstOrder, rFstOrder] => rFstOrder - [rage, rage, rage] => rage - - :dynamic_variables - [v_aging, v_aging, v_aging] => v_aging - [v_fstOrder, v_fstOrder, v_fstOrder] => v_fstOrder - [v_birth, v_birth, v_birth] => v_birth - [v_death, v_death, v_death] => v_death - - :flows - [f_aging, f_aging, f_aging] => f_aging - [f_fstOrder, f_fstOrder, f_fstOrder] => f_fstOrder - [f_birth, f_birth, f_birth] => f_birth - [f_death, f_death, f_death] => f_death - - :sums - [N, N, N] => N - end - - - @test chain_ltype == chain_ltype_nstratify - - - ltype_nstratify = @n_stratify l_type l_type begin - - :stocks - [pop] => pop - - :parameters - [μ] => μ - [δ] => δ - [rFstOrder] => rFstOrder - [rage] => rage - - :dynamic_variables - [v_aging] => v_aging - [v_fstOrder] => v_fstOrder - [v_birth] => v_birth - [v_death] => v_death - - :flows - [f_aging] => f_aging - [f_fstOrder] => f_fstOrder - [f_birth] => f_birth - [f_death] => f_death - - :sums - [N] => N - end - - @test ltype_nstratify == l_type - + l_type = @stock_and_flow begin + :stocks + pop + + :parameters + μ + δ + rFstOrder + rage + + :dynamic_variables + v_aging = pop * rage + v_fstOrder = pop * rFstOrder + v_birth = N * μ + v_death = pop * δ + + :flows + pop => f_aging(v_aging) => pop + pop => f_fstOrder(v_fstOrder) => pop + CLOUD => f_birth(v_birth) => pop + pop => f_death(v_death) => CLOUD + + :sums + N = [pop] + + end + + chain_ltype = @stock_and_flow begin + :stocks + poppoppop + + :parameters + μμμ + δδδ + rFstOrderrFstOrderrFstOrder + rageragerage + + :dynamic_variables + v_agingv_agingv_aging = poppoppop * rageragerage + v_fstOrderv_fstOrderv_fstOrder = poppoppop * rFstOrderrFstOrderrFstOrder + v_birthv_birthv_birth = NNN * μμμ + v_deathv_deathv_death = poppoppop * δδδ + + :flows + poppoppop => f_agingf_agingf_aging(v_agingv_agingv_aging) => poppoppop + poppoppop => f_fstOrderf_fstOrderf_fstOrder(v_fstOrderv_fstOrderv_fstOrder) => poppoppop + CLOUD => f_birthf_birthf_birth(v_birthv_birthv_birth) => poppoppop + poppoppop => f_deathf_deathf_death(v_deathv_deathv_death) => CLOUD + + :sums + NNN = [poppoppop] + end + + chain_ltype_nstratify = @n_stratify l_type l_type l_type l_type begin + + :stocks + [pop, ~pop, _] => pop + + :parameters + [μ, μ, μ] => μ + [δ, δ, δ] => δ + [rFstOrder, rFstOrder, rFstOrder] => rFstOrder + [rage, rage, rage] => rage + + :dynamic_variables + [v_aging, v_aging, v_aging] => v_aging + [v_fstOrder, v_fstOrder, v_fstOrder] => v_fstOrder + [v_birth, v_birth, v_birth] => v_birth + [v_death, v_death, v_death] => v_death + + :flows + [f_aging, f_aging, f_aging] => f_aging + [f_fstOrder, f_fstOrder, f_fstOrder] => f_fstOrder + [f_birth, f_birth, f_birth] => f_birth + [f_death, f_death, f_death] => f_death + + :sums + [N, N, N] => N + end + + + @test chain_ltype == chain_ltype_nstratify + + + ltype_nstratify = @n_stratify l_type l_type begin + + :stocks + [pop] => pop + + :parameters + [μ] => μ + [δ] => δ + [rFstOrder] => rFstOrder + [rage] => rage + + :dynamic_variables + [v_aging] => v_aging + [v_fstOrder] => v_fstOrder + [v_birth] => v_birth + [v_death] => v_death + + :flows + [f_aging] => f_aging + [f_fstOrder] => f_fstOrder + [f_birth] => f_birth + [f_death] => f_death + + :sums + [N] => N + end + + @test ltype_nstratify == l_type + end