Building Regulatory Networks

Here we will build a modeling framework for regulatory networks with Lotka-Volterra semantics using Catlab.jl

using RegNets, RegNets.ASKEMRegNets
using Catlab.Graphs, Catlab.CategoricalAlgebra, Catlab.Graphics
using JSON, HTTP, OrdinaryDiffEq, Plots

Define the schema

Graph Schema

Catlab provides a basic Graph schema out of the box

@present SchGraph(FreeSchema) begin
  V::Ob
  E::Ob
  src::Hom(E,V)
  tgt::Hom(E,V)
end
to_graphviz(SchGraph, edge_attrs=Dict(:len=>"1.5"))

Basic Signed Graph Schema

The basic structure of a regulatory network can be represented by a signed graph. We simply want to add signs to each edge

@present SchSignedGraph <: SchGraph begin
  Sign::AttrType
  sign::Attr(E,Sign)
end
to_graphviz(SchSignedGraph, edge_attrs=Dict(:len=>"1.5"))

Basic Signed Graph Schema with Rates

We also may want to keep track of rates of these interactions as well. Implicit rates on each vertice as well as rates on each edge.

@present SchRateSignedGraph <: SchSignedGraph begin
  A::AttrType
  vrate::Attr(V,A)
  erate::Attr(E,A)
end
to_graphviz(SchRateSignedGraph, edge_attrs=Dict(:len=>"1.5"))

ASKEM RegNet Schema

For our regulatory network models in ASKEM we also want to have labels on our edges and vertices as well as capture the initial concentrations along with this. We can easily extend our schema with these added attributes:

@present SchASKEMRegNet <: SchRateSignedGraph begin
 C::AttrType
 Name::AttrType
 initial::Attr(V,C)
 vname::Attr(V,Name)
 ename::Attr(E,Name)
end
to_graphviz(SchASKEMRegNet, edge_attrs=Dict(:len=>"1.5"))

Load the model

ASKEM's model representation repository defines a common structure for us to share models, we can use a simple JSON parser to load that into our new schema.

function parse_askem_model(input::AbstractDict)
  regnet = ASKEMRegNet()
  param_vals = Dict(p["id"]=>p["value"] for p in input["model"]["parameters"])
  resolve_val(x) = typeof(x) == String ? param_vals[x] : x

  vertice_idxs = Dict(vertice["id"]=> add_part!(regnet, :V;
    vname=Symbol(vertice["id"]),
    vrate = 0
    if haskey(vertice, "rate_constant")
      vrate = (vertice["sign"] ? 1 : -1) * resolve_val(vertice["rate_constant"])
    end
    initial=haskey(vertice, "initial") ? resolve_val(vertice["initial"]) : 0
  ) for vertice in input["model"]["vertices"])

  for edge in input["model"]["edges"]
    rate = 0
    if haskey(edge, "properties") && haskey(edge["properties"], "rate_constant")
      rate = resolve_val(edge["properties"]["rate_constant"])
      rate >= 0 || error("Edge rates must be strictly positive")
    end
    add_part!(regnet, :E; src=vertice_idxs[edge["source"]],
                          tgt=vertice_idxs[edge["target"]],
                          sign=edge["sign"],
                          ename=Symbol(edge["id"]),
                          erate=rate)
  end

  regnet
end
lotka_volterra = HTTP.get(
  "https://raw.githubusercontent.com/DARPA-ASKEM/Model-Representations/main/regnet/examples/lotka_volterra.json"
).body |> String |> parse_askem_model
ASKEMRegNet with elements V = 1:2, E = 1:2
V vrate initial vname
1 0.667 2.0 R
2 -1.0 1.0 W
E src tgt sign erate ename
1 2 1 false 1.333 wolf_eats_rabbit
2 1 2 true 1.0 rabbit_feeds_wolf

Visualize the model

Catlab provides methods which can be overloaded with our new type to get modeling framework specific visualizations.

function Catlab.Graphics.to_graphviz_property_graph(sg::AbstractSignedGraph; kw...)
  get_attr_str(attr, i) = String(has_subpart(sg, attr) ? subpart(sg, i, attr) : Symbol(i))
  # make a new property graph
  pg = PropertyGraph{Any}(;kw...)
  # add vertices with labels for the visualization
  map(parts(sg, :V)) do v
    add_vertex!(pg, label=get_attr_str(:vname, v))
  end
  # add edges with labels and change the arrowhead
  # based on the sign of the edge for the visualization
  map(parts(sg, :E)) do e
    add_edge!(pg,
      sg[e, :src],
      sg[e, :tgt],
      label=get_attr_str(:ename, e),
      arrowhead=(sg[e,:sign] ? "normal" : "tee")
    )
  end
  pg
end

Then we can simply call to_graphviz and see our model:

to_graphviz(lotka_volterra)

Simulate the model

Next we want to have a method for calculating the dynamics from the model.

We can simply encode the Lotka-Volterra dynamics as a vectorfield function:

function vectorfield(sg::AbstractSignedGraph)
  (u, p, t) -> [
    p[:vrate][i]*u[i] + sum(
        (sg[e,:sign] ? 1 : -1)*p[:erate][e]*u[i]u[sg[e, :src]]
      for e in incident(sg, i, :tgt); init=0.0)
    for i in 1:nv(sg)
  ]
end

And we can use that to pass into an ODEProblem using DifferentialEquations.jl

ODEProblem(
  vectorfield(lotka_volterra), # generate the vectorfield
  lotka_volterra[:initial],    # get the initial concentrations
  (0.0, 100.0),                # set the time period
  lotka_volterra,              # pass in model which contains the rate parameters
  alg=Tsit5()
) |> solve |> plot

Autogenerated JSON Serialization

Catlab provides automatic serialization to JSON with these types both the models that fit within a given schema as well as the schema itself.

Serialize the model

JSON.print(generate_json_acset(lotka_volterra), 2)
{
  "V": [
    {
      "vrate": 0.667,
      "initial": 2.0,
      "vname": "R"
    },
    {
      "vrate": -1.0,
      "initial": 1.0,
      "vname": "W"
    }
  ],
  "E": [
    {
      "src": 2,
      "tgt": 1,
      "sign": false,
      "erate": 1.333,
      "ename": "wolf_eats_rabbit"
    },
    {
      "src": 1,
      "tgt": 2,
      "sign": true,
      "erate": 1.0,
      "ename": "rabbit_feeds_wolf"
    }
  ]
}

Serialize the ACSet schema

JSON.print(generate_json_acset_schema(SchASKEMRegNet), 2)
{
  "version": {
    "ACSetSchema": "0.0.1",
    "Catlab": "0.0.0"
  },
  "Ob": [
    {
      "name": "V"
    },
    {
      "name": "E"
    }
  ],
  "Hom": [
    {
      "name": "src",
      "codom": "V",
      "dom": "E"
    },
    {
      "name": "tgt",
      "codom": "V",
      "dom": "E"
    }
  ],
  "AttrType": [
    {
      "name": "Sign"
    },
    {
      "name": "A"
    },
    {
      "name": "C"
    },
    {
      "name": "Name"
    }
  ],
  "Attr": [
    {
      "name": "sign",
      "codom": "Sign",
      "dom": "E"
    },
    {
      "name": "vrate",
      "codom": "A",
      "dom": "V"
    },
    {
      "name": "erate",
      "codom": "A",
      "dom": "E"
    },
    {
      "name": "initial",
      "codom": "C",
      "dom": "V"
    },
    {
      "name": "vname",
      "codom": "Name",
      "dom": "V"
    },
    {
      "name": "ename",
      "codom": "Name",
      "dom": "E"
    }
  ]
}