Multiple-strain Disease Model with Vaccination

using AlgebraicPetri, AlgebraicPetri.TypedPetri
using Catlab.Programs, Catlab.Graphics
using Catlab.CategoricalAlgebra
using Catlab.WiringDiagrams
using DisplayAs, Markdown

This example presents models incorporating multiple strains of disease and vaccine type. Importantly, it shows why stratification by disease strain is different from other stratifications, e.g. geography or age, and requires using a different type system. If you are unfamiliar with stratification, we recommend first reading the stratification tutorial.

Define basic type system

We start by defining our basic type system, $P_{infectious}$ for infectious disease models.

const infectious_ontology = LabelledPetriNet(
  [:Pop],
  :infect => ((:Pop, :Pop) => (:Pop, :Pop)),
  :disease => (:Pop => :Pop),
  :strata => (:Pop => :Pop)
)

to_graphviz(infectious_ontology)

We define a simple SIR model with reflexive transitions typed as "strata" to indicate which stratified states will interact with transitions amongst strata.

sir_uwd = @relation (S,I,R) where (S::Pop, I::Pop, R::Pop) begin
  infect(S, I, I, I)
  disease(I, R)
end

sir_model = oapply_typed(infectious_ontology, sir_uwd, [:infection, :recovery])
sir_model = add_reflexives(sir_model, [[:strata], [:strata], [:strata]], infectious_ontology)

to_graphviz(dom(sir_model))

Define a model of multiple vaccine types

We also define a model of vaccination with multiple vaccine types, typed over $P_{infectious}$. Each stratum has reflexive "disease" transitions. Transitions which represent the administration of a vaccine to an individual are typed as "strata". Infection transitions between individuals of different vaccination status is possible (i.e.; we do not assume a perfect vaccine).

function vax_model(n)
  uwd = RelationDiagram(repeat([:Pop], n+1))

  variable = :Unvaxxed
  junction = add_junction!(uwd, :Pop, variable=variable)
  port = ports(uwd, outer=true)[1]
  set_junction!(uwd, port, junction, outer=true)
  junctions = Dict(variable => junction)
  for i in 1:n
    variable = Symbol("VaxType$(i)")
    junction = add_junction!(uwd, :Pop, variable=variable)
    port = ports(uwd, outer=true)[i+1]
    set_junction!(uwd, port, junction, outer=true)
    junctions[variable] = junction
  end

  strains = filter((x) -> x != Symbol("Unvaxxed"), keys(junctions))
  for s in strains
    pair = (:Unvaxxed, s)
    box = add_box!(uwd, [junction_type(uwd, junctions[p]) for p in pair], name=:strata)
    for (rgn, port) in zip(pair, ports(uwd, box))
      set_junction!(uwd, port, junctions[rgn])
    end
  end
  tnames = [Symbol("vax_$(b)") for b in strains]

  pairs = collect(Iterators.product(keys(junctions), keys(junctions)))
  for pair in pairs
    ins_outs = (pair[1], pair[2], pair[1], pair[2])
    box = add_box!(uwd, [junction_type(uwd, junctions[p]) for p in ins_outs], name=:infect)
    for (rgn, port) in zip(ins_outs, ports(uwd, box))
      set_junction!(uwd, port, junctions[rgn])
    end
    push!(tnames,Symbol("inf_$(pair[1])_$(pair[2])"))
  end
  act = oapply_typed(infectious_ontology, uwd, tnames)
  add_reflexives(act, repeat([[:disease]], n+1), infectious_ontology)
end

to_graphviz(dom(vax_model(2)))
Example block output

Stratify the SIR model with vaccinations for two vaccine types

We can now stratify the two typed models to get a model of SIR with vaccination by multiple possible vaccine types.

typed_product(sir_model, vax_model(2)) |> dom |> to_graphviz
Example block output

Define a model of multiple disease strains

Here we define a model for multiple disease strains, typed by $P_{infectious}$. In this model, uninfected individuals can become infected by individuals carrying one of the strains. We add reflexive transitions of types :disease and :strata for the strain states but only :strata for the uninfected state, because those individuals should only correspond to $S$ in the disease model.

function strain_model′(n)
  uwd = RelationDiagram(repeat([:Pop], n+1))
  variable = :Uninfected
  junction = add_junction!(uwd, :Pop, variable=variable)
  port = ports(uwd, outer=true)[1]
  set_junction!(uwd, port, junction, outer=true)
  junctions = Dict(variable => junction)
  for i in 1:n
    variable = Symbol("Strain$(i)")
    junction = add_junction!(uwd, :Pop, variable=variable)
    port = ports(uwd, outer=true)[i+1]
    set_junction!(uwd, port, junction, outer=true)
    junctions[variable] = junction
  end
  strains = filter((x) -> x != Symbol("Uninfected"), keys(junctions))
  for s in strains
    pair = (:Uninfected, s, s, s)
    box = add_box!(uwd, [junction_type(uwd, junctions[p]) for p in pair], name=:infect)
    for (rgn, port) in zip(pair, ports(uwd, box))
      set_junction!(uwd, port, junctions[rgn])
    end
  end
  act = oapply_typed(infectious_ontology, uwd, [Symbol("inf_$(b)") for b in strains])
  add_reflexives(act, vcat([[:strata]],repeat([[:disease,:strata]], n)), infectious_ontology)
end

to_graphviz(dom(strain_model′(2)))
Example block output

Stratify the SIRD model for two strains

Unfortunately, stratification of these models does not produce the desired result, leading to many states and transitions. The problem is that it does not make sense for the uninfected population to be stratified over the full set of states of the SIRD model (i.e.; uninfected persons can never have any disease state other than "S"). This is due to the fact that there is only one type of place in $P_{infectious}$, so instead the pullback of places simply becomes the product.

We can address this by changing the type system to include multiple place types.

typed_product(sir_model, strain_model′(2)) |> dom |> to_graphviz
Example block output

Define a new type system and corresponding disease and multi-strain models

The new type system has separate states for uninfected and infected to account for the asymmetry in their role in infection and for the corresponding asymmetry in the intended stratification. Accordingly, the :Inf state has :disease and :strataI transitions. The :Uninf state has a :strataU transition, but no :disease transition. We denote the new type system by $P_{strain}$.

const strain_ontology = LabelledPetriNet(
  [:Uninf, :Inf],
  :infect => ((:Uninf, :Inf) => (:Inf, :Inf)),
  :disease => (:Inf => :Inf),
  :strataI => (:Inf => :Inf),
  :strataU => (:Uninf => :Uninf)
)

to_graphviz(strain_ontology)

We now recreate the SIR model using the new type system. Note that the where clause now specifies the differing place types for each state.

sir_for_strains_uwd = @relation (S,I,R) where (S::Uninf, I::Inf, R::Inf) begin
  infect(S, I, I, I)
  disease(I, R)
end
sir_for_strains_model = oapply_typed(strain_ontology, sir_for_strains_uwd, [:infection, :recovery])
sir_for_strains_model = add_reflexives(sir_for_strains_model, [[:strataU], [:strataI], [:strataI]], strain_ontology)

to_graphviz(sir_for_strains_model)
Example block output

And similarly reform the multi-strain model.

function strain_model(n)
  uwd = RelationDiagram(vcat([:Uninf],repeat([:Inf], n)))
  variable = :Uninfected
  junction = add_junction!(uwd, :Uninf, variable=variable)
  port = ports(uwd, outer=true)[1]
  set_junction!(uwd, port, junction, outer=true)
  junctions = Dict(variable => junction)
  for i in 1:n
    variable = Symbol("Strain$(i)")
    junction = add_junction!(uwd, :Inf, variable=variable)
    port = ports(uwd, outer=true)[i+1]
    set_junction!(uwd, port, junction, outer=true)
    junctions[variable] = junction
  end
  strains = filter((x) -> x != Symbol("Uninfected"), keys(junctions))
  for s in strains
    pair = (:Uninfected, s, s, s)
    box = add_box!(uwd, [junction_type(uwd, junctions[p]) for p in pair], name=:infect)
    for (rgn, port) in zip(pair, ports(uwd, box))
      set_junction!(uwd, port, junctions[rgn])
    end
  end
  act = oapply_typed(strain_ontology, uwd, [Symbol("inf_$(b)") for b in strains])
  add_reflexives(act, vcat([[:strataU]],repeat([[:disease,:strataI]], n)), strain_ontology)
end

to_graphviz(strain_model(2))
Example block output

When we now stratify we get the desired model. Note however that there are extraneous "strata" transitions; this is because there were no non-trivial stratum transitions in either input typed Petri net. (note that only "disease" and "infect" transition types were needed to fully define the multiple strain dynamics).

sir_strain = typed_product(sir_for_strains_model, strain_model(2))

to_graphviz(dom(sir_strain))
Example block output

Post-composition: Retyping the type system

In some instances, we may want to relate models typed to different type systems. For example, we usually type our simple_trip model of geographic regions to the infectious_ontology such that we can stratify a disease model by geographic regions, but the multi-strain disease model above is typed by the new strain_ontology.

Crucially, we can accomplish this if there is an appropriate morphism (map) between the type systems because post-composition by a morphism of type systems is functorial. In this case, there is a morphism from strain_ontology to infectious_ontology ($P_{strain} \to P_{infectious}$), so we will be able to "switch" type systems.

Morphism from strain_ontology to infectious_ontology

We use oapply_typed on a UWD representation of the strain_ontology, but note that we could also directly define the map using ACSetTransformation. The result is a morphism $P_{strain}\to P_{infectious}$.

strain_ont_uwd = @relation (Uninf,Inf) where (Uninf::Pop, Inf::Pop) begin
  infect(Uninf, Inf, Inf, Inf)
  disease(Inf, Inf)
  strata(Inf, Inf)
  strata(Uninf, Uninf)
end
strain_ont_act = oapply_typed(infectious_ontology,strain_ont_uwd,[:infect,:disease,:strataI,:strataU])

to_graphviz(strain_ont_act)
Example block output

Define simple-trip model between $N$ regions

To demonstrate stratification utilizing post-composition to re-type the models, we use the simple-trip geographic model, defined fully in the stratification tutorial. It is typed by infectious_ontology ($P_{infectious}$).

function travel_model(n)
  uwd = RelationDiagram(repeat([:Pop], n))
  junctions = Dict(begin
    variable = Symbol("Region$(i)")
    junction = add_junction!(uwd, :Pop, variable=variable)
    set_junction!(uwd, port, junction, outer=true)
    variable => junction
  end for (i, port) in enumerate(ports(uwd, outer=true)))

  pairs = filter(x -> first(x) != last(x), collect(Iterators.product(keys(junctions), keys(junctions))))
  for pair in pairs
    box = add_box!(uwd, [junction_type(uwd, junctions[p]) for p in pair], name=:strata)
    for (rgn, port) in zip(pair, ports(uwd, box))
      set_junction!(uwd, port, junctions[rgn])
    end
  end

  act = oapply_typed(infectious_ontology, uwd, [Symbol("$(a)_$(b)") for (a, b) in pairs])
  add_reflexives(act, repeat([[:infect, :disease]], n), infectious_ontology)
end

function living_model(n)
  typed_living = pairwise_id_typed_petri(infectious_ontology, :Pop, :infect, [Symbol("Living$(i)") for i in 1:n])
  add_reflexives(typed_living, repeat([[:disease, :strata]], n), infectious_ontology)
end

simple_trip_model = typed_product(travel_model(2), living_model(2))

to_graphviz(simple_trip_model)
Example block output

Stratify multi-strain SIR and simple-trip models

If we want to use the simple trip model to stratify our multi-strain SIR model, we have a problem because the multi-strain SIR model is typed over $P_{strain}$, that is, it is a morphism $\phi : P \to P_{strain}$. Therefore, we can compose with the morphism strain_ont_act ($P_{strain}\to P_{infectious}$) we made earlier, to get a new morphism from $P \to P_{infectious}$.

sir_strain_retyped = compose(sir_strain,strain_ont_act)
ACSetTransformation((T = compose(compose(compose(FinFunction([1, 6, 13, 17, 24, 25, 27, 34, 35], 9, 35), FinFunction(#57, FinSet(35), FinSet(5))), FinFunction([1, 2, 4, 3, 3], 5, 4)), FinFunction([1, 2, 3, 3], 4, 3)), S = compose(compose(compose(FinFunction([1, 5, 6, 8, 9], 5, 9), FinFunction(#57, FinSet(9), FinSet(3))), FinFunction([1, 2, 2], 3, 2)), FinFunction([1, 1], 2, 1)), I = compose(compose(compose(FinFunction([1, 8, 13, 20, 28, 33, 41, 42, 45, 53, 54], 11, 54), FinFunction(#57, FinSet(54), FinSet(6))), FinFunction([1, 2, 3, 5, 4, 4], 6, 5)), FinFunction([1, 2, 3, 4, 4], 5, 4)), O = compose(compose(compose(FinFunction([1, 8, 13, 20, 28, 33, 41, 42, 45, 53, 54], 11, 54), FinFunction(#57, FinSet(54), FinSet(6))), FinFunction([1, 2, 3, 5, 4, 4], 6, 5)), FinFunction([1, 2, 3, 4, 4], 5, 4)), Name = Catlab.CategoricalAlgebra.FinSets.LooseVarFunction{Tuple{Symbol, Symbol}, Symbol}(FinDomFunction(Union{ACSets.ColumnImplementations.AttrVar, Symbol}[], FinSet(0), TypeSet(Union{ACSets.ColumnImplementations.AttrVar, Symbol})), compose(compose(SetFunction(#23, TypeSet(Tuple{Symbol, Symbol}), TypeSet(Symbol)), SetFunction(#17, TypeSet(Symbol), TypeSet(Symbol))), SetFunction(#10, TypeSet(Symbol), TypeSet(Symbol))), FinSet(0))), AlgebraicPetri.LabelledPetriNetUntyped{Tuple{Symbol, Symbol}} {T:9, S:5, I:11, O:11, Name:0}, LabelledPetriNet {T:3, S:1, I:4, O:4, Name:0})

We can now take the typed product to get the multi-strain SIR model stratified over the simple trip model of movement between different regions.

sir_strain_trip = typed_product(sir_strain_retyped,simple_trip_model)

to_graphviz(dom(sir_strain_trip))
Example block output

Define a multi-strain SIR model with vaccination by multiple vaccine types

Because the multi-vaccine model was typed according to $P_{infectious}$, our retyped multi-strain SIR can be stratified according to the multiple vaccine model in a similar way.

sir_strain_vax = typed_product(sir_strain_retyped,vax_model(2))

to_graphviz(dom(sir_strain_vax))
Example block output

Re-stratify the multi-strain multi-vax SIRD model with the simple trip model

If we would like to stratify our SIR-strain-vaccine model (sir_strain_vax) with the simple trip model, we again face a difficulty. These two models are both typed by $P_{infectious}$, but both the "vaccination" transitions of the first model and the "travel" transitions of the second are currently typed to the :strata transition of the infectious_ontology type system. Naively stratifying would thus produce transitions in which persons traveled and were vaccinated simultaneously.

To appropriately stratify, we need an additional "strata" transition to distinguish between the two types of transitions. We can again use post-compostion with the morphism between type systems to reuse our existing models.

Define an augmented version of the infectious_ontology type system with an additional "strata" transition

const aug_inf_ontology = LabelledPetriNet(
  [:nPop],
  :ninfect => ((:nPop, :nPop) => (:nPop, :nPop)),
  :ndisease => (:nPop => :nPop),
  :nstrata => (:nPop => :nPop),
  :nstrata2 => (:nPop => :nPop)
)

to_graphviz(aug_inf_ontology)

Define morphisms from the original type system to the new augmented type system

We form one morphism that maps the :strata transition to :nstrata. This morphism will serve to re-type the SIRD-strain-vax model.

function retype_inf_ont(strata_map)
  uwd = RelationDiagram([:nPop])
  variable = :Pop
  junction = add_junction!(uwd, :nPop, variable=variable)
  port = ports(uwd, outer=true)[1]
  set_junction!(uwd, port, junction, outer=true)
  junctions = Dict(variable => junction)

  boxes = [:ninfect, :ndisease, strata_map]
  for bname in boxes
    if bname == :ninfect
      pair = (:Pop, :Pop, :Pop, :Pop)
    else
      pair = (:Pop, :Pop)
    end
    box = add_box!(uwd, [junction_type(uwd, junctions[p]) for p in pair], name=bname)
    for (rgn, port) in zip(pair, ports(uwd, box))
      set_junction!(uwd, port, junctions[rgn])
    end
  end

  act = oapply_typed(aug_inf_ontology, uwd, [:infect, :disease, :strata])
end

inf_ont_act = retype_inf_ont(:nstrata)

to_graphviz(inf_ont_act)
Example block output

We form another morphism that maps the :strata transition to :nstrata2. This morphism will serve to re-type the simple trip model.

rgn_ont_act = retype_inf_ont(:nstrata2)

to_graphviz(rgn_ont_act)
Example block output

Add reflexive transitions

To finish preparing for stratification, we need to add the new reflexive transitions to the component models. To the SIR-strain-vax model, we add an :nstrata2 tranisiton to each state. Note that if the model included a decreased (or any other immobile) state, we would not add a reflexive transition to them, as they cannot travel.

sir_strain_vax_retyped = flatten_labels(compose(sir_strain_vax,inf_ont_act))
reflx = [[:nstrata2]]
for ii in 2:ns(dom(sir_strain_vax_retyped))
    push!(reflx,[:nstrata2])
end
aug_sir_strain_vax = add_reflexives(sir_strain_vax_retyped, reflx, aug_inf_ontology);

To the simple trip model, we add an :nstrata tranisiton for each state.

simple_trip_retyped = flatten_labels(compose(simple_trip_model,rgn_ont_act))
aug_trip = add_reflexives(simple_trip_retyped, repeat([[:nstrata]],ns(dom(simple_trip_retyped))), aug_inf_ontology);

Stratify the SIR-strain-vax and simple trip models

sir_strain_vax_trip = typed_product([aug_sir_strain_vax,aug_trip])

to_graphviz(dom(sir_strain_vax_trip))
Example block output