AlgebraicPetri: the Petri-net view
petri_net returns a LabelledPetriNet alongside the name-indexed rate and initial-condition Dicts that AlgebraicPetri's own vectorfield needs (it indexes by species and transition name, not by position). It starts from the same Gamma(3, 1.5) lowering as the other backends — see Lowering a distribution to a dynamical system.
This backend's demo runs in its own environment: the package keeps one at test/algebraic_petri, and the demo below runs against it.
If you are reading this as a downloaded script rather than building the docs, this section needs a checked-out copy of the repository (it resolves paths relative to the package source).
using LoweredDistributions
using Distributions
using CairoMakie
using AlgebraOfGraphics
using DataFramesMeta
CairoMakie.activate!(type = "png", px_per_unit = 2)
set_theme!(theme_latexfonts(); fontsize = 14)
d = Gamma(3.0, 1.5)
petri_env = joinpath(pkgdir(LoweredDistributions), "test", "algebraic_petri")
petri_script = joinpath(pkgdir(LoweredDistributions), "docs",
"algebraic_petri", "demo.jl")"/home/runner/work/LoweredDistributions.jl/LoweredDistributions.jl/docs/algebraic_petri/demo.jl"The demo script itself, printed from the file that is about to be run, so the page cannot drift from the code:
print(read(petri_script, String))# Run by the getting-started tutorial in the isolated `test/algebraic_petri`
# environment (AlgebraicPetri 0.10 caps Catalyst at 13, so it cannot share an
# environment with this package's Catalyst 16 extension). Run it by hand with:
#
# julia --project=test/algebraic_petri docs/algebraic_petri/demo.jl
using LoweredDistributions, Distributions, AlgebraicPetri
# The same delay the rest of the tutorial lowers: Gamma(3, 1.5), an exact
# three-compartment Erlang chain.
d = Gamma(3.0, 1.5)
built = petri_net(d)
println("places: ", snames(built.petri_net))
println("transitions: ", tnames(built.petri_net))
println("rates: ", sort(collect(built.rates), by = first))
println("u0: ", sort(collect(built.u0), by = first))
# AlgebraicPetri's own mass-action vectorfield, evaluated at t = 0. All the
# mass starts in the first compartment, so it drains into the second at the
# chain's per-stage rate and nothing else has moved yet.
f! = vectorfield(built.petri_net)
du = Dict(k => 0.0 for k in keys(built.u0))
f!(du, built.u0, built.rates, 0.0)
println("du at t = 0: ", sort(collect(du), by = first))
# A trajectory through the vectorfield, so the calling tutorial can plot it
# alongside the other backends'. This environment has no ODE solver (only
# AlgebraicPetri and Distributions, kept minimal to dodge the Catalyst version
# clash — see the module header), so a plain fixed-step RK4 over the
# `vectorfield` closure is enough: the generator is linear, so RK4 at a small
# step is accurate well past plotting resolution.
species_names = snames(built.petri_net)
function deriv(u::Vector{Float64})
udict = Dict(species_names[i] => u[i] for i in eachindex(species_names))
dudict = Dict(n => 0.0 for n in species_names)
f!(dudict, udict, built.rates, 0.0)
return [dudict[n] for n in species_names]
end
function rk4_step(u::Vector{Float64}, dt::Float64)
k1 = deriv(u)
k2 = deriv(u .+ (dt / 2) .* k1)
k3 = deriv(u .+ (dt / 2) .* k2)
k4 = deriv(u .+ dt .* k3)
return u .+ (dt / 6) .* (k1 .+ 2 .* k2 .+ 2 .* k3 .+ k4)
end
function print_trajectory(; dt::Float64 = 0.01, print_every::Int = 25)
tspan = 0.0:dt:15.0 # print_every * dt = 0.25 time units
u = [built.u0[n] for n in species_names]
for (i, t) in enumerate(tspan)
if (i - 1) % print_every == 0
for (n, v) in zip(species_names, u)
println("TRAJ,", t, ",", n, ",", v)
end
end
t == last(tspan) && break
u = rk4_step(u, dt)
end
return nothing
end
print_trajectory()And its real output, from a subprocess against that isolated environment. read throws on a non-zero exit, so a broken demo fails the docs build rather than silently printing nothing.
setup = `$(Base.julia_cmd()) --project=$petri_env -e "using Pkg; Pkg.instantiate()"`
demo = `$(Base.julia_cmd()) --project=$petri_env $petri_script`
demo_output = read(demo, String)
demo_lines = split(demo_output, '\n')
traj_lines = filter(l -> startswith(l, "TRAJ,"), demo_lines)
print(join(filter(l -> !startswith(l, "TRAJ,"), demo_lines), '\n'))[32m[1m Updating[22m[39m registry at `~/.julia/registries/General.toml`
[32m[1m Installed[22m[39m XML2_jll ───────────── v2.15.3+0
[32m[1m Installed[22m[39m JSON3 ──────────────── v1.14.3
[32m[1m Installed[22m[39m NameResolution ─────── v0.1.5
[32m[1m Installed[22m[39m Permutations ───────── v0.4.23
[32m[1m Installed[22m[39m GeneralizedGenerated ─ v0.3.3
[32m[1m Installed[22m[39m LabelledArrays ─────── v1.20.2
[32m[1m Installed[22m[39m GATlab ─────────────── v0.2.4
[32m[1m Installed[22m[39m Measures ───────────── v0.3.3
[32m[1m Installed[22m[39m Combinatorics ──────── v1.1.0
[32m[1m Installed[22m[39m Catlab ─────────────── v0.17.6
[32m[1m Installed[22m[39m CompTime ───────────── v0.1.2
[32m[1m Installed[22m[39m MLStyle ────────────── v0.4.17
[32m[1m Installed[22m[39m AlgebraicInterfaces ── v0.1.4
[32m[1m Installed[22m[39m StructEquality ─────── v2.1.0
[32m[1m Installed[22m[39m AlgebraicPetri ─────── v0.10.0
[32m[1m Installed[22m[39m JuliaVariables ─────── v0.2.4
[32m[1m Installed[22m[39m DataStructures ─────── v0.18.22
[32m[1m Installed[22m[39m StructTypes ────────── v1.11.0
[32m[1m Installed[22m[39m Compose ────────────── v0.9.7
[32m[1m Installed[22m[39m PrettyPrint ────────── v0.2.0
[32m[1m Installed[22m[39m LightXML ───────────── v0.9.3
[32m[1m Installed[22m[39m PEG ────────────────── v1.0.4
[33m[1m┌ [22m[39m[33m[1mWarning: [22m[39mtarball content does not match git-tree-sha1
[33m[1m└ [22m[39m[90m@ Pkg.Operations /opt/hostedtoolcache/julia/1.12.6/x64/share/julia/stdlib/v1.12/Pkg/src/Operations.jl:814[39m
[32m[1m Cloning[22m[39m [227ef7b5-1206-438b-ac65-934d6da304b8] ACSets from https://github.com/AlgebraicJulia/ACSets.jl.git
[32m[1m Installed[22m[39m ACSets ─────────────── v0.2.29
[32m[1m Installing[22m[39m 1 artifacts
[32m[1m Installed[22m[39m artifact XML2 2.2 MiB
[32m[1m Updating[22m[39m `~/work/LoweredDistributions.jl/LoweredDistributions.jl/test/algebraic_petri/Project.toml`
[90m[4f99eebe] [39m[92m+ AlgebraicPetri v0.10.0[39m
[90m[31c24e10] [39m[92m+ Distributions v0.25.130[39m
[90m[8539b7eb] [39m[92m+ LoweredDistributions v0.1.0 `../..`[39m
[90m[f8b46487] [39m[92m+ TestItemRunner v1.1.5[39m
[90m[1c621080] [39m[92m+ TestItems v1.0.0[39m
[90m[8dfed614] [39m[93m~ Test ⇒ v1.11.0[39m
[32m[1m Updating[22m[39m `~/work/LoweredDistributions.jl/LoweredDistributions.jl/test/algebraic_petri/Manifest.toml`
[90m[227ef7b5] [39m[92m+ ACSets v0.2.29[39m
[90m[7d9f7c33] [39m[92m+ Accessors v0.1.45[39m
[90m[79e6a3ab] [39m[92m+ Adapt v4.7.0[39m
[90m[23cfdc9f] [39m[92m+ AlgebraicInterfaces v0.1.4[39m
[90m[4f99eebe] [39m[92m+ AlgebraicPetri v0.10.0[39m
[90m[66dad0bd] [39m[92m+ AliasTables v1.1.3[39m
[90m[4fba245c] [39m[92m+ ArrayInterface v7.28.1[39m
[90m[134e5e36] [39m[92m+ Catlab v0.17.6[39m
[90m[d360d2e6] [39m[92m+ ChainRulesCore v1.26.1[39m
[90m[3da002f7] [39m[92m+ ColorTypes v0.12.1[39m
[90m[5ae59095] [39m[92m+ Colors v0.13.1[39m
[90m[861a8166] [39m[92m+ Combinatorics v1.1.0[39m
[90m[38540f10] [39m[92m+ CommonSolve v0.2.12[39m
[90m[bbf7d656] [39m[92m+ CommonSubexpressions v0.3.1[39m
[90m[0fb5dd42] [39m[92m+ CompTime v0.1.2[39m
[90m[34da2185] [39m[92m+ Compat v4.18.1[39m
[90m[a81c6b42] [39m[92m+ Compose v0.9.7[39m
[90m[a33af91c] [39m[92m+ CompositionsBase v0.1.2[39m
[90m[187b0558] [39m[92m+ ConstructionBase v1.6.0[39m
[90m[a8cc5b0e] [39m[92m+ Crayons v4.2.0[39m
[90m[9a962f9c] [39m[92m+ DataAPI v1.16.0[39m
[33m⌅[39m [90m[864edb3b] [39m[92m+ DataStructures v0.18.22[39m
[90m[e2d170a0] [39m[92m+ DataValueInterfaces v1.0.0[39m
[90m[163ba53b] [39m[92m+ DiffResults v1.1.0[39m
[90m[b552c78f] [39m[92m+ DiffRules v1.16.0[39m
[90m[31c24e10] [39m[92m+ Distributions v0.25.130[39m
[90m[ffbed154] [39m[92m+ DocStringExtensions v0.9.5[39m
[90m[e2ba6199] [39m[92m+ ExprTools v0.1.11[39m
[90m[1a297f60] [39m[92m+ FillArrays v1.17.0[39m
[33m⌅[39m [90m[53c48c17] [39m[92m+ FixedPointNumbers v0.8.6[39m
[90m[f6369f11] [39m[92m+ ForwardDiff v1.4.4[39m
[90m[f0ffcf3b] [39m[92m+ GATlab v0.2.4[39m
[90m[46192b85] [39m[92m+ GPUArraysCore v0.2.0[39m
[90m[a0844989] [39m[92m+ Gamma v1.1.0[39m
[90m[6b9d7cbe] [39m[92m+ GeneralizedGenerated v0.3.3[39m
[90m[34004b35] [39m[92m+ HypergeometricFunctions v0.3.30[39m
[90m[3587e190] [39m[92m+ InverseFunctions v0.1.17[39m
[90m[92d709cd] [39m[92m+ IrrationalConstants v0.2.6[39m
[90m[c8e1da08] [39m[92m+ IterTools v1.10.0[39m
[90m[82899510] [39m[92m+ IteratorInterfaceExtensions v1.0.0[39m
[90m[692b3bcd] [39m[92m+ JLLWrappers v1.8.0[39m
[33m⌅[39m [90m[682c06a0] [39m[92m+ JSON v0.21.4[39m
[90m[0f8b85d8] [39m[92m+ JSON3 v1.14.3[39m
[90m[b14d175d] [39m[92m+ JuliaVariables v0.2.4[39m
[90m[b964fa9f] [39m[92m+ LaTeXStrings v1.4.0[39m
[90m[2ee39098] [39m[92m+ LabelledArrays v1.20.2[39m
[90m[9c8b4983] [39m[92m+ LightXML v0.9.3[39m
[90m[2ab3a3ac] [39m[92m+ LogExpFunctions v1.0.1[39m
[90m[8539b7eb] [39m[92m+ LoweredDistributions v0.1.0 `../..`[39m
[90m[d8e11817] [39m[92m+ MLStyle v0.4.17[39m
[90m[1914dd2f] [39m[92m+ MacroTools v0.5.16[39m
[90m[442fdcdd] [39m[92m+ Measures v0.3.3[39m
[90m[e1d29d7a] [39m[92m+ Missings v1.2.0[39m
[90m[77ba4419] [39m[92m+ NaNMath v1.1.4[39m
[90m[71a1bf82] [39m[92m+ NameResolution v0.1.5[39m
[33m⌅[39m [90m[bac558e1] [39m[92m+ OrderedCollections v1.8.2[39m
[90m[90014a1f] [39m[92m+ PDMats v0.11.41[39m
[90m[12d937ae] [39m[92m+ PEG v1.0.4[39m
[90m[69de0a69] [39m[92m+ Parsers v2.8.6[39m
[90m[2ae35dd2] [39m[92m+ Permutations v0.4.23[39m
[90m[d236fae5] [39m[92m+ PreallocationTools v1.4.0[39m
[90m[aea7be01] [39m[92m+ PrecompileTools v1.3.4[39m
[90m[21216c6a] [39m[92m+ Preferences v1.5.2[39m
[90m[8162dcfd] [39m[92m+ PrettyPrint v0.2.0[39m
[90m[08abe8d2] [39m[92m+ PrettyTables v3.4.4[39m
[90m[43287f4e] [39m[92m+ PtrArrays v1.4.0[39m
[90m[1fd47b50] [39m[92m+ QuadGK v2.11.3[39m
[90m[3cdcf5f2] [39m[92m+ RecipesBase v1.3.4[39m
[90m[731186ca] [39m[92m+ RecursiveArrayTools v4.3.5[39m
[90m[189a3867] [39m[92m+ Reexport v1.2.2[39m
[90m[ae029012] [39m[92m+ Requires v1.3.1[39m
[90m[79098fc4] [39m[92m+ Rmath v0.9.0[39m
[90m[f2b01f46] [39m[92m+ Roots v3.0.6[39m
[90m[7e49a35a] [39m[92m+ RuntimeGeneratedFunctions v0.5.24[39m
[90m[431bcebd] [39m[92m+ SciMLPublic v1.2.4[39m
[90m[a2af1166] [39m[92m+ SortingAlgorithms v1.2.3[39m
[90m[276daf66] [39m[92m+ SpecialFunctions v2.8.1[39m
[90m[90137ffa] [39m[92m+ StaticArrays v1.9.18[39m
[90m[1e83bf80] [39m[92m+ StaticArraysCore v1.4.4[39m
[90m[10745b16] [39m[92m+ Statistics v1.11.1[39m
[90m[82ae8749] [39m[92m+ StatsAPI v1.8.0[39m
[90m[2913bbd2] [39m[92m+ StatsBase v0.34.12[39m
[90m[4c63d2b9] [39m[92m+ StatsFuns v2.2.1[39m
[90m[892a3eda] [39m[92m+ StringManipulation v0.4.7[39m
[90m[6ec83bb0] [39m[92m+ StructEquality v2.1.0[39m
[90m[856f2bd8] [39m[92m+ StructTypes v1.11.0[39m
[90m[2efcf032] [39m[92m+ SymbolicIndexingInterface v0.3.51[39m
[90m[3783bdb8] [39m[92m+ TableTraits v1.0.1[39m
[90m[bd369af6] [39m[92m+ Tables v1.13.0[39m
[90m[f8b46487] [39m[92m+ TestItemRunner v1.1.5[39m
[90m[1c621080] [39m[92m+ TestItems v1.0.0[39m
[90m[94ce4f54] [39m[92m+ Libiconv_jll v1.18.0+0[39m
[90m[efe28fd5] [39m[92m+ OpenSpecFun_jll v0.5.6+0[39m
[90m[f50d1b31] [39m[92m+ Rmath_jll v0.5.2+0[39m
[90m[02c8fc9c] [39m[92m+ XML2_jll v2.15.3+0[39m
[90m[0dad84c5] [39m[92m+ ArgTools v1.1.2[39m
[90m[56f22d72] [39m[92m+ Artifacts v1.11.0[39m
[90m[2a0f44e3] [39m[92m+ Base64 v1.11.0[39m
[90m[ade2ca70] [39m[92m+ Dates v1.11.0[39m
[90m[f43a241f] [39m[92m+ Downloads v1.7.0[39m
[90m[7b1f6079] [39m[92m+ FileWatching v1.11.0[39m
[90m[b77e0a4c] [39m[92m+ InteractiveUtils v1.11.0[39m
[90m[ac6e5ff7] [39m[92m+ JuliaSyntaxHighlighting v1.12.0[39m
[90m[b27032c2] [39m[92m+ LibCURL v0.6.4[39m
[90m[76f85450] [39m[92m+ LibGit2 v1.11.0[39m
[90m[8f399da3] [39m[92m+ Libdl v1.11.0[39m
[90m[37e2e46d] [39m[92m+ LinearAlgebra v1.12.0[39m
[90m[56ddb016] [39m[92m+ Logging v1.11.0[39m
[90m[d6f4376e] [39m[92m+ Markdown v1.11.0[39m
[90m[a63ad114] [39m[92m+ Mmap v1.11.0[39m
[90m[ca575930] [39m[92m+ NetworkOptions v1.3.0[39m
[90m[44cfe95a] [39m[92m+ Pkg v1.12.1[39m
[90m[de0858da] [39m[92m+ Printf v1.11.0[39m
[90m[3fa0cd96] [39m[92m+ REPL v1.11.0[39m
[90m[9a3f8284] [39m[92m+ Random v1.11.0[39m
[90m[ea8e919c] [39m[92m+ SHA v0.7.0[39m
[90m[9e88b42a] [39m[92m+ Serialization v1.11.0[39m
[90m[6462fe0b] [39m[92m+ Sockets v1.11.0[39m
[90m[2f01184e] [39m[92m+ SparseArrays v1.12.0[39m
[90m[f489334b] [39m[92m+ StyledStrings v1.11.0[39m
[90m[4607b0f0] [39m[92m+ SuiteSparse[39m
[90m[fa267f1f] [39m[92m+ TOML v1.0.3[39m
[90m[a4e569a6] [39m[92m+ Tar v1.10.0[39m
[90m[8dfed614] [39m[93m~ Test ⇒ v1.11.0[39m
[90m[cf7118a7] [39m[92m+ UUIDs v1.11.0[39m
[90m[4ec0a83e] [39m[92m+ Unicode v1.11.0[39m
[90m[e66e0078] [39m[92m+ CompilerSupportLibraries_jll v1.3.0+1[39m
[90m[deac9b47] [39m[92m+ LibCURL_jll v8.15.0+0[39m
[90m[e37daf67] [39m[92m+ LibGit2_jll v1.9.0+0[39m
[90m[29816b5a] [39m[92m+ LibSSH2_jll v1.11.3+1[39m
[90m[14a3606d] [39m[92m+ MozillaCACerts_jll v2025.11.4[39m
[90m[4536629a] [39m[92m+ OpenBLAS_jll v0.3.29+0[39m
[90m[05823500] [39m[92m+ OpenLibm_jll v0.8.7+0[39m
[90m[458c3c95] [39m[92m+ OpenSSL_jll v3.5.4+0[39m
[90m[bea87d4a] [39m[92m+ SuiteSparse_jll v7.8.3+2[39m
[90m[83775a58] [39m[92m+ Zlib_jll v1.3.1+2[39m
[90m[8e850b90] [39m[92m+ libblastrampoline_jll v5.15.0+0[39m
[90m[8e850ede] [39m[92m+ nghttp2_jll v1.64.0+1[39m
[90m[3f19e933] [39m[92m+ p7zip_jll v17.7.0+0[39m
[36m[1m Info[22m[39m Packages marked with [33m⌅[39m have new versions available but compatibility constraints restrict them from upgrading. To see why use `status --outdated -m`
[32m[1mPrecompiling[22m[39m packages...
644.9 ms[32m ✓ [39m[90mPEG[39m
661.8 ms[32m ✓ [39m[90mPrettyPrint[39m
802.2 ms[32m ✓ [39m[90mAlgebraicInterfaces[39m
859.3 ms[32m ✓ [39m[90mMeasures[39m
579.5 ms[32m ✓ [39m[90mStructEquality[39m
1816.5 ms[32m ✓ [39m[90mCombinatorics[39m
1221.2 ms[32m ✓ [39m[90mXML2_jll[39m
2841.0 ms[32m ✓ [39m[90mStructTypes[39m
2759.8 ms[32m ✓ [39m[90mDataStructures[39m
895.8 ms[32m ✓ [39m[90mNameResolution[39m
890.9 ms[32m ✓ [39m[90mPermutations[39m
1010.9 ms[32m ✓ [39m[90mLightXML[39m
1117.2 ms[32m ✓ [39m[90mSortingAlgorithms[39m
5247.5 ms[32m ✓ [39m[90mLabelledArrays[39m
3509.0 ms[32m ✓ [39m[90mQuadGK[39m
3845.2 ms[32m ✓ [39m[90mCompose[39m
5075.6 ms[32m ✓ [39m[90mStatsBase[39m
11125.4 ms[32m ✓ [39m[90mJSON3[39m
865.4 ms[32m ✓ [39m[90mPDMats → StatsBaseExt[39m
6291.7 ms[32m ✓ [39mDistributions
3434.8 ms[32m ✓ [39mDistributions → DistributionsTestExt
3440.5 ms[32m ✓ [39mLoweredDistributions
4771.0 ms[32m ✓ [39mDistributions → DistributionsChainRulesCoreExt
25747.9 ms[32m ✓ [39mTestItemRunner
29983.4 ms[32m ✓ [39m[90mMLStyle[39m
1939.8 ms[32m ✓ [39m[90mJuliaVariables[39m
2337.7 ms[32m ✓ [39m[90mCompTime[39m
4297.8 ms[32m ✓ [39m[90mGeneralizedGenerated[39m
11227.7 ms[32m ✓ [39m[90mACSets[39m
32790.6 ms[32m ✓ [39m[90mGATlab[39m
43959.9 ms[32m ✓ [39m[90mCatlab[39m
13820.3 ms[32m ✓ [39mAlgebraicPetri
2878.9 ms[32m ✓ [39mLoweredDistributions → LoweredDistributionsAlgebraicPetriExt
33 dependencies successfully precompiled in 124 seconds. 141 already precompiled.
places: [:state1, :state2, :state3, :state4]
transitions: [:state1_state2, :state2_state3, :state3_state4]
rates: [:state1_state2 => 0.6666666666666666, :state2_state3 => 0.6666666666666666, :state3_state4 => 0.6666666666666666]
u0: [:state1 => 1.0, :state2 => 0.0, :state3 => 0.0, :state4 => 0.0]
du at t = 0: [:state1 => -0.6666666666666666, :state2 => 0.6666666666666666, :state3 => 0.0, :state4 => 0.0]The transitions are the chain's interior hops plus its exit, the rates are all the same per-stage rate 1/1.5, and du at time zero shows the mass draining out of the first compartment into the second — the same generator the ODE and jump backends read, in Petri-net clothing.
The vectorfield reproduces the same trajectory
The demo script also integrates vectorfield(built.petri_net) forward (a plain fixed-step RK4, since the isolated environment carries no ODE solver of its own — see the demo source above) and prints the result as TRAJ rows, parsed back here into a DataFrame for plotting.
traj_df = reduce(vcat,
let parts = split(line, ',')
DataFrame(t = parse(Float64, parts[2]), place = parts[3],
value = parse(Float64, parts[4]))
end
for line in traj_lines)
places_by_order = sort(unique(traj_df.place);
by = s -> parse(Int, replace(s, "state" => "")))
absorbed = places_by_order[end]
traj_df.label = [p == absorbed ? "absorbed" : p for p in traj_df.place]
draw(
data(traj_df) *
mapping(:t, :value, color = :label) *
visual(Lines, linewidth = 2);
axis = (xlabel = "Time", ylabel = "Occupancy probability")
)The absorbed place is the Petri net's own view of the CDF: plotting it against the exact Gamma(3, 1.5) CDF is the same faithfulness check the ODE page makes, reached through the mass-action vectorfield instead of the linear Kolmogorov ODE.
absorbed_df = @rsubset(traj_df, :place == absorbed)
cdf_df = vcat(
DataFrame(t = absorbed_df.t, value = absorbed_df.value,
kind = "Absorbed mass (Petri vectorfield)"),
DataFrame(t = absorbed_df.t, value = cdf.(d, absorbed_df.t),
kind = "Exact Gamma CDF")
)
draw(
data(cdf_df) *
mapping(:t, :value, color = :kind, linestyle = :kind) *
visual(Lines, linewidth = 2);
axis = (xlabel = "Time", ylabel = "Probability")
)