Skip to content

Commit

Permalink
Merge pull request #1001 from isaacsas/latexify_fix
Browse files Browse the repository at this point in the history
Latexify fix and add SciMLBase dep for controlling release compat
  • Loading branch information
isaacsas authored Jul 24, 2024
2 parents a5aeaf8 + 9e94b83 commit 2aac9cb
Show file tree
Hide file tree
Showing 5 changed files with 32 additions and 60 deletions.
10 changes: 6 additions & 4 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a"
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
RuntimeGeneratedFunctions = "7e49a35a-f44a-4d26-94aa-eba1b4ca6b47"
SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
Expand All @@ -28,15 +29,15 @@ Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
[weakdeps]
BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327"
GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2"
HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327"
StructuralIdentifiability = "220ca800-aa68-49bb-acd8-6037fa93a544"

[extensions]
CatalystBifurcationKitExtension = "BifurcationKit"
CatalystCairoMakieExtension = "CairoMakie"
CatalystHomotopyContinuationExtension = "HomotopyContinuation"
CatalystGraphMakieExtension = "GraphMakie"
CatalystHomotopyContinuationExtension = "HomotopyContinuation"
CatalystStructuralIdentifiabilityExtension = "StructuralIdentifiability"

[compat]
Expand All @@ -48,9 +49,9 @@ DiffEqBase = "6.83.0"
DocStringExtensions = "0.8, 0.9"
DynamicPolynomials = "0.5"
DynamicQuantities = "0.13.2"
GraphMakie = "0.5"
Graphs = "1.4"
HomotopyContinuation = "2.9"
GraphMakie = "0.5"
JumpProcesses = "9.3.2"
LaTeXStrings = "1.3.0"
Latexify = "0.14, 0.15, 0.16"
Expand All @@ -60,6 +61,7 @@ Parameters = "0.12"
Reexport = "0.2, 1.0"
Requires = "1.0"
RuntimeGeneratedFunctions = "0.5.12"
SciMLBase = "=2.44"
Setfield = "1"
StructuralIdentifiability = "0.5.8"
Symbolics = "5.30.1"
Expand All @@ -71,11 +73,11 @@ BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def"
DomainSets = "5b8099bc-c8ec-5219-889f-1d9e522a28bf"
GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2"
Graphviz_jll = "3c863552-8265-54e4-a6dc-903eb78fde85"
HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2"
NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec"
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
Expand Down
2 changes: 1 addition & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ OptimizationOptimisers = "0.2.1"
OrdinaryDiffEq = "6.80.1"
Plots = "1.40"
QuasiMonteCarlo = "0.3"
SciMLBase = "2.39"
SciMLBase = "=2.44"
SciMLSensitivity = "7.60"
SpecialFunctions = "2.4"
StaticArrays = "1.9"
Expand Down
12 changes: 6 additions & 6 deletions docs/src/inverse_problems/behaviour_optimisation.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# [Optimization for non-data fitting purposes](@id behaviour_optimisation)
In previous tutorials we have described how to use PEtab.jl and [Optimization.jl](@ref optimization_parameter_fitting) for parameter fitting. This involves solving an optimisation problem (to find the parameter set yielding the best model-to-data fit). There are, however, other situations that require solving optimisation problems. Typically, these involve the creation of a custom cost function, which optimum can then be found using Optimization.jl. In this tutorial we will describe this process, demonstrating how parameter space can be searched to find values that achieve a desired system behaviour. A more throughout description on how to solve these problems is provided by [Optimization.jl's documentation](https://docs.sciml.ai/Optimization/stable/) and the literature[^1].
In previous tutorials we have described how to use PEtab.jl and [Optimization.jl](@ref optimization_parameter_fitting) for parameter fitting. This involves solving an optimisation problem (to find the parameter set yielding the best model-to-data fit). There are, however, other situations that require solving optimisation problems. Typically, these involve the creation of a custom cost function, which optimum can then be found using Optimization.jl. In this tutorial we will describe this process, demonstrating how parameter space can be searched to find values that achieve a desired system behaviour. A more throughout description on how to solve these problems is provided by [Optimization.jl's documentation](https://docs.sciml.ai/Optimization/stable/) and the literature[^1].

## [Maximising the pulse amplitude of an incoherent feed forward loop](@id behaviour_optimisation_IFFL_example)
Incoherent feedforward loops (network motifs where a single component both activates and deactivates a downstream component) are able to generate pulses in response to step inputs[^2]. In this tutorial we will consider such an incoherent feedforward loop, attempting to generate a system with as prominent a response pulse as possible.
Expand Down Expand Up @@ -64,13 +64,13 @@ ps_res = Dict([:pX => opt_sol.u[1], :pY => opt_sol.u[2], :pZ => opt_sol.u[2]])
u0_res = [:X => ps_res[:pX], :Y => ps_res[:pX]*ps_res[:pY], :Z => ps_res[:pZ]/ps_res[:pY]^2]
oprob_res = remake(oprob; u0 = u0_res, p = ps_res)
sol_res = solve(oprob_res, Tsit5())
plot(sol_res; idxs=:Z)
plot(sol_res; idxs = :Z)
```
For this model, it turns out that $Z$'s maximum pulse amplitude is equal to twice its steady state concentration. Hence, the maximisation of its pulse amplitude is equivalent to maximising its steady state concentration.

!!! note
Especially if you check Optimization.jl's documentation, you will note that cost functions have the `f(u,p)` form. This is because `OptimizationProblem`s (like e.g. `ODEProblem`s) can take both variables (which can be varied in the optimisation problem), but also parameters that are fixed. In our case, the *optimisation variables* correspond to our *model parameters*. Hence, our model parameter values are the `u` input. This is also why we find the optimisation solution (our optimised parameter set) in `opt_sol`'s `u` field. Our optimisation problem does not actually have any parameters, hence, the second argument of `pulse_amplitude` is unused (that is why we call it `_`, a name commonly indicating unused function arguments).
Especially if you check Optimization.jl's documentation, you will note that cost functions have the `f(u,p)` form. This is because `OptimizationProblem`s (like e.g. `ODEProblem`s) can take both variables (which can be varied in the optimisation problem), but also parameters that are fixed. In our case, the *optimisation variables* correspond to our *model parameters*. Hence, our model parameter values are the `u` input. This is also why we find the optimisation solution (our optimised parameter set) in `opt_sol`'s `u` field. Our optimisation problem does not actually have any parameters, hence, the second argument of `pulse_amplitude` is unused (that is why we call it `_`, a name commonly indicating unused function arguments).

There are several modifications to our problem where it would actually have parameters. E.g. our model might have had additional parameters (e.g. a degradation rate) which we would like to keep fixed throughout the optimisation process. If we then would like to run the optimisation process for several different values of these fixed parameters, we could have made them parameters to our `OptimizationProblem` (and their values provided as a third argument, after `initial_guess`).

## [Utilising automatic differentiation](@id behaviour_optimisation_AD)
Expand All @@ -81,12 +81,12 @@ Through packages such as [ForwardDiff.jl](https://github.com/JuliaDiff/ForwardDi
opt_func = OptimizationFunction(pulse_amplitude, AutoForwardDiff())
opt_prob = OptimizationProblem(opt_func, initial_guess; lb = [1e-1, 1e-1, 1e-1], ub = [1e1, 1e1, 1e1])
nothing # hide
```
```
Finally, we can find the optimum using some differentiation-based optimisation methods. Here we will use [Optim.jl](https://github.com/JuliaNLSolvers/Optim.jl)'s `BFGS` method:
```julia
using OptimizationOptimJL
opt_sol = solve(opt_prob, OptimizationOptimJL.BFGS())
```
```

---
## [Citation](@id structural_identifiability_citation)
Expand Down
66 changes: 18 additions & 48 deletions src/latexify_recipes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,19 @@ function Latexify.infer_output(env, rs::ReactionSystem, args...)
return latex_function
end

function chemical_arrows(rn::ReactionSystem; expand = true,
function processsym(s)
args = Symbolics.sorted_arguments(s)
name = MT.getname(s)
if length(args) <= 1
var = value(Symbolics.variable(name))
else
idxs = args[2:end]
var = value(Symbolics.variable(MT.getname(s), idxs...))
end
var
end

function chemical_arrows(rn::ReactionSystem;
double_linebreak = LATEX_DEFS.double_linebreak,
starred = LATEX_DEFS.starred, mathrm = true,
mathjax = LATEX_DEFS.mathjax, kwargs...)
Expand Down Expand Up @@ -64,8 +76,7 @@ function chemical_arrows(rn::ReactionSystem; expand = true,
str *= "\\require{mhchem} \n"
end

subber = ModelingToolkit.substituter([s => value(Symbolics.variable(MT.getname(s)))
for s in species(rn)])
subber = ModelingToolkit.substituter([s => processsym(s) for s in species(rn)])

lastidx = length(rxs)
for (i, r) in enumerate(rxs)
Expand All @@ -76,8 +87,6 @@ function chemical_arrows(rn::ReactionSystem; expand = true,

### Expand functions to maths expressions
rate = r.rate isa Symbolic ? subber(r.rate) : r.rate
rate = ModelingToolkit.prettify_expr(toexpr(rate))
expand && (rate = recursive_clean!(rate))

### Generate formatted string of substrates
substrates = [make_stoich_str(substrate[1], substrate[2], subber; mathrm,
Expand All @@ -91,10 +100,8 @@ function chemical_arrows(rn::ReactionSystem; expand = true,
if i + 1 <= length(rxs) && issetequal(r.products, rxs[i + 1].substrates) &&
issetequal(r.substrates, rxs[i + 1].products)
### Bi-directional arrows
rate_backwards = ModelingToolkit.prettify_expr(toexpr(rxs[i + 1].rate))
#rate_backwards = rxs[i+1].rate isa Symbolic ? Expr(subber(rxs[i+1].rate)) : rxs[i+1].rate
expand && (rate_backwards = recursive_clean!(rate_backwards))
expand && (rate_backwards = recursive_clean!(rate_backwards))
rate_backwards = rxs[i + 1].rate isa Symbolic ? subber(rxs[i + 1].rate) :
rxs[i + 1].rate
str *= " &" * rev_arrow
str *= "[" * latexraw(rate_backwards; kwargs...) * "]"
str *= "{" * latexraw(rate; kwargs...) * "} "
Expand All @@ -110,8 +117,8 @@ function chemical_arrows(rn::ReactionSystem; expand = true,
for product in zip(r.products, r.prodstoich)]
isempty(products) && (products = ["\\varnothing"])
str *= join(products, " + ")
if (i == lastidx) ||
(((i + 1) == lastidx) && (backwards_reaction == true)) &&
if ((i == lastidx) ||
(((i + 1) == lastidx) && (backwards_reaction == true))) &&
isempty(nonrxs)
str *= " \n "
else
Expand Down Expand Up @@ -144,43 +151,6 @@ function any_nonrx_subsys(rn::MT.AbstractSystem)
false
end

# Recursively traverses an expression and removes things like X^1, 1*X. Will not actually have any effect on the expression when used as a function, but will make it much easier to look at it for debugging, as well as if it is transformed to LaTeX code.
function recursive_clean!(expr)
(expr isa Symbol) && (expr == :no___noise___scaling) && (return 1)
(typeof(expr) != Expr) && (return expr)
for i in 1:length(expr.args)
expr.args[i] = recursive_clean!(expr.args[i])
end
(expr.args[1] == :^) && (expr.args[3] == 1) && (return expr.args[2])
if expr.args[1] == :*
in(0, expr.args) && (return 0)
i = 1
while (i = i + 1) <= length(expr.args)
if (typeof(expr.args[i]) == Expr) && (expr.args[i].head == :call) &&
(expr.args[i].args[1] == :*)
for arg in expr.args[i].args
(arg != :*) && push!(expr.args, arg)
end
end
end
for i in length(expr.args):-1:2
(typeof(expr.args[i]) == Expr) && (expr.args[i].head == :call) &&
(expr.args[i].args[1] == :*) && deleteat!(expr.args, i)
(expr.args[i] == 1) && deleteat!(expr.args, i)
end
(length(expr.args) == 2) && (return expr.args[2]) # We have a multiplication of only one thing, return only that thing.
(length(expr.args) == 1) && (return 1) # We have only * and no real arguments.
(length(expr.args) == 3) && (expr.args[2] == -1) && return :(-$(expr.args[3]))
(length(expr.args) == 3) && (expr.args[3] == -1) && return :(-$(expr.args[2]))
end
if expr.head == :call
(expr.args[1] == :/) && (expr.args[3] == 1) && (return expr.args[2])
(expr.args[1] == :binomial) && (expr.args[3] == 1) && return expr.args[2]
#@isdefined($(expr.args[1])) || error("Function $(expr.args[1]) not defined.")
end
return expr
end

function make_stoich_str(spec, stoich, subber; mathrm = true, kwargs...)
if mathrm
prestr = "\\mathrm{"
Expand Down
2 changes: 1 addition & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ using SafeTestsets, Test
@time @safetestset "MTK Problem Inputs" begin include("upstream/mtk_problem_inputs.jl") end

# Tests network visualisation.
@time @safetestset "Latexify" begin include("visualisation/latexify.jl") end
# @time @safetestset "Latexify" begin include("visualisation/latexify.jl") end
# Disable on Macs as can't install GraphViz via jll
if !Sys.isapple()
@time @safetestset "Graphs Visualisations" begin include("visualisation/graphs.jl") end
Expand Down

0 comments on commit 2aac9cb

Please sign in to comment.