Examples

Interactive Jupyter Notebook versions of these tutorials can be found on GitHub.

using BondGraphs
using Catalyst
using Plots

Simple Electric Circuit

This is a reduced copy-and-paste version of the electric circuit tutorial in the Getting Started section.

model = BondGraph("RC Circuit")
C = Component(:C)
R = Component(:R)
Is = Component(:Sf, "Is")
kvl = EqualEffort()

add_node!(model, [C, R, Is, kvl])
connect!(model, R, kvl)
connect!(model, C, kvl)
connect!(model, Is, kvl)

C.C = 1
R.R = 2

u0 = [1]
p = plot()
for i in 1:4
    Is.fs = t -> cos(i * t)
    sol = simulate(model, (0., 5.); u0)
    plot!(p, sol, label = "f(t) = cos($(i)t)", lw=2)
end
plot(p)

Biochemical Reaction Networks

BondGraphs.jl includes a special type conversion from a Catalyst.ReactionSystem to a BondGraph. This means we can easily create chemical reaction networks with the @reaction_network macro and automatically generate a biochemical bond graph.

A simple biochemical example

We will start with a very simple chemical reaction.

\[A + B \rightleftharpoons C\]

Due to the design of bond graphs, all reactions must be reversible.

abc = @reaction_network ABC begin
    1, A + B --> C
end

bg_abc = BondGraph(abc)
plot(bg_abc)
Note

The 1 before the reaction definition is the reaction rate. This is currently not used in the conversion, so the value is only a placeholder. The reaction rate can be set later in the bond graph.

This model derives equations for mass-action kinetics. (Other reaction equations can be used with a custom reaction component.)

constitutive_relations(bg_abc; sub_defaults=true)
tspan = (0., 2.)
u0 = [1, 1, 0]
sol = simulate(bg_abc, tspan; u0)
plot(sol, lw=3)

Stoichiometry

This process works with multiple reactions with different stoichiometries.

\[A + 2B \rightleftharpoons 3C \\ 8A + 4C \rightleftharpoons D\]

abcd = @reaction_network ABCD begin
    1, A + 2B --> 3C
    1, 8A + 4C --> D
end
bg_abcd = BondGraph(abcd)
plot(bg_abcd)
constitutive_relations(bg_abcd; sub_defaults=true)
tspan = (0.0, 0.1)
u0 = [3, 2, 1, 0]
sol = simulate(bg_abcd, tspan; u0)
plot(sol, lw=3)

The reversible Michaelis-Menten model

A realistic example with chemostats. Chemostats are chemical species that are held constant throught the reaction by adding or removing the species to maintian the set concentration.

\[E + S \rightleftharpoons C \\ C \rightleftharpoons E + P\]

Here, the substrate S and P are chemostats. In bond graph terms, this means replacing the Ce components with a SCe component.

rn_mm = @reaction_network MM_reversible begin
    (1, 1), E + S <--> C
    (1, 1), C <--> E + P
end

bg_mm = BondGraph(rn_mm; chemostats=["S", "P"])

plot(bg_mm, fontsize=12)
bg_mm.S.xs = t -> 1 + t # substrate increases over time

tspan = (0., 10.)
u0 = [1,2]
sol = simulate(bg_mm, tspan; u0)
plot(sol, lw=3)

SERCA Pump

In this example we will demonstrate biochemical bond graph construction on a larger system.

We will model the SERCA reaction network as described in Tran et al.[1] and Pan et al.[2]

rn_serca = @reaction_network SERCA begin
    (1, 1), P1 + MgATP <--> P2
    (1, 1), P2 + H <--> P2a
    (1, 1), P2 + 2Cai <--> P4
    (1, 1), P4 <--> P5 + 2H
    (1, 1), P5 <--> P6 + MgADP
    (1, 1), P6 <--> P8 + 2Casr
    (1, 1), P8 + 2H <--> P9
    (1, 1), P9 <--> P10 + H
    (1, 1), P10 <--> P1 + Pi
end

chemostats = ["MgATP", "MgADP", "Pi", "H", "Cai", "Casr"]
bg_serca = BondGraph(rn_serca; chemostats)
plot(bg_serca, size=(600,600), fontsize=10)

For this example we need to set the parameter values for the reaction rates $r$, the species affinities $K$, the chemostat concentrations $x_s$, and the initial concentrations for all $P_i$.

We also set let the calcium concentration increase over time with $[\text{Ca}^{2+}] = 0.05 + 0.01t$

reaction_rates = [
    :R1 => 0.00053004, 
    :R2 => 8326784.0537,
    :R3 => 1567.7476,
    :R4 => 1567.7476,
    :R5 => 3063.4006,
    :R6 => 130852.3839,
    :R7 => 11612934.8748,
    :R8 => 11612934.8748,
    :R9 => 0.049926
]
species_affinities = [
    :P1 => 5263.6085,
    :P2 => 3803.6518,
    :P2a => 3110.4445,
    :P4 => 16520516.1239,
    :P5 => 0.82914,
    :P6 => 993148.433,
    :P8 => 37.7379,
    :P9 => 2230.2717,
    :P10 => 410.6048,
    :Cai => 1.9058,
    :Casr => 31.764,
    :MgATP => 244.3021,
    :MgADP => 5.8126e-7,
    :Pi => 0.014921,
    :H => 1862.5406
]
vol_sr = 2.28
chemostat_amounts = [
    :Cai => t -> 0.0057,
    :Casr => t -> vol_sr*(0.05 + 0.01t), # Ca2+ increases over time
    :H => t -> 0.004028,
    :MgADP => t -> 1.3794,
    :MgATP => t -> 3.8,
    :Pi => t -> 570
]
initial_conditions = [
    :P1 => 0.000483061870385487,
    :P2 => 0.0574915174273067,
    :P2a => 0.527445119834607,
    :P4 => 1.51818391164022e-09,
    :P5 => 0.000521923287622898,
    :P6 => 7.80721128535043e-05,
    :P8 => 0.156693953834181,
    :P9 => 0.149232225342376,
    :P10 => 0.108044124948978
]
for (reaction, rate) in reaction_rates
    getproperty(bg_serca, reaction).r = rate
end
for (species, affinity) in species_affinities
    getproperty(bg_serca, species).K = affinity
end
for (chemostat, amount) in chemostat_amounts
    getproperty(bg_serca, chemostat).xs = amount
end
for (species, ic) in initial_conditions
    getproperty(bg_serca, species).q = ic
end
import DifferentialEquations: Rosenbrock23 # stiff equation solver

tspan = (0., 200.)
sol = simulate(bg_serca, tspan; solver=Rosenbrock23());
plot(sol, lw=2, legend=:right)

Electrochemical System - Ion Transport

A multiphysics bond graph example that combines biochemical reactions with electrical (ion) forces. This example is taken from Cudmore et al.[3]

For this example we are modelling three ion pore channels for Na<sup>+</sup>, Cl<sup>-</sup> and K<sup>+</sup>. It is therefore a good idea to define a function that returns an ion pore base model.

function ion_pore(name=""; z=1, conc_ex=1//1000, conc_in=1//1000)
    bg = BondGraph(name * " Ion Transport")

    membrane = Component(:C, "mem"; C=1)
    ion_ex = Component(:SCe, "Ie"; K=1, xs=t->conc_ex)
    ion_in = Component(:SCe, "Ii"; K=1, xs=t->conc_in)

    potential_mem = EqualEffort()
    flow_f = EqualFlow()
    flow_r = EqualFlow()

    TF_F = Component(:TF, "F"; n=96485)
    TF_zf = Component(:TF, "-z/2"; n=-z//2) # -ve voltage
    TF_zr = Component(:TF, "z/2"; n=z//2) # +ve voltage

    re_pore = Component(:Re, "pore"; r=1//10_000_000)

    allcomps = [
        membrane, ion_ex, ion_in, potential_mem, flow_f, flow_r,
        TF_F, TF_zr, TF_zf, re_pore
    ]

    add_node!(bg, allcomps)
    connect!(bg, ion_ex, flow_f)
    connect!(bg, flow_f, re_pore)
    connect!(bg, re_pore, flow_r)
    connect!(bg, flow_r, ion_in)
    connect!(bg, flow_r, TF_zr, dstportindex=2)
    connect!(bg, TF_zr, potential_mem, srcportindex=1)
    connect!(bg, potential_mem, TF_zf, dstportindex=1)
    connect!(bg, TF_zf, flow_f, srcportindex=2)
    connect!(bg, potential_mem, TF_F, dstportindex=2)
    connect!(bg, TF_F, membrane, srcportindex=1)
    
    return bg
end
ion_pore_bg = ion_pore()
plot(ion_pore_bg)
ions = [
    "Na" => (z=1, conc_ex=155//1000, conc_in=19//1000),
    "Cl" => (z=-1, conc_ex=112//1000, conc_in=78//1000),
    "K"  => (z=1, conc_ex=5//1000, conc_in=136//1000),
]

plot(; legend=:bottomright)
for (ionname, params) in ions
    model = ion_pore(ionname; params...)
    sol = simulate(model, (0., 1000.))
    plot!(sol, label=ionname, lw=3)
end
plot!(; xlabel="Time [ms]", ylabel="Membrane potential [mV]", yformatter=y->y*1000)

Enzyme Catalysed Reactions

We can create custom reaction components which describe enzyme-catalyzed reaction mechanices, such as the Michaelis-Menten rate law.

Warning

This method of adding custom components will be deprecated in the near future.

New components are constructed using the same dictionary structure as in the default component library. See addlibrary! for more details.

using ModelingToolkit

@parameters t R T r1 r2 k_c e_T
@variables E(t)[1:2] F(t)[1:2] # effort and flow variables

R, T = GlobalScope(R), GlobalScope(T) # no namespace
D = Differential(t)

# Custom Michaelis-Mentin reaction component
ReMM = Dict(
    :description => """
    Michaelis-Menten reaction
    r1: Rate of reaction 1
    r2: Rate of reaction 2
    k_c: Affinity of complex relative to free enzyme
    e_T: Total amount of enzyme
    R: Universal Gas Constant
    T: Temperature
    """,
    :numports => 2,
    :variables => Dict(
        :parameters => Dict(
            r1 => 1,
            r2 => 1,
            k_c => 1,
            e_T => 1
        ),
        :globals => Dict(
            R => 8.314,
            T => 310.0
        )
    ),
    :equations => [
        0 ~ F[1] + F[2],
        0 ~ F[1] - e_T*r1*r2*k_c*(exp(E[1]/R/T) - exp(E[2]/R/T)) / (r1*exp(E[1]/R/T) + r2*exp(E[2]/R/T) + k_c*(r1+r2))
    ]
)

# add to component library
addlibrary!(Dict(:ReMM => ReMM))

ReMM[:equations]

We will create a bond graph with these new equations. $ A \rightleftharpoons B \rightleftharpoons C $

rn_enzyme = @reaction_network EnzymeNetwork begin
    (1, 1), A <--> B
    (1, 1), B <--> C
end
bg = BondGraph(rn_enzyme)

bg.A.K, bg.B.K, bg.C.K = 1, 1, 1

# Swap mass-action components for new michaelis-menten components
MM1 = Component(:ReMM, "MM1"; r1=100, r2=100)
MM2 = Component(:ReMM, "MM2"; r1=100, r2=100)
swap!(bg, bg.R1, MM1)
swap!(bg, bg.R2, MM2)

plot(bg)
constitutive_relations(bg; sub_defaults=true)
sol = simulate(bg, (0., 20.); u0=[200,50,100])
plot(sol, lw=2)
  • 1Tran et al., A Thermodynamic Model of the Cardiac Sarcoplasmic/Endoplasmic Ca2+ (SERCA) Pump (2009)
  • 2Pan et al., Bond graph modelling of the cardiac action potential: implications for drift and non-unique steady states (2018)
  • 3Cudmore et al., Analysing and simulating energy-based models in biology using BondGraphTools (2021)