From 9d774bd48e29e390b4de120156a779eebe345871 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 27 Dec 2023 17:21:35 +0100 Subject: [PATCH 01/80] init --- README.md | 4 + .../bifurcation_diagrams.md | 2 +- .../homotopy_continuation.md | 2 +- .../catalyst_applications/nonlinear_solve.md | 2 +- .../catalyst_for_new_julia_users.md | 164 +++++++++++------- .../petab_ode_param_fitting.md | 2 +- 6 files changed, 114 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 587a71b7e6..bdb1ef3004 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ the current master branch. Several Youtube video tutorials and overviews are also available, but note these use older Catalyst versions with slightly different notation (for example, in building reaction networks): +- From JuliaCon 2023: A short 15 minute overview of Catalyst as of version 13 is +available in the talk [Catalyst.jl, Modeling Chemical Reaction Networks](https://www.youtube.com/watch?v=yreW94n98eM&ab_channel=TheJuliaProgrammingLanguage). - From JuliaCon 2022: A three hour tutorial workshop overviewing how to use Catalyst and its more advanced features as of version 12.1. [Workshop video](https://youtu.be/tVfxT09AtWQ), [Workshop Pluto.jl @@ -60,6 +62,8 @@ Catalyst.jl](https://www.youtube.com/watch?v=5p1PJE5A5Jw). Modelling of Biochemical Reaction Networks](https://www.youtube.com/watch?v=s1e72k5XD6s) +Finally, an overview of the package and its features (as of version 13) can also be found in its corresponding research paper, [Catalyst: Fast and flexible modeling of reaction networks](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1011530). + ## Features - A DSL provides a simple and readable format for manually specifying chemical diff --git a/docs/src/catalyst_applications/bifurcation_diagrams.md b/docs/src/catalyst_applications/bifurcation_diagrams.md index bb73ae368a..55eab485a2 100644 --- a/docs/src/catalyst_applications/bifurcation_diagrams.md +++ b/docs/src/catalyst_applications/bifurcation_diagrams.md @@ -1,6 +1,6 @@ # [Bifurcation Diagrams](@id bifurcation_diagrams) -Bifurcation diagrams describe how, for a dynamical system, the quantity and type of its steady states change as a parameter is varied[^1]. When using Catalyst-generated models, these can be computed with the [BifurcationKit.jl](https://github.com/bifurcationkit/BifurcationKit.jl) package. Catalyst provides a simple interface for creating BifurcationKit compatible `BifurcationProblem`s from `ReactionSystem`s. If you use this feature in your research, please [cite the BifurcationKit.jl](@ref bifurcation_kit_citation) and [Catalyst.jl](@ref catalyst_citation) publications. +Bifurcation diagrams describe how, for a dynamical system, the quantity and type of its steady states change as a parameter is varied[^1]. When using Catalyst-generated models, these can be computed with the [BifurcationKit.jl](https://github.com/bifurcationkit/BifurcationKit.jl) package. Catalyst provides a simple interface for creating BifurcationKit compatible `BifurcationProblem`s from `ReactionSystem`s. This tutorial briefly introduces how to use Catalyst with BifurcationKit through basic examples, with BifurcationKit.jl providing [a more extensive documentation](https://bifurcationkit.github.io/BifurcationKitDocs.jl/stable/). Especially for more complicated systems, where careful tuning of algorithm options might be required, reading the BifurcationKit documentation is recommended. Finally, BifurcationKit provides many additional features not described here, including [computation of periodic orbits](https://bifurcationkit.github.io/BifurcationKitDocs.jl/stable/periodicOrbit/), [tracking of bifurcation points along secondary parameters](https://bifurcationkit.github.io/BifurcationKitDocs.jl/dev/branchswitching/), and [bifurcation computations for PDEs](https://bifurcationkit.github.io/BifurcationKitDocs.jl/dev/tutorials/tutorials/#PDEs:-bifurcations-of-equilibria). diff --git a/docs/src/catalyst_applications/homotopy_continuation.md b/docs/src/catalyst_applications/homotopy_continuation.md index 9b961ffb23..1fb751bc15 100644 --- a/docs/src/catalyst_applications/homotopy_continuation.md +++ b/docs/src/catalyst_applications/homotopy_continuation.md @@ -9,7 +9,7 @@ integer Hill exponents). The roots of these can reliably be found through a *homotopy continuation* algorithm. This is implemented in Julia through the [HomotopyContinuation.jl](https://www.juliahomotopycontinuation.org/) package. -Catalyst contains a special homotopy continuation extension that is loaded whenever HomotopyContinuation.jl is. This exports a single function, `hc_steady_states`, that can be used to find the steady states of any `ReactionSystem` structure. If you use this in your research, please [cite the HomotopyContinuation.jl](@ref homotopy_continuation_citation) and [Catalyst.jl](@ref catalyst_citation) publications. +Catalyst contains a special homotopy continuation extension that is loaded whenever HomotopyContinuation.jl is. This exports a single function, `hc_steady_states`, that can be used to find the steady states of any `ReactionSystem` structure. ## Basic example For this tutorial, we will use a model from Wilhelm (2009)[^1] (which diff --git a/docs/src/catalyst_applications/nonlinear_solve.md b/docs/src/catalyst_applications/nonlinear_solve.md index 4692105d56..0d4a903714 100644 --- a/docs/src/catalyst_applications/nonlinear_solve.md +++ b/docs/src/catalyst_applications/nonlinear_solve.md @@ -6,7 +6,7 @@ We have previously described how `ReactionSystem` steady states can be found thr However, if all (or multiple) steady states are sought, using homotopy continuation is better. -This tutorial describes how to create `NonlinearProblem`s from Catalyst's `ReactionSystemn`s, and how to solve them using NonlinearSolve. More extensive descriptions of available solvers and options can be found in [NonlinearSolve's documentation](https://docs.sciml.ai/NonlinearSolve/stable/). If you use this in your research, please [cite the NonlinearSolve.jl](@ref nonlinear_solve_citation) and [Catalyst.jl](@ref catalyst_citation) publications. +This tutorial describes how to create `NonlinearProblem`s from Catalyst's `ReactionSystemn`s, and how to solve them using NonlinearSolve. More extensive descriptions of available solvers and options can be found in [NonlinearSolve's documentation](https://docs.sciml.ai/NonlinearSolve/stable/). ## Basic example Let us consider a simple dimerisation network, where a protein ($P$) can exist in a monomer and a dimer form. The protein is produced at a constant rate from its mRNA, which is also produced at a constant rate. diff --git a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md index 48c3593ec2..1d359a6803 100644 --- a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md +++ b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md @@ -1,181 +1,229 @@ # [Introduction to Catalyst and Julia for New Julia users](@id catalyst_for_new_julia_users) The Catalyst tool for the modelling of chemical reaction networks is based in the Julia programming language. While experience in Julia programming is advantageous for using Catalyst, it is not necessary for accessing some of its basic features. This tutorial serves as an introduction to Catalyst for those unfamiliar with Julia, also introducing some basic Julia concepts. Anyone who plans on using Catalyst extensively is recommended to familiarise oneself more thoroughly with the Julia programming language. A collection of resources for learning Julia can be found [here](https://julialang.org/learning/), and a full documentation is available [here](https://docs.julialang.org/en/v1/). -Julia can be downloaded [here](https://julialang.org/downloads/). +Julia can be downloaded [here](https://julialang.org/downloads/). Generally, it is recommended to use the [*juliaup*](https://github.com/JuliaLang/juliaup) tool to install and update Julia. Furthermore, *Visual Studio Code* is a good IDE with [extensive Julia support](https://code.visualstudio.com/docs/languages/julia), and a good default choice IDE. *Users who are already familiar with Julia can skip to the [Introduction to Catalyst](@ref introduction_to_catalyst) tutorial.* ## Basic Julia usage On the surface, Julia has many similarities to languages like MATLAB, Python, and R. -*Values* can be assigned to *variables* through the use of a `=` sign. Values (possibly stored in variables) can be used for most basic computations. +*Values* can be assigned to *variables* through `=` sign. Values (possibly stored in variables) can be used for most basic computations. ```@example ex1 length = 2.0 width = 4.0 -area = length*width +area = length * width ``` -*Functions* take one or more inputs (enclosed by `()`) and return some output. E.g. the `min` function returns the minimum of two values +*Functions* take one or more inputs (enclosed by `()`) and return some output. E.g. the `min` function returns the minimum of two values. ```@example ex1 min(1.0, 3.0) ``` -A line of Julia code is not required to end with `;`, however, if it does, the output of that line is not displayed. -```julia -min(1.0, 3.0); -``` - Each Julia variable has a specific *type*, designating what type of value it is. While not directly required to use Catalyst, this is useful to be aware of. To learn the type of a specific variable, use the `typeof` function. More information about types can be [found here](https://docs.julialang.org/en/v1/manual/types/). ```@example ex1 typeof(1.0) ``` - Here, `Float64` denotes decimal-valued numbers. Integer-valued numbers instead are of the `Int64` type. ```@example ex1 typeof(1) ``` +There exists a large number of Julia types (with even more being defined by various packages). Additional examples include `String`s (defined by enclosing text within `" "`): +```@example ex1 +"Hello world!" +``` +and `Symbol`s (defined by pre-appending an expression with `:`): +```@example ex1 +:Julia +``` -Finally, we note that the first time some code is run in Julia, it has to be *compiled*. However, this is only required once per Julia session. Hence, the second time the same code is run, it runs much faster. E.g. try running this line of code first one time, and then one additional time. You will note that the second run is much faster. +Finally, we note that the first time some code is run in Julia, it has to be [*compiled*](https://en.wikipedia.org/wiki/Just-in-time_compilation). However, this is only required once per Julia session. Hence, the second time the same code is run, it runs much faster. E.g. try running this line of code first one time, and then one additional time. You will note that the second run is much faster. ```@example ex1 rand(100, 100)^3.5 nothing # hide ``` (This code creates a random 100x100 matrix, and takes it to the power of 3.5) -This is useful to know when you e.g. declare, simulate, or plot, a Catalyst model. The first time you run a command there might be a slight delay. However, subsequent runs will execute much quicker. This holds even if you do minor adjustments before the second run (such as changing simulation initial conditions). +This is useful to know when you e.g. declare, simulate, or plot, a Catalyst model. The first time you run a command there might be a slight delay. However, subsequent runs will be much quicker. This holds even if you make minor adjustments before the second run (such as changing simulation initial conditions). -## Installing and activating packages -Except for some base Julia packages (such as Pkg, the package manager) that are available by default, Julia packages must be installed locally before they can be used. Most packages are registered with Julia, and can be added through the `Pkg.add("DesiredPackage")` command (where `DesiredPackage` is the name of the package you wish to install). We can thus install Catalyst: +## [Package management in Julia](@id catalyst_for_new_julia_users_packages) +Due to its native package manager, and a registry of almost all packages of relevancy, package management in Julia is unusually easy. In principle, Catalyst can be both installed, and then imported into a session, using only: ```julia using Pkg Pkg.add("Catalyst") +using Catalyst ``` +However, over the next three subsections we describe some additional considerations and useful commands. It is recommended to read and understand these at a relatively early stage. For a more detailed introduction to Julia package management, please read [the Pkg documentation](https://docs.julialang.org/en/v1/stdlib/Pkg/). -Here, the command `using Pkg` is required to activate the Pkg package manager. +### [Setting up a new Julia environment](@id catalyst_for_new_julia_users_packages_environments) +Whenever you run Julia, it will run in a specific *environment*. You can specify any folder on your computer as a Julia environment. If you start Julia in that folder, that environment will be used (or, this will at least be the case for most modes of running Julia). If you start Julia in a folder not corresponding to an environment, your *default* Julia environment is used. While it is possible to not worry about environments (and always use the default environment), this can lead to long-term problems as more packages are installed. -Next, we also wish to add the `DifferentialEquations` and `Plots` packages (for numeric simulation of models, and plotting, respectively). +To activate your current folder as an environment, run the following commands: ```julia -Pkg.add("DifferentialEquations") -Pkg.add("Plots") +using Pkg +Pkg.activate(".") ``` -Once a package has been installed through the `Pkg.add` command, this command does not have to be repeated in further Julia sessions on the same machine. +This will: +1. If your current folder (which can be displayed using the `pwd()` command) is not designated as a possible Julia environment, designate it as such. +2. Switch your current Julia session to use the current folder's environment. -Installing a Julia package is, however, not enough to use it. Before a package's features are used in a Julia session, it has to be loaded through the `using DesiredPackage` command (where `DesiredPackage` is the name of the package you wish to activate). This command has to be repeated whenever a Julia session is restarted. +!!! note + If you check any folder which has been designated as a Julia environment, it contains a Project.toml and a Manifest.toml file. These store all information regarding the corresponding environment. For non-advanced users, it is recommended to never touch these files directly (and instead do so using various functions from the Pkg package, the important ones which are described in the next two subsections). -We thus activate our three desired packages: +### [Installing and importing packages in Julia](@id catalyst_for_new_julia_users_packages_installing) +To import a Julia package into a session, you can use the `using PackageName` command (where `PackageName` is the name of the package you wish to activate). However, before you can do so, it must first be installed on your computer. Almost all relevant Julia packages are registered, and can be added through the `Pkg.add("PackageName")` command. We can thus install Catalyst through: +```julia +using Pkg +Pkg.add("Catalyst") +``` +Here, the Julia package manager package (`Pkg`) is by default installed on your computer when Julia is installed, and can be activated directly. -```@example ex1 +We can now use Catalyst and its features by running: +```@example ex2 using Catalyst +``` +This will only make Catalyst available for the current Julia session. If you exit Julia, you will have to run `using Catalyst` again to use its features (however, `Pkg.add("Catalyst")` does not need to be rerun). Next, we wish to install the `DifferentialEquations` and `Plots` packages (for numeric simulation of models, and plotting, respectively): +```julia +Pkg.add("DifferentialEquations") +Pkg.add("Plots") +``` +and also to import them into our current session: +```@example ex2 using DifferentialEquations using Plots ``` -For a more detailed introduction to Julia packages, please read [the Pkg documentation](https://docs.julialang.org/en/v1/stdlib/Pkg/). +### [Why environments are important](@id catalyst_for_new_julia_users_packages_environment_importance) +We have previously described how to set up new Julia environments, how to install Julia packages, and how to import them into a current session. Let us say that you were to restart Julia in a new folder and activate this as a separate environment. If you then try to import Catalyst through `using Catalyst` you will receive an error claiming that Catalyst was not found. The reason is that the `Pkg.add("Catalyst")` command actually carries out two separate tasks: +1. If Catalyst is not already installed on your computer, install it. +2. Add Catalyst as an available package to your current environment. + +Here, while Catalyst has previously been installed on your computer, it has not been added to the new environment you created. To do so, simply run +```julia +using Pkg +Pkg.add("Catalyst") +``` +after which Catalyst can be imported through `using Catalyst`. You can get a list of all packages available in your current environment using: +```julia +Pkg.status() +``` + +So, why is this required, and why cannot we simply import any package installed on our computer? The reason is that most packages depend on other packages, and these dependencies may be restricted to only specific versions of these packages. This creates complicated dependency graphs that restrict what versions of what packages are compatible with each other. When you use `Pkg.add("PackageName")`, only a specific version of that package is actually added (the latest possible version as permitted by the dependency graph). Here, Julia environments both define what packages are available *and* their respective versions (these versions are also displayed by the `Pkg.status()` command). By doing this, Julia can guarantee that the packages (and their versions) specified in an environment are compatible with each other. + +The reason why all this is important is that it is *highly recommended* to, for each project, define a separate environment. To these, only add the required packages. General-purpose environments with a large number of packages often produce package-incompatibility issues. While these might not prevent you from installing all desired package, they often mean that you are unable to use the latest version of some packages. + +!!! note + A not-infrequent cause for reported errors with Catalyst (typically the inability to replicate code in tutorials) is package incompatibilities in large environments preventing the latest version of Catalyst from being installed. Hence, whenever an issue is encountered, it is useful to run `Pkg.status()` to check whenever the latest version of Catalyst is being used. + +Some additional useful Pkg commands are: +- `Pk.rm("PackageName")` removes a package from the current environment. +- `Pkg.update(PackageName")`: updates the designated package. +!!! note + A useful feature of Julia's environment system is that enables the exact definition of what packages and versions were used to execute a script. This supports e.g. reproducibility in academic research. Here, by providing the corresponding Project.toml and Manifest.toml files, you can enable someone to reproduce the exact program just to perform some set of analyses. + ## Simulating a basic Catalyst model -Now that we have some basic familiarity with Julia, and have installed and activated the required packages, we will create and simulate a basic chemical reaction network model through Catalyst. +Now that we have some basic familiarity with Julia, and have installed and imported the required packages, we will create and simulate a basic chemical reaction network model using Catalyst. -Catalyst models are created through the `@reaction_network` *macro*. For more information on macros, please read [the Julia documentation on macros](https://docs.julialang.org/en/v1/manual/metaprogramming/#man-macros). This documentation is, however, rather advanced (and not required to use Catalyst). We instead recommend that you simply familiarise yourself with the Catalyst syntax, without studying in detail how macros work and what they are. +Catalyst models are created through the `@reaction_network` *macro*. For more information on macros, please read [the Julia documentation on macros](https://docs.julialang.org/en/v1/manual/metaprogramming/#man-macros). This documentation is, however, rather advanced (and not required to use Catalyst). We instead recommend that you simply familiarise yourself with the Catalyst syntax, without studying in detail how macros work and what they are. -The `@reaction_network` command is followed by the `begin` keyword, which is followed by one line for each *reaction* of the model. Each reaction consists of a *reaction rate*, followed by the reaction itself. The reaction itself contains a set of *substrates* and a set of *products* (what is consumed and produced by the reaction, respectively). These are separated by a `-->` arrow. Finally, the model ends with the `end` keyword. +The `@reaction_network` command is followed by the `begin` keyword, which is followed by one line for each *reaction* of the model. Each reaction consists of a *reaction rate*, followed by the reaction itself. The reaction contains a set of *substrates* and a set of *products* (what is consumed and produced by the reaction, respectively). These are separated by a `-->` arrow. Finally, the model ends with the `end` keyword. Here, we create a simple *birth-death* model, where a single species (*X*) is created at rate *b*, and degraded at rate *d*. The model is stored in the variable `rn`. - -```@example ex1 +```@example ex2 rn = @reaction_network begin b, 0 --> X d, X --> 0 end ``` - For more information on how to use the Catalyst model creator (also known as the Catalyst DSL), please read [the corresponding documentation](https://docs.sciml.ai/Catalyst/stable/catalyst_functionality/dsl_description/). Next, we wish to simulate our model. To do this, we need to provide some additional information to the simulator. This is -* The initial condition. That is, the concentration or numbers of each species at the start of the simulation. +* The initial condition. That is, the concentration (or copy numbers) of each species at the start of the simulation. * The timespan. That is, the timeframe over which we wish to run the simulation. * The parameter values. That is, the values of the model's parameters for this simulation. -The initial condition is given as a *Vector*. This is a type which collects several different values. To declare a vector, the values are specific within brackets, `[]`, and separated by `,`. Since we only have one species, the vector holds a single element. In this element, we set the value of *X* using the `:X => 1.0` syntax. Here, we first denote the name of the species (with a `:` pre-appended), next follows a `=>` and then the value of *X*. Since we wish to simulate the *concentration* of X over time, we will let the initial condition be decimal valued. -```@example ex1 +The initial condition is given as a *Vector*. This is a type which collects several different values. To declare a vector, the values are specific within brackets, `[]`, and separated by `,`. Since we only have one species, the vector holds a single element. In this element, we set the value of $X$ using the `:X => 1.0` syntax. Here, we first denote the name of the species (with a `:` pre-appended, i.e. creating a `Symbol`), next follows a `=>` and then the value of $X$. Since we wish to simulate the *concentration* of X over time, we will let the initial condition be decimal valued. +```@example ex2 u0 = [:X => 1.0] ``` -The timespan sets the time point at which we start the simulation (typically `0.0` is used) and the final time point of the simulation. These are combined into a two-valued *Tuple*. Tuples are similar to vectors, but are enclosed by `()` and not `[]`. Again, we will let both time points be decimal valued. -```@example ex1 +The timespan sets the time point at which we start the simulation (typically `0.0` is used) and the final time point of the simulation. These are combined into a two-valued *tuple*. Tuples are similar to vectors, but are enclosed by `()` and not `[]`. Again, we will let both time points be decimal valued. +```@example ex2 tspan = (0.0, 10.0) ``` -Finally, the parameter values are, like the initial conditions, given as a vector. Since we have two parameters (*b* and *d*), the parameter vector has two values. We use a similar notation for setting the parameter values as the initial condition (first the parameter, then an arrow, then the value). -```@example ex1 +Finally, the parameter values are, like the initial conditions, given in a vector. Since we have two parameters ($b$ and $d$), the parameter vector has two values. We use a similar notation for setting the parameter values as the initial condition (first the parameter, then an arrow, then the value). +```@example ex2 params = [:b => 1.0, :d => 0.2] ``` -Please read here for more information on [Vectors](https://docs.julialang.org/en/v1/manual/arrays/) and [Tuples](https://docs.julialang.org/en/v1/manual/types/#Tuple-Types). +Please read here for more information on [vectors](https://docs.julialang.org/en/v1/manual/arrays/) and [tuples](https://docs.julialang.org/en/v1/manual/types/#Tuple-Types). Next, before we can simulate our model, we bundle all the required information together in a so-called `ODEProblem`. Note that the order in which the input (the model, the initial condition, the timespan, and the parameter values) is provided to the ODEProblem matters. E.g. the parameter values cannot be provided as the first argument, but have to be the fourth argument. Here, we save our `ODEProblem` in the `oprob` variable. - - -```@example ex1 +```@example ex2 oprob = ODEProblem(rn, u0, tspan, params) ``` We can now simulate our model. We do this by providing the `ODEProblem` to the `solve` function. We save the output to the `sol` variable. -```@example ex1 +```@example ex2 sol = solve(oprob) ``` Finally, we can plot the solution through the `plot` function. -```@example ex1 +```@example ex2 plot(sol) ``` -Here, the plot shows the time evolution of the concentration of the species *X* from its initial condition. +Here, the plot shows the time evolution of the concentration of the species $X$ from its initial condition. For more information about the numerical simulation package, please see the [DifferentialEquations documentation](https://docs.sciml.ai/DiffEqDocs/stable/). For more information about the plotting package, please see the [Plots documentation](https://docs.juliaplots.org/stable/). ## Additional modelling example -To make this introduction more comprehensive, we here provide another example, using a more complicated model. In addition, instead of simulating our model as concentrations evolve over time, we will simulate the individual reaction events through the [Gillespie algorithm](https://en.wikipedia.org/wiki/Gillespie_algorithm). This is a way to add *noise* to our model. +To make this introduction more comprehensive, we here provide another example, using a more complicated model. Instead of simulating our model as concentrations evolve over time, we will now simulate the individual reaction events through the [Gillespie algorithm](https://en.wikipedia.org/wiki/Gillespie_algorithm) (a common approach for adding *noise* to models). Remember, unless we have restarted Julia, we do not need to activate our packages (through the `using` command) again. -This time, we will declare the so-called [SIR model for an infectious disease](https://en.wikipedia.org/wiki/Compartmental_models_in_epidemiology#The_SIR_model). Note that even if this model does not describe a set of chemical reactions, it can be modelled using the same dynamics. The model consists of 3 species: -* *S*, the amount of *susceptible* individuals. -* *I*, the amount of *infected* individuals. -* *R*, the amount of *recovered* (or *removed*) individuals. +This time, we will declare a so-called [SIR model for an infectious disease](https://en.wikipedia.org/wiki/Compartmental_models_in_epidemiology#The_SIR_model). Note that even if this model does not describe a set of chemical reactions, it can be modelled using the same framework. The model consists of 3 species: +* $S$, the amount of *susceptible* individuals. +* $I$, the amount of *infected* individuals. +* $R$, the amount of *recovered* (or *removed*) individuals. + It also has 2 reaction events: * Infection, where a susceptible individual meets an infected individual and also becomes infected. -* Recovery, where an infected individual recovers. +* Recovery, where an infected individual recovers from the infection. + Each reaction is also associated with a specific rate (corresponding to a parameter). * *b*, the infection rate. * *k*, the recovery rate. We declare the model using the `@reaction_network` macro, and store it in the `sir_model` variable. -```@example ex1 +```@example ex2 sir_model = @reaction_network begin b, S + I --> 2I k, I --> R end ``` - Note that the first reaction contains two different substrates (separated by a `+` sign). While there is only a single product (*I*), two copies of *I* are produced. The *2* in front of the product *I* denotes this. -Next, we declare our initial condition, time span, and parameter values. Since we want to simulate the individual reaction events, that discretely change the state of our model, we want our initial conditions to be integer-valued. We will start with a mostly susceptible population, but where a single individual has been infected through some means. -```@example ex1 -u0 = [:S => 50, :I => 1, :R => 0.0] +Next, we declare our initial condition, time span, and parameter values. Since we want to simulate the individual reaction events that discretely change the state of our model, we want our initial conditions to be integer-valued. We will start with a mostly susceptible population, but where a single individual has been infected through some means. +```@example ex2 +u0 = [:S => 50, :I => 1, :R => 0] tspan = (0.0, 10.0) params = [:b => 0.2, :k => 1.0] +nothing # hide ``` -Previously we have bundled this information into an `ODEProblem` (denoting a deterministic *ordinary differential equation*). Now we wish to simulate our model as a jump process (where each reaction event denotes a single jump in the state of the system). We do this by first creating a `DiscreteProblem`, and then using this as an input to a `JumpProblem`. -```@example ex1 +Previously we have bundled this information into an `ODEProblem` (denoting a deterministic *ordinary differential equation*). Now we wish to simulate our model as a jump process (where each reaction event corresponds to a single jump in the state of the system). We do this by first creating a `DiscreteProblem`, and then using this as an input to a `JumpProblem`. +```@example ex2 dprob = DiscreteProblem(sir_model, u0, tspan, params) jprob = JumpProblem(sir_model, dprob, Direct()) ``` -Again, the order in which the inputs are given to the `DiscreteProblem` and the `JumpProblem` is important. The last argument to the `JumpProblem` (`Direct()`) denotes which simulation method we wish to use. For now, we recommend the user simply use the `Direct()` option, and then consider alternative ones (see the [JumpProcesses.jl docs](https://docs.sciml.ai/JumpProcesses/stable/)) when they are more familiar with modelling in Catalyst and Julia. +Again, the order with which the inputs are given to the `DiscreteProblem` and the `JumpProblem` is important. The last argument to the `JumpProblem` (`Direct()`) denotes which simulation method we wish to use. For now, we recommend that users simply use the `Direct()` option, and then consider alternative ones (see the [JumpProcesses.jl docs](https://docs.sciml.ai/JumpProcesses/stable/)) when they are more familiar with modelling in Catalyst and Julia. Finally, we can simulate our model using the `solve` function, and plot the solution using the `plot` function. Here, the `solve` function also has a second argument (`SSAStepper()`). This is a time stepping algorithm that calls the `Direct` solver to advance a simulation. Again, we recommend at this stage you simply use this option, and then explore exactly what this means at a later stage. -```@example ex1 +```@example ex2 sol = solve(jprob, SSAStepper()) sol = solve(jprob, SSAStepper(); seed=1234) # hide plot(sol) diff --git a/docs/src/inverse_problems/petab_ode_param_fitting.md b/docs/src/inverse_problems/petab_ode_param_fitting.md index 47f9056572..951af4c0a2 100644 --- a/docs/src/inverse_problems/petab_ode_param_fitting.md +++ b/docs/src/inverse_problems/petab_ode_param_fitting.md @@ -1,5 +1,5 @@ # [Parameter Fitting for ODEs using PEtab.jl](@id petab_parameter_fitting) -The [PEtab.jl package](https://github.com/sebapersson/PEtab.jl) implements the [PEtab format](https://petab.readthedocs.io/en/latest/) for fitting the parameters of deterministic CRN models to data ([please cite the corresponding papers if you use the PEtab approach in your research](@ref petab_citations))[^1]. PEtab.jl both implements methods for creating cost functions (determining how well parameter sets fit to data), and for minimizing these cost functions. The PEtab approach covers most cases of fitting deterministic (ODE) models to data and is a good default choice when fitting reaction rate equation ODE models. This page describes how to combine PEtab.jl and Catalyst for parameter fitting, with the PEtab.jl package providing [a more extensive documentation](https://sebapersson.github.io/PEtab.jl/stable/) (this tutorial is partially an adaptation of this documentation). +The [PEtab.jl package](https://github.com/sebapersson/PEtab.jl) implements the [PEtab format](https://petab.readthedocs.io/en/latest/) for fitting the parameters of deterministic CRN models to data [^1]. PEtab.jl both implements methods for creating cost functions (determining how well parameter sets fit to data), and for minimizing these cost functions. The PEtab approach covers most cases of fitting deterministic (ODE) models to data and is a good default choice when fitting reaction rate equation ODE models. This page describes how to combine PEtab.jl and Catalyst for parameter fitting, with the PEtab.jl package providing [a more extensive documentation](https://sebapersson.github.io/PEtab.jl/stable/) (this tutorial is partially an adaptation of this documentation). ## Introductory example From 2b2f0994dad635d1a76190de00ca42f9bb007b90 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Wed, 27 Dec 2023 22:20:18 +0100 Subject: [PATCH 02/80] Update docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md Co-authored-by: Sam Isaacson --- .../introduction_to_catalyst/catalyst_for_new_julia_users.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md index 1d359a6803..56d3638e59 100644 --- a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md +++ b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md @@ -1,7 +1,7 @@ # [Introduction to Catalyst and Julia for New Julia users](@id catalyst_for_new_julia_users) The Catalyst tool for the modelling of chemical reaction networks is based in the Julia programming language. While experience in Julia programming is advantageous for using Catalyst, it is not necessary for accessing some of its basic features. This tutorial serves as an introduction to Catalyst for those unfamiliar with Julia, also introducing some basic Julia concepts. Anyone who plans on using Catalyst extensively is recommended to familiarise oneself more thoroughly with the Julia programming language. A collection of resources for learning Julia can be found [here](https://julialang.org/learning/), and a full documentation is available [here](https://docs.julialang.org/en/v1/). -Julia can be downloaded [here](https://julialang.org/downloads/). Generally, it is recommended to use the [*juliaup*](https://github.com/JuliaLang/juliaup) tool to install and update Julia. Furthermore, *Visual Studio Code* is a good IDE with [extensive Julia support](https://code.visualstudio.com/docs/languages/julia), and a good default choice IDE. +Julia can be downloaded [here](https://julialang.org/downloads/). Generally, it is recommended to use the [*juliaup*](https://github.com/JuliaLang/juliaup) tool to install and update Julia. Furthermore, *Visual Studio Code* is a good IDE with [extensive Julia support](https://code.visualstudio.com/docs/languages/julia), and a good default choice. *Users who are already familiar with Julia can skip to the [Introduction to Catalyst](@ref introduction_to_catalyst) tutorial.* From 81b34d719f44906cb215425c958aacee59474bf6 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Wed, 27 Dec 2023 22:20:27 +0100 Subject: [PATCH 03/80] Update docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md Co-authored-by: Sam Isaacson --- .../introduction_to_catalyst/catalyst_for_new_julia_users.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md index 56d3638e59..7991063d4c 100644 --- a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md +++ b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md @@ -129,7 +129,7 @@ Catalyst models are created through the `@reaction_network` *macro*. For more in The `@reaction_network` command is followed by the `begin` keyword, which is followed by one line for each *reaction* of the model. Each reaction consists of a *reaction rate*, followed by the reaction itself. The reaction contains a set of *substrates* and a set of *products* (what is consumed and produced by the reaction, respectively). These are separated by a `-->` arrow. Finally, the model ends with the `end` keyword. -Here, we create a simple *birth-death* model, where a single species (*X*) is created at rate *b*, and degraded at rate *d*. The model is stored in the variable `rn`. +Here, we create a simple *birth-death* model, where a single species ($X$) is created at rate $b$, and degraded at rate $d$. The model is stored in the variable `rn`. ```@example ex2 rn = @reaction_network begin b, 0 --> X From 2a5928cf59ab25538909fafc8432b9eac9a47212 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 29 Dec 2023 12:06:53 +0100 Subject: [PATCH 04/80] up --- HISTORY.md | 3 +- .../catalyst_for_new_julia_users.md | 132 ++++++++++-------- 2 files changed, 73 insertions(+), 62 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 65a8fd4374..adc880593d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,8 @@ # Breaking updates and feature summaries across releases ## Catalyst unreleased (master branch) + +## Catalyst 13.6 - Simulation of spatial ODEs now supported. For full details, please see https://github.com/SciML/Catalyst.jl/pull/644 and upcoming documentation. Note that these methods are currently considered alpha, with the interface and approach changing even in non-breaking Catalyst releases. - LatticeReactionSystem structure represents a spatial reaction network: ```julia @@ -35,7 +37,6 @@ wilhelm_2009_model = @reaction_network begin k5, 0 --> X end - using BifurcationKit bif_par = :k1 u_guess = [:X => 5.0, :Y => 2.0] diff --git a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md index 7991063d4c..e1e8af412c 100644 --- a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md +++ b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md @@ -46,82 +46,29 @@ nothing # hide This is useful to know when you e.g. declare, simulate, or plot, a Catalyst model. The first time you run a command there might be a slight delay. However, subsequent runs will be much quicker. This holds even if you make minor adjustments before the second run (such as changing simulation initial conditions). -## [Package management in Julia](@id catalyst_for_new_julia_users_packages) -Due to its native package manager, and a registry of almost all packages of relevancy, package management in Julia is unusually easy. In principle, Catalyst can be both installed, and then imported into a session, using only: -```julia -using Pkg -Pkg.add("Catalyst") -using Catalyst -``` -However, over the next three subsections we describe some additional considerations and useful commands. It is recommended to read and understand these at a relatively early stage. For a more detailed introduction to Julia package management, please read [the Pkg documentation](https://docs.julialang.org/en/v1/stdlib/Pkg/). - -### [Setting up a new Julia environment](@id catalyst_for_new_julia_users_packages_environments) -Whenever you run Julia, it will run in a specific *environment*. You can specify any folder on your computer as a Julia environment. If you start Julia in that folder, that environment will be used (or, this will at least be the case for most modes of running Julia). If you start Julia in a folder not corresponding to an environment, your *default* Julia environment is used. While it is possible to not worry about environments (and always use the default environment), this can lead to long-term problems as more packages are installed. - -To activate your current folder as an environment, run the following commands: -```julia -using Pkg -Pkg.activate(".") -``` -This will: -1. If your current folder (which can be displayed using the `pwd()` command) is not designated as a possible Julia environment, designate it as such. -2. Switch your current Julia session to use the current folder's environment. - -!!! note - If you check any folder which has been designated as a Julia environment, it contains a Project.toml and a Manifest.toml file. These store all information regarding the corresponding environment. For non-advanced users, it is recommended to never touch these files directly (and instead do so using various functions from the Pkg package, the important ones which are described in the next two subsections). +## [Installing and activating packages](@id catalyst_for_new_julia_users_packages_intro) +Due to its native package manager (Pkg), and a registry of almost all packages of relevancy, package management in Julia is unusually easy. Here, we will briefly describe how to install and activate Catalyst (and two additional packages relevant to this tutorial). -### [Installing and importing packages in Julia](@id catalyst_for_new_julia_users_packages_installing) -To import a Julia package into a session, you can use the `using PackageName` command (where `PackageName` is the name of the package you wish to activate). However, before you can do so, it must first be installed on your computer. Almost all relevant Julia packages are registered, and can be added through the `Pkg.add("PackageName")` command. We can thus install Catalyst through: +To import a Julia package into a session, you can use the `using PackageName` command (where `PackageName` is the name of the package you wish to import). However, before you can do so, it must first be installed on your computer. This is done through the `Pkg.add("PackageName")` command: ```julia using Pkg Pkg.add("Catalyst") ``` -Here, the Julia package manager package (`Pkg`) is by default installed on your computer when Julia is installed, and can be activated directly. - -We can now use Catalyst and its features by running: -```@example ex2 -using Catalyst -``` -This will only make Catalyst available for the current Julia session. If you exit Julia, you will have to run `using Catalyst` again to use its features (however, `Pkg.add("Catalyst")` does not need to be rerun). Next, we wish to install the `DifferentialEquations` and `Plots` packages (for numeric simulation of models, and plotting, respectively): +Here, the Julia package manager package (`Pkg`) is by default installed on your computer when Julia is installed, and can be activated directly. Next, we also wish to install the `DifferentialEquations` and `Plots` packages (for numeric simulation of models, and plotting, respectively). ```julia Pkg.add("DifferentialEquations") Pkg.add("Plots") ``` -and also to import them into our current session: +Once a package has been installed through the `Pkg.add` command, this command does not have to be repeated if we restart our Julia session. We can now import all three packages into our current session with: ```@example ex2 +using Catalyst using DifferentialEquations using Plots ``` +Here, if we restart Julia, these commands *do need to be rerun. -### [Why environments are important](@id catalyst_for_new_julia_users_packages_environment_importance) -We have previously described how to set up new Julia environments, how to install Julia packages, and how to import them into a current session. Let us say that you were to restart Julia in a new folder and activate this as a separate environment. If you then try to import Catalyst through `using Catalyst` you will receive an error claiming that Catalyst was not found. The reason is that the `Pkg.add("Catalyst")` command actually carries out two separate tasks: -1. If Catalyst is not already installed on your computer, install it. -2. Add Catalyst as an available package to your current environment. - -Here, while Catalyst has previously been installed on your computer, it has not been added to the new environment you created. To do so, simply run -```julia -using Pkg -Pkg.add("Catalyst") -``` -after which Catalyst can be imported through `using Catalyst`. You can get a list of all packages available in your current environment using: -```julia -Pkg.status() -``` - -So, why is this required, and why cannot we simply import any package installed on our computer? The reason is that most packages depend on other packages, and these dependencies may be restricted to only specific versions of these packages. This creates complicated dependency graphs that restrict what versions of what packages are compatible with each other. When you use `Pkg.add("PackageName")`, only a specific version of that package is actually added (the latest possible version as permitted by the dependency graph). Here, Julia environments both define what packages are available *and* their respective versions (these versions are also displayed by the `Pkg.status()` command). By doing this, Julia can guarantee that the packages (and their versions) specified in an environment are compatible with each other. - -The reason why all this is important is that it is *highly recommended* to, for each project, define a separate environment. To these, only add the required packages. General-purpose environments with a large number of packages often produce package-incompatibility issues. While these might not prevent you from installing all desired package, they often mean that you are unable to use the latest version of some packages. - -!!! note - A not-infrequent cause for reported errors with Catalyst (typically the inability to replicate code in tutorials) is package incompatibilities in large environments preventing the latest version of Catalyst from being installed. Hence, whenever an issue is encountered, it is useful to run `Pkg.status()` to check whenever the latest version of Catalyst is being used. - -Some additional useful Pkg commands are: -- `Pk.rm("PackageName")` removes a package from the current environment. -- `Pkg.update(PackageName")`: updates the designated package. +A more comprehensive (but still short) introduction to package management in Julia is provided at [the end of this documentation page](catalyst_for_new_julia_users_packages). It contains some useful information and is hence highly recommended reading. For a more detailed introduction to Julia package management, please read [the Pkg documentation](https://docs.julialang.org/en/v1/stdlib/Pkg/). -!!! note - A useful feature of Julia's environment system is that enables the exact definition of what packages and versions were used to execute a script. This supports e.g. reproducibility in academic research. Here, by providing the corresponding Project.toml and Manifest.toml files, you can enable someone to reproduce the exact program just to perform some set of analyses. - ## Simulating a basic Catalyst model Now that we have some basic familiarity with Julia, and have installed and imported the required packages, we will create and simulate a basic chemical reaction network model using Catalyst. @@ -231,6 +178,69 @@ plot(sol) **Exercise:** Try simulating the model several times. Note that the epidemic doesn't always take off, but sometimes dies out without spreading through the population. Try changing the infection rate (*b*), determining how this value affects the probability that the epidemic goes through the population. +## [Package management in Julia](@id catalyst_for_new_julia_users_packages) +We have previously introduced how to install and activate Julia packages. While this is enough to get started with Catalyst, for long-term users, there are some additional considerations for a smooth experience. These are described here. + +### [Setting up a new Julia environment](@id catalyst_for_new_julia_users_packages_environments) +Whenever you run Julia, it will run in a specific *environment*. You can specify any folder on your computer as a Julia environment. Some modes of running Julia will automatically use the environment corresponding to the folder you start Julia in. Others (or if you start Julia in a folder without an environment), will use your *default* environment. In these cases you can, during your session, switch to another environment. While it is possible to not worry about environments (and always use the default one), this can lead to long-term problems as more packages are installed. + +To activate your current folder as an environment, run the following commands: +```julia +using Pkg +Pkg.activate(".") +``` +This will: +1. If your current folder (which can be displayed using the `pwd()` command) is not designated as a possible Julia environment, designate it as such. +2. Switch your current Julia session to use the current folder's environment. + +!!! note + If you check any folder which has been designated as a Julia environment, it contains a Project.toml and a Manifest.toml file. These store all information regarding the corresponding environment. For non-advanced users, it is recommended to never touch these files directly (and instead do so using various functions from the Pkg package, the important ones which are described in the next two subsections). + +### [Installing and importing packages in Julia](@id catalyst_for_new_julia_users_packages_installing) +Package installation and import have been described [previously](@ref catalyst_for_new_julia_users_packages_intro). However, for the sake of this extended tutorial, let us repeat the description by demonstrating how to install the [Latexify.jl](https://github.com/korsbo/Latexify.jl) package (which enables e.g. displaying Catalyst models in Latex format). First, we import the Julia Package manager ([Pkg](https://github.com/JuliaLang/Pkg.jl)) (which is required to install Julia packages): +```@example ex3 +using Pkg +``` +Latexify is a registered package, so it can be installed directly using: + ```julia +Pkg.add("Latexify") +``` +Finally, to import Latexify into our current Julia session we use: +```@example ex3 +using Latexify +``` +Here, `using Latexify` must be rerun whenever you restart a Julia session. However, you only need to run `Pkg.add("Latexify")` once to install it on your computer (but possibly additional times to add it to new environments, see the next section). + +### [Why environments are important](@id catalyst_for_new_julia_users_packages_environment_importance) +We have previously described how to set up new Julia environments, how to install Julia packages, and how to import them into a current session. Let us say that you were to restart Julia in a new folder and activate this as a separate environment. If you then try to import Latexify through `using Latexify` you will receive an error claiming that Latexify was not found. The reason is that the `Pkg.add("Latexify")` command actually carries out two separate tasks: +1. If Latexify is not already installed on your computer, install it. +2. Add Latexify as an available package to your current environment. + +Here, while Catalyst has previously been installed on your computer, it has not been added to the new environment you created. To do so, simply run +```julia +using Pkg +Pkg.add("Latexify") +``` +after which Catalyst can be imported through `using Latexify`. You can get a list of all packages available in your current environment using: +```julia +Pkg.status() +``` + +So, why is this required, and why cannot we simply import any package installed on our computer? The reason is that most packages depend on other packages, and these dependencies may be restricted to only specific versions of these packages. This creates complicated dependency graphs that restrict what versions of what packages are compatible with each other. When you use `Pkg.add("PackageName")`, only a specific version of that package is actually added (the latest possible version as permitted by the dependency graph). Here, Julia environments both define what packages are available *and* their respective versions (these versions are also displayed by the `Pkg.status()` command). By doing this, Julia can guarantee that the packages (and their versions) specified in an environment are compatible with each other. + +The reason why all this is important is that it is *highly recommended* to, for each project, define a separate environment. To these, only add the required packages. General-purpose environments with a large number of packages often produce package-incompatibility issues. While these might not prevent you from installing all desired package, they often mean that you are unable to use the latest version of some packages. + +!!! note + A not-infrequent cause for reported errors with Catalyst (typically the inability to replicate code in tutorials) is package incompatibilities in large environments preventing the latest version of Catalyst from being installed. Hence, whenever an issue is encountered, it is useful to run `Pkg.status()` to check whenever the latest version of Catalyst is being used. + +Some additional useful Pkg commands are: +- `Pk.rm("PackageName")` removes a package from the current environment. +- `Pkg.update(PackageName")`: updates the designated package. + +!!! note + A useful feature of Julia's environment system is that enables the exact definition of what packages and versions were used to execute a script. This supports e.g. reproducibility in academic research. Here, by providing the corresponding Project.toml and Manifest.toml files, you can enable someone to reproduce the exact program just to perform some set of analyses. + + --- ## Feedback If you are a new Julia user who has used this tutorial, and there was something you struggled with or would have liked to have explained better, please [raise an issue](https://github.com/SciML/Catalyst.jl/issues). That way, we can continue improving this tutorial. From 3d36751fc6dcba82c3fb7fe528930f56dc636e85 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 29 Dec 2023 14:05:28 +0100 Subject: [PATCH 05/80] update to `Symbolics = "5.14"` in Projec.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 5f1a189aff..bb451d3c57 100644 --- a/Project.toml +++ b/Project.toml @@ -49,7 +49,7 @@ Requires = "1.0" RuntimeGeneratedFunctions = "0.5.12" Setfield = "1" SymbolicUtils = "1.0.3" -Symbolics = "5.0.3" +Symbolics = "5.14" Unitful = "1.12.4" julia = "1.9" From b9387c63c94971a739b68867059f080dcde50e84 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 29 Dec 2023 14:10:37 +0100 Subject: [PATCH 06/80] up Symbolics in docs/Prokject.toml as well --- docs/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index 2ea3b3fda9..e8612c2367 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -47,4 +47,4 @@ Setfield = "1.1" SpecialFunctions = "2.1" SteadyStateDiffEq = "1" StochasticDiffEq = "6" -Symbolics = "5.0.3" +Symbolics = "5.14" From 6d74f5fdef2b645c45a78ca27c5178adc1d7a7f2 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Fri, 29 Dec 2023 08:17:32 -0500 Subject: [PATCH 07/80] Update Doc Project.toml --- docs/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index e8612c2367..cf31becb18 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -41,7 +41,7 @@ OptimizationOptimisers = "0.1.1" OrdinaryDiffEq = "6" PEtab = "2" Plots = "1.36" -SciMLBase = "~2.5" +SciMLBase = "2.13" SciMLSensitivity = "7.19" Setfield = "1.1" SpecialFunctions = "2.1" From 3708455dd0bf668e0b983513d64da0afa660ee86 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Fri, 29 Dec 2023 09:28:18 -0500 Subject: [PATCH 08/80] update doc deps --- docs/Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index cf31becb18..c9770aa61f 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -34,7 +34,7 @@ Documenter = "0.27" HomotopyContinuation = "2.6" Latexify = "0.15, 0.16" ModelingToolkit = "8.47" -NonlinearSolve = "1.6.0, 2" +NonlinearSolve = "3" Optim = "1" Optimization = "3.19" OptimizationOptimisers = "0.1.1" @@ -45,6 +45,6 @@ SciMLBase = "2.13" SciMLSensitivity = "7.19" Setfield = "1.1" SpecialFunctions = "2.1" -SteadyStateDiffEq = "1" +SteadyStateDiffEq = "2" StochasticDiffEq = "6" Symbolics = "5.14" From 3b91168b1d779e14dfa2a9a7bc2dc2f5845863f5 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 30 Dec 2023 14:20:43 +0100 Subject: [PATCH 09/80] doc project.toml update --- docs/Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index c9770aa61f..5f92019a43 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -34,7 +34,7 @@ Documenter = "0.27" HomotopyContinuation = "2.6" Latexify = "0.15, 0.16" ModelingToolkit = "8.47" -NonlinearSolve = "3" +NonlinearSolve = "3.4.0" Optim = "1" Optimization = "3.19" OptimizationOptimisers = "0.1.1" @@ -45,6 +45,6 @@ SciMLBase = "2.13" SciMLSensitivity = "7.19" Setfield = "1.1" SpecialFunctions = "2.1" -SteadyStateDiffEq = "2" +SteadyStateDiffEq = "2.0.1" StochasticDiffEq = "6" Symbolics = "5.14" From 9048c6c09d5fe5a55f251963d37297310c8beabd Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 24 Jan 2024 17:47:23 -0500 Subject: [PATCH 10/80] Change history from v13.5 to v13.6 --- HISTORY.md | 2 +- README.md | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index adc880593d..35763dd106 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,7 +2,7 @@ ## Catalyst unreleased (master branch) -## Catalyst 13.6 +## Catalyst 14.0 - Simulation of spatial ODEs now supported. For full details, please see https://github.com/SciML/Catalyst.jl/pull/644 and upcoming documentation. Note that these methods are currently considered alpha, with the interface and approach changing even in non-breaking Catalyst releases. - LatticeReactionSystem structure represents a spatial reaction network: ```julia diff --git a/README.md b/README.md index bdb1ef3004..4359a4f6ec 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,7 @@ etc). ## Breaking changes and new features -**NOTE:** version 13 is a breaking release, with changes to simplify the DSL -notation while also adding more features, changes to how chemical species are -specified symbolically when directly building `ReactionSystem`s, and changes that -simplify how to include ODE or algebraic constraint equation. +**NOTE:** version 14 is a breaking release, prompted by the release of ModelingToolkit.jl version 9. This caused several breaking changes in how Catalyst models are represented and interfaced with. Breaking changes and new functionality are summarized in the [HISTORY.md](HISTORY.md) file. From 17998d8acf6a24db21f5d9657c6ba2618256d3a5 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 7 Nov 2023 16:32:20 -0500 Subject: [PATCH 11/80] init --- .../homotopy_continuation_extension.jl | 25 ++++++++++++++++++- src/reactionsystem.jl | 12 +++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/ext/CatalystHomotopyContinuationExtension/homotopy_continuation_extension.jl b/ext/CatalystHomotopyContinuationExtension/homotopy_continuation_extension.jl index f9c1a5c19b..844cedfc19 100644 --- a/ext/CatalystHomotopyContinuationExtension/homotopy_continuation_extension.jl +++ b/ext/CatalystHomotopyContinuationExtension/homotopy_continuation_extension.jl @@ -104,4 +104,27 @@ function filter_negative_f(sols; neg_thres=-1e-20) (neg_thres < sol[idx] < 0) && (sol[idx] = 0) end return filter(sol -> all(>=(0), sol), sols) -end \ No newline at end of file +end + +### Archived ### + +# # Unfolds a function (like mm or hill). +# function deregister(fs::Vector{T}, expr) where T +# for f in fs +# expr = deregister(f, expr) +# end +# return expr +# end +# # Provided by Shashi Gowda. +# deregister(f, expr) = wrap(Rewriters.Postwalk(Rewriters.PassThrough(___deregister(f)))(unwrap(expr))) +# function ___deregister(f) +# (expr) -> +# if istree(expr) && operation(expr) == f +# args = arguments(expr) +# invoke_with = map(args) do a +# t = typeof(a) +# issym(a) || istree(a) ? wrap(a) => symtype(a) : a => typeof(a) +# end +# invoke(f, Tuple{last.(invoke_with)...}, first.(invoke_with)...) +# end +# end \ No newline at end of file diff --git a/src/reactionsystem.jl b/src/reactionsystem.jl index 83387ef921..dc30e3a6b7 100644 --- a/src/reactionsystem.jl +++ b/src/reactionsystem.jl @@ -1296,6 +1296,11 @@ function Base.convert(::Type{<:ODESystem}, rs::ReactionSystem; name = nameof(rs) eqs = assemble_drift(fullrs, ispcs; combinatoric_ratelaws, remove_conserved, include_zero_odes) eqs, sts, ps, obs, defs = addconstraints!(eqs, fullrs, ists, ispcs; remove_conserved) + + # Converts expressions like mm(X,v,K) to v*X/(X+K). + expand_functions && for eq in eqs + eq.rhs = expand_registered_functions!(eq.rhs) + end ODESystem(eqs, get_iv(fullrs), sts, ps; observed = obs, @@ -1327,7 +1332,6 @@ Keyword args and default values: function Base.convert(::Type{<:NonlinearSystem}, rs::ReactionSystem; name = nameof(rs), combinatoric_ratelaws = get_combinatoric_ratelaws(rs), include_zero_odes = true, remove_conserved = false, checks = false, - default_u0 = Dict(), default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)), kwargs...) spatial_convert_err(rs::ReactionSystem, NonlinearSystem) fullrs = Catalyst.flatten(rs) @@ -1377,7 +1381,6 @@ function Base.convert(::Type{<:SDESystem}, rs::ReactionSystem; noise_scaling = nothing, name = nameof(rs), combinatoric_ratelaws = get_combinatoric_ratelaws(rs), include_zero_odes = true, checks = false, remove_conserved = false, - default_u0 = Dict(), default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)), kwargs...) spatial_convert_err(rs::ReactionSystem, SDESystem) @@ -1404,6 +1407,11 @@ function Base.convert(::Type{<:SDESystem}, rs::ReactionSystem; eqs, sts, ps, obs, defs = addconstraints!(eqs, flatrs, ists, ispcs; remove_conserved) ps = (noise_scaling === nothing) ? ps : vcat(ps, toparam(noise_scaling)) + # Converts expressions like mm(X,v,K) to v*X/(X+K). + expand_functions && for eq in eqs + eq.rhs = expand_registered_functions!(eq.rhs) + end + if any(isbc, get_states(flatrs)) @info "Boundary condition species detected. As constraint equations are not currently supported when converting to SDESystems, the resulting system will be undetermined. Consider using constant species instead." end From 12359c7c361ba2f0a573d59a8348c35ca901d066 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 7 Nov 2023 17:04:57 -0500 Subject: [PATCH 12/80] add tests --- src/reactionsystem.jl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/reactionsystem.jl b/src/reactionsystem.jl index dc30e3a6b7..e788f5a207 100644 --- a/src/reactionsystem.jl +++ b/src/reactionsystem.jl @@ -1298,9 +1298,7 @@ function Base.convert(::Type{<:ODESystem}, rs::ReactionSystem; name = nameof(rs) eqs, sts, ps, obs, defs = addconstraints!(eqs, fullrs, ists, ispcs; remove_conserved) # Converts expressions like mm(X,v,K) to v*X/(X+K). - expand_functions && for eq in eqs - eq.rhs = expand_registered_functions!(eq.rhs) - end + expand_functions && (eqs = [eq.lhs ~ expand_registered_functions!(eq.rhs) for eq in eqs]) ODESystem(eqs, get_iv(fullrs), sts, ps; observed = obs, @@ -1408,8 +1406,9 @@ function Base.convert(::Type{<:SDESystem}, rs::ReactionSystem; ps = (noise_scaling === nothing) ? ps : vcat(ps, toparam(noise_scaling)) # Converts expressions like mm(X,v,K) to v*X/(X+K). - expand_functions && for eq in eqs - eq.rhs = expand_registered_functions!(eq.rhs) + if expand_functions + eqs = [eq.lhs ~ expand_registered_functions!(eq.rhs) for eq in eqs] + noiseeqs = [expand_registered_functions!(neq) for neq in noiseeqs] end if any(isbc, get_states(flatrs)) From 665a6e705b2c3e397b5249ca6dc1ba48353fda6e Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 13 Nov 2023 22:22:12 -0500 Subject: [PATCH 13/80] partial progress --- src/reactionsystem.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/reactionsystem.jl b/src/reactionsystem.jl index e788f5a207..90486fca38 100644 --- a/src/reactionsystem.jl +++ b/src/reactionsystem.jl @@ -538,6 +538,11 @@ struct ReactionSystem{V <: NetworkProperties} <: checks && validate(rs) rs end + + # Copies a reaction system, but with the option of having some fields replaced + function ReactionSystem(rs::ReactionSystem; eqs = rs.eqs, rxs = rs.rxs, iv = rs.iv, sivs = rs.sivs, states = rs.states, species = rs.species, ps = rs.ps, var_to_name = rs.var_to_name, observed = rs.observed, name = rs.name, systems = rs.systems, defaults = rs.defaults, connection_type = rs.connection_type, networkproperties = rs.networkproperties, combinatoric_ratelaws = rs.combinatoric_ratelaws, continuous_events = rs.continuous_events, discrete_events = rs.discrete_events, complete = rs.complete) + new{typeof(networkproperties)}(eqs, rxs, ModelingToolkit.unwrap(iv), ModelingToolkit.unwrap.(sivs), ModelingToolkit.unwrap.(states), ModelingToolkit.unwrap.(species), ModelingToolkit.unwrap.(ps), var_to_name, observed, name, systems, defaults, connection_type, networkproperties, combinatoric_ratelaws, continuous_events, discrete_events, complete) + end end function get_speciestype(iv, states, systems) From 749916bcdff5609c917554354bdbf1a1b0aa9412 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 14 Nov 2023 08:43:33 -0500 Subject: [PATCH 14/80] up --- .../homotopy_continuation_extension.jl | 25 +------------------ src/reactionsystem.jl | 14 ----------- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/ext/CatalystHomotopyContinuationExtension/homotopy_continuation_extension.jl b/ext/CatalystHomotopyContinuationExtension/homotopy_continuation_extension.jl index 844cedfc19..f9c1a5c19b 100644 --- a/ext/CatalystHomotopyContinuationExtension/homotopy_continuation_extension.jl +++ b/ext/CatalystHomotopyContinuationExtension/homotopy_continuation_extension.jl @@ -104,27 +104,4 @@ function filter_negative_f(sols; neg_thres=-1e-20) (neg_thres < sol[idx] < 0) && (sol[idx] = 0) end return filter(sol -> all(>=(0), sol), sols) -end - -### Archived ### - -# # Unfolds a function (like mm or hill). -# function deregister(fs::Vector{T}, expr) where T -# for f in fs -# expr = deregister(f, expr) -# end -# return expr -# end -# # Provided by Shashi Gowda. -# deregister(f, expr) = wrap(Rewriters.Postwalk(Rewriters.PassThrough(___deregister(f)))(unwrap(expr))) -# function ___deregister(f) -# (expr) -> -# if istree(expr) && operation(expr) == f -# args = arguments(expr) -# invoke_with = map(args) do a -# t = typeof(a) -# issym(a) || istree(a) ? wrap(a) => symtype(a) : a => typeof(a) -# end -# invoke(f, Tuple{last.(invoke_with)...}, first.(invoke_with)...) -# end -# end \ No newline at end of file +end \ No newline at end of file diff --git a/src/reactionsystem.jl b/src/reactionsystem.jl index 90486fca38..49435182b2 100644 --- a/src/reactionsystem.jl +++ b/src/reactionsystem.jl @@ -538,11 +538,6 @@ struct ReactionSystem{V <: NetworkProperties} <: checks && validate(rs) rs end - - # Copies a reaction system, but with the option of having some fields replaced - function ReactionSystem(rs::ReactionSystem; eqs = rs.eqs, rxs = rs.rxs, iv = rs.iv, sivs = rs.sivs, states = rs.states, species = rs.species, ps = rs.ps, var_to_name = rs.var_to_name, observed = rs.observed, name = rs.name, systems = rs.systems, defaults = rs.defaults, connection_type = rs.connection_type, networkproperties = rs.networkproperties, combinatoric_ratelaws = rs.combinatoric_ratelaws, continuous_events = rs.continuous_events, discrete_events = rs.discrete_events, complete = rs.complete) - new{typeof(networkproperties)}(eqs, rxs, ModelingToolkit.unwrap(iv), ModelingToolkit.unwrap.(sivs), ModelingToolkit.unwrap.(states), ModelingToolkit.unwrap.(species), ModelingToolkit.unwrap.(ps), var_to_name, observed, name, systems, defaults, connection_type, networkproperties, combinatoric_ratelaws, continuous_events, discrete_events, complete) - end end function get_speciestype(iv, states, systems) @@ -1301,9 +1296,6 @@ function Base.convert(::Type{<:ODESystem}, rs::ReactionSystem; name = nameof(rs) eqs = assemble_drift(fullrs, ispcs; combinatoric_ratelaws, remove_conserved, include_zero_odes) eqs, sts, ps, obs, defs = addconstraints!(eqs, fullrs, ists, ispcs; remove_conserved) - - # Converts expressions like mm(X,v,K) to v*X/(X+K). - expand_functions && (eqs = [eq.lhs ~ expand_registered_functions!(eq.rhs) for eq in eqs]) ODESystem(eqs, get_iv(fullrs), sts, ps; observed = obs, @@ -1410,12 +1402,6 @@ function Base.convert(::Type{<:SDESystem}, rs::ReactionSystem; eqs, sts, ps, obs, defs = addconstraints!(eqs, flatrs, ists, ispcs; remove_conserved) ps = (noise_scaling === nothing) ? ps : vcat(ps, toparam(noise_scaling)) - # Converts expressions like mm(X,v,K) to v*X/(X+K). - if expand_functions - eqs = [eq.lhs ~ expand_registered_functions!(eq.rhs) for eq in eqs] - noiseeqs = [expand_registered_functions!(neq) for neq in noiseeqs] - end - if any(isbc, get_states(flatrs)) @info "Boundary condition species detected. As constraint equations are not currently supported when converting to SDESystems, the resulting system will be undetermined. Consider using constant species instead." end From ad763160eb0f5a00068c7f47b14ade38442b970f Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 31 Oct 2023 11:40:55 -0400 Subject: [PATCH 15/80] init --- Project.toml | 3 +++ ext/CatalystStructuralIdentifiabilityExtension.jl | 11 +++++++++++ .../structural_identifiability_extension.jl | 13 +++++++++++++ test/extensions/structural_identifiability.jl | 5 +++++ test/runtests.jl | 2 +- 5 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 ext/CatalystStructuralIdentifiabilityExtension.jl create mode 100644 ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl create mode 100644 test/extensions/structural_identifiability.jl diff --git a/Project.toml b/Project.toml index bb451d3c57..bdd2470308 100644 --- a/Project.toml +++ b/Project.toml @@ -26,10 +26,12 @@ Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [weakdeps] BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327" +StructuralIdentifiability = "220ca800-aa68-49bb-acd8-6037fa93a544" [extensions] CatalystBifurcationKitExtension = "BifurcationKit" CatalystHomotopyContinuationExtension = "HomotopyContinuation" +CatalystStructuralIdentifiabilityExtension = "StructuralIdentifiability" [compat] BifurcationKit = "0.3" @@ -69,6 +71,7 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" SteadyStateDiffEq = "9672c7b4-1e72-59bd-8a11-6ac3964bc41f" StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0" +StructuralIdentifiability = "220ca800-aa68-49bb-acd8-6037fa93a544" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" diff --git a/ext/CatalystStructuralIdentifiabilityExtension.jl b/ext/CatalystStructuralIdentifiabilityExtension.jl new file mode 100644 index 0000000000..0a054e9e3f --- /dev/null +++ b/ext/CatalystStructuralIdentifiabilityExtension.jl @@ -0,0 +1,11 @@ +module CatalystHomotopyContinuationExtension + +# Fetch packages. +using Catalyst +import StructuralIdentifiability: assess_identifiability, assess_local_identifiability + + +# Creates and exports hc_steady_states function. +include("CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl") + +end \ No newline at end of file diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl new file mode 100644 index 0000000000..4915360c89 --- /dev/null +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -0,0 +1,13 @@ +### Structural Identifiability Analysis ### + +# Local identifiability. +function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), kwargs...) + + return StructuralIdentifiability.assess_local_identifiability(convert(ODESystem, rs), args...; measured_quantities=measured_quantities, kwargs...) +end + +# Global identifiability. +function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), kwargs...) + + return StructuralIdentifiability.assess_identifiability(convert(ODESystem, rs), args...; measured_quantities=measured_quantities, kwargs...) +end \ No newline at end of file diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl new file mode 100644 index 0000000000..00f8ae5dad --- /dev/null +++ b/test/extensions/structural_identifiability.jl @@ -0,0 +1,5 @@ +### Fetch Packages ### +using Catalyst, Test +using StructuralIdentifiability + +### Run Tests ### \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index e855dfd3d8..90cf6aab4b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -54,5 +54,5 @@ using SafeTestsets ### Tests extensions. ### @time @safetestset "BifurcationKit Extension" begin include("extensions/bifurcation_kit.jl") end @time @safetestset "HomotopyContinuation Extension" begin include("extensions/homotopy_continuation.jl") end - + @time @safetestset "Structural Identifiability Extension" begin include("extensions/structural_identifiability.jl") end end # @time From 6c466aff3f15bad098255dc9fac92e68451c9435 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 31 Oct 2023 17:17:22 -0400 Subject: [PATCH 16/80] update --- ext/CatalystStructuralIdentifiabilityExtension.jl | 5 ++--- .../structural_identifiability_extension.jl | 15 +++++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/ext/CatalystStructuralIdentifiabilityExtension.jl b/ext/CatalystStructuralIdentifiabilityExtension.jl index 0a054e9e3f..58fece8ac5 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension.jl @@ -1,9 +1,8 @@ -module CatalystHomotopyContinuationExtension +module CatalystStructuralIdentifiabilityExtension # Fetch packages. using Catalyst -import StructuralIdentifiability: assess_identifiability, assess_local_identifiability - +import StructuralIdentifiability # Creates and exports hc_steady_states function. include("CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl") diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 4915360c89..f246feac78 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -2,12 +2,19 @@ # Local identifiability. function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), kwargs...) - - return StructuralIdentifiability.assess_local_identifiability(convert(ODESystem, rs), args...; measured_quantities=measured_quantities, kwargs...) + return StructuralIdentifiability.assess_local_identifiability(convert(ODESystem, rs), args...; measured_quantities=get_measured_quantities(rs, measured_quantities), kwargs...) end # Global identifiability. function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), kwargs...) + return StructuralIdentifiability.assess_identifiability(convert(ODESystem, rs), args...; measured_quantities=get_measured_quantities(rs, measured_quantities), kwargs...) +end + +# For input measured quantities, if this is not a vector of equations, convert it to a proper form. +get_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{Equation}) = measured_quantities # If form is already a vector of equations. +function get_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{Symbol}) + (measured_quantities isa Vector{Symbol}) && (measured_quantities = [Catalyst._symbol_to_var(rs, sym) for sym in measured_quantities]) + @variables t ___internal_observables[1:length(measured_quantities)](t) + return [eq[1] ~ eq[2] for eq in zip(___internal_observables, measured_quantities)] +end - return StructuralIdentifiability.assess_identifiability(convert(ODESystem, rs), args...; measured_quantities=measured_quantities, kwargs...) -end \ No newline at end of file From 3511c2948b52c15d5318e7251d9ac9ac081492c7 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 1 Nov 2023 17:57:17 -0400 Subject: [PATCH 17/80] save progress --- .../structural_identifiability.md | 2 ++ .../structural_identifiability_extension.jl | 16 ++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 docs/src/catalyst_applications/structural_identifiability.md diff --git a/docs/src/catalyst_applications/structural_identifiability.md b/docs/src/catalyst_applications/structural_identifiability.md new file mode 100644 index 0000000000..350c18da41 --- /dev/null +++ b/docs/src/catalyst_applications/structural_identifiability.md @@ -0,0 +1,2 @@ +# [StructuralIdentifiability Analysis](@id structural_identifiability) +During parameter fitting, parameter values are inferred from data. Identifiability is a concept describing to what extent the identification of parameter values for a certain model is actually feasible. Ideally, parameter fitting should always be accompanied with an identifiability analysis of the problem. Identifiability can be divided into *structural* and *practical* identifiability. Structural identifiability considers only the system and what quantities we can measure to determine which quantities can be identified. Practical identifiability instead considers teh available data, and determines what system quantities can be infeed from it. Generally, in the hypothetical case of an infinite amount of without noise, practical identifiability becomes identical to structural identifiability. \ No newline at end of file diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index f246feac78..376ff529ae 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -1,20 +1,24 @@ ### Structural Identifiability Analysis ### # Local identifiability. -function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), kwargs...) - return StructuralIdentifiability.assess_local_identifiability(convert(ODESystem, rs), args...; measured_quantities=get_measured_quantities(rs, measured_quantities), kwargs...) +function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), known_p = [], kwargs...) + return StructuralIdentifiability.assess_local_identifiability(convert(ODESystem, rs), args...; measured_quantities=get_measured_quantities(rs, measured_quantities, known_p), kwargs...) end # Global identifiability. -function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), kwargs...) - return StructuralIdentifiability.assess_identifiability(convert(ODESystem, rs), args...; measured_quantities=get_measured_quantities(rs, measured_quantities), kwargs...) +function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), known_p = [], kwargs...) + return StructuralIdentifiability.assess_identifiability(convert(ODESystem, rs), args...; measured_quantities=get_measured_quantities(rs, measured_quantities, known_p), kwargs...) end +# For a reaction system, list of measured quantities and known parameters, generate a StructuralIdentifiability compatible ODE. + # For input measured quantities, if this is not a vector of equations, convert it to a proper form. get_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{Equation}) = measured_quantities # If form is already a vector of equations. -function get_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{Symbol}) +function get_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{T}, known_p::Vector{S}) where {T,S} (measured_quantities isa Vector{Symbol}) && (measured_quantities = [Catalyst._symbol_to_var(rs, sym) for sym in measured_quantities]) + (known_p isa Vector{Symbol}) && (known_p = [Catalyst._symbol_to_var(rs, sym) for sym in known_p]) @variables t ___internal_observables[1:length(measured_quantities)](t) - return [eq[1] ~ eq[2] for eq in zip(___internal_observables, measured_quantities)] + measured_quantities = [eq[1] ~ eq[2] for eq in zip(___internal_observables, measured_quantities)] + return [measured_quantities; known_p] end From 1da978eeeb18ca3ca0afbdec3302ddafa6bd561d Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 1 Nov 2023 18:41:23 -0400 Subject: [PATCH 18/80] update --- .../structural_identifiability.md | 37 ++++++++++++- .../structural_identifiability_extension.jl | 52 ++++++++++++++----- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/docs/src/catalyst_applications/structural_identifiability.md b/docs/src/catalyst_applications/structural_identifiability.md index 350c18da41..65a8cdfa2d 100644 --- a/docs/src/catalyst_applications/structural_identifiability.md +++ b/docs/src/catalyst_applications/structural_identifiability.md @@ -1,2 +1,35 @@ -# [StructuralIdentifiability Analysis](@id structural_identifiability) -During parameter fitting, parameter values are inferred from data. Identifiability is a concept describing to what extent the identification of parameter values for a certain model is actually feasible. Ideally, parameter fitting should always be accompanied with an identifiability analysis of the problem. Identifiability can be divided into *structural* and *practical* identifiability. Structural identifiability considers only the system and what quantities we can measure to determine which quantities can be identified. Practical identifiability instead considers teh available data, and determines what system quantities can be infeed from it. Generally, in the hypothetical case of an infinite amount of without noise, practical identifiability becomes identical to structural identifiability. \ No newline at end of file +# [Structural Identifiability Analysis](@id structural_identifiability) +During parameter fitting, parameter values are inferred from data. Identifiability is a concept describing to what extent the identification of parameter values for a certain model is actually feasible. Ideally, parameter fitting should always be accompanied with an identifiability analysis of the problem. Identifiability can be divided into *structural* and *practical* identifiability[^1]. Structural identifiability considers only the system and what quantities we can measure to determine which quantities can be identified. Practical identifiability instead considers the available data, and determines what system quantities can be infeed from it. Generally, in the hypothetical case of an infinite amount of without noise, practical identifiability becomes identical to structural identifiability. Generally, structural identifiability is assessed before parameters are fitted, while practical identifiability is assessed afterwards. + +Structural identifiability can be illustrated in the following example network: +${dx \over dt} = p1*p2*x(t)$ +where, however much data I have on *x*, it is impossible to determine the values of *p1* and *p2* (these are non-identifiable). + +Catalyst contains a special extension for carrying out structural identifiability analysis using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability` and `assess_local_identifiability` functions to be called directly on Catalyst `ReactionSystems`. How to use these functions are described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). If you use this in your research, please [cite the StructuralIdentifiability.jl](@ref structural_identifiability_citation) and [Catalyst.jl](@ref catalyst_citation) publications. + +## Global sensitivity analysis + +## Local sensitivity analysis + +## Creating StructuralIdentifiability compatible ODE models from Catalyst `ReactionSystem`s + + +--- +## [Citation](@id structural_identifiability_citation) +If you use this functionality in your research, please cite the following paper to support the authors of the StructuralIdentifiability package: +``` +@article{structidjl, + author = {Dong, R. and Goodbrake, C. and Harrington, H. and Pogudin G.}, + title = {Differential Elimination for Dynamical Models via Projections with Applications to Structural Identifiability}, + journal = {SIAM Journal on Applied Algebra and Geometry}, + url = {https://doi.org/10.1137/22M1469067}, + year = {2023} + volume = {7}, + number = {1}, + pages = {194-235} +} +``` + +--- +## References +[^1]: [Guillaume H.A. Joseph et al., *Introductory overview of identifiability analysis: A guide to evaluating whether you have the right type of data for your modeling purpose*, Environmental Modelling & Software (2019).](https://www.sciencedirect.com/science/article/pii/S1364815218307278) \ No newline at end of file diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 376ff529ae..2ba115912e 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -1,24 +1,50 @@ -### Structural Identifiability Analysis ### +### Structural Identifiability ODE Creation ### -# Local identifiability. -function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), known_p = [], kwargs...) - return StructuralIdentifiability.assess_local_identifiability(convert(ODESystem, rs), args...; measured_quantities=get_measured_quantities(rs, measured_quantities, known_p), kwargs...) -end +# For a reaction system, list of measured quantities and known parameters, generate a StructuralIdentifiability compatible ODE. +""" + si_ode(rs::ReactionSystem; measured_quantities=observed(rs), known_p = Num[], ignore_no_measured_warn=false) -# Global identifiability. -function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), known_p = [], kwargs...) - return StructuralIdentifiability.assess_identifiability(convert(ODESystem, rs), args...; measured_quantities=get_measured_quantities(rs, measured_quantities, known_p), kwargs...) -end +Creates a ODE system of the form used within the StructuralIdentifiability.jl package. The output system is compatible with all StructuralIdentifiability functions. -# For a reaction system, list of measured quantities and known parameters, generate a StructuralIdentifiability compatible ODE. +Arguments: +- `rs::ReactionSystem`; The reaction system we wish to convert to an ODE. +- `measured_quantities=observed(rs)`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. `:x`). Defaults to the systems observables. +- `known_p = Num[]`: List of parameters which values are known. +- `ignore_no_measured_warn=false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. + +Example: +```julia +rs = @reaction_network begin + (p,d), 0 <--> X +end +si_ode(rs; measured_quantities = [:X], known_p = [:p]) +``` +""" +function make_si_ode(rs::ReactionSystem; measured_quantities=observed(rs), known_p = Num[], ignore_no_measured_warn=false) + ignore_no_meassured_warn || isempty(measured_quantities) && @warn "No measured quantity provided to the `measured_quantities` argument, any further identifiability analysis will likely fail." + known_quantities = make_measured_quantities(rs, measured_quantities, known_p) + return preprocess_ode(convert(ODESystem, rs), known_quantities) +end # For input measured quantities, if this is not a vector of equations, convert it to a proper form. -get_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{Equation}) = measured_quantities # If form is already a vector of equations. -function get_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{T}, known_p::Vector{S}) where {T,S} +make_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{Equation}) = measured_quantities # If form is already a vector of equations. +function make_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{T}, known_p::Vector{S}) where {T,S} (measured_quantities isa Vector{Symbol}) && (measured_quantities = [Catalyst._symbol_to_var(rs, sym) for sym in measured_quantities]) (known_p isa Vector{Symbol}) && (known_p = [Catalyst._symbol_to_var(rs, sym) for sym in known_p]) @variables t ___internal_observables[1:length(measured_quantities)](t) measured_quantities = [eq[1] ~ eq[2] for eq in zip(___internal_observables, measured_quantities)] - return [measured_quantities; known_p] + return [Num.(measured_quantities); known_p] +end + +### Structural Identifiability Wrappers ### + +# Local identifiability. +function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), known_p = Num[], kwargs...) + return StructuralIdentifiability.assess_local_identifiability(make_si_ode(rs, measured_quantities, known_p), args...; kwargs...) +end + +# Global identifiability. +function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), known_p = Num[], kwargs...) + return StructuralIdentifiability.assess_identifiability(make_si_ode(rs, measured_quantities, known_p), args...; kwargs...) end From 29a6cbe331880d28e1defd181c9ff825d849edde Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 5 Nov 2023 20:47:40 -0500 Subject: [PATCH 19/80] some updates --- .../structural_identifiability_extension.jl | 17 ++++++++--------- src/Catalyst.jl | 4 ++++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 2ba115912e..e9fe40a37d 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -20,31 +20,30 @@ end si_ode(rs; measured_quantities = [:X], known_p = [:p]) ``` """ -function make_si_ode(rs::ReactionSystem; measured_quantities=observed(rs), known_p = Num[], ignore_no_measured_warn=false) - ignore_no_meassured_warn || isempty(measured_quantities) && @warn "No measured quantity provided to the `measured_quantities` argument, any further identifiability analysis will likely fail." +function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities=observed(rs), known_p = [], ignore_no_measured_warn=false) + ignore_no_measured_warn || isempty(measured_quantities) && @warn "No measured quantity provided to the `measured_quantities` argument, any further identifiability analysis will likely fail." known_quantities = make_measured_quantities(rs, measured_quantities, known_p) - return preprocess_ode(convert(ODESystem, rs), known_quantities) + return StructuralIdentifiability.preprocess_ode(convert(ODESystem, rs), known_quantities) end # For input measured quantities, if this is not a vector of equations, convert it to a proper form. -make_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{Equation}) = measured_quantities # If form is already a vector of equations. function make_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{T}, known_p::Vector{S}) where {T,S} (measured_quantities isa Vector{Symbol}) && (measured_quantities = [Catalyst._symbol_to_var(rs, sym) for sym in measured_quantities]) (known_p isa Vector{Symbol}) && (known_p = [Catalyst._symbol_to_var(rs, sym) for sym in known_p]) - @variables t ___internal_observables[1:length(measured_quantities)](t) - measured_quantities = [eq[1] ~ eq[2] for eq in zip(___internal_observables, measured_quantities)] - return [Num.(measured_quantities); known_p] + all_quantities = [measured_quantities; known_p] + @variables t (___internal_observables(t))[1:length(all_quantities)] + return Equation[(all_quantities[i] isa Equation) ? all_quantities[i] : (___internal_observables[i] ~ all_quantities[i]) for i in 1:length(all_quantities)] end ### Structural Identifiability Wrappers ### # Local identifiability. function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), known_p = Num[], kwargs...) - return StructuralIdentifiability.assess_local_identifiability(make_si_ode(rs, measured_quantities, known_p), args...; kwargs...) + return StructuralIdentifiability.assess_local_identifiability(Catalyst.make_si_ode(rs; measured_quantities, known_p), args...; kwargs...) end # Global identifiability. function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), known_p = Num[], kwargs...) - return StructuralIdentifiability.assess_identifiability(make_si_ode(rs, measured_quantities, known_p), args...; kwargs...) + return StructuralIdentifiability.assess_identifiability(Catalyst.make_si_ode(rs; measured_quantities, known_p), args...; kwargs...) end diff --git a/src/Catalyst.jl b/src/Catalyst.jl index fa2b3e51c2..233b2274bf 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -112,6 +112,10 @@ export balance_reaction function hc_steady_states end export hc_steady_states +# StructuralIdentifiability +function make_si_ode end +export make_si_ode + ### Spatial Reaction Networks ### # spatial reactions From 44606d7cb275212734fc66acbb398ce61f3ffe52 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 6 Nov 2023 18:59:44 -0500 Subject: [PATCH 20/80] improve docs --- .../structural_identifiability.md | 85 ++++++++++++++++++- .../structural_identifiability_extension.jl | 7 +- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/docs/src/catalyst_applications/structural_identifiability.md b/docs/src/catalyst_applications/structural_identifiability.md index 65a8cdfa2d..f46f20f54c 100644 --- a/docs/src/catalyst_applications/structural_identifiability.md +++ b/docs/src/catalyst_applications/structural_identifiability.md @@ -3,16 +3,93 @@ During parameter fitting, parameter values are inferred from data. Identifiabili Structural identifiability can be illustrated in the following example network: ${dx \over dt} = p1*p2*x(t)$ -where, however much data I have on *x*, it is impossible to determine the values of *p1* and *p2* (these are non-identifiable). +where, however much data is collected on *x*, it is impossible to determine the distinct values of *p1* and *p2* (these are non-identifiable). -Catalyst contains a special extension for carrying out structural identifiability analysis using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability` and `assess_local_identifiability` functions to be called directly on Catalyst `ReactionSystems`. How to use these functions are described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). If you use this in your research, please [cite the StructuralIdentifiability.jl](@ref structural_identifiability_citation) and [Catalyst.jl](@ref catalyst_citation) publications. +Catalyst contains a special extension for carrying out structural identifiability analysis using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystems`. It also implements specialised routines to make these more efficient when applied to reaction network models. How to use these functions are described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). If you use this in your research, please [cite the StructuralIdentifiability.jl](@ref structural_identifiability_citation) and [Catalyst.jl](@ref catalyst_citation) publications. -## Global sensitivity analysis +Structural identifiability can be divided into *local* and *global* identifiability. If a model quantity (which can either be a parameter or an initial condition) is locally identifiable, it means that its true value can be determined down to a finite-number of possible options. This also means that there is some limited region around its true value, where the true value is the only possible value. Globally identifiable quantities' values, on the other hand, can be uniquely determined. Again, while parameter (and initial condition) identifiability can be confirmed structurally for a model, it does not necessarily mean that they are practically* identifiable for some given data. -## Local sensitivity analysis + +## Global identifiability analysis + +### Basic example + +Local identifiability can be assessed using the `assess_identifiability` function. For each model quantity (parameters and initial conditions), it will asses whether they are: +- globally identifiable. +- locally identifiable. +- Unidentifiable. + +To it, we provide our `ReactionSystem` model and a list of quantities that we are able to measure. Here, we consider a ... model. Let us say that we are able to measure teh values of ... and ..., we provide these at the `measured_quantities` argument. We can now assess identifiability in the following way: +```example si1 +``` +Here, ... are determined to be globally identifiable (and could theoretically be determined from data) and ... are locally identifiable (and for each, a finite number of candidate values can be determined from the data). Finally, ... are unidentifiable, and cannot be determined from data. + +### Indicating known parameters +In the previous case we assumed that all parameters are unknown, however, this is not necessarily true. If there are parameters which value's are known, we can supply these using the `known_ps` argument. Indeed, this might turn other, previously unidentifiable, parameters identifiable. Let us consider this simple example: +```example si2 +using Catalyst, StructuralIdentifiability # hide +rn = @reaction_network begin + (p1+p2, d), 0 <--> X +end +``` +Typically, the two production parameters ($p1$ and $p2$) are unidentifiable. However, we we already know the value of $p1$, then $p2$#s value becomes identifiable: +```example si2 +assess_identifiability(rn; measured_quantities=[:X], known_ps=[:p1]) +``` + +### Providing non-trivial measured quantities +Sometimes, we are not actually measuring species species, but rather some combinations of species (or possibly parameters). Here, any algebraic expression can be used in `measured_quantities`. If so, used species and parameters have to first be `@unpack`'ed from the model. Say that we have a model where an enzyme ($E$) is converted between an active and inactive form, which in turns activates the production of a product, $P$: +```example si3 +using Catalyst, StructuralIdentifiability # hide +enzyme_activation = @reaction_network begin + (kA,kD), Ei <--> Ea + (Ea, d), 0 <-->P +end +``` +and we can measure the total amount of $E$ ($=$Ei+Ea$), as well as the amount of $P$, we can use the following to assess identifiability: +```example si3 +@unpack Ea, Ei = enzyme_activation +assess_identifiability(enzyme_activation; measured_quantities=[Ei+Ea, :P]) +``` + +### Checking identifiability of specific quantities +By default, `asses_identifiability` assesses the identifiability of all parameters and species initial conditions. Sometimes, it is desirable to assess the identifiability of specific quantities. This can be done through the `funcs_to_check` argument. Let us consider our previous example, but say that we can only measure the amount of active enzyme ($Ea$), as well as the product ($P$). If we wish to determine whether the total amount of enzyme ($Ei+Ea$) is identifiable, we could use the following (again using `@unpack` to enable the formation of algebraic expression using the specific quantities): +```example si3 +assess_identifiability(enzyme_activation; measured_quantities=[:Ei, :P], funcs_to_check=[Ei+Ea]) +``` + +### Probability of correctness +The identifiabiltiy methods used can, in theory, produce erroneous results. However, it is possible to adjust the lower bound for the probability of correctness using the argument `p` (by default set to `0.99`, that is, at least a $99%$ chance of correctness). We can e.g. increase the bound through: +```example si2 +assess_identifiability(rn; measured_quantities=[:X], p=0.999) +nothing # hide +``` +giving a minimum bound of $99.9%$ chance of correctness. In practise, the bounds used by StructuralIdentifiability are very conservative, which means that while the minimum guaranteed probability of correctness in the default case is $99%$, in practise it is higher. While increasing teh value of `p` increases the certainty of correctness, it will also increase the time required to assess correctness. + +## Local identifiability analysis +Local identifiability can be assessed using the `assess_local_identifiability` function. While this is already determined by the `assess_identifiability` function, local identifiability have the advantage that it is easier to compute. Hence, there might be models where global identifiability analysis fails (or takes prohibitively long time), where instead `assess_local_identifiability` can be used. This functions takes the same inputs as `assess_identifiability` and returns, for each quantity, `true` if iti is locally identifiable and `false` if it is not. Here we assesses local identifiability for the same model as used in the previous example: +```example si1 +``` +We note that all parameters that `assess_identifiability` determined as either globally or locally identifiable are determined to be locally identifiable, while teh remaining are considered unidentifiable. + + +## Finding identifiable functions +Finally, StructuralIdentifiability provides the `find_identifiable_functions` function. Rather than determining the identifiability of each parameter and initial condition of the model, it finds a minimal set of identifiable functions, such as any other identifiable expression of the model can be generated by these. Here, let us consider the following model ... +```example si5 +``` + +The `find_identifiable_functions` functions tries to simplify its output functions to create nice expression. The degree to which it does this can be adjusted using the `simplify` keywords. Using the `:weak`, `:standard` (default), and `:strong` arguments, increased simplification can be forced (at the expense of longer runtimes). ## Creating StructuralIdentifiability compatible ODE models from Catalyst `ReactionSystem`s +While the functionality described above covers the vast majority of analysis as user might want to perform, the StructuralIdentifiability package supports several additional features . While these does not have inherent Catalyst support, we do provide the `make_si_ode` function to simplify their use. Similarly to the previous functions, it takes a `ReactionSystem` and lists of measured quantities and known parameter values. The output is a [ode of the standard form supported by StructuralIdentifiability](https://docs.sciml.ai/StructuralIdentifiability/stable/tutorials/creating_ode/#Defining-the-model-using-@ODEmodel-macro). It can be created using the following syntax: +```example si4 +using Catalyst, StructuralIdentifiability # hide +``` +and then used as input to various StructuralIdentifiability functions. In the following example we use the produced ode to +```example si4 + +``` --- ## [Citation](@id structural_identifiability_citation) diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index e9fe40a37d..6209471fb4 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -23,7 +23,7 @@ si_ode(rs; measured_quantities = [:X], known_p = [:p]) function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities=observed(rs), known_p = [], ignore_no_measured_warn=false) ignore_no_measured_warn || isempty(measured_quantities) && @warn "No measured quantity provided to the `measured_quantities` argument, any further identifiability analysis will likely fail." known_quantities = make_measured_quantities(rs, measured_quantities, known_p) - return StructuralIdentifiability.preprocess_ode(convert(ODESystem, rs), known_quantities) + return StructuralIdentifiability.preprocess_ode(convert(ODESystem, rs), known_quantities)[1] end # For input measured quantities, if this is not a vector of equations, convert it to a proper form. @@ -47,3 +47,8 @@ function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, ar return StructuralIdentifiability.assess_identifiability(Catalyst.make_si_ode(rs; measured_quantities, known_p), args...; kwargs...) end +# Identifiable functions. +function StructuralIdentifiability.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities=observed(rs), known_p = Num[], kwargs...) + return StructuralIdentifiability.find_identifiable_functions(Catalyst.make_si_ode(rs; measured_quantities, known_p), args...; kwargs...) +end + From 9157d3ef998ab91e9ab3c3647a658626e1e3957f Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 7 Nov 2023 15:48:56 -0500 Subject: [PATCH 21/80] finish documentation --- .../structural_identifiability.md | 97 ++++++++++--------- .../structural_identifiability_extension.jl | 12 +-- 2 files changed, 57 insertions(+), 52 deletions(-) diff --git a/docs/src/catalyst_applications/structural_identifiability.md b/docs/src/catalyst_applications/structural_identifiability.md index f46f20f54c..78c1163c9c 100644 --- a/docs/src/catalyst_applications/structural_identifiability.md +++ b/docs/src/catalyst_applications/structural_identifiability.md @@ -1,13 +1,13 @@ # [Structural Identifiability Analysis](@id structural_identifiability) -During parameter fitting, parameter values are inferred from data. Identifiability is a concept describing to what extent the identification of parameter values for a certain model is actually feasible. Ideally, parameter fitting should always be accompanied with an identifiability analysis of the problem. Identifiability can be divided into *structural* and *practical* identifiability[^1]. Structural identifiability considers only the system and what quantities we can measure to determine which quantities can be identified. Practical identifiability instead considers the available data, and determines what system quantities can be infeed from it. Generally, in the hypothetical case of an infinite amount of without noise, practical identifiability becomes identical to structural identifiability. Generally, structural identifiability is assessed before parameters are fitted, while practical identifiability is assessed afterwards. +During parameter fitting, parameter values are inferred from data. Identifiability is a concept describing to what extent the identification of parameter values for a certain model is actually feasible. Ideally, parameter fitting should always be accompanied with an identifiability analysis of the problem. Identifiability can be divided into *structural* and *practical* identifiability[^1]. Structural identifiability considers only the system and what quantities we can measure to determine which quantities can be identified. Practical identifiability instead considers the available data, and determines what system quantities can be inferred from it. Generally, in the hypothetical case of an infinite amounts of noise-less data, practical identifiability becomes identical to structural identifiability. Generally, structural identifiability is assessed before parameters are fitted, while practical identifiability is assessed afterwards. -Structural identifiability can be illustrated in the following example network: +Structural identifiability (which is what this tutorial considers) can be illustrated by the following differential equation: ${dx \over dt} = p1*p2*x(t)$ -where, however much data is collected on *x*, it is impossible to determine the distinct values of *p1* and *p2* (these are non-identifiable). +where, however much data is collected on *x*, it is impossible to determine the distinct values of *p1* and *p2*. Hence, these parameters are these are non-identifiable. -Catalyst contains a special extension for carrying out structural identifiability analysis using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystems`. It also implements specialised routines to make these more efficient when applied to reaction network models. How to use these functions are described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). If you use this in your research, please [cite the StructuralIdentifiability.jl](@ref structural_identifiability_citation) and [Catalyst.jl](@ref catalyst_citation) publications. +Catalyst contains a special extension for carrying out structural identifiability analysis using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. It also implements specialised routines to make these more efficient when applied to reaction network models. How to use these functions are described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). If you use this in your research, please [cite the StructuralIdentifiability.jl](@ref structural_identifiability_citation) and [Catalyst.jl](@ref catalyst_citation) publications. -Structural identifiability can be divided into *local* and *global* identifiability. If a model quantity (which can either be a parameter or an initial condition) is locally identifiable, it means that its true value can be determined down to a finite-number of possible options. This also means that there is some limited region around its true value, where the true value is the only possible value. Globally identifiable quantities' values, on the other hand, can be uniquely determined. Again, while parameter (and initial condition) identifiability can be confirmed structurally for a model, it does not necessarily mean that they are practically* identifiable for some given data. +Structural identifiability can be divided into *local* and *global* identifiability. If a model quantity (which can either be a parameter or an initial condition) is locally identifiable, it means that its true value can be determined down to a finite-number of possible options. This also means that there is some limited region around its true value, where the true value is the only possible value. Globally identifiable quantities' values, on the other hand, can be uniquely determined. Again, while parameter (and initial condition) identifiability can be confirmed structurally for a model, it does not necessarily mean that they are practically identifiable for some given data. ## Global identifiability analysis @@ -19,76 +19,79 @@ Local identifiability can be assessed using the `assess_identifiability` functio - locally identifiable. - Unidentifiable. -To it, we provide our `ReactionSystem` model and a list of quantities that we are able to measure. Here, we consider a ... model. Let us say that we are able to measure teh values of ... and ..., we provide these at the `measured_quantities` argument. We can now assess identifiability in the following way: +To it, we provide our `ReactionSystem` model and a list of quantities that we are able to measure. Here, we consider a Goodwind oscillator (a simple 3-component model, where the three species $M$, $E$, and $P$ are produced and degraded, which may exhibit oscillations)[^2]. Let us say that we are able to measure the concentration of $M$, we then provide designate this using the `measured_quantities` argument. We can now assess identifiability in the following way: ```example si1 +using StructuralIdentifiability, Catalyst +goodwind_oscillator = @reaction_network begin + (pₘ/(1+P), dₘ), 0 <--> M + (pₑ*M,dₑ), 0 <--> E + (pₚ*E,dₚ), 0 <--> P +end +assess_identifiability(goodwind_oscillator; measured_quantities=[:M]) +``` +From the output, we find that `E`, `pₑ`, and `pₚ` (the initial concentration of $E$, and the production rates of $E$ and $P$, respectively) are non-identifiable. Next, `dₑ` and `dₚ` (the degradation rates of $E$ and $P$, respectively) are locally identifiable. Finally, `P`, `M`, `pₘ`, and `dₘ` (the initial concentrations of `P` and `M`, the production and degradation rate of `M`, respectively) are all globally identifiable. Next, we can assess identifiability in the case where we can measure all three species concentrations: +```example si1 +assess_identifiability(goodwind_oscillator; measured_quantities=[:M, :P, :E]) ``` -Here, ... are determined to be globally identifiable (and could theoretically be determined from data) and ... are locally identifiable (and for each, a finite number of candidate values can be determined from the data). Finally, ... are unidentifiable, and cannot be determined from data. +in which case all initial conditions and parameters become identifiable. ### Indicating known parameters -In the previous case we assumed that all parameters are unknown, however, this is not necessarily true. If there are parameters which value's are known, we can supply these using the `known_ps` argument. Indeed, this might turn other, previously unidentifiable, parameters identifiable. Let us consider this simple example: -```example si2 -using Catalyst, StructuralIdentifiability # hide -rn = @reaction_network begin - (p1+p2, d), 0 <--> X -end -``` -Typically, the two production parameters ($p1$ and $p2$) are unidentifiable. However, we we already know the value of $p1$, then $p2$#s value becomes identifiable: -```example si2 -assess_identifiability(rn; measured_quantities=[:X], known_ps=[:p1]) +In the previous case we assumed that all parameters are unknown, however, this is not necessarily true. If there are parameters which value's are known, we can supply these using the `known_p` argument. Indeed, this might turn other, previously unidentifiable, parameters identifiable. Let us consider the previous example, where we measure the concentration of $M$ only, but also happen to know the production rate of $E$ ($pₑ$): +```example si1 +assess_identifiability(gwo; measured_quantities=[:M], known_p=[:pₑ]) ``` +Not only does this turn the previously non-identifiable `pₑ` (globally) identifiable (which is obvious, given that its value is now known), but this additional information improve identifiability for several other network components. ### Providing non-trivial measured quantities Sometimes, we are not actually measuring species species, but rather some combinations of species (or possibly parameters). Here, any algebraic expression can be used in `measured_quantities`. If so, used species and parameters have to first be `@unpack`'ed from the model. Say that we have a model where an enzyme ($E$) is converted between an active and inactive form, which in turns activates the production of a product, $P$: -```example si3 +```example si2 using Catalyst, StructuralIdentifiability # hide enzyme_activation = @reaction_network begin - (kA,kD), Ei <--> Ea - (Ea, d), 0 <-->P + (kA,kD), Eᵢ <--> Eₐ + (Eₐ, d), 0 <-->P end ``` -and we can measure the total amount of $E$ ($=$Ei+Ea$), as well as the amount of $P$, we can use the following to assess identifiability: -```example si3 -@unpack Ea, Ei = enzyme_activation -assess_identifiability(enzyme_activation; measured_quantities=[Ei+Ea, :P]) -``` - -### Checking identifiability of specific quantities -By default, `asses_identifiability` assesses the identifiability of all parameters and species initial conditions. Sometimes, it is desirable to assess the identifiability of specific quantities. This can be done through the `funcs_to_check` argument. Let us consider our previous example, but say that we can only measure the amount of active enzyme ($Ea$), as well as the product ($P$). If we wish to determine whether the total amount of enzyme ($Ei+Ea$) is identifiable, we could use the following (again using `@unpack` to enable the formation of algebraic expression using the specific quantities): -```example si3 -assess_identifiability(enzyme_activation; measured_quantities=[:Ei, :P], funcs_to_check=[Ei+Ea]) +and we can measure the total amount of $E$ ($=$Eᵢ+Eₐ$), as well as the amount of $P$, we can use the following to assess identifiability: +```example si2 +@unpack Eᵢ, Eₐ = enzyme_activation +assess_identifiability(enzyme_activation; measured_quantities=[Eᵢ+Eₐ, :P]) +nothing # hide ``` ### Probability of correctness -The identifiabiltiy methods used can, in theory, produce erroneous results. However, it is possible to adjust the lower bound for the probability of correctness using the argument `p` (by default set to `0.99`, that is, at least a $99%$ chance of correctness). We can e.g. increase the bound through: +The identifiability methods used can, in theory, produce erroneous results. However, it is possible to adjust the lower bound for the probability of correctness using the argument `p` (by default set to `0.99`, that is, at least a $99%$ chance of correctness). We can e.g. increase the bound through: ```example si2 -assess_identifiability(rn; measured_quantities=[:X], p=0.999) +assess_identifiability(goodwind_oscillator; measured_quantities=[:M], p=0.999) nothing # hide ``` -giving a minimum bound of $99.9%$ chance of correctness. In practise, the bounds used by StructuralIdentifiability are very conservative, which means that while the minimum guaranteed probability of correctness in the default case is $99%$, in practise it is higher. While increasing teh value of `p` increases the certainty of correctness, it will also increase the time required to assess correctness. +giving a minimum bound of $99.9%$ chance of correctness. In practise, the bounds used by StructuralIdentifiability are very conservative, which means that while the minimum guaranteed probability of correctness in the default case is $99%$, in practise it is higher. While increasing the value of `p` increases the certainty of correctness, it will also increase the time required to assess identifiability. ## Local identifiability analysis -Local identifiability can be assessed using the `assess_local_identifiability` function. While this is already determined by the `assess_identifiability` function, local identifiability have the advantage that it is easier to compute. Hence, there might be models where global identifiability analysis fails (or takes prohibitively long time), where instead `assess_local_identifiability` can be used. This functions takes the same inputs as `assess_identifiability` and returns, for each quantity, `true` if iti is locally identifiable and `false` if it is not. Here we assesses local identifiability for the same model as used in the previous example: +Local identifiability can be assessed through the `assess_local_identifiability` function. While this is already determined by `assess_identifiability`, assessing local identifiability only have the advantage that it is easier to compute. Hence, there might be models where global identifiability analysis fails (or takes prohibitively long time), where instead `assess_local_identifiability` can be used. This functions takes the same inputs as `assess_identifiability` and returns, for each quantity, `true` if it is locally identifiable (and `false` if it is not). Here, for the Goodwind oscillator, we assesses it for local identifiability only: ```example si1 +assess_local_identifiability(goodwind_oscillator; measured_quantities=[:M]) ``` -We note that all parameters that `assess_identifiability` determined as either globally or locally identifiable are determined to be locally identifiable, while teh remaining are considered unidentifiable. - +We note that the results are consistent with those produced by `assess_identifiability` (with globally or locally identifiable quantities here all being assessed as at least locally identifiable). ## Finding identifiable functions -Finally, StructuralIdentifiability provides the `find_identifiable_functions` function. Rather than determining the identifiability of each parameter and initial condition of the model, it finds a minimal set of identifiable functions, such as any other identifiable expression of the model can be generated by these. Here, let us consider the following model ... -```example si5 +Finally, StructuralIdentifiability provides the `find_identifiable_functions` function. Rather than determining the identifiability of each parameter and initial condition of the model, it finds a minimal set of identifiable functions, such as any other identifiable expression of the model can be generated by these. Let us again consider the Goodwind oscillator, using the `find_identifiable_functions` function we find that identifiability can be reduced two five globally identifiable expressions: +```example si1 +find_identifiable_functions(goodwind_oscillator; measured_quantities=[:M]) ``` +Again, these results are consistent with those of produced by `assess_identifiability`. There, `pₑ` and `pₚ` where found to be globally identifiable. Here, they correspond directly to identifiable expressions. The remaining four parameters (`pₘ`, `dₘ`, `dₑ`, and `dₚ`) occurs as part of more complicated composite expressions. The `find_identifiable_functions` functions tries to simplify its output functions to create nice expression. The degree to which it does this can be adjusted using the `simplify` keywords. Using the `:weak`, `:standard` (default), and `:strong` arguments, increased simplification can be forced (at the expense of longer runtimes). ## Creating StructuralIdentifiability compatible ODE models from Catalyst `ReactionSystem`s -While the functionality described above covers the vast majority of analysis as user might want to perform, the StructuralIdentifiability package supports several additional features . While these does not have inherent Catalyst support, we do provide the `make_si_ode` function to simplify their use. Similarly to the previous functions, it takes a `ReactionSystem` and lists of measured quantities and known parameter values. The output is a [ode of the standard form supported by StructuralIdentifiability](https://docs.sciml.ai/StructuralIdentifiability/stable/tutorials/creating_ode/#Defining-the-model-using-@ODEmodel-macro). It can be created using the following syntax: -```example si4 -using Catalyst, StructuralIdentifiability # hide - +While the functionality described above covers the vast majority of analysis as user might want to perform, the StructuralIdentifiability package supports several additional features. While these does not have inherent Catalyst support, we do provide the `make_si_ode` function to simplify their use. Similarly to the previous functions, it takes a `ReactionSystem` and lists of measured quantities and known parameter values. The output is a [ode of the standard form supported by StructuralIdentifiability](https://docs.sciml.ai/StructuralIdentifiability/stable/tutorials/creating_ode/#Defining-the-model-using-@ODEmodel-macro). It can be created using the following syntax: +```example si1 +si_ode = make_si_ode(goodwind_oscillator; measured_quantities=[:M]) +nothing # hide ``` -and then used as input to various StructuralIdentifiability functions. In the following example we use the produced ode to -```example si4 - +and then used as input to various StructuralIdentifiability functions. In the following example we uses StructuralIdentifiability's `print_for_DAISY` function, printing the model as an expression that can be used by the [DAISY](https://daisy.dei.unipd.it/) software for identifiability analysis[^3]. +```example si1 +print_for_DAISY(si_ode) +nothing # hide ``` --- @@ -109,4 +112,6 @@ If you use this functionality in your research, please cite the following paper --- ## References -[^1]: [Guillaume H.A. Joseph et al., *Introductory overview of identifiability analysis: A guide to evaluating whether you have the right type of data for your modeling purpose*, Environmental Modelling & Software (2019).](https://www.sciencedirect.com/science/article/pii/S1364815218307278) \ No newline at end of file +[^1]: [Guillaume H.A. Joseph et al., *Introductory overview of identifiability analysis: A guide to evaluating whether you have the right type of data for your modeling purpose*, Environmental Modelling & Software (2019).](https://www.sciencedirect.com/science/article/pii/S1364815218307278) +[^2]: [Goodwin B.C., *Oscillatory Behavior in Enzymatic Control Processes*, Advances in Enzyme Regulation (1965).](https://www.sciencedirect.com/science/article/pii/0065257165900671?via%3Dihub) +[^3]: [Bellu G., et al., *DAISY: A new software tool to test global identifiability of biological and physiological systems*, Computer Methods and Programs in Biomedicine (2007).](https://www.sciencedirect.com/science/article/abs/pii/S0169260707001605) \ No newline at end of file diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 6209471fb4..05fd1bc909 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -20,7 +20,7 @@ end si_ode(rs; measured_quantities = [:X], known_p = [:p]) ``` """ -function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities=observed(rs), known_p = [], ignore_no_measured_warn=false) +function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities = [], known_p = [], ignore_no_measured_warn=false) ignore_no_measured_warn || isempty(measured_quantities) && @warn "No measured quantity provided to the `measured_quantities` argument, any further identifiability analysis will likely fail." known_quantities = make_measured_quantities(rs, measured_quantities, known_p) return StructuralIdentifiability.preprocess_ode(convert(ODESystem, rs), known_quantities)[1] @@ -28,8 +28,8 @@ end # For input measured quantities, if this is not a vector of equations, convert it to a proper form. function make_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{T}, known_p::Vector{S}) where {T,S} - (measured_quantities isa Vector{Symbol}) && (measured_quantities = [Catalyst._symbol_to_var(rs, sym) for sym in measured_quantities]) - (known_p isa Vector{Symbol}) && (known_p = [Catalyst._symbol_to_var(rs, sym) for sym in known_p]) + measured_quantities = [(mq isa Symbol) ? Catalyst._symbol_to_var(rs, mq) : mq for mq in measured_quantities] + known_p = [(p isa Symbol) ? Catalyst._symbol_to_var(rs, p) : p for p in known_p] all_quantities = [measured_quantities; known_p] @variables t (___internal_observables(t))[1:length(all_quantities)] return Equation[(all_quantities[i] isa Equation) ? all_quantities[i] : (___internal_observables[i] ~ all_quantities[i]) for i in 1:length(all_quantities)] @@ -38,17 +38,17 @@ end ### Structural Identifiability Wrappers ### # Local identifiability. -function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), known_p = Num[], kwargs...) +function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], kwargs...) return StructuralIdentifiability.assess_local_identifiability(Catalyst.make_si_ode(rs; measured_quantities, known_p), args...; kwargs...) end # Global identifiability. -function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities=observed(rs), known_p = Num[], kwargs...) +function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], kwargs...) return StructuralIdentifiability.assess_identifiability(Catalyst.make_si_ode(rs; measured_quantities, known_p), args...; kwargs...) end # Identifiable functions. -function StructuralIdentifiability.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities=observed(rs), known_p = Num[], kwargs...) +function StructuralIdentifiability.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], kwargs...) return StructuralIdentifiability.find_identifiable_functions(Catalyst.make_si_ode(rs; measured_quantities, known_p), args...; kwargs...) end From 10d647b8a66ca6913dd8009be25e2b4778230459 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 7 Nov 2023 19:08:38 -0500 Subject: [PATCH 22/80] add tests and change docs --- docs/pages.jl | 3 +- .../petab_ode_param_fitting.md | 2 +- .../structural_identifiability.md | 0 .../structural_identifiability_extension.jl | 2 +- test/extensions/structural_identifiability.jl | 136 +++++++++++++++++- 5 files changed, 139 insertions(+), 4 deletions(-) rename docs/src/{catalyst_applications => inverse_problems}/structural_identifiability.md (100%) diff --git a/docs/pages.jl b/docs/pages.jl index 9b21e12b10..8bfd65c9cb 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -16,6 +16,7 @@ pages = Any["Home" => "index.md", "catalyst_applications/nonlinear_solve.md", "catalyst_applications/bifurcation_diagrams.md"], "Inverse Problems" => Any["inverse_problems/parameter_estimation.md", - "inverse_problems/petab_ode_param_fitting.md"], + "inverse_problems/petab_ode_param_fitting.md", + "inverse_problems/structural_identifiability.md"], "FAQs" => "faqs.md", "API" => "api/catalyst_api.md"] \ No newline at end of file diff --git a/docs/src/inverse_problems/petab_ode_param_fitting.md b/docs/src/inverse_problems/petab_ode_param_fitting.md index 951af4c0a2..fce182e78e 100644 --- a/docs/src/inverse_problems/petab_ode_param_fitting.md +++ b/docs/src/inverse_problems/petab_ode_param_fitting.md @@ -127,7 +127,7 @@ fitted_sol = solve(oprob_fitted, Tsit5()) plot!(fitted_sol; idxs=:P, label="Fitted solution", linestyle=:dash, lw=6, color=:lightblue) ``` -Here we use the `get_ps` function to retrieve a full parameter set using the optimal parameters. Alternatively, the `ODEProblem` or fitted simulation can be retrieved directly using the `get_odeproblem` or `get_odesol` [functions](https://sebapersson.github.io/PEtab.jl/dev/API_choosen/#PEtab.get_odeproblem), respectively (and the initial condition using the `get_u0` function). The calibration result can also be found in `res.xmin`, however, note that PEtab automatically ([unless a linear scale is selected](@ref petab_parameters_scales)) converts parameters to logarithmic scale, so typically `10 .^res.xmin` are the values of interest. If you investigate the result from this example you might note, that even if PEtab.jl has found the global optimum (which fits the data well), this does not actually correspond to the true parameter set. This phenomenon is related to the concept of *identifiability*, which is very important for parameter fitting. +Here we use the `get_ps` function to retrieve a full parameter set using the optimal parameters. Alternatively, the `ODEProblem` or fitted simulation can be retrieved directly using the `get_odeproblem` or `get_odesol` [functions](https://sebapersson.github.io/PEtab.jl/dev/API_choosen/#PEtab.get_odeproblem), respectively (and the initial condition using the `get_u0` function). The calibration result can also be found in `res.xmin`, however, note that PEtab automatically ([unless a linear scale is selected](@ref petab_parameters_scales)) converts parameters to logarithmic scale, so typically `10 .^res.xmin` are the values of interest. If you investigate the result from this example you might note, that even if PEtab.jl has found the global optimum (which fits the data well), this does not actually correspond to the true parameter set. This phenomenon is related to the [concept of *identifiability*](@ref structural_identifiability), which is very important for parameter fitting. ### Final notes PEtab.jl also supports [multistart optimisation](@ref petab_multistart_optimisation), [automatic pre-equilibration before simulations](https://sebapersson.github.io/PEtab.jl/stable/Brannmark/), and [events](@ref petab_events). Various [plot recipes](@ref petab_plotting) exist for investigating the optimisation process. Please read the [PETab.jl documentation](https://sebapersson.github.io/PEtab.jl/stable/) for a more complete description of the package's features. Below follows additional details of various options and features (generally, PEtab is able to find good default values for most options that are not specified). diff --git a/docs/src/catalyst_applications/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md similarity index 100% rename from docs/src/catalyst_applications/structural_identifiability.md rename to docs/src/inverse_problems/structural_identifiability.md diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 05fd1bc909..e4deb0b742 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -23,7 +23,7 @@ si_ode(rs; measured_quantities = [:X], known_p = [:p]) function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities = [], known_p = [], ignore_no_measured_warn=false) ignore_no_measured_warn || isempty(measured_quantities) && @warn "No measured quantity provided to the `measured_quantities` argument, any further identifiability analysis will likely fail." known_quantities = make_measured_quantities(rs, measured_quantities, known_p) - return StructuralIdentifiability.preprocess_ode(convert(ODESystem, rs), known_quantities)[1] + return StructuralIdentifiability.preprocess_ode(convert(ODESystem, rs; expand_functions = true), known_quantities)[1] end # For input measured quantities, if this is not a vector of equations, convert it to a proper form. diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index 00f8ae5dad..5eb7142d9a 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -1,5 +1,139 @@ ### Fetch Packages ### + using Catalyst, Test using StructuralIdentifiability -### Run Tests ### \ No newline at end of file + +### Helper Function ### + +# Converts the output dicts from StructuralIdentifiability functions from "weird symbol => stuff" to "symbol => stuff" (the output have some strange meta data which prevents equality checks, this enables this). +function sym_dict(dict_in) + dict_out = Dict{Symbol,Any}() + for key in keys(dict_in) + dict_out[Symbol(key)] = dict_in[key] + end + return dict_out +end + + +### Run Tests ### + +# Tests for Goodwin model (model with both global, local, and non identifiable components). +# Tests for system using Catalyst function (in this case, michaelis-menten function) +let + goodwind_oscillator_catalyst = @reaction_network begin + (mmr(P,pₘ,1), dₘ), 0 <--> M + (pₑ*M,dₑ), 0 <--> E + (pₚ*E,dₚ), 0 <--> P + end + goodwind_oscillator_si = @ODEmodel( + M'(t) = pₘ / (1 + P(t)) - dₘ*M(t), + E'(t) = -dₑ*E(t) + pₑ*M(t), + P'(t) = -dₚ*P(t) + pₚ*E(t), + y1(t) = M(t) + ) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[:M]) + + gi_1 = assess_identifiability(goodwind_oscillator_catalyst; measured_quantities=[:M]) + gi_2 = assess_identifiability(goodwind_oscillator_si) + gi_3 = assess_identifiability(si_catalyst_ode) + sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) + + li_1 = assess_local_identifiability(goodwind_oscillator_catalyst; measured_quantities=[:M]) + li_2 = assess_local_identifiability(goodwind_oscillator_si) + li_3 = assess_local_identifiability(si_catalyst_ode) + sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) + + ifs1 = find_identifiable_functions(goodwind_oscillator_catalyst; measured_quantities=[:M]) + ifs2 = find_identifiable_functions(goodwind_oscillator_si) + ifs3 = find_identifiable_functions(si_catalyst_ode) + length(ifs1) == length(ifs2) == length(ifs3) +end + +# Tests on a made-up reaction network with mix of identifiable and non-identifiable components. +# Tests for symbolics input. +# Tests using known_p argument. +let + rs_catalyst = @reaction_network begin + (p1, d), 0 <--> X1 + k1, X1 --> X2 + (k2f,k2b), X2 <--> X3 + k3, X3 --> X4 + d, X4 --> 0 + end + @unpack X2, X3 = rs_catalyst + rs_si = @ODEmodel( + X1'(t) = p1 - d*X1(t) - k1*X1(t), + X2'(t) = k1*X1(t) + k2b*X3(t) - k2f*X2(t), + X3'(t) = -k2b*X3(t) + k2f*X2(t) - k3*X3(t), + X4'(t) = d*X4(t) + k3*X3(t), + y1(t) = X2, + y2(t) = X3, + y3(t) = k2f + ) + rs_ode = make_si_ode(rs_catalyst; measured_quantities=[X2, X3], known_p=[:k2f]) + + known_quantities = make_measured_quantities(rs, measured_quantities, known_p) + + gi_1 = assess_identifiability(rs_catalyst; measured_quantities=[X2, X3], known_p=[:k2f]) + gi_2 = assess_identifiability(rs_si) + gi_3 = assess_identifiability(rs_ode) + sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) + + li_1 = assess_local_identifiability(rs_catalyst; measured_quantities=[X2, X3], known_p=[:k2f]) + li_2 = assess_local_identifiability(rs_si) + li_3 = assess_local_identifiability(rs_ode) + sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) + + ifs1 = find_identifiable_functions(rs_catalyst; measured_quantities=[X2, X3], known_p=[:k2f]) + ifs2 = find_identifiable_functions(rs_si) + ifs3 = find_identifiable_functions(rs_ode) + length(ifs1) == length(ifs2) == length(ifs3) +end + +# Tests on a made-up reaction network with mix of identifiable and non-identifiable components. +# Tests for symbolics known_p +# Tests using an equation for measured quantity. +let + rs_catalyst = @reaction_network begin + p, 0 --> X1 + k1, X1 --> X2 + k2, X2 --> X3 + k3, X3 --> X4 + k3, X3 --> X5 + d, (X4,X5) --> 0 + (kA*X3, kD), Yi <--> Ya + end + @unpack X1, X2, X3, X4, k1, k2, Yi, Ya = rs_catalyst + rs_si = @ODEmodel( + X1'(t) = p - k1*X1(t), + X2'(t) = k1*X1(t) - k2*X2(t), + X3'(t) = k2*X2(t) - 2k3*X3(t), + X4'(t) = -d*X4(t) + k3*X3(t), + X5'(t) = -d*X5(t) + k3*X3(t), + Yi'(t) = kD*Ya(t) - kA*Yi(t)*X3(t), + Ya'(t) = -kD*Ya(t) + kA*Yi(t)*X3(t), + y1(t) = X1 + Ya, + y2(t) = X2, + y3(t) = k1, + y4(t) = k2 + ) + rs_ode = make_si_ode(rs_catalyst; measured_quantities=[X + Yi, YaX2], known_p=[k1, kD]) + + known_quantities = make_measured_quantities(rs, measured_quantities, known_p) + + gi_1 = assess_identifiability(rs_catalyst; measured_quantities=[X + Yi, YaX2], known_p=[k1, kD]) + gi_2 = assess_identifiability(rs_si) + gi_3 = assess_identifiability(rs_ode) + sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) + + li_1 = assess_local_identifiability(rs_catalyst; measured_quantities=[X + Yi, YaX2], known_p=[k1, kD]) + li_2 = assess_local_identifiability(rs_si) + li_3 = assess_local_identifiability(rs_ode) + sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) + + ifs1 = find_identifiable_functions(rs_catalyst; measured_quantities=[X + Yi, YaX2], known_p=[k1, kD]) + ifs2 = find_identifiable_functions(rs_si) + ifs3 = find_identifiable_functions(rs_ode) + length(ifs1) == length(ifs2) == length(ifs3) +end \ No newline at end of file From ad44887e8982239e62ba3d03ec4756e6eb44a112 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 8 Nov 2023 14:02:54 -0500 Subject: [PATCH 23/80] update --- .../structural_identifiability.md | 2 + .../structural_identifiability_extension.jl | 5 +- test/extensions/structural_identifiability.jl | 127 +++++++++++------- 3 files changed, 84 insertions(+), 50 deletions(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 78c1163c9c..b6d21a6efa 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -42,6 +42,8 @@ assess_identifiability(gwo; measured_quantities=[:M], known_p=[:pₑ]) ``` Not only does this turn the previously non-identifiable `pₑ` (globally) identifiable (which is obvious, given that its value is now known), but this additional information improve identifiability for several other network components. +To, in a similar manner, indicate that certain initial conditions are known is a work in progress. Hopefully it should be an available feature in the near future. + ### Providing non-trivial measured quantities Sometimes, we are not actually measuring species species, but rather some combinations of species (or possibly parameters). Here, any algebraic expression can be used in `measured_quantities`. If so, used species and parameters have to first be `@unpack`'ed from the model. Say that we have a model where an enzyme ($E$) is converted between an active and inactive form, which in turns activates the production of a product, $P$: ```example si2 diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index e4deb0b742..700b8bf239 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -21,16 +21,15 @@ si_ode(rs; measured_quantities = [:X], known_p = [:p]) ``` """ function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities = [], known_p = [], ignore_no_measured_warn=false) - ignore_no_measured_warn || isempty(measured_quantities) && @warn "No measured quantity provided to the `measured_quantities` argument, any further identifiability analysis will likely fail." + ignore_no_measured_warn || isempty(measured_quantities) && @warn "No measured quantity provided to the `measured_quantities` argument, any further identifiability analysis will likely fail. You can disable this warning by setting `ignore_no_measured_warn=true`." known_quantities = make_measured_quantities(rs, measured_quantities, known_p) return StructuralIdentifiability.preprocess_ode(convert(ODESystem, rs; expand_functions = true), known_quantities)[1] end # For input measured quantities, if this is not a vector of equations, convert it to a proper form. function make_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{T}, known_p::Vector{S}) where {T,S} - measured_quantities = [(mq isa Symbol) ? Catalyst._symbol_to_var(rs, mq) : mq for mq in measured_quantities] - known_p = [(p isa Symbol) ? Catalyst._symbol_to_var(rs, p) : p for p in known_p] all_quantities = [measured_quantities; known_p] + all_quantities = [(quant isa Symbol) ? Catalyst._symbol_to_var(rs, quant) : quant for quant in all_quantities] @variables t (___internal_observables(t))[1:length(all_quantities)] return Equation[(all_quantities[i] isa Equation) ? all_quantities[i] : (___internal_observables[i] ~ all_quantities[i]) for i in 1:length(all_quantities)] end diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index 5eb7142d9a..da73eb08b4 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -21,39 +21,44 @@ end # Tests for Goodwin model (model with both global, local, and non identifiable components). # Tests for system using Catalyst function (in this case, michaelis-menten function) let + # Identifiability analysis for Catalyst model. goodwind_oscillator_catalyst = @reaction_network begin (mmr(P,pₘ,1), dₘ), 0 <--> M (pₑ*M,dₑ), 0 <--> E (pₚ*E,dₚ), 0 <--> P end + gi_1 = assess_identifiability(goodwind_oscillator_catalyst; measured_quantities=[:M]) + li_1 = assess_local_identifiability(goodwind_oscillator_catalyst; measured_quantities=[:M]) + ifs_1 = find_identifiable_functions(goodwind_oscillator_catalyst; measured_quantities=[:M]) + + # Identifiability analysis for Catalyst converted to StructuralIdentifiability.jl model. + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[:M]) + gi_2 = assess_identifiability(si_catalyst_ode) + li_2 = assess_local_identifiability(si_catalyst_ode) + ifs_2 = find_identifiable_functions(si_catalyst_ode) + + # Identifiability analysis for StructuralIdentifiability.jl model (declare this overwrites e.g. X2 variable etc.). goodwind_oscillator_si = @ODEmodel( M'(t) = pₘ / (1 + P(t)) - dₘ*M(t), E'(t) = -dₑ*E(t) + pₑ*M(t), P'(t) = -dₚ*P(t) + pₚ*E(t), y1(t) = M(t) ) - si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[:M]) - - gi_1 = assess_identifiability(goodwind_oscillator_catalyst; measured_quantities=[:M]) - gi_2 = assess_identifiability(goodwind_oscillator_si) - gi_3 = assess_identifiability(si_catalyst_ode) + gi_3 = assess_identifiability(goodwind_oscillator_si) + li_3 = assess_local_identifiability(goodwind_oscillator_si) + ifs_3 = find_identifiable_functions(goodwind_oscillator_si) + + # Check outputs. sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) - - li_1 = assess_local_identifiability(goodwind_oscillator_catalyst; measured_quantities=[:M]) - li_2 = assess_local_identifiability(goodwind_oscillator_si) - li_3 = assess_local_identifiability(si_catalyst_ode) sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) - - ifs1 = find_identifiable_functions(goodwind_oscillator_catalyst; measured_quantities=[:M]) - ifs2 = find_identifiable_functions(goodwind_oscillator_si) - ifs3 = find_identifiable_functions(si_catalyst_ode) - length(ifs1) == length(ifs2) == length(ifs3) + length(ifs_1) == length(ifs_2) == length(ifs_3) end # Tests on a made-up reaction network with mix of identifiable and non-identifiable components. # Tests for symbolics input. # Tests using known_p argument. let + # Identifiability analysis for Catalyst model. rs_catalyst = @reaction_network begin (p1, d), 0 <--> X1 k1, X1 --> X2 @@ -61,7 +66,18 @@ let k3, X3 --> X4 d, X4 --> 0 end - @unpack X2, X3 = rs_catalyst + @unpack X2, X3 = rs_catalyst + gi_1 = assess_identifiability(rs_catalyst; measured_quantities=[X2, X3], known_p=[:k2f]) + li_1 = assess_local_identifiability(rs_catalyst; measured_quantities=[X2, X3], known_p=[:k2f]) + ifs_1 = find_identifiable_functions(rs_catalyst; measured_quantities=[X2, X3], known_p=[:k2f]) + + # Identifiability analysis for Catalyst converted to StructuralIdentifiability.jl model. + rs_ode = make_si_ode(rs_catalyst; measured_quantities=[X2, X3], known_p=[:k2f]) + gi_2 = assess_identifiability(rs_ode) + li_2 = assess_local_identifiability(rs_ode) + ifs_2 = find_identifiable_functions(rs_ode) + + # Identifiability analysis for StructuralIdentifiability.jl model (declare this overwrites e.g. X2 variable etc.). rs_si = @ODEmodel( X1'(t) = p1 - d*X1(t) - k1*X1(t), X2'(t) = k1*X1(t) + k2b*X3(t) - k2f*X2(t), @@ -71,30 +87,21 @@ let y2(t) = X3, y3(t) = k2f ) - rs_ode = make_si_ode(rs_catalyst; measured_quantities=[X2, X3], known_p=[:k2f]) - - known_quantities = make_measured_quantities(rs, measured_quantities, known_p) - - gi_1 = assess_identifiability(rs_catalyst; measured_quantities=[X2, X3], known_p=[:k2f]) - gi_2 = assess_identifiability(rs_si) - gi_3 = assess_identifiability(rs_ode) + gi_3 = assess_identifiability(rs_si) + li_3 = assess_local_identifiability(rs_si) + ifs_3 = find_identifiable_functions(rs_si) + + # Check outputs. sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) - - li_1 = assess_local_identifiability(rs_catalyst; measured_quantities=[X2, X3], known_p=[:k2f]) - li_2 = assess_local_identifiability(rs_si) - li_3 = assess_local_identifiability(rs_ode) sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) - - ifs1 = find_identifiable_functions(rs_catalyst; measured_quantities=[X2, X3], known_p=[:k2f]) - ifs2 = find_identifiable_functions(rs_si) - ifs3 = find_identifiable_functions(rs_ode) - length(ifs1) == length(ifs2) == length(ifs3) + length(ifs_1) == length(ifs_2) == length(ifs_3) end # Tests on a made-up reaction network with mix of identifiable and non-identifiable components. # Tests for symbolics known_p # Tests using an equation for measured quantity. let + # Identifiability analysis for Catalyst model. rs_catalyst = @reaction_network begin p, 0 --> X1 k1, X1 --> X2 @@ -104,7 +111,18 @@ let d, (X4,X5) --> 0 (kA*X3, kD), Yi <--> Ya end - @unpack X1, X2, X3, X4, k1, k2, Yi, Ya = rs_catalyst + @unpack X1, X2, X3, X4, k1, k2, Yi, Ya, k1, kD = rs_catalyst + gi_1 = assess_identifiability(rs_catalyst; measured_quantities=[X1 + Yi, Ya], known_p=[k1, kD]) + li_1 = assess_local_identifiability(rs_catalyst; measured_quantities=[X1 + Yi, Ya], known_p=[k1, kD]) + ifs_1 = find_identifiable_functions(rs_catalyst; measured_quantities=[X1 + Yi, Ya], known_p=[k1, kD]) + + # Identifiability analysis for Catalyst converted to StructuralIdentifiability.jl model. + rs_ode = make_si_ode(rs_catalyst; measured_quantities=[X1 + Yi, Ya], known_p=[k1, kD]) + gi_2 = assess_identifiability(rs_ode) + li_2 = assess_local_identifiability(rs_ode) + ifs_2 = find_identifiable_functions(rs_ode) + + # Identifiability analysis for StructuralIdentifiability.jl model (declare this overwrites e.g. X2 variable etc.). rs_si = @ODEmodel( X1'(t) = p - k1*X1(t), X2'(t) = k1*X1(t) - k2*X2(t), @@ -118,22 +136,37 @@ let y3(t) = k1, y4(t) = k2 ) - rs_ode = make_si_ode(rs_catalyst; measured_quantities=[X + Yi, YaX2], known_p=[k1, kD]) - - known_quantities = make_measured_quantities(rs, measured_quantities, known_p) + gi_3 = assess_identifiability(rs_si) + li_3 = assess_local_identifiability(rs_si) + ifs_3 = find_identifiable_functions(rs_si) - gi_1 = assess_identifiability(rs_catalyst; measured_quantities=[X + Yi, YaX2], known_p=[k1, kD]) - gi_2 = assess_identifiability(rs_si) - gi_3 = assess_identifiability(rs_ode) + # Check outputs. sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) - - li_1 = assess_local_identifiability(rs_catalyst; measured_quantities=[X + Yi, YaX2], known_p=[k1, kD]) - li_2 = assess_local_identifiability(rs_si) - li_3 = assess_local_identifiability(rs_ode) sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) - - ifs1 = find_identifiable_functions(rs_catalyst; measured_quantities=[X + Yi, YaX2], known_p=[k1, kD]) - ifs2 = find_identifiable_functions(rs_si) - ifs3 = find_identifiable_functions(rs_ode) - length(ifs1) == length(ifs2) == length(ifs3) + length(ifs_1) == length(ifs_2) == length(ifs_3) +end + +# Tests that various inputs types work. +let + goodwind_oscillator_catalyst = @reaction_network begin + (mmr(P,pₘ,1), dₘ), 0 <--> M + (pₑ*M,dₑ), 0 <--> E + (pₚ*E,dₚ), 0 <--> P + end + @unpack M, E, P, pₑ, pₚ, pₘ = goodwind_oscillator_catalyst + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[:M]) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; known_p=[:pₑ]) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[:M], known_p=[:pₑ]) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[:M, :E], known_p=[:pₑ]) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[:M], known_p=[:pₑ, :pₚ]) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[:M, :E], known_p=[:pₑ, :pₚ]) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[M]) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; known_p=[pₑ]) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[M], known_p=[pₑ]) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[M, E], known_p=[pₑ]) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[M], known_p=[pₑ, pₚ]) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[M, E], known_p=[pₑ, pₚ]) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[M + pₑ]) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[M + E, pₑ*M], known_p=[:pₑ]) + si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[pₑ, pₚ], known_p=[pₑ]) end \ No newline at end of file From d310175a8d509d68530ffd1f7ca9326cf96e53a9 Mon Sep 17 00:00:00 2001 From: Torkel Date: Thu, 9 Nov 2023 09:42:51 -0500 Subject: [PATCH 24/80] add sasha's tests --- test/extensions/structural_identifiability.jl | 72 ++++++++++++++++++- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index da73eb08b4..764a0046f9 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -131,10 +131,10 @@ let X5'(t) = -d*X5(t) + k3*X3(t), Yi'(t) = kD*Ya(t) - kA*Yi(t)*X3(t), Ya'(t) = -kD*Ya(t) + kA*Yi(t)*X3(t), - y1(t) = X1 + Ya, - y2(t) = X2, + y1(t) = X1 + Yi, + y2(t) = Ya, y3(t) = k1, - y4(t) = k2 + y4(t) = kD ) gi_3 = assess_identifiability(rs_si) li_3 = assess_local_identifiability(rs_si) @@ -169,4 +169,70 @@ let si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[M + pₑ]) si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[M + E, pₑ*M], known_p=[:pₑ]) si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[pₑ, pₚ], known_p=[pₑ]) + + # Tests using model.component style (have to make system complete first). + gw_osc_complt = complete(goodwind_oscillator_catalyst) + make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M]) + make_si_ode(gw_osc_complt; known_p=[gw_osc_complt.pₑ]) + make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M], known_p=[gw_osc_complt.pₑ]) + make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M, gw_osc_complt.E], known_p=[gw_osc_complt.pₑ]) + make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M], known_p=[gw_osc_complt.pₑ, gw_osc_complt.pₚ]) + make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M], known_p = [:pₚ]) + make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M*gw_osc_complt.E]) +end + +# Tests directly on reaction systems with known identifiability structures. +# Test provided by Alexander Demin. +let + rs = @reaction_network begin + k1, x1 --> x2 + end + # Measure the source + id_report = assess_identifiability(rs, measured_quantities = [:x1]) + @test sym_dict(id_report) == Dict( + :x1 => :globally, + :x2 => :nonidentifiable, + :k1 => :globally + ) + # Measure the target instead + id_report = assess_identifiability(rs, measured_quantities = [:x2]) + @test sym_dict(id_report) == Dict( + :x1 => :globally, + :x2 => :globally, + :k1 => :globally + ) + + # Example from + # Identifiability of chemical reaction networks + # DOI: 10.1007/s10910-007-9307-x + # The rate constants a, b, c are not identifiable even if all of the species + # are observed. + rs = @reaction_network begin + a, A0 --> 2A1 + b, A0 --> 2A2 + c, A0 --> A1 + A2 + end + id_report = assess_identifiability(rs, measured_quantities = [:A0, :A1, :A2]) + @test sym_dict(id_report) == Dict( + :A0 => :globally, + :A1 => :globally, + :A2 => :globally, + :a => :nonidentifiable, + :b => :nonidentifiable, + :c => :nonidentifiable + ) + + # Test with no parameters + rs = @reaction_network begin + 1, x1 --> x2 + 1, x2 --> x3 + end + id_report = assess_identifiability(rs, measured_quantities = [:x3]) + @test sym_dict(id_report) == Dict( + :x1 => :globally, + :x2 => :globally, + :x3 => :globally, + ) + # Will probably be fixed in the 0.5 release of SI.jl + @test_broken find_identifiable_functions(rs, measured_quantities = [:x3]) end \ No newline at end of file From 5376770e0d4ba7a63c2c2df930dc2ad8ce74c3f6 Mon Sep 17 00:00:00 2001 From: Torkel Date: Thu, 9 Nov 2023 14:08:20 -0500 Subject: [PATCH 25/80] up --- test/extensions/structural_identifiability.jl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index 764a0046f9..22d57903d2 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -49,9 +49,9 @@ let ifs_3 = find_identifiable_functions(goodwind_oscillator_si) # Check outputs. - sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) - sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) - length(ifs_1) == length(ifs_2) == length(ifs_3) + @test sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) + @test sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) + @test length(ifs_1) == length(ifs_2) == length(ifs_3) end # Tests on a made-up reaction network with mix of identifiable and non-identifiable components. @@ -92,9 +92,9 @@ let ifs_3 = find_identifiable_functions(rs_si) # Check outputs. - sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) - sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) - length(ifs_1) == length(ifs_2) == length(ifs_3) + @test sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) + @test sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) + @test length(ifs_1) == length(ifs_2) == length(ifs_3) end # Tests on a made-up reaction network with mix of identifiable and non-identifiable components. @@ -141,9 +141,9 @@ let ifs_3 = find_identifiable_functions(rs_si) # Check outputs. - sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) - sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) - length(ifs_1) == length(ifs_2) == length(ifs_3) + @test sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) + @test sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) + @test length(ifs_1) == length(ifs_2) == length(ifs_3) end # Tests that various inputs types work. From 9610c92ec1eee49f27bde916b0b5ac2523203afe Mon Sep 17 00:00:00 2001 From: Torkel Date: Thu, 9 Nov 2023 14:08:45 -0500 Subject: [PATCH 26/80] MTK itnernal remake, causes problems though --- .../structural_identifiability_extension.jl | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 700b8bf239..6747df1c63 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -21,13 +21,13 @@ si_ode(rs; measured_quantities = [:X], known_p = [:p]) ``` """ function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities = [], known_p = [], ignore_no_measured_warn=false) - ignore_no_measured_warn || isempty(measured_quantities) && @warn "No measured quantity provided to the `measured_quantities` argument, any further identifiability analysis will likely fail. You can disable this warning by setting `ignore_no_measured_warn=true`." - known_quantities = make_measured_quantities(rs, measured_quantities, known_p) + known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) return StructuralIdentifiability.preprocess_ode(convert(ODESystem, rs; expand_functions = true), known_quantities)[1] end # For input measured quantities, if this is not a vector of equations, convert it to a proper form. -function make_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{T}, known_p::Vector{S}) where {T,S} +function make_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{T}, known_p::Vector{S}; ignore_no_measured_warn=false) where {T,S} + ignore_no_measured_warn || isempty(measured_quantities) && @warn "No measured quantity provided to the `measured_quantities` argument, any further identifiability analysis will likely fail. You can disable this warning by setting `ignore_no_measured_warn=true`." all_quantities = [measured_quantities; known_p] all_quantities = [(quant isa Symbol) ? Catalyst._symbol_to_var(rs, quant) : quant for quant in all_quantities] @variables t (___internal_observables(t))[1:length(all_quantities)] @@ -37,17 +37,23 @@ end ### Structural Identifiability Wrappers ### # Local identifiability. -function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], kwargs...) - return StructuralIdentifiability.assess_local_identifiability(Catalyst.make_si_ode(rs; measured_quantities, known_p), args...; kwargs...) +function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], ignore_no_measured_warn=false, kwargs...) + known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) + osys = convert(ODESystem, rs; expand_functions = true) + return StructuralIdentifiability.assess_local_identifiability(osys, args...; measured_quantities=known_quantities, kwargs...) end # Global identifiability. -function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], kwargs...) - return StructuralIdentifiability.assess_identifiability(Catalyst.make_si_ode(rs; measured_quantities, known_p), args...; kwargs...) +function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], ignore_no_measured_warn=false, kwargs...) + known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) + osys = convert(ODESystem, rs; expand_functions = true) + return StructuralIdentifiability.assess_identifiability(osys, args...; measured_quantities=known_quantities, kwargs...) end # Identifiable functions. -function StructuralIdentifiability.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], kwargs...) - return StructuralIdentifiability.find_identifiable_functions(Catalyst.make_si_ode(rs; measured_quantities, known_p), args...; kwargs...) +function StructuralIdentifiability.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], ignore_no_measured_warn=false, kwargs...) + known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) + osys = convert(ODESystem, rs; expand_functions = true) + return StructuralIdentifiability.find_identifiable_functions(osys, args...; measured_quantities=known_quantities, kwargs...) end From 2f2be40d9e2c66a754d84f40292fa5b64a3518e4 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 14 Nov 2023 17:39:44 -0500 Subject: [PATCH 27/80] up --- .../structural_identifiability.md | 46 +++++++++++++------ .../structural_identifiability_extension.jl | 8 ++-- test/extensions/structural_identifiability.jl | 2 +- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index b6d21a6efa..12fb998b60 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -7,14 +7,20 @@ where, however much data is collected on *x*, it is impossible to determine the Catalyst contains a special extension for carrying out structural identifiability analysis using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. It also implements specialised routines to make these more efficient when applied to reaction network models. How to use these functions are described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). If you use this in your research, please [cite the StructuralIdentifiability.jl](@ref structural_identifiability_citation) and [Catalyst.jl](@ref catalyst_citation) publications. -Structural identifiability can be divided into *local* and *global* identifiability. If a model quantity (which can either be a parameter or an initial condition) is locally identifiable, it means that its true value can be determined down to a finite-number of possible options. This also means that there is some limited region around its true value, where the true value is the only possible value. Globally identifiable quantities' values, on the other hand, can be uniquely determined. Again, while parameter (and initial condition) identifiability can be confirmed structurally for a model, it does not necessarily mean that they are practically identifiable for some given data. +Structural identifiability can be divided into *local* and *global* identifiability. If a model quantity is locally identifiable, it means that its true value can be determined down to a finite-number of possible options. This also means that there is some limited region around its true value, where the true value is the only possible value. Globally identifiable quantities' values, on the other hand, can be uniquely determined. Again, while identifiability can be confirmed structurally for a model, it does not necessarily mean that they are practically identifiable for some given data. +Generally, there are three types of quantities for which identifiability can be assessed. +- Parameters (e.g. $p$). +- Full variable trajectories (e.g. $x(t)$). +- Variable initial conditions (e.g. $x(0)$). + +StructutralIdentifiability currently assesses identifiability for the first two (however, if $x(t)$ is identifiable, $x(0)$ will be as well). ## Global identifiability analysis ### Basic example -Local identifiability can be assessed using the `assess_identifiability` function. For each model quantity (parameters and initial conditions), it will asses whether they are: +Local identifiability can be assessed using the `assess_identifiability` function. For each model quantity (parameters and variables), it will asses whether they are: - globally identifiable. - locally identifiable. - Unidentifiable. @@ -27,22 +33,26 @@ goodwind_oscillator = @reaction_network begin (pₑ*M,dₑ), 0 <--> E (pₚ*E,dₚ), 0 <--> P end -assess_identifiability(goodwind_oscillator; measured_quantities=[:M]) +assess_identifiability(goodwind_oscillator; measured_quantities=[:M], loglevel=Logging.Error) ``` -From the output, we find that `E`, `pₑ`, and `pₚ` (the initial concentration of $E$, and the production rates of $E$ and $P$, respectively) are non-identifiable. Next, `dₑ` and `dₚ` (the degradation rates of $E$ and $P$, respectively) are locally identifiable. Finally, `P`, `M`, `pₘ`, and `dₘ` (the initial concentrations of `P` and `M`, the production and degradation rate of `M`, respectively) are all globally identifiable. Next, we can assess identifiability in the case where we can measure all three species concentrations: +From the output, we find that `E(t)`, `pₑ`, and `pₚ` (the trajectory of $E$, and the production rates of $E$ and $P$, respectively) are non-identifiable. Next, `dₑ` and `dₚ` (the degradation rates of $E$ and $P$, respectively) are locally identifiable. Finally, `P(t)`, `M(t)`, `pₘ`, and `dₘ` (the trajectories of `P` and `M`, and the production and degradation rate of `M`, respectively) are all globally identifiable. + +Next, we can assess identifiability in the case where we can measure all three species concentrations: ```example si1 -assess_identifiability(goodwind_oscillator; measured_quantities=[:M, :P, :E]) +assess_identifiability(goodwind_oscillator; measured_quantities=[:M, :P, :E], loglevel=Logging.Error) ``` in which case all initial conditions and parameters become identifiable. +StructuralIdentifiability functions generally provides a large number of output logging messages. Hence, in the above examples, and throughout the rest of this tutorial, we use the `loglevel=Logging.Error` option to turn these off. + ### Indicating known parameters In the previous case we assumed that all parameters are unknown, however, this is not necessarily true. If there are parameters which value's are known, we can supply these using the `known_p` argument. Indeed, this might turn other, previously unidentifiable, parameters identifiable. Let us consider the previous example, where we measure the concentration of $M$ only, but also happen to know the production rate of $E$ ($pₑ$): ```example si1 -assess_identifiability(gwo; measured_quantities=[:M], known_p=[:pₑ]) +assess_identifiability(gwo; measured_quantities=[:M], known_p=[:pₑ], loglevel=Logging.Error) ``` Not only does this turn the previously non-identifiable `pₑ` (globally) identifiable (which is obvious, given that its value is now known), but this additional information improve identifiability for several other network components. -To, in a similar manner, indicate that certain initial conditions are known is a work in progress. Hopefully it should be an available feature in the near future. +To, in a similar manner, indicate that certain initial conditions are known is a work in progress. Hopefully the feature should be an available in the near future. ### Providing non-trivial measured quantities Sometimes, we are not actually measuring species species, but rather some combinations of species (or possibly parameters). Here, any algebraic expression can be used in `measured_quantities`. If so, used species and parameters have to first be `@unpack`'ed from the model. Say that we have a model where an enzyme ($E$) is converted between an active and inactive form, which in turns activates the production of a product, $P$: @@ -56,29 +66,37 @@ end and we can measure the total amount of $E$ ($=$Eᵢ+Eₐ$), as well as the amount of $P$, we can use the following to assess identifiability: ```example si2 @unpack Eᵢ, Eₐ = enzyme_activation -assess_identifiability(enzyme_activation; measured_quantities=[Eᵢ+Eₐ, :P]) +assess_identifiability(enzyme_activation; measured_quantities=[Eᵢ+Eₐ, :P], loglevel=Logging.Error) nothing # hide ``` +### Assessing identifiability for specified quantities only +By default, StructuralIdentifiability assesses identifiability for all parameters and variables. It is, however, possible to designate precisely which quantities you want to check using the `funcs_to_check` option. This both includes selecting a smaller subset of parameters and variables to check, or defining customised expressions. Let us consider the Goodwind from previously, and say that we would like to check whether the production parameters ($pₘ$, $pₑ$, and $pₚ$) and the total amount of the three species ($P + M + E$) are identifiable quantities. Here, we would first unpack these (allowing us to form algebraic expressions) and then use the following code: +```example si1 +@unpack pₘ, pₑ, and pₚ, M, E, P = goodwind_oscillator +assess_identifiability(goodwind_oscillator; funcs_to_check=[pₘ, pₑ, pₚ, M + E + P], loglevel=Logging.Error) +``` + + ### Probability of correctness The identifiability methods used can, in theory, produce erroneous results. However, it is possible to adjust the lower bound for the probability of correctness using the argument `p` (by default set to `0.99`, that is, at least a $99%$ chance of correctness). We can e.g. increase the bound through: ```example si2 -assess_identifiability(goodwind_oscillator; measured_quantities=[:M], p=0.999) +assess_identifiability(goodwind_oscillator; measured_quantities=[:M], p=0.999, loglevel=Logging.Error) nothing # hide ``` -giving a minimum bound of $99.9%$ chance of correctness. In practise, the bounds used by StructuralIdentifiability are very conservative, which means that while the minimum guaranteed probability of correctness in the default case is $99%$, in practise it is higher. While increasing the value of `p` increases the certainty of correctness, it will also increase the time required to assess identifiability. +giving a minimum bound of $99.9%$ chance of correctness. In practise, the bounds used by StructuralIdentifiability are very conservative, which means that while the minimum guaranteed probability of correctness in the default case is $99%$, in practise it is much higher. While increasing the value of `p` increases the certainty of correctness, it will also increase the time required to assess identifiability. ## Local identifiability analysis -Local identifiability can be assessed through the `assess_local_identifiability` function. While this is already determined by `assess_identifiability`, assessing local identifiability only have the advantage that it is easier to compute. Hence, there might be models where global identifiability analysis fails (or takes prohibitively long time), where instead `assess_local_identifiability` can be used. This functions takes the same inputs as `assess_identifiability` and returns, for each quantity, `true` if it is locally identifiable (and `false` if it is not). Here, for the Goodwind oscillator, we assesses it for local identifiability only: +Local identifiability can be assessed through the `assess_local_identifiability` function. While this is already determined by `assess_identifiability`, assessing local identifiability only have the advantage that it is easier to compute. Hence, there might be models where global identifiability analysis fails (or takes prohibitively long time), where instead `assess_local_identifiability` can be used. This functions takes the same inputs as `assess_identifiability` and returns, for each quantity, `true` if it is locally identifiable (or `false` if it is not). Here, for the Goodwind oscillator, we assesses it for local identifiability only: ```example si1 -assess_local_identifiability(goodwind_oscillator; measured_quantities=[:M]) +assess_local_identifiability(goodwind_oscillator; measured_quantities=[:M], loglevel=Logging.Error) ``` We note that the results are consistent with those produced by `assess_identifiability` (with globally or locally identifiable quantities here all being assessed as at least locally identifiable). ## Finding identifiable functions -Finally, StructuralIdentifiability provides the `find_identifiable_functions` function. Rather than determining the identifiability of each parameter and initial condition of the model, it finds a minimal set of identifiable functions, such as any other identifiable expression of the model can be generated by these. Let us again consider the Goodwind oscillator, using the `find_identifiable_functions` function we find that identifiability can be reduced two five globally identifiable expressions: +Finally, StructuralIdentifiability provides the `find_identifiable_functions` function. Rather than determining the identifiability of each parameter and initial condition of the model, it finds a minimal set of identifiable functions, such as any other identifiable expression of the model can be generated by these. Let us again consider the Goodwind oscillator, using the `find_identifiable_functions` function we find that identifiability can be reduced to five globally identifiable expressions: ```example si1 -find_identifiable_functions(goodwind_oscillator; measured_quantities=[:M]) +find_identifiable_functions(goodwind_oscillator; measured_quantities=[:M], loglevel=Logging.Error) ``` Again, these results are consistent with those of produced by `assess_identifiability`. There, `pₑ` and `pₚ` where found to be globally identifiable. Here, they correspond directly to identifiable expressions. The remaining four parameters (`pₘ`, `dₘ`, `dₑ`, and `dₚ`) occurs as part of more complicated composite expressions. diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 6747df1c63..d57a730161 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -22,7 +22,7 @@ si_ode(rs; measured_quantities = [:X], known_p = [:p]) """ function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities = [], known_p = [], ignore_no_measured_warn=false) known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) - return StructuralIdentifiability.preprocess_ode(convert(ODESystem, rs; expand_functions = true), known_quantities)[1] + return StructuralIdentifiability.preprocess_ode(convert(ODESystem, rs), known_quantities)[1] end # For input measured quantities, if this is not a vector of equations, convert it to a proper form. @@ -39,21 +39,21 @@ end # Local identifiability. function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], ignore_no_measured_warn=false, kwargs...) known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) - osys = convert(ODESystem, rs; expand_functions = true) + osys = convert(ODESystem, rs) return StructuralIdentifiability.assess_local_identifiability(osys, args...; measured_quantities=known_quantities, kwargs...) end # Global identifiability. function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], ignore_no_measured_warn=false, kwargs...) known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) - osys = convert(ODESystem, rs; expand_functions = true) + osys = convert(ODESystem, rs) return StructuralIdentifiability.assess_identifiability(osys, args...; measured_quantities=known_quantities, kwargs...) end # Identifiable functions. function StructuralIdentifiability.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], ignore_no_measured_warn=false, kwargs...) known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) - osys = convert(ODESystem, rs; expand_functions = true) + osys = convert(ODESystem, rs) return StructuralIdentifiability.find_identifiable_functions(osys, args...; measured_quantities=known_quantities, kwargs...) end diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index 22d57903d2..d6a14de4c8 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -19,7 +19,7 @@ end ### Run Tests ### # Tests for Goodwin model (model with both global, local, and non identifiable components). -# Tests for system using Catalyst function (in this case, michaelis-menten function) +# Tests for system using Catalyst function (in this case, Michaelis-Menten function) let # Identifiability analysis for Catalyst model. goodwind_oscillator_catalyst = @reaction_network begin From 5fcdd06e99161ab87adebcb22cffc9683d855a2a Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 14 Nov 2023 18:27:44 -0500 Subject: [PATCH 28/80] update --- docs/src/inverse_problems/structural_identifiability.md | 6 +++--- .../structural_identifiability_extension.jl | 4 ++++ test/extensions/structural_identifiability.jl | 7 +++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 12fb998b60..8abbde642b 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -73,11 +73,11 @@ nothing # hide ### Assessing identifiability for specified quantities only By default, StructuralIdentifiability assesses identifiability for all parameters and variables. It is, however, possible to designate precisely which quantities you want to check using the `funcs_to_check` option. This both includes selecting a smaller subset of parameters and variables to check, or defining customised expressions. Let us consider the Goodwind from previously, and say that we would like to check whether the production parameters ($pₘ$, $pₑ$, and $pₚ$) and the total amount of the three species ($P + M + E$) are identifiable quantities. Here, we would first unpack these (allowing us to form algebraic expressions) and then use the following code: ```example si1 -@unpack pₘ, pₑ, and pₚ, M, E, P = goodwind_oscillator -assess_identifiability(goodwind_oscillator; funcs_to_check=[pₘ, pₑ, pₚ, M + E + P], loglevel=Logging.Error) +@unpack pₘ, pₑ, pₚ, M, E, P = goodwind_oscillator +assess_identifiability(goodwind_oscillator; measured_quantities=[:M], funcs_to_check=[pₘ, pₑ, pₚ, M + E + P], loglevel=Logging.Error) +nothing # hide ``` - ### Probability of correctness The identifiability methods used can, in theory, produce erroneous results. However, it is possible to adjust the lower bound for the probability of correctness using the argument `p` (by default set to `0.99`, that is, at least a $99%$ chance of correctness). We can e.g. increase the bound through: ```example si2 diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index d57a730161..75f0978336 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -21,6 +21,7 @@ si_ode(rs; measured_quantities = [:X], known_p = [:p]) ``` """ function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities = [], known_p = [], ignore_no_measured_warn=false) + rs = Catalyst.expand_registered_functions(rs) known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) return StructuralIdentifiability.preprocess_ode(convert(ODESystem, rs), known_quantities)[1] end @@ -38,6 +39,7 @@ end # Local identifiability. function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], ignore_no_measured_warn=false, kwargs...) + rs = Catalyst.expand_registered_functions(rs) known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) osys = convert(ODESystem, rs) return StructuralIdentifiability.assess_local_identifiability(osys, args...; measured_quantities=known_quantities, kwargs...) @@ -45,6 +47,7 @@ end # Global identifiability. function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], ignore_no_measured_warn=false, kwargs...) + rs = Catalyst.expand_registered_functions(rs) known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) osys = convert(ODESystem, rs) return StructuralIdentifiability.assess_identifiability(osys, args...; measured_quantities=known_quantities, kwargs...) @@ -52,6 +55,7 @@ end # Identifiable functions. function StructuralIdentifiability.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], ignore_no_measured_warn=false, kwargs...) + rs = Catalyst.expand_registered_functions(rs) known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) osys = convert(ODESystem, rs) return StructuralIdentifiability.find_identifiable_functions(osys, args...; measured_quantities=known_quantities, kwargs...) diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index d6a14de4c8..df24c86488 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -7,11 +7,14 @@ using StructuralIdentifiability ### Helper Function ### # Converts the output dicts from StructuralIdentifiability functions from "weird symbol => stuff" to "symbol => stuff" (the output have some strange meta data which prevents equality checks, this enables this). +# Structural identifiability also provides variables like x (rather than x(t)). This is a bug, but we have to convert to make it work (now just remove any (t) to make them all equal). function sym_dict(dict_in) dict_out = Dict{Symbol,Any}() for key in keys(dict_in) - dict_out[Symbol(key)] = dict_in[key] - end + sym_key = Symbol(key) + sym_key = Symbol(replace(String(sym_key), "(t)" => "")) + dict_out[sym_key] = dict_in[key] + end return dict_out end From b6127990adfdbff174aa0f460bce30b14f16338c Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 15 Nov 2023 12:28:04 -0500 Subject: [PATCH 29/80] enable conserved quantitites handling --- .../structural_identifiability.md | 20 +++++ .../structural_identifiability_extension.jl | 89 +++++++++++++------ test/extensions/structural_identifiability.jl | 4 +- 3 files changed, 85 insertions(+), 28 deletions(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 8abbde642b..c0f6f19d97 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -114,6 +114,26 @@ print_for_DAISY(si_ode) nothing # hide ``` +## Notes on systems with conservation laws +Several reaction network models, such as +```example si2 +rs = @reaction_network begin + (k1,k2), X1 <--> X2 +end +``` +contain conservation laws (in this case $Γ = X1 + X2$, where $Γ = X1(0) + X2(0)$ is a constant). Because the presence of such conservation laws makes structural identifiability analysis prohibitively computationally expensive (for all but the simplest of cases), these are automatically eliminated by Catalyst (removing one ODE from the resulting ODE system for each conservation law). For the `assess_identifiability` and `assess_local_identifiability` functions, this will be unnoticed by the user. However, for the `find_identifiable_functions` and `make_si_ode` functions, this may result in one, or several, parameters on the form `Γ[i]` (where `i` is an integer) appearing in the produced expressions. These correspond to the conservation law constants and can be found through +```example si2 +conservedequations(rs) +``` +E.g. if you run: +```example si2 +find_identifiable_functions(rs; measured_quantities = [:X1, :X2]) +``` +we see that `Γ[1]` (`= X1 + X2`) is detected as an identifiable expression. If we want to disable this feature for any function, we can use the `remove_conserved = false` option: +```example si2 +find_identifiable_functions(rs; measured_quantities = [:X1, :X2], remove_conserved = false) +``` + --- ## [Citation](@id structural_identifiability_citation) If you use this functionality in your research, please cite the following paper to support the authors of the StructuralIdentifiability package: diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 75f0978336..561fa388ee 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -20,44 +20,81 @@ end si_ode(rs; measured_quantities = [:X], known_p = [:p]) ``` """ -function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities = [], known_p = [], ignore_no_measured_warn=false) +function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities = [], known_p = [], ignore_no_measured_warn=false, remove_conserved = true) + osys, conseqs, _ = make_osys(rs; remove_conserved) + measured_quantities = make_measured_quantities(rs, measured_quantities, known_p, conseqs; ignore_no_measured_warn) + return StructuralIdentifiability.preprocess_ode(osys, measured_quantities)[1] +end + +### Structural Identifiability Wrappers ### + +# Local identifiability. +function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], funcs_to_check = Vector(), remove_conserved = true, ignore_no_measured_warn=false, kwargs...) + osys, conseqs, vars = make_osys(rs; remove_conserved) + measured_quantities = make_measured_quantities(rs, measured_quantities, known_p, conseqs; ignore_no_measured_warn) + funcs_to_check = make_ftc(funcs_to_check, conseqs, vars) + out = StructuralIdentifiability.assess_local_identifiability(osys, args...; measured_quantities, funcs_to_check, kwargs...) + return make_output(out, funcs_to_check, reverse.(conseqs)) +end + +# Global identifiability. +function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], funcs_to_check = Vector(), remove_conserved = true, ignore_no_measured_warn=false, kwargs...) + osys, conseqs, vars = make_osys(rs; remove_conserved) + measured_quantities = make_measured_quantities(rs, measured_quantities, known_p, conseqs; ignore_no_measured_warn) + funcs_to_check = make_ftc(funcs_to_check, conseqs, vars) + out = StructuralIdentifiability.assess_identifiability(osys, args...; measured_quantities, funcs_to_check, kwargs...) + return make_output(out, funcs_to_check, reverse.(conseqs)) +end + +# Identifiable functions. +function StructuralIdentifiability.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], remove_conserved = true, ignore_no_measured_warn=false, kwargs...) + osys, conseqs, vars = make_osys(rs; remove_conserved) + measured_quantities = make_measured_quantities(rs, measured_quantities, known_p, conseqs; ignore_no_measured_warn) + out = StructuralIdentifiability.find_identifiable_functions(osys, args...; measured_quantities, kwargs...) + return vector_subs(out, reverse.(conseqs)) +end + +### Helper Functions ### + +# From a reaction system, creates the corresponding ODESystem for SI application (and also compute the, later needed, conservation law equations and list of system symbols). +function make_osys(rs::ReactionSystem; remove_conserved=true) rs = Catalyst.expand_registered_functions(rs) - known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) - return StructuralIdentifiability.preprocess_ode(convert(ODESystem, rs), known_quantities)[1] + osys = convert(ODESystem, rs; remove_conserved) + vars = [states(rs); parameters(rs)] + + # Fixes conservation law equations. These cannot be computed for hierarchical systems (and hence this is skipped). If none is found, still have to put on the right form. + if !isempty(Catalyst.get_systems(rs)) || !remove_conserved + conseqs = Vector{Pair{Any, Any}}[] + else + conseqs = [ceq.lhs => ceq.rhs for ceq in conservedequations(rs)] + isempty(conseqs) && (conseqs = Vector{Pair{Any, Any}}[]) + end + return osys, conseqs, vars end # For input measured quantities, if this is not a vector of equations, convert it to a proper form. -function make_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{T}, known_p::Vector{S}; ignore_no_measured_warn=false) where {T,S} +function make_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{T}, known_p::Vector{S}, conseqs; ignore_no_measured_warn=false) where {T,S} ignore_no_measured_warn || isempty(measured_quantities) && @warn "No measured quantity provided to the `measured_quantities` argument, any further identifiability analysis will likely fail. You can disable this warning by setting `ignore_no_measured_warn=true`." all_quantities = [measured_quantities; known_p] all_quantities = [(quant isa Symbol) ? Catalyst._symbol_to_var(rs, quant) : quant for quant in all_quantities] + all_quantities = vector_subs(all_quantities, conseqs) @variables t (___internal_observables(t))[1:length(all_quantities)] return Equation[(all_quantities[i] isa Equation) ? all_quantities[i] : (___internal_observables[i] ~ all_quantities[i]) for i in 1:length(all_quantities)] end -### Structural Identifiability Wrappers ### - -# Local identifiability. -function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], ignore_no_measured_warn=false, kwargs...) - rs = Catalyst.expand_registered_functions(rs) - known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) - osys = convert(ODESystem, rs) - return StructuralIdentifiability.assess_local_identifiability(osys, args...; measured_quantities=known_quantities, kwargs...) -end - -# Global identifiability. -function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], ignore_no_measured_warn=false, kwargs...) - rs = Catalyst.expand_registered_functions(rs) - known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) - osys = convert(ODESystem, rs) - return StructuralIdentifiability.assess_identifiability(osys, args...; measured_quantities=known_quantities, kwargs...) +# Creates the functions that we wish to check for identifiability (if none give, by default, a list of parameters and species). Also replaces conservation law equations in. +function make_ftc(funcs_to_check, conseqs, vars) + isempty(funcs_to_check) && (funcs_to_check = vars) + return vector_subs(funcs_to_check, conseqs) end -# Identifiable functions. -function StructuralIdentifiability.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], ignore_no_measured_warn=false, kwargs...) - rs = Catalyst.expand_registered_functions(rs) - known_quantities = make_measured_quantities(rs, measured_quantities, known_p; ignore_no_measured_warn) - osys = convert(ODESystem, rs) - return StructuralIdentifiability.find_identifiable_functions(osys, args...; measured_quantities=known_quantities, kwargs...) +# Replaces conservation law equations back in the output, and also sorts it according to their input order (defaults to [states; parameters] order). +function make_output(out, funcs_to_check, conseqs) + funcs_to_check = vector_subs(funcs_to_check, conseqs) + out = Dict(zip(vector_subs(keys(out), conseqs), values(out))) + out = sort(out; by = x -> findfirst(isequal(x, ftc) for ftc in funcs_to_check)) + return out end +# For a vector of expressions and a conservation law, replaces the law in. +vector_subs(eqs, subs) = [substitute(eq, subs) for eq in eqs] \ No newline at end of file diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index df24c86488..c8e80bd4c7 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -120,7 +120,7 @@ let ifs_1 = find_identifiable_functions(rs_catalyst; measured_quantities=[X1 + Yi, Ya], known_p=[k1, kD]) # Identifiability analysis for Catalyst converted to StructuralIdentifiability.jl model. - rs_ode = make_si_ode(rs_catalyst; measured_quantities=[X1 + Yi, Ya], known_p=[k1, kD]) + rs_ode = make_si_ode(rs_catalyst; measured_quantities=[X1 + Yi, Ya], known_p=[k1, kD], remove_conserved=false) gi_2 = assess_identifiability(rs_ode) li_2 = assess_local_identifiability(rs_ode) ifs_2 = find_identifiable_functions(rs_ode) @@ -146,7 +146,7 @@ let # Check outputs. @test sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) @test sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) - @test length(ifs_1) == length(ifs_2) == length(ifs_3) + @test length(ifs_1[2:end]) == length(ifs_2) == length(ifs_3) # In the first case, the conservation law parameter is also identifiable. end # Tests that various inputs types work. From cc0f3c8aea0af2b720360fb4a2e7be0452c519ae Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 15 Nov 2023 15:06:37 -0500 Subject: [PATCH 30/80] exponent param variable doc update --- .../inverse_problems/structural_identifiability.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index c0f6f19d97..b90031197e 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -27,7 +27,7 @@ Local identifiability can be assessed using the `assess_identifiability` functio To it, we provide our `ReactionSystem` model and a list of quantities that we are able to measure. Here, we consider a Goodwind oscillator (a simple 3-component model, where the three species $M$, $E$, and $P$ are produced and degraded, which may exhibit oscillations)[^2]. Let us say that we are able to measure the concentration of $M$, we then provide designate this using the `measured_quantities` argument. We can now assess identifiability in the following way: ```example si1 -using StructuralIdentifiability, Catalyst +using Catalyst, StructuralIdentifiability goodwind_oscillator = @reaction_network begin (pₘ/(1+P), dₘ), 0 <--> M (pₑ*M,dₑ), 0 <--> E @@ -117,6 +117,7 @@ nothing # hide ## Notes on systems with conservation laws Several reaction network models, such as ```example si2 +using Catalyst, StructuralIdentifiability # hide rs = @reaction_network begin (k1,k2), X1 <--> X2 end @@ -134,6 +135,16 @@ we see that `Γ[1]` (`= X1 + X2`) is detected as an identifiable expression. If find_identifiable_functions(rs; measured_quantities = [:X1, :X2], remove_conserved = false) ``` +## Systems with exponent parameters +Structural identifiability cannot currently be applied to systems with parameters (or species) in exponents. E.g. this +```julia +rn = @reaction_network begin + (hill(X,v,K,n),d), 0 <--> X +end +assess_identifiability(rn; measured_quantities=[:X]) +``` +is currently not possible. Hopefully this will be a supported feature in the future. For now, these expression will have to be rewritten to not include such exponents. For some cases, e.g. `10^k` this is trivial. However, it is also possible generally (but more involved and often includes introducing additional variables). If you have a system where this is required, it is recommended to contact an expert. + --- ## [Citation](@id structural_identifiability_citation) If you use this functionality in your research, please cite the following paper to support the authors of the StructuralIdentifiability package: From 176ff3cb98b2cb8d0e2d2a7d5e57fefb8ef1b321 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 15 Nov 2023 15:12:16 -0500 Subject: [PATCH 31/80] doc project update --- docs/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Project.toml b/docs/Project.toml index 5f92019a43..a446b85f65 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -22,6 +22,7 @@ Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" SteadyStateDiffEq = "9672c7b4-1e72-59bd-8a11-6ac3964bc41f" StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0" +StructuralIdentifiability = "220ca800-aa68-49bb-acd8-6037fa93a544" Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" [compat] From b0db36f32d9f4c78c885e24b0784360203614124 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 15 Nov 2023 16:10:19 -0500 Subject: [PATCH 32/80] remove logging --- docs/Project.toml | 1 + docs/src/inverse_problems/structural_identifiability.md | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index a446b85f65..07d233ecb6 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -7,6 +7,7 @@ Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327" Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" Optim = "429524aa-4258-5aef-a3af-852621145aeb" diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index b90031197e..a0bb1d5f18 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -27,7 +27,7 @@ Local identifiability can be assessed using the `assess_identifiability` functio To it, we provide our `ReactionSystem` model and a list of quantities that we are able to measure. Here, we consider a Goodwind oscillator (a simple 3-component model, where the three species $M$, $E$, and $P$ are produced and degraded, which may exhibit oscillations)[^2]. Let us say that we are able to measure the concentration of $M$, we then provide designate this using the `measured_quantities` argument. We can now assess identifiability in the following way: ```example si1 -using Catalyst, StructuralIdentifiability +using Catalyst, Logging, StructuralIdentifiability goodwind_oscillator = @reaction_network begin (pₘ/(1+P), dₘ), 0 <--> M (pₑ*M,dₑ), 0 <--> E @@ -35,7 +35,8 @@ goodwind_oscillator = @reaction_network begin end assess_identifiability(goodwind_oscillator; measured_quantities=[:M], loglevel=Logging.Error) ``` -From the output, we find that `E(t)`, `pₑ`, and `pₚ` (the trajectory of $E$, and the production rates of $E$ and $P$, respectively) are non-identifiable. Next, `dₑ` and `dₚ` (the degradation rates of $E$ and $P$, respectively) are locally identifiable. Finally, `P(t)`, `M(t)`, `pₘ`, and `dₘ` (the trajectories of `P` and `M`, and the production and degradation rate of `M`, respectively) are all globally identifiable. +From the output, we find that `E(t)`, `pₑ`, and `pₚ` (the trajectory of $E$, and the production rates of $E$ and $P$, respectively) are non-identifiable. Next, `dₑ` and `dₚ` (the degradation rates of $E$ and $P$, respectively) are locally identifiable. Finally, `P(t)`, `M(t)`, `pₘ`, and `dₘ` (the trajectories of `P` and `M`, and the production and degradation rate of `M`, respectively) are all globally identifiable. We note that we also imported the Logging package, and provided the `loglevel=Logging.Error` input argument StructuralIdentifiability functions generally provides a large number of output logging messages. Hence, we will use this argument (which requires the Logging package) throughout this tutorial. + Next, we can assess identifiability in the case where we can measure all three species concentrations: ```example si1 @@ -43,7 +44,6 @@ assess_identifiability(goodwind_oscillator; measured_quantities=[:M, :P, :E], lo ``` in which case all initial conditions and parameters become identifiable. -StructuralIdentifiability functions generally provides a large number of output logging messages. Hence, in the above examples, and throughout the rest of this tutorial, we use the `loglevel=Logging.Error` option to turn these off. ### Indicating known parameters In the previous case we assumed that all parameters are unknown, however, this is not necessarily true. If there are parameters which value's are known, we can supply these using the `known_p` argument. Indeed, this might turn other, previously unidentifiable, parameters identifiable. Let us consider the previous example, where we measure the concentration of $M$ only, but also happen to know the production rate of $E$ ($pₑ$): @@ -117,7 +117,7 @@ nothing # hide ## Notes on systems with conservation laws Several reaction network models, such as ```example si2 -using Catalyst, StructuralIdentifiability # hide +using Catalyst, Logging, StructuralIdentifiability # hide rs = @reaction_network begin (k1,k2), X1 <--> X2 end From 964fcc1b510b053643a14fb46d1c5bfb879f9c9c Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 15 Nov 2023 16:15:20 -0500 Subject: [PATCH 33/80] history file update --- HISTORY.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 35763dd106..52e6e74df1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,6 +3,18 @@ ## Catalyst unreleased (master branch) ## Catalyst 14.0 +- Added CatalystStructuralIdentifiabilityExtension, which permits StructuralIdentifiability.jl function to be applied directly to Catalyst systems. E.g. use +```julia +goodwind_oscillator = @reaction_network begin + (mmr(P,pₘ,1), dₘ), 0 <--> M + (pₑ*M,dₑ), 0 <--> E + (pₚ*E,dₚ), 0 <--> P +end +assess_identifiability(goodwind_oscillator; measured_quantities=[:M]) +``` +to assess (global) structural identifiability for all parameters an variables of the `goodwind_oscillator` model (under the presumption that we can measure `M` only). +- Automatically handles conservation laws for structural identifiability problems (eliminates these internally to speed up computations). +- Add documentation for these features. - Simulation of spatial ODEs now supported. For full details, please see https://github.com/SciML/Catalyst.jl/pull/644 and upcoming documentation. Note that these methods are currently considered alpha, with the interface and approach changing even in non-breaking Catalyst releases. - LatticeReactionSystem structure represents a spatial reaction network: ```julia From 03310a93bb4811d59cda0a3c0506f1f0dd7f581e Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 4 Dec 2023 10:22:44 -0500 Subject: [PATCH 34/80] rebase update --- ...alystStructuralIdentifiabilityExtension.jl | 2 +- .../structural_identifiability_extension.jl | 98 ++++++++++++++----- test/extensions/structural_identifiability.jl | 56 ++++++++++- test/runtests.jl | 3 +- 4 files changed, 128 insertions(+), 31 deletions(-) diff --git a/ext/CatalystStructuralIdentifiabilityExtension.jl b/ext/CatalystStructuralIdentifiabilityExtension.jl index 58fece8ac5..026fbe6122 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension.jl @@ -2,7 +2,7 @@ module CatalystStructuralIdentifiabilityExtension # Fetch packages. using Catalyst -import StructuralIdentifiability +import StructuralIdentifiability as SI # Creates and exports hc_steady_states function. include("CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl") diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 561fa388ee..44f7ad9aad 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -18,77 +18,121 @@ rs = @reaction_network begin (p,d), 0 <--> X end si_ode(rs; measured_quantities = [:X], known_p = [:p]) + +Notes: +This function is part of the StructuralIdentifiability.jl extension. StructuralIdentifiability.jl must be imported to access it. ``` """ -function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities = [], known_p = [], ignore_no_measured_warn=false, remove_conserved = true) +function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities = [], known_p = [], + ignore_no_measured_warn=false, remove_conserved = true) + # Creates a MTK ODESystem, and a list of measured quantities (there are equations). + # Gives these to SI to create an SI ode model of its preferred form. osys, conseqs, _ = make_osys(rs; remove_conserved) measured_quantities = make_measured_quantities(rs, measured_quantities, known_p, conseqs; ignore_no_measured_warn) - return StructuralIdentifiability.preprocess_ode(osys, measured_quantities)[1] + return SI.preprocess_ode(osys, measured_quantities)[1] end ### Structural Identifiability Wrappers ### -# Local identifiability. -function StructuralIdentifiability.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], funcs_to_check = Vector(), remove_conserved = true, ignore_no_measured_warn=false, kwargs...) +# Creates dispatch for SI's local identifiability analysis function. +function SI.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], + known_p = Num[], funcs_to_check = Vector(), remove_conserved = true, + ignore_no_measured_warn=false, kwargs...) + # Creates a ODESystem, list of measured quantities, and functions to check, of SI's preferred form. osys, conseqs, vars = make_osys(rs; remove_conserved) measured_quantities = make_measured_quantities(rs, measured_quantities, known_p, conseqs; ignore_no_measured_warn) funcs_to_check = make_ftc(funcs_to_check, conseqs, vars) - out = StructuralIdentifiability.assess_local_identifiability(osys, args...; measured_quantities, funcs_to_check, kwargs...) + + # Computes identifiability and converts it to a easy to read form. + out = SI.assess_local_identifiability(osys, args...; measured_quantities, funcs_to_check, kwargs...) return make_output(out, funcs_to_check, reverse.(conseqs)) end -# Global identifiability. -function StructuralIdentifiability.assess_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], funcs_to_check = Vector(), remove_conserved = true, ignore_no_measured_warn=false, kwargs...) +# Creates dispatch for SI's global identifiability analysis function. +function SI.assess_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], + funcs_to_check = Vector(), remove_conserved = true, ignore_no_measured_warn=false, + kwargs...) + # Creates a ODESystem, list of measured quantities, and functions to check, of SI's preferred form. osys, conseqs, vars = make_osys(rs; remove_conserved) measured_quantities = make_measured_quantities(rs, measured_quantities, known_p, conseqs; ignore_no_measured_warn) funcs_to_check = make_ftc(funcs_to_check, conseqs, vars) - out = StructuralIdentifiability.assess_identifiability(osys, args...; measured_quantities, funcs_to_check, kwargs...) + + # Computes identifiability and converts it to a easy to read form. + out = SI.assess_identifiability(osys, args...; measured_quantities, funcs_to_check, kwargs...) return make_output(out, funcs_to_check, reverse.(conseqs)) end -# Identifiable functions. -function StructuralIdentifiability.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], remove_conserved = true, ignore_no_measured_warn=false, kwargs...) - osys, conseqs, vars = make_osys(rs; remove_conserved) +# Creates dispatch for SI's function to find all identifiable functions. +function SI.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = Num[], + known_p = Num[], remove_conserved = true, ignore_no_measured_warn=false, + kwargs...) + # Creates a ODESystem, and list of measured quantities, of SI's preferred form. + osys, conseqs = make_osys(rs; remove_conserved) measured_quantities = make_measured_quantities(rs, measured_quantities, known_p, conseqs; ignore_no_measured_warn) - out = StructuralIdentifiability.find_identifiable_functions(osys, args...; measured_quantities, kwargs...) + + # Computes identifiable functions and converts it to a easy to read form. + out = SI.find_identifiable_functions(osys, args...; measured_quantities, kwargs...) return vector_subs(out, reverse.(conseqs)) end ### Helper Functions ### -# From a reaction system, creates the corresponding ODESystem for SI application (and also compute the, later needed, conservation law equations and list of system symbols). +# From a reaction system, creates the corresponding MTK-style ODESystem for SI application +# Also compute the, later needed, conservation law equations and list of system symbols (states and parameters). function make_osys(rs::ReactionSystem; remove_conserved=true) - rs = Catalyst.expand_registered_functions(rs) + # Creates the ODESystem corresponding to the ReactionSystem (expanding functions and flattening it). + # Creates a list of the systems all symbols (states and parameters). + rs = Catalyst.expand_registered_functions(flatten(rs)) osys = convert(ODESystem, rs; remove_conserved) vars = [states(rs); parameters(rs)] - # Fixes conservation law equations. These cannot be computed for hierarchical systems (and hence this is skipped). If none is found, still have to put on the right form. - if !isempty(Catalyst.get_systems(rs)) || !remove_conserved + # Computes equations for system conservation laws. + # These cannot be computed for hierarchical systems (and hence this is skipped). + # If there are no conserved equations, the `conseqs` variable must still have the `Vector{Pair{Any, Any}}` type. + if !remove_conserved conseqs = Vector{Pair{Any, Any}}[] else conseqs = [ceq.lhs => ceq.rhs for ceq in conservedequations(rs)] isempty(conseqs) && (conseqs = Vector{Pair{Any, Any}}[]) end + return osys, conseqs, vars end -# For input measured quantities, if this is not a vector of equations, convert it to a proper form. -function make_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{T}, known_p::Vector{S}, conseqs; ignore_no_measured_warn=false) where {T,S} - ignore_no_measured_warn || isempty(measured_quantities) && @warn "No measured quantity provided to the `measured_quantities` argument, any further identifiability analysis will likely fail. You can disable this warning by setting `ignore_no_measured_warn=true`." - all_quantities = [measured_quantities; known_p] - all_quantities = [(quant isa Symbol) ? Catalyst._symbol_to_var(rs, quant) : quant for quant in all_quantities] - all_quantities = vector_subs(all_quantities, conseqs) - @variables t (___internal_observables(t))[1:length(all_quantities)] - return Equation[(all_quantities[i] isa Equation) ? all_quantities[i] : (___internal_observables[i] ~ all_quantities[i]) for i in 1:length(all_quantities)] +# Creates a list of measured quantities of a form that SI can read. +# Each measured quantity must have a form like: +# `obs_var ~ X` # (Here, `obs_var` is a variable, and X is whatever we can measure). +function make_measured_quantities(rs::ReactionSystem, measured_quantities::Vector{T}, known_p::Vector{S}, + conseqs; ignore_no_measured_warn=false) where {T,S} + # Warning if the user didn't give any measured quantities. + if ignore_no_measured_warn || isempty(measured_quantities) + @warn "No measured quantity provided to the `measured_quantities` argument, any further identifiability analysis will likely fail. You can disable this warning by setting `ignore_no_measured_warn=true`." + end + + # Appends the known parameters to the measured_quantities vector. Converts any Symbols to symbolics. + measured_quantities = [measured_quantities; known_p] + measured_quantities = [(q isa Symbol) ? Catalyst._symbol_to_var(rs, q) : q for q in measured_quantities] + measured_quantities = vector_subs(measured_quantities, conseqs) + + # Creates one internal observation variable for each measured quantity (`___internal_observables`). + # Creates a vector of equations, setting each measured quantity equal to one observation variable. + @variables t (___internal_observables(t))[1:length(measured_quantities)] + return Equation[(q isa Equation) ? q : (___internal_observables[i] ~ q) for (i,q) in enumerate(measured_quantities)] end -# Creates the functions that we wish to check for identifiability (if none give, by default, a list of parameters and species). Also replaces conservation law equations in. +# Creates the functions that we wish to check for identifiability. +# If no `funcs_to_check` are given, defaults to checking identifiability for all states and parameters. +# Also, for conserved equations, replaces these in (creating a system without conserved quantities). +# E.g. for `X1 <--> X2`, replaces `X2` with `Γ[1] - X2`. +# Removing conserved quantities makes SI's algorithms much more performant. function make_ftc(funcs_to_check, conseqs, vars) isempty(funcs_to_check) && (funcs_to_check = vars) return vector_subs(funcs_to_check, conseqs) end -# Replaces conservation law equations back in the output, and also sorts it according to their input order (defaults to [states; parameters] order). +# Processes the outputs to a better form. +# Replaces conservation law equations back in the output (so that e.g. Γ are not displayed). +# Sorts the output according to their input order (defaults to the `[states; parameters]` order). function make_output(out, funcs_to_check, conseqs) funcs_to_check = vector_subs(funcs_to_check, conseqs) out = Dict(zip(vector_subs(keys(out), conseqs), values(out))) @@ -96,5 +140,5 @@ function make_output(out, funcs_to_check, conseqs) return out end -# For a vector of expressions and a conservation law, replaces the law in. +# For a vector of expressions and a conservation law, substitutes the law into every equation. vector_subs(eqs, subs) = [substitute(eq, subs) for eq in eqs] \ No newline at end of file diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index c8e80bd4c7..6d1a5d4931 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -101,6 +101,7 @@ let end # Tests on a made-up reaction network with mix of identifiable and non-identifiable components. +# Tests for system with conserved quantity. # Tests for symbolics known_p # Tests using an equation for measured quantity. let @@ -184,6 +185,58 @@ let make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M*gw_osc_complt.E]) end +# Tests for hierarchical model with conservation laws at both top and internal levels. +let + # Identifiability analysis for Catalyst model. + rs1 = @reaction_network rn1 begin + (k1, k2), X1 <--> X2 + end + rs2 = @reaction_network rn2 begin + (k3, k4), X3 <--> X4 + end + @named rs_catalyst = flatten(compose(rs1, [rs2])) + @unpack X1, X2, k1, k2 = rn1 + gi_1 = assess_identifiability(rs_catalyst; measured_quantities=[X1, X2, rs2.X3], known_p=[k1]) + li_1 = assess_local_identifiability(rs_catalyst; measured_quantities=[X1, X2, rs2.X3], known_p=[k1]) + ifs_1 = find_identifiable_functions(rs_catalyst; measured_quantities=[X1, X2, rs2.X3], known_p=[k1]) + + # Identifiability analysis for Catalyst converted to StructuralIdentifiability.jl model. + rs_ode = make_si_ode(rs_catalyst; measured_quantities=[X1, X2, rs2.X3], known_p=[k1]) + gi_2 = assess_identifiability(rs_ode) + li_2 = assess_local_identifiability(rs_ode) + ifs_2 = find_identifiable_functions(rs_ode) + + # Identifiability analysis for StructuralIdentifiability.jl model (declare this overwrites e.g. X2 variable etc.). + rs_si = @ODEmodel( + X1'(t) = -k1*X1(t) + k2*X2(t), + X2'(t) = k1*X1(t) - k2*X2(t), + rn2₊X3'(t) = -rn2₊k3*rn2₊X3(t) + rn2₊k4*rn2₊X4(t), + rn2₊X4'(t) = rn2₊k3*rn2₊X3(t) - rn2₊k4*rn2₊X4(t), + y1(t) = X1, + y2(t) = X2, + y3(t) = rn2₊X3, + y4(t) = k1 + ) + gi_3 = assess_identifiability(rs_si) + li_3 = assess_local_identifiability(rs_si) + ifs_3 = find_identifiable_functions(rs_si) + + # Check outputs. + @test sym_dict(gi_1) == sym_dict(gi_3) + @test sym_dict(li_1) == sym_dict(li_3) + @test length(ifs_1)-2 == length(ifs_2)-2 == length(ifs_3) # In the first case, the conservation law parameter is also identifiable. + + # Checks output for the SI converted version of the catalyst model. + # For nested systems with conservation laws, conserved quantities like Γ[1], cannot be replaced back. + # Hence, here you display identifiability for `Γ[1]` instead of X2. + gi_1_no_cq = filter(x -> !occursin("X2",String(x[1])) && !occursin("X4",String(x[1])), sym_dict(gi_1)) + gi_2_no_cq = filter(x -> !occursin("Γ",String(x[1])), sym_dict(gi_2)) + li_1_no_cq = filter(x -> !occursin("X2",String(x[1])) && !occursin("X4",String(x[1])), sym_dict(li_1)) + li_2_no_cq = filter(x -> !occursin("Γ",String(x[1])), sym_dict(li_2)) + @test gi_1_no_cq == gi_2_no_cq + @test li_1_no_cq == li_2_no_cq +end + # Tests directly on reaction systems with known identifiability structures. # Test provided by Alexander Demin. let @@ -236,6 +289,5 @@ let :x2 => :globally, :x3 => :globally, ) - # Will probably be fixed in the 0.5 release of SI.jl - @test_broken find_identifiable_functions(rs, measured_quantities = [:x3]) + @test length(find_identifiable_functions(rs, measured_quantities = [:x3])) == 1 end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 90cf6aab4b..cd2b6550e3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -54,5 +54,6 @@ using SafeTestsets ### Tests extensions. ### @time @safetestset "BifurcationKit Extension" begin include("extensions/bifurcation_kit.jl") end @time @safetestset "HomotopyContinuation Extension" begin include("extensions/homotopy_continuation.jl") end - @time @safetestset "Structural Identifiability Extension" begin include("extensions/structural_identifiability.jl") end + @time @safetestset "Structural Identifiability Extension" begin include("extensions/structural_identifiability.jl") end + end # @time From 2828662f6566344f53daa7b8cacfe1194e44edcf Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 4 Dec 2023 10:38:16 -0500 Subject: [PATCH 35/80] up --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index bdd2470308..b608687e94 100644 --- a/Project.toml +++ b/Project.toml @@ -76,4 +76,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [targets] -test = ["BifurcationKit", "DomainSets", "Graphviz_jll", "HomotopyContinuation", "NonlinearSolve", "OrdinaryDiffEq", "Random", "SafeTestsets", "SciMLBase", "SciMLNLSolve", "StableRNGs", "Statistics", "SteadyStateDiffEq", "StochasticDiffEq", "Test", "Unitful"] +test = ["BifurcationKit", "DomainSets", "Graphviz_jll", "HomotopyContinuation", "NonlinearSolve", "OrdinaryDiffEq", "Random", "SafeTestsets", "SciMLBase", "SciMLNLSolve", "StableRNGs", "Statistics", "SteadyStateDiffEq", "StochasticDiffEq", "StructuralIdentifiability", "Test", "Unitful"] From 29a7795bb83f58db0893b53446ab1b94b697de4b Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 4 Dec 2023 11:43:06 -0500 Subject: [PATCH 36/80] rebase fix --- src/reactionsystem.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/reactionsystem.jl b/src/reactionsystem.jl index 49435182b2..83387ef921 100644 --- a/src/reactionsystem.jl +++ b/src/reactionsystem.jl @@ -1327,6 +1327,7 @@ Keyword args and default values: function Base.convert(::Type{<:NonlinearSystem}, rs::ReactionSystem; name = nameof(rs), combinatoric_ratelaws = get_combinatoric_ratelaws(rs), include_zero_odes = true, remove_conserved = false, checks = false, + default_u0 = Dict(), default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)), kwargs...) spatial_convert_err(rs::ReactionSystem, NonlinearSystem) fullrs = Catalyst.flatten(rs) @@ -1376,6 +1377,7 @@ function Base.convert(::Type{<:SDESystem}, rs::ReactionSystem; noise_scaling = nothing, name = nameof(rs), combinatoric_ratelaws = get_combinatoric_ratelaws(rs), include_zero_odes = true, checks = false, remove_conserved = false, + default_u0 = Dict(), default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)), kwargs...) spatial_convert_err(rs::ReactionSystem, SDESystem) From aa86f8825220c10028029e440fded84d225f643b Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 4 Dec 2023 12:08:21 -0500 Subject: [PATCH 37/80] test up --- test/extensions/structural_identifiability.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index 6d1a5d4931..7d219406e1 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -195,7 +195,7 @@ let (k3, k4), X3 <--> X4 end @named rs_catalyst = flatten(compose(rs1, [rs2])) - @unpack X1, X2, k1, k2 = rn1 + @unpack X1, X2, k1, k2 = rs1 gi_1 = assess_identifiability(rs_catalyst; measured_quantities=[X1, X2, rs2.X3], known_p=[k1]) li_1 = assess_local_identifiability(rs_catalyst; measured_quantities=[X1, X2, rs2.X3], known_p=[k1]) ifs_1 = find_identifiable_functions(rs_catalyst; measured_quantities=[X1, X2, rs2.X3], known_p=[k1]) @@ -210,11 +210,11 @@ let rs_si = @ODEmodel( X1'(t) = -k1*X1(t) + k2*X2(t), X2'(t) = k1*X1(t) - k2*X2(t), - rn2₊X3'(t) = -rn2₊k3*rn2₊X3(t) + rn2₊k4*rn2₊X4(t), - rn2₊X4'(t) = rn2₊k3*rn2₊X3(t) - rn2₊k4*rn2₊X4(t), + rs2₊X3'(t) = -rs2₊k3*rs2₊X3(t) + rs2₊k4*rs2₊X4(t), + rs2₊X4'(t) = rs2₊k3*rs2₊X3(t) - rs2₊k4*rs2₊X4(t), y1(t) = X1, y2(t) = X2, - y3(t) = rn2₊X3, + y3(t) = rs2₊X3, y4(t) = k1 ) gi_3 = assess_identifiability(rs_si) From 7a4696069815083a19f8aee8cfc69c5aa91988bd Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 4 Dec 2023 12:37:19 -0500 Subject: [PATCH 38/80] up --- test/extensions/structural_identifiability.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index 7d219406e1..f0a5625f02 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -188,10 +188,10 @@ end # Tests for hierarchical model with conservation laws at both top and internal levels. let # Identifiability analysis for Catalyst model. - rs1 = @reaction_network rn1 begin + rs1 = @reaction_network rs1 begin (k1, k2), X1 <--> X2 end - rs2 = @reaction_network rn2 begin + rs2 = @reaction_network rs2 begin (k3, k4), X3 <--> X4 end @named rs_catalyst = flatten(compose(rs1, [rs2])) From 23ad15d5947528265c0ba1cc92603847958bfe65 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 5 Dec 2023 12:13:55 -0500 Subject: [PATCH 39/80] compat up --- Project.toml | 1 + docs/Project.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Project.toml b/Project.toml index b608687e94..6c0f521f2d 100644 --- a/Project.toml +++ b/Project.toml @@ -50,6 +50,7 @@ Reexport = "0.2, 1.0" Requires = "1.0" RuntimeGeneratedFunctions = "0.5.12" Setfield = "1" +StructuralIdentifiability = "0.5.1" SymbolicUtils = "1.0.3" Symbolics = "5.14" Unitful = "1.12.4" diff --git a/docs/Project.toml b/docs/Project.toml index 07d233ecb6..1621bc3f26 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -49,4 +49,5 @@ Setfield = "1.1" SpecialFunctions = "2.1" SteadyStateDiffEq = "2.0.1" StochasticDiffEq = "6" +StructuralIdentifiability = "0.5.1" Symbolics = "5.14" From d77af34a3009ce09cb05b15046710111d9534294 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Tue, 5 Dec 2023 14:12:44 -0500 Subject: [PATCH 40/80] Update HISTORY.md Co-authored-by: Sam Isaacson --- HISTORY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 52e6e74df1..435191824a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -12,7 +12,7 @@ goodwind_oscillator = @reaction_network begin end assess_identifiability(goodwind_oscillator; measured_quantities=[:M]) ``` -to assess (global) structural identifiability for all parameters an variables of the `goodwind_oscillator` model (under the presumption that we can measure `M` only). +to assess (global) structural identifiability for all parameters and variables of the `goodwind_oscillator` model (under the presumption that we can measure `M` only). - Automatically handles conservation laws for structural identifiability problems (eliminates these internally to speed up computations). - Add documentation for these features. - Simulation of spatial ODEs now supported. For full details, please see https://github.com/SciML/Catalyst.jl/pull/644 and upcoming documentation. Note that these methods are currently considered alpha, with the interface and approach changing even in non-breaking Catalyst releases. From 71295b435b04e93cd77f4f2f85fccca9badfaa68 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Tue, 5 Dec 2023 14:12:51 -0500 Subject: [PATCH 41/80] Update HISTORY.md Co-authored-by: Sam Isaacson --- HISTORY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 435191824a..ecd29c09b9 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -14,7 +14,7 @@ assess_identifiability(goodwind_oscillator; measured_quantities=[:M]) ``` to assess (global) structural identifiability for all parameters and variables of the `goodwind_oscillator` model (under the presumption that we can measure `M` only). - Automatically handles conservation laws for structural identifiability problems (eliminates these internally to speed up computations). -- Add documentation for these features. +- Adds a tutorial to illustrate the use of the extension. - Simulation of spatial ODEs now supported. For full details, please see https://github.com/SciML/Catalyst.jl/pull/644 and upcoming documentation. Note that these methods are currently considered alpha, with the interface and approach changing even in non-breaking Catalyst releases. - LatticeReactionSystem structure represents a spatial reaction network: ```julia From 1ce79c7800631cbdaa866d32a2395d36de05fe55 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Tue, 5 Dec 2023 14:15:17 -0500 Subject: [PATCH 42/80] Update ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl Co-authored-by: Sam Isaacson --- .../structural_identifiability_extension.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 44f7ad9aad..a2c7af8a73 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -8,7 +8,7 @@ Creates a ODE system of the form used within the StructuralIdentifiability.jl pa Arguments: - `rs::ReactionSystem`; The reaction system we wish to convert to an ODE. -- `measured_quantities=observed(rs)`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. `:x`). Defaults to the systems observables. +- `measured_quantities=observed(rs)`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.s`, or the symbol `:x`). Defaults to the system's observables. - `known_p = Num[]`: List of parameters which values are known. - `ignore_no_measured_warn=false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. From 2db2e24fe91cd6f25a51113965eb0379469a76f0 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Tue, 5 Dec 2023 14:19:11 -0500 Subject: [PATCH 43/80] Update ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl Co-authored-by: Sam Isaacson --- .../structural_identifiability_extension.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index a2c7af8a73..701ca177ce 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -87,7 +87,6 @@ function make_osys(rs::ReactionSystem; remove_conserved=true) vars = [states(rs); parameters(rs)] # Computes equations for system conservation laws. - # These cannot be computed for hierarchical systems (and hence this is skipped). # If there are no conserved equations, the `conseqs` variable must still have the `Vector{Pair{Any, Any}}` type. if !remove_conserved conseqs = Vector{Pair{Any, Any}}[] From 37d594e1aa2701cfd738cff30a366867d050ffe5 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Tue, 5 Dec 2023 14:22:39 -0500 Subject: [PATCH 44/80] Update ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl Co-authored-by: Sam Isaacson --- .../structural_identifiability_extension.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 701ca177ce..80bb13de4d 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -109,9 +109,9 @@ function make_measured_quantities(rs::ReactionSystem, measured_quantities::Vecto end # Appends the known parameters to the measured_quantities vector. Converts any Symbols to symbolics. - measured_quantities = [measured_quantities; known_p] - measured_quantities = [(q isa Symbol) ? Catalyst._symbol_to_var(rs, q) : q for q in measured_quantities] - measured_quantities = vector_subs(measured_quantities, conseqs) + mqiterator = Iterators.flatten((measured_quantities, known_p)) + mqs = [(q isa Symbol) ? Catalyst._symbol_to_var(rs, q) : q for q in mqiterator] + mqs = vector_subs(measured_quantities, conseqs) # Creates one internal observation variable for each measured quantity (`___internal_observables`). # Creates a vector of equations, setting each measured quantity equal to one observation variable. From e1a58111fc05e0fe7690b179a0c3bb5c0858e16e Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Tue, 5 Dec 2023 14:52:56 -0500 Subject: [PATCH 45/80] Update test/extensions/structural_identifiability.jl Co-authored-by: Sam Isaacson --- test/extensions/structural_identifiability.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index f0a5625f02..29677be00a 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -194,7 +194,7 @@ let rs2 = @reaction_network rs2 begin (k3, k4), X3 <--> X4 end - @named rs_catalyst = flatten(compose(rs1, [rs2])) + @named rs_catalyst = compose(rs1, [rs2]) @unpack X1, X2, k1, k2 = rs1 gi_1 = assess_identifiability(rs_catalyst; measured_quantities=[X1, X2, rs2.X3], known_p=[k1]) li_1 = assess_local_identifiability(rs_catalyst; measured_quantities=[X1, X2, rs2.X3], known_p=[k1]) From 2222f900430ebffe6cdd790df1ddd8da15e7d092 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 5 Dec 2023 14:53:21 -0500 Subject: [PATCH 46/80] Auto stash before merge of "structuralidentifiabilityextension" and "origin/structuralidentifiabilityextension" --- HISTORY.md | 1 + .../structural_identifiability_extension.jl | 26 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index ecd29c09b9..e0479f8cb8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,6 +5,7 @@ ## Catalyst 14.0 - Added CatalystStructuralIdentifiabilityExtension, which permits StructuralIdentifiability.jl function to be applied directly to Catalyst systems. E.g. use ```julia +using Catalyst, StructuralIdentifiability goodwind_oscillator = @reaction_network begin (mmr(P,pₘ,1), dₘ), 0 <--> M (pₑ*M,dₑ), 0 <--> E diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 80bb13de4d..e36d6b5718 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -2,29 +2,33 @@ # For a reaction system, list of measured quantities and known parameters, generate a StructuralIdentifiability compatible ODE. """ - si_ode(rs::ReactionSystem; measured_quantities=observed(rs), known_p = Num[], ignore_no_measured_warn=false) +make_si_ode(rs::ReactionSystem; measured_quantities=observed(rs), known_p = Num[], ignore_no_measured_warn=false) Creates a ODE system of the form used within the StructuralIdentifiability.jl package. The output system is compatible with all StructuralIdentifiability functions. Arguments: - `rs::ReactionSystem`; The reaction system we wish to convert to an ODE. -- `measured_quantities=observed(rs)`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.s`, or the symbol `:x`). Defaults to the system's observables. +- `measured_quantities=[]`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.s`, or the symbol `:x`). Defaults to the system's observables. - `known_p = Num[]`: List of parameters which values are known. -- `ignore_no_measured_warn=false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. +- `ignore_no_measured_warn = false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. +- `remove_conserved = true`: Whether to eliminate conservation laws when computing the ode (this can reduce runtime of identifiability analysis significantly). Example: ```julia +using Catalyst, StructuralIdentifiability rs = @reaction_network begin (p,d), 0 <--> X end -si_ode(rs; measured_quantities = [:X], known_p = [:p]) +make_si_ode(rs; measured_quantities = [:X], known_p = [:p]) +``` Notes: -This function is part of the StructuralIdentifiability.jl extension. StructuralIdentifiability.jl must be imported to access it. +- This function is part of the StructuralIdentifiability.jl extension. StructuralIdentifiability.jl must be imported to access it. +- `measured_quantities` and `known_p` input may also be symbolic (e.g. measured_quantities = [rs.X]) ``` """ function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities = [], known_p = [], - ignore_no_measured_warn=false, remove_conserved = true) + ignore_no_measured_warn = false, remove_conserved = true) # Creates a MTK ODESystem, and a list of measured quantities (there are equations). # Gives these to SI to create an SI ode model of its preferred form. osys, conseqs, _ = make_osys(rs; remove_conserved) @@ -111,12 +115,12 @@ function make_measured_quantities(rs::ReactionSystem, measured_quantities::Vecto # Appends the known parameters to the measured_quantities vector. Converts any Symbols to symbolics. mqiterator = Iterators.flatten((measured_quantities, known_p)) mqs = [(q isa Symbol) ? Catalyst._symbol_to_var(rs, q) : q for q in mqiterator] - mqs = vector_subs(measured_quantities, conseqs) + mqs = vector_subs(mqs, conseqs) # Creates one internal observation variable for each measured quantity (`___internal_observables`). # Creates a vector of equations, setting each measured quantity equal to one observation variable. - @variables t (___internal_observables(t))[1:length(measured_quantities)] - return Equation[(q isa Equation) ? q : (___internal_observables[i] ~ q) for (i,q) in enumerate(measured_quantities)] + @variables t (___internal_observables(Catalyst.get_iv(rs)))[1:length(mqs)] + return Equation[(q isa Equation) ? q : (___internal_observables[i] ~ q) for (i,q) in enumerate(mqs)] end # Creates the functions that we wish to check for identifiability. @@ -135,8 +139,8 @@ end function make_output(out, funcs_to_check, conseqs) funcs_to_check = vector_subs(funcs_to_check, conseqs) out = Dict(zip(vector_subs(keys(out), conseqs), values(out))) - out = sort(out; by = x -> findfirst(isequal(x, ftc) for ftc in funcs_to_check)) - return out + sortdict = Dict(ftc => i for (i,ftc) in enumerate(funcs_to_check)) + return sort(out; by = x -> sortdict[x]) end # For a vector of expressions and a conservation law, substitutes the law into every equation. From b92002cc359a1dec364fbc9bb9c9d4a4463311ed Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 5 Dec 2023 16:22:32 -0500 Subject: [PATCH 47/80] up --- .../structural_identifiability_extension.jl | 83 +++++++++++++++++-- test/extensions/structural_identifiability.jl | 20 ++++- 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index e36d6b5718..f41ad04617 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -8,7 +8,7 @@ Creates a ODE system of the form used within the StructuralIdentifiability.jl pa Arguments: - `rs::ReactionSystem`; The reaction system we wish to convert to an ODE. -- `measured_quantities=[]`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.s`, or the symbol `:x`). Defaults to the system's observables. +- `measured_quantities=[]`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.s`, or the symbol `:x`). - `known_p = Num[]`: List of parameters which values are known. - `ignore_no_measured_warn = false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. - `remove_conserved = true`: Whether to eliminate conservation laws when computing the ode (this can reduce runtime of identifiability analysis significantly). @@ -25,7 +25,6 @@ make_si_ode(rs; measured_quantities = [:X], known_p = [:p]) Notes: - This function is part of the StructuralIdentifiability.jl extension. StructuralIdentifiability.jl must be imported to access it. - `measured_quantities` and `known_p` input may also be symbolic (e.g. measured_quantities = [rs.X]) -``` """ function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities = [], known_p = [], ignore_no_measured_warn = false, remove_conserved = true) @@ -33,12 +32,36 @@ function Catalyst.make_si_ode(rs::ReactionSystem; measured_quantities = [], know # Gives these to SI to create an SI ode model of its preferred form. osys, conseqs, _ = make_osys(rs; remove_conserved) measured_quantities = make_measured_quantities(rs, measured_quantities, known_p, conseqs; ignore_no_measured_warn) - return SI.preprocess_ode(osys, measured_quantities)[1] + return SI.mtk_to_si(osys, measured_quantities)[1] end ### Structural Identifiability Wrappers ### -# Creates dispatch for SI's local identifiability analysis function. +""" +assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[],, remove_conserved = true, ignore_no_measured_warn=false, kwargs...) + +Applies StructuralIdentifiability.jl's `assess_local_identifiability` function to a Catalyst `ReactionSystem`. Internally it is converted ot a `ODESystem`, for which structural identifiability is computed. + +Arguments: +- `rs::ReactionSystem`; The reaction system we wish to compute structural identifiability for. +- `measured_quantities=[]`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.s`, or the symbol `:x`). +- `known_p = Num[]`: List of parameters which values are known. +- `ignore_no_measured_warn = false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. +- `remove_conserved = true`: Whether to eliminate conservation laws when computing the ode (this can reduce runtime of identifiability analysis significantly). + +Example: +```julia +using Catalyst, StructuralIdentifiability +rs = @reaction_network begin + (p,d), 0 <--> X +end +assess_local_identifiability(rs; measured_quantities = [:X], known_p = [:p]) +``` + +Notes: +- This function is part of the StructuralIdentifiability.jl extension. StructuralIdentifiability.jl must be imported to access it. +- `measured_quantities` and `known_p` input may also be symbolic (e.g. measured_quantities = [rs.X]) +""" function SI.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], funcs_to_check = Vector(), remove_conserved = true, ignore_no_measured_warn=false, kwargs...) @@ -52,7 +75,31 @@ function SI.assess_local_identifiability(rs::ReactionSystem, args...; measured_q return make_output(out, funcs_to_check, reverse.(conseqs)) end -# Creates dispatch for SI's global identifiability analysis function. +""" +assess_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[],, remove_conserved = true, ignore_no_measured_warn=false, kwargs...) + +Applies StructuralIdentifiability.jl's `assess_identifiability` function to a Catalyst `ReactionSystem`. Internally it is converted ot a `ODESystem`, for which structural identifiability is computed. + +Arguments: +- `rs::ReactionSystem`; The reaction system we wish to compute structural identifiability for. +- `measured_quantities=[]`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.s`, or the symbol `:x`). +- `known_p = Num[]`: List of parameters which values are known. +- `ignore_no_measured_warn = false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. +- `remove_conserved = true`: Whether to eliminate conservation laws when computing the ode (this can reduce runtime of identifiability analysis significantly). + +Example: +```julia +using Catalyst, StructuralIdentifiability +rs = @reaction_network begin + (p,d), 0 <--> X +end +assess_identifiability(rs; measured_quantities = [:X], known_p = [:p]) +``` + +Notes: +- This function is part of the StructuralIdentifiability.jl extension. StructuralIdentifiability.jl must be imported to access it. +- `measured_quantities` and `known_p` input may also be symbolic (e.g. measured_quantities = [rs.X]) +""" function SI.assess_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], funcs_to_check = Vector(), remove_conserved = true, ignore_no_measured_warn=false, kwargs...) @@ -66,7 +113,31 @@ function SI.assess_identifiability(rs::ReactionSystem, args...; measured_quantit return make_output(out, funcs_to_check, reverse.(conseqs)) end -# Creates dispatch for SI's function to find all identifiable functions. +""" +find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[],, remove_conserved = true, ignore_no_measured_warn=false, kwargs...) + +Applies StructuralIdentifiability.jl's `find_identifiable_functions` function to a Catalyst `ReactionSystem`. Internally it is converted ot a `ODESystem`, for which structurally identifiable functions are computed. + +Arguments: +- `rs::ReactionSystem`; The reaction system we wish to compute structural identifiability for. +- `measured_quantities=[]`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.s`, or the symbol `:x`). +- `known_p = Num[]`: List of parameters which values are known. +- `ignore_no_measured_warn = false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. +- `remove_conserved = true`: Whether to eliminate conservation laws when computing the ode (this can reduce runtime of identifiability analysis significantly). + +Example: +```julia +using Catalyst, StructuralIdentifiability +rs = @reaction_network begin + (p,d), 0 <--> X +end +find_identifiable_functions(rs; measured_quantities = [:X], known_p = [:p]) +``` + +Notes: +- This function is part of the StructuralIdentifiability.jl extension. StructuralIdentifiability.jl must be imported to access it. +- `measured_quantities` and `known_p` input may also be symbolic (e.g. measured_quantities = [rs.X]) +""" function SI.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], remove_conserved = true, ignore_no_measured_warn=false, kwargs...) diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index 29677be00a..f5fb31449a 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -55,6 +55,12 @@ let @test sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) @test sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) @test length(ifs_1) == length(ifs_2) == length(ifs_3) + + # Checks output to manually checked correct answers. + @test isequal(collect(keys(gi_1)), [states(goodwind_oscillator_catalyst); parameters(goodwind_oscillator_catalyst)]) + @test isequal(collect(values(gi_1)), [:globally, :nonidentifiable, :globally, :globally, :globally, :nonidentifiable, :locally, :nonidentifiable, :locally]) + @test isequal(collect(keys(li_1)), [states(goodwind_oscillator_catalyst); parameters(goodwind_oscillator_catalyst)]) + @test isequal(collect(values(li_1)), [1, 0, 1, 1, 1, 0, 1, 0, 1]) end # Tests on a made-up reaction network with mix of identifiable and non-identifiable components. @@ -97,7 +103,13 @@ let # Check outputs. @test sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) @test sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) - @test length(ifs_1) == length(ifs_2) == length(ifs_3) + @test length(ifs_1) == length(ifs_2) == length(ifs_3) + + # Checks output to manually checked correct answers. + @test isequal(collect(keys(gi_1)),[states(rs_catalyst); parameters(rs_catalyst)]) + @test isequal(collect(values(gi_1)),[:nonidentifiable, :globally, :globally, :nonidentifiable, :nonidentifiable, :nonidentifiable, :nonidentifiable, :globally, :globally, :globally]) + @test isequal(collect(keys(li_1)),[states(rs_catalyst); parameters(rs_catalyst)]) + @test isequal(collect(values(li_1)),[0, 1, 1, 0, 0, 0, 0, 1, 1, 1]) end # Tests on a made-up reaction network with mix of identifiable and non-identifiable components. @@ -148,6 +160,12 @@ let @test sym_dict(gi_1) == sym_dict(gi_2) == sym_dict(gi_3) @test sym_dict(li_1) == sym_dict(li_2) == sym_dict(li_3) @test length(ifs_1[2:end]) == length(ifs_2) == length(ifs_3) # In the first case, the conservation law parameter is also identifiable. + + # Checks output to manually checked correct answers. + @test isequal(collect(keys(gi_1)),[states(rs_catalyst); parameters(rs_catalyst)]) + @test isequal(collect(values(gi_1)),[:globally, :locally, :locally, :nonidentifiable, :nonidentifiable, :globally, :globally, :globally, :globally, :locally, :locally, :nonidentifiable, :locally, :globally]) + @test isequal(collect(keys(li_1)),[states(rs_catalyst); parameters(rs_catalyst)]) + @test isequal(collect(values(li_1)),[1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1]) end # Tests that various inputs types work. From 81ec37cdfbd5e7c0ce65e2a0520dd647f1f12074 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 5 Dec 2023 16:24:32 -0500 Subject: [PATCH 48/80] up --- .../structural_identifiability_extension.jl | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index f41ad04617..cc4b2bc6d9 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -2,14 +2,14 @@ # For a reaction system, list of measured quantities and known parameters, generate a StructuralIdentifiability compatible ODE. """ -make_si_ode(rs::ReactionSystem; measured_quantities=observed(rs), known_p = Num[], ignore_no_measured_warn=false) +make_si_ode(rs::ReactionSystem; measured_quantities=observed(rs), known_p = [], ignore_no_measured_warn=false) Creates a ODE system of the form used within the StructuralIdentifiability.jl package. The output system is compatible with all StructuralIdentifiability functions. Arguments: - `rs::ReactionSystem`; The reaction system we wish to convert to an ODE. - `measured_quantities=[]`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.s`, or the symbol `:x`). -- `known_p = Num[]`: List of parameters which values are known. +- `known_p = []`: List of parameters which values are known. - `ignore_no_measured_warn = false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. - `remove_conserved = true`: Whether to eliminate conservation laws when computing the ode (this can reduce runtime of identifiability analysis significantly). @@ -38,14 +38,14 @@ end ### Structural Identifiability Wrappers ### """ -assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[],, remove_conserved = true, ignore_no_measured_warn=false, kwargs...) +assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = [], known_p = [], remove_conserved = true, ignore_no_measured_warn=false, kwargs...) Applies StructuralIdentifiability.jl's `assess_local_identifiability` function to a Catalyst `ReactionSystem`. Internally it is converted ot a `ODESystem`, for which structural identifiability is computed. Arguments: - `rs::ReactionSystem`; The reaction system we wish to compute structural identifiability for. - `measured_quantities=[]`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.s`, or the symbol `:x`). -- `known_p = Num[]`: List of parameters which values are known. +- `known_p = []`: List of parameters which values are known. - `ignore_no_measured_warn = false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. - `remove_conserved = true`: Whether to eliminate conservation laws when computing the ode (this can reduce runtime of identifiability analysis significantly). @@ -62,8 +62,8 @@ Notes: - This function is part of the StructuralIdentifiability.jl extension. StructuralIdentifiability.jl must be imported to access it. - `measured_quantities` and `known_p` input may also be symbolic (e.g. measured_quantities = [rs.X]) """ -function SI.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], - known_p = Num[], funcs_to_check = Vector(), remove_conserved = true, +function SI.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = [], + known_p = [], funcs_to_check = Vector(), remove_conserved = true, ignore_no_measured_warn=false, kwargs...) # Creates a ODESystem, list of measured quantities, and functions to check, of SI's preferred form. osys, conseqs, vars = make_osys(rs; remove_conserved) @@ -76,14 +76,14 @@ function SI.assess_local_identifiability(rs::ReactionSystem, args...; measured_q end """ -assess_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[],, remove_conserved = true, ignore_no_measured_warn=false, kwargs...) +assess_identifiability(rs::ReactionSystem, args...; measured_quantities = [], known_p = [], remove_conserved = true, ignore_no_measured_warn=false, kwargs...) Applies StructuralIdentifiability.jl's `assess_identifiability` function to a Catalyst `ReactionSystem`. Internally it is converted ot a `ODESystem`, for which structural identifiability is computed. Arguments: - `rs::ReactionSystem`; The reaction system we wish to compute structural identifiability for. - `measured_quantities=[]`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.s`, or the symbol `:x`). -- `known_p = Num[]`: List of parameters which values are known. +- `known_p = []`: List of parameters which values are known. - `ignore_no_measured_warn = false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. - `remove_conserved = true`: Whether to eliminate conservation laws when computing the ode (this can reduce runtime of identifiability analysis significantly). @@ -100,7 +100,7 @@ Notes: - This function is part of the StructuralIdentifiability.jl extension. StructuralIdentifiability.jl must be imported to access it. - `measured_quantities` and `known_p` input may also be symbolic (e.g. measured_quantities = [rs.X]) """ -function SI.assess_identifiability(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[], +function SI.assess_identifiability(rs::ReactionSystem, args...; measured_quantities = [], known_p = [], funcs_to_check = Vector(), remove_conserved = true, ignore_no_measured_warn=false, kwargs...) # Creates a ODESystem, list of measured quantities, and functions to check, of SI's preferred form. @@ -114,14 +114,14 @@ function SI.assess_identifiability(rs::ReactionSystem, args...; measured_quantit end """ -find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = Num[], known_p = Num[],, remove_conserved = true, ignore_no_measured_warn=false, kwargs...) +find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = [], known_p = [], remove_conserved = true, ignore_no_measured_warn=false, kwargs...) Applies StructuralIdentifiability.jl's `find_identifiable_functions` function to a Catalyst `ReactionSystem`. Internally it is converted ot a `ODESystem`, for which structurally identifiable functions are computed. Arguments: - `rs::ReactionSystem`; The reaction system we wish to compute structural identifiability for. - `measured_quantities=[]`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.s`, or the symbol `:x`). -- `known_p = Num[]`: List of parameters which values are known. +- `known_p = []`: List of parameters which values are known. - `ignore_no_measured_warn = false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. - `remove_conserved = true`: Whether to eliminate conservation laws when computing the ode (this can reduce runtime of identifiability analysis significantly). @@ -138,8 +138,8 @@ Notes: - This function is part of the StructuralIdentifiability.jl extension. StructuralIdentifiability.jl must be imported to access it. - `measured_quantities` and `known_p` input may also be symbolic (e.g. measured_quantities = [rs.X]) """ -function SI.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = Num[], - known_p = Num[], remove_conserved = true, ignore_no_measured_warn=false, +function SI.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = [], + known_p = [], remove_conserved = true, ignore_no_measured_warn=false, kwargs...) # Creates a ODESystem, and list of measured quantities, of SI's preferred form. osys, conseqs = make_osys(rs; remove_conserved) From fcd91dc3cbb38598272144be1a84b918ac81eac7 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Wed, 6 Dec 2023 09:09:16 -0500 Subject: [PATCH 49/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index a0bb1d5f18..db653a4c3c 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -1,5 +1,7 @@ # [Structural Identifiability Analysis](@id structural_identifiability) -During parameter fitting, parameter values are inferred from data. Identifiability is a concept describing to what extent the identification of parameter values for a certain model is actually feasible. Ideally, parameter fitting should always be accompanied with an identifiability analysis of the problem. Identifiability can be divided into *structural* and *practical* identifiability[^1]. Structural identifiability considers only the system and what quantities we can measure to determine which quantities can be identified. Practical identifiability instead considers the available data, and determines what system quantities can be inferred from it. Generally, in the hypothetical case of an infinite amounts of noise-less data, practical identifiability becomes identical to structural identifiability. Generally, structural identifiability is assessed before parameters are fitted, while practical identifiability is assessed afterwards. +During parameter fitting, parameter values are inferred from data. Parameter identifiability refers to whether inferring parameter values for a given model is mathematically feasible. Ideally, parameter fitting should always be accompanied with an identifiability analysis of the problem. + +Identifiability can be divided into *structural* and *practical* identifiability[^1]. Structural identifiability considers only the mathematical model, and which parameters are and are not inherently identifiable due to model structure. Practical identifiability also considers the available data, and determines what system quantities can be inferred from it. In the idealized case of an infinite amount of noise-less data, practical identifiability becomes identical to structural identifiability. Generally, structural identifiability is assessed before parameters are fitted, while practical identifiability is assessed afterwards. Structural identifiability (which is what this tutorial considers) can be illustrated by the following differential equation: ${dx \over dt} = p1*p2*x(t)$ From 74983af75964ad5b89012920bd984f3edb61793e Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Wed, 6 Dec 2023 09:09:33 -0500 Subject: [PATCH 50/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index db653a4c3c..9f5a48d9f0 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -7,7 +7,7 @@ Structural identifiability (which is what this tutorial considers) can be illust ${dx \over dt} = p1*p2*x(t)$ where, however much data is collected on *x*, it is impossible to determine the distinct values of *p1* and *p2*. Hence, these parameters are these are non-identifiable. -Catalyst contains a special extension for carrying out structural identifiability analysis using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. It also implements specialised routines to make these more efficient when applied to reaction network models. How to use these functions are described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). If you use this in your research, please [cite the StructuralIdentifiability.jl](@ref structural_identifiability_citation) and [Catalyst.jl](@ref catalyst_citation) publications. +Catalyst contains a special extension for carrying out structural identifiability analysis using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. It also implements specialised routines to make these more efficient when applied to reaction network models. How to use these functions is described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). If you use this in your research, please [cite the StructuralIdentifiability.jl](@ref structural_identifiability_citation) and [Catalyst.jl](@ref catalyst_citation) publications. Structural identifiability can be divided into *local* and *global* identifiability. If a model quantity is locally identifiable, it means that its true value can be determined down to a finite-number of possible options. This also means that there is some limited region around its true value, where the true value is the only possible value. Globally identifiable quantities' values, on the other hand, can be uniquely determined. Again, while identifiability can be confirmed structurally for a model, it does not necessarily mean that they are practically identifiable for some given data. From 93fce3cf63ce9530a39361fe6925c2532c7fa846 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Wed, 6 Dec 2023 09:09:59 -0500 Subject: [PATCH 51/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 9f5a48d9f0..a87fade8ae 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -22,7 +22,7 @@ StructutralIdentifiability currently assesses identifiability for the first two ### Basic example -Local identifiability can be assessed using the `assess_identifiability` function. For each model quantity (parameters and variables), it will asses whether they are: +Local identifiability can be assessed using the `assess_identifiability` function. For each model quantity (parameters and variables), it will assess whether they are: - globally identifiable. - locally identifiable. - Unidentifiable. From 43952d18640fd7e0baf4125708735f309c33da2d Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Wed, 6 Dec 2023 09:10:15 -0500 Subject: [PATCH 52/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index a87fade8ae..5af85edb26 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -37,7 +37,7 @@ goodwind_oscillator = @reaction_network begin end assess_identifiability(goodwind_oscillator; measured_quantities=[:M], loglevel=Logging.Error) ``` -From the output, we find that `E(t)`, `pₑ`, and `pₚ` (the trajectory of $E$, and the production rates of $E$ and $P$, respectively) are non-identifiable. Next, `dₑ` and `dₚ` (the degradation rates of $E$ and $P$, respectively) are locally identifiable. Finally, `P(t)`, `M(t)`, `pₘ`, and `dₘ` (the trajectories of `P` and `M`, and the production and degradation rate of `M`, respectively) are all globally identifiable. We note that we also imported the Logging package, and provided the `loglevel=Logging.Error` input argument StructuralIdentifiability functions generally provides a large number of output logging messages. Hence, we will use this argument (which requires the Logging package) throughout this tutorial. +From the output, we find that `E(t)`, `pₑ`, and `pₚ` (the trajectory of $E$, and the production rates of $E$ and $P$, respectively) are non-identifiable. Next, `dₑ` and `dₚ` (the degradation rates of $E$ and $P$, respectively) are locally identifiable. Finally, `P(t)`, `M(t)`, `pₘ`, and `dₘ` (the trajectories of `P` and `M`, and the production and degradation rate of `M`, respectively) are all globally identifiable. We note that we also imported the Logging package, and provided the `loglevel=Logging.Error` input argument. StructuralIdentifiability functions generally provide a large number of output messages. Hence, we will use this argument (which requires the Logging package) throughout this tutorial to decrease the amount of printed text. Next, we can assess identifiability in the case where we can measure all three species concentrations: From 32f82b4bf4c480114739dccf48030eee738a1601 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Wed, 6 Dec 2023 09:10:33 -0500 Subject: [PATCH 53/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 5af85edb26..4ae3ff052a 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -48,7 +48,7 @@ in which case all initial conditions and parameters become identifiable. ### Indicating known parameters -In the previous case we assumed that all parameters are unknown, however, this is not necessarily true. If there are parameters which value's are known, we can supply these using the `known_p` argument. Indeed, this might turn other, previously unidentifiable, parameters identifiable. Let us consider the previous example, where we measure the concentration of $M$ only, but also happen to know the production rate of $E$ ($pₑ$): +In the previous case we assumed that all parameters are unknown, however, this is not necessarily true. If there are parameters with known values, we can supply these using the `known_p` argument. Providing this additional information might also make other, previously unidentifiable, parameters identifiable. Let us consider the previous example, where we measure the concentration of $M$ only, but now assume we also know the production rate of $E$ ($pₑ$): ```example si1 assess_identifiability(gwo; measured_quantities=[:M], known_p=[:pₑ], loglevel=Logging.Error) ``` From ba5905667e4e6663325a9a0ce1ca6c62ac902338 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Wed, 6 Dec 2023 09:14:08 -0500 Subject: [PATCH 54/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 4ae3ff052a..1ed5eacb2d 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -65,7 +65,7 @@ enzyme_activation = @reaction_network begin (Eₐ, d), 0 <-->P end ``` -and we can measure the total amount of $E$ ($=$Eᵢ+Eₐ$), as well as the amount of $P$, we can use the following to assess identifiability: +and we can measure the total amount of $E$ ($=Eᵢ+Eₐ$), as well as the amount of $P$, we can use the following to assess identifiability: ```example si2 @unpack Eᵢ, Eₐ = enzyme_activation assess_identifiability(enzyme_activation; measured_quantities=[Eᵢ+Eₐ, :P], loglevel=Logging.Error) From b1a02b90a15ab778c58b70f3e47742b7b09cf07c Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Wed, 6 Dec 2023 09:14:17 -0500 Subject: [PATCH 55/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 1ed5eacb2d..996ec4c9dd 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -25,7 +25,7 @@ StructutralIdentifiability currently assesses identifiability for the first two Local identifiability can be assessed using the `assess_identifiability` function. For each model quantity (parameters and variables), it will assess whether they are: - globally identifiable. - locally identifiable. -- Unidentifiable. +- unidentifiable. To it, we provide our `ReactionSystem` model and a list of quantities that we are able to measure. Here, we consider a Goodwind oscillator (a simple 3-component model, where the three species $M$, $E$, and $P$ are produced and degraded, which may exhibit oscillations)[^2]. Let us say that we are able to measure the concentration of $M$, we then provide designate this using the `measured_quantities` argument. We can now assess identifiability in the following way: ```example si1 From 0504f9c138717b2233d152d0d0a3c2e4312e1ec5 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Wed, 6 Dec 2023 09:14:32 -0500 Subject: [PATCH 56/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 996ec4c9dd..91d40a00d3 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -12,7 +12,7 @@ Catalyst contains a special extension for carrying out structural identifiabilit Structural identifiability can be divided into *local* and *global* identifiability. If a model quantity is locally identifiable, it means that its true value can be determined down to a finite-number of possible options. This also means that there is some limited region around its true value, where the true value is the only possible value. Globally identifiable quantities' values, on the other hand, can be uniquely determined. Again, while identifiability can be confirmed structurally for a model, it does not necessarily mean that they are practically identifiable for some given data. Generally, there are three types of quantities for which identifiability can be assessed. -- Parameters (e.g. $p$). +- Parameters (e.g. $p1$ and $p2$). - Full variable trajectories (e.g. $x(t)$). - Variable initial conditions (e.g. $x(0)$). From be767637aa526f86f6f2c7c27281e777d045ffad Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 6 Dec 2023 09:21:13 -0500 Subject: [PATCH 57/80] up --- docs/src/inverse_problems/structural_identifiability.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 91d40a00d3..69c590c83e 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -5,7 +5,7 @@ Identifiability can be divided into *structural* and *practical* identifiability Structural identifiability (which is what this tutorial considers) can be illustrated by the following differential equation: ${dx \over dt} = p1*p2*x(t)$ -where, however much data is collected on *x*, it is impossible to determine the distinct values of *p1* and *p2*. Hence, these parameters are these are non-identifiable. +where, however much data is collected on $x$, it is impossible to determine the distinct values of $p1$ and $p2$. Hence, these parameters are these are non-identifiable (however, their product, $p1*p2$, *is* identifiable). Catalyst contains a special extension for carrying out structural identifiability analysis using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. It also implements specialised routines to make these more efficient when applied to reaction network models. How to use these functions is described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). If you use this in your research, please [cite the StructuralIdentifiability.jl](@ref structural_identifiability_citation) and [Catalyst.jl](@ref catalyst_citation) publications. @@ -57,7 +57,7 @@ Not only does this turn the previously non-identifiable `pₑ` (globally) identi To, in a similar manner, indicate that certain initial conditions are known is a work in progress. Hopefully the feature should be an available in the near future. ### Providing non-trivial measured quantities -Sometimes, we are not actually measuring species species, but rather some combinations of species (or possibly parameters). Here, any algebraic expression can be used in `measured_quantities`. If so, used species and parameters have to first be `@unpack`'ed from the model. Say that we have a model where an enzyme ($E$) is converted between an active and inactive form, which in turns activates the production of a product, $P$: +Sometimes, we are not actually measuring species species, but rather some combinations of species (or possibly parameters). Here, any algebraic expression can be used in `measured_quantities`. Species and parameters have to first be `@unpack`'ed from the model to be used in such expressions. Say that we have a model where an enzyme ($E$) is converted between an active and inactive form, which in turns activates the production of a product, $P$: ```example si2 using Catalyst, StructuralIdentifiability # hide enzyme_activation = @reaction_network begin From f766ee960011601ab3cec5a2a7b3880b7ab197e6 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 6 Dec 2023 09:58:20 -0500 Subject: [PATCH 58/80] up --- .../structural_identifiability.md | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 69c590c83e..e356b8b53d 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -1,33 +1,33 @@ # [Structural Identifiability Analysis](@id structural_identifiability) During parameter fitting, parameter values are inferred from data. Parameter identifiability refers to whether inferring parameter values for a given model is mathematically feasible. Ideally, parameter fitting should always be accompanied with an identifiability analysis of the problem. -Identifiability can be divided into *structural* and *practical* identifiability[^1]. Structural identifiability considers only the mathematical model, and which parameters are and are not inherently identifiable due to model structure. Practical identifiability also considers the available data, and determines what system quantities can be inferred from it. In the idealized case of an infinite amount of noise-less data, practical identifiability becomes identical to structural identifiability. Generally, structural identifiability is assessed before parameters are fitted, while practical identifiability is assessed afterwards. +Identifiability can be divided into *structural* and *practical* identifiability[^1]. Structural identifiability considers only the mathematical model, and which parameters are and are not inherently identifiable due to model structure. Practical identifiability also considers the available data, and determines what system quantities can be inferred from it. In the idealised case of an infinite amount of non-noisy data, practical identifiability converges to structural identifiability. Generally, structural identifiability is assessed before parameters are fitted, while practical identifiability is assessed afterwards. Structural identifiability (which is what this tutorial considers) can be illustrated by the following differential equation: ${dx \over dt} = p1*p2*x(t)$ where, however much data is collected on $x$, it is impossible to determine the distinct values of $p1$ and $p2$. Hence, these parameters are these are non-identifiable (however, their product, $p1*p2$, *is* identifiable). -Catalyst contains a special extension for carrying out structural identifiability analysis using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. It also implements specialised routines to make these more efficient when applied to reaction network models. How to use these functions is described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). If you use this in your research, please [cite the StructuralIdentifiability.jl](@ref structural_identifiability_citation) and [Catalyst.jl](@ref catalyst_citation) publications. +Catalyst contains a special extension for carrying out structural identifiability analysis using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. It also implements specialised routines to make these more efficient when applied to reaction network models. How to use these functions is described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). -Structural identifiability can be divided into *local* and *global* identifiability. If a model quantity is locally identifiable, it means that its true value can be determined down to a finite-number of possible options. This also means that there is some limited region around its true value, where the true value is the only possible value. Globally identifiable quantities' values, on the other hand, can be uniquely determined. Again, while identifiability can be confirmed structurally for a model, it does not necessarily mean that they are practically identifiable for some given data. +Structural identifiability can be divided into *local* and *global* identifiability. If a model quantity is locally identifiable, it means that its true value can be determined down to a finite-number of possible options. This also means that there is some limited region around the quantity's true value where this true value is the only possible value (and hence, within this region, the quantity is fully identifiable). Globally identifiable quantities' values, on the other hand, can be uniquely determined. Again, while identifiability can be confirmed structurally for a model, it does not necessarily mean that they are practically identifiable for some given data. Generally, there are three types of quantities for which identifiability can be assessed. - Parameters (e.g. $p1$ and $p2$). - Full variable trajectories (e.g. $x(t)$). - Variable initial conditions (e.g. $x(0)$). -StructutralIdentifiability currently assesses identifiability for the first two (however, if $x(t)$ is identifiable, $x(0)$ will be as well). +StructuralIdentifiability currently assesses identifiability for the first two only (however, if $x(t)$ is identifiable, then $x(0)$ will be as well). ## Global identifiability analysis ### Basic example Local identifiability can be assessed using the `assess_identifiability` function. For each model quantity (parameters and variables), it will assess whether they are: -- globally identifiable. -- locally identifiable. -- unidentifiable. +- Globally identifiable. +- Locally identifiable. +- Unidentifiable. -To it, we provide our `ReactionSystem` model and a list of quantities that we are able to measure. Here, we consider a Goodwind oscillator (a simple 3-component model, where the three species $M$, $E$, and $P$ are produced and degraded, which may exhibit oscillations)[^2]. Let us say that we are able to measure the concentration of $M$, we then provide designate this using the `measured_quantities` argument. We can now assess identifiability in the following way: +To it, we provide our `ReactionSystem` model and a list of quantities that we are able to measure. Here, we consider a Goodwind oscillator (a simple 3-component model, where the three species $M$, $E$, and $P$ are produced and degraded, which may exhibit oscillations)[^2]. Let us say that we are able to measure the concentration of $M$, we then designate this using the `measured_quantities` argument. We can now assess identifiability in the following way: ```example si1 using Catalyst, Logging, StructuralIdentifiability goodwind_oscillator = @reaction_network begin @@ -37,15 +37,13 @@ goodwind_oscillator = @reaction_network begin end assess_identifiability(goodwind_oscillator; measured_quantities=[:M], loglevel=Logging.Error) ``` -From the output, we find that `E(t)`, `pₑ`, and `pₚ` (the trajectory of $E$, and the production rates of $E$ and $P$, respectively) are non-identifiable. Next, `dₑ` and `dₚ` (the degradation rates of $E$ and $P$, respectively) are locally identifiable. Finally, `P(t)`, `M(t)`, `pₘ`, and `dₘ` (the trajectories of `P` and `M`, and the production and degradation rate of `M`, respectively) are all globally identifiable. We note that we also imported the Logging package, and provided the `loglevel=Logging.Error` input argument. StructuralIdentifiability functions generally provide a large number of output messages. Hence, we will use this argument (which requires the Logging package) throughout this tutorial to decrease the amount of printed text. +From the output, we find that `E(t)`, `pₑ`, and `pₚ` (the trajectory of $E$, and the production rates of $E$ and $P$, respectively) are non-identifiable. Next, `dₑ` and `dₚ` (the degradation rates of $E$ and $P$, respectively) are locally identifiable. Finally, `P(t)`, `M(t)`, `pₘ`, and `dₘ` (the trajectories of `P` and `M`, and the production and degradation rate of `M`, respectively) are all globally identifiable. We note that we also imported the Logging.jl package, and provided the `loglevel=Logging.Error` input argument. StructuralIdentifiability functions generally provide a large number of output messages. Hence, we will use this argument (which requires the Logging package) throughout this tutorial to decrease the amount of printed text. - -Next, we can assess identifiability in the case where we can measure all three species concentrations: +Next, we also assess identifiability in the case where we can measure all three species concentrations: ```example si1 assess_identifiability(goodwind_oscillator; measured_quantities=[:M, :P, :E], loglevel=Logging.Error) ``` -in which case all initial conditions and parameters become identifiable. - +in which case all species trajectories and parameters become identifiable. ### Indicating known parameters In the previous case we assumed that all parameters are unknown, however, this is not necessarily true. If there are parameters with known values, we can supply these using the `known_p` argument. Providing this additional information might also make other, previously unidentifiable, parameters identifiable. Let us consider the previous example, where we measure the concentration of $M$ only, but now assume we also know the production rate of $E$ ($pₑ$): @@ -57,7 +55,7 @@ Not only does this turn the previously non-identifiable `pₑ` (globally) identi To, in a similar manner, indicate that certain initial conditions are known is a work in progress. Hopefully the feature should be an available in the near future. ### Providing non-trivial measured quantities -Sometimes, we are not actually measuring species species, but rather some combinations of species (or possibly parameters). Here, any algebraic expression can be used in `measured_quantities`. Species and parameters have to first be `@unpack`'ed from the model to be used in such expressions. Say that we have a model where an enzyme ($E$) is converted between an active and inactive form, which in turns activates the production of a product, $P$: +Sometimes, we are not actually measuring species species, but rather some combinations of species (or possibly parameters). To account for this, `measured_quantities` accepts any algebraic expressions (and not just single species). To form such expressions, species and parameters have to first be `@unpack`'ed from the model. Say that we have a model where an enzyme ($E$) is converted between an active and inactive form, which in turns activates the production of a product, $P$: ```example si2 using Catalyst, StructuralIdentifiability # hide enzyme_activation = @reaction_network begin @@ -81,12 +79,12 @@ nothing # hide ``` ### Probability of correctness -The identifiability methods used can, in theory, produce erroneous results. However, it is possible to adjust the lower bound for the probability of correctness using the argument `p` (by default set to `0.99`, that is, at least a $99%$ chance of correctness). We can e.g. increase the bound through: +The identifiability methods used can, in theory, produce erroneous results. However, it is possible to adjust the lower bound for the probability of correctness using the argument `p` (by default set to `0.99`, that is, at least a $99\%$ chance of correctness). We can e.g. increase the bound through: ```example si2 assess_identifiability(goodwind_oscillator; measured_quantities=[:M], p=0.999, loglevel=Logging.Error) nothing # hide ``` -giving a minimum bound of $99.9%$ chance of correctness. In practise, the bounds used by StructuralIdentifiability are very conservative, which means that while the minimum guaranteed probability of correctness in the default case is $99%$, in practise it is much higher. While increasing the value of `p` increases the certainty of correctness, it will also increase the time required to assess identifiability. +giving a minimum bound of $99.9\%$ chance of correctness. In practise, the bounds used by StructuralIdentifiability are very conservative, which means that while the minimum guaranteed probability of correctness in the default case is $99\%$, in practise it is much higher. While increasing the value of `p` increases the certainty of correctness, it will also increase the time required to assess identifiability. ## Local identifiability analysis Local identifiability can be assessed through the `assess_local_identifiability` function. While this is already determined by `assess_identifiability`, assessing local identifiability only have the advantage that it is easier to compute. Hence, there might be models where global identifiability analysis fails (or takes prohibitively long time), where instead `assess_local_identifiability` can be used. This functions takes the same inputs as `assess_identifiability` and returns, for each quantity, `true` if it is locally identifiable (or `false` if it is not). Here, for the Goodwind oscillator, we assesses it for local identifiability only: @@ -100,12 +98,12 @@ Finally, StructuralIdentifiability provides the `find_identifiable_functions` fu ```example si1 find_identifiable_functions(goodwind_oscillator; measured_quantities=[:M], loglevel=Logging.Error) ``` -Again, these results are consistent with those of produced by `assess_identifiability`. There, `pₑ` and `pₚ` where found to be globally identifiable. Here, they correspond directly to identifiable expressions. The remaining four parameters (`pₘ`, `dₘ`, `dₑ`, and `dₚ`) occurs as part of more complicated composite expressions. +Again, these results are consistent with those produced by `assess_identifiability`. There, `pₑ` and `pₚ` where found to be globally identifiable. Here, they correspond directly to identifiable expressions. The remaining four parameters (`pₘ`, `dₘ`, `dₑ`, and `dₚ`) occurs as part of more complicated composite expressions. -The `find_identifiable_functions` functions tries to simplify its output functions to create nice expression. The degree to which it does this can be adjusted using the `simplify` keywords. Using the `:weak`, `:standard` (default), and `:strong` arguments, increased simplification can be forced (at the expense of longer runtimes). +The `find_identifiable_functions` functions tries to simplify its output functions to create nice expressions. The degree to which it does this can be adjusted using the `simplify` keywords. Using the `:weak`, `:standard` (default), and `:strong` arguments, increased simplification can be forced (at the expense of longer runtimes). ## Creating StructuralIdentifiability compatible ODE models from Catalyst `ReactionSystem`s -While the functionality described above covers the vast majority of analysis as user might want to perform, the StructuralIdentifiability package supports several additional features. While these does not have inherent Catalyst support, we do provide the `make_si_ode` function to simplify their use. Similarly to the previous functions, it takes a `ReactionSystem` and lists of measured quantities and known parameter values. The output is a [ode of the standard form supported by StructuralIdentifiability](https://docs.sciml.ai/StructuralIdentifiability/stable/tutorials/creating_ode/#Defining-the-model-using-@ODEmodel-macro). It can be created using the following syntax: +While the functionality described above covers the vast majority of analysis that user might want to perform, the StructuralIdentifiability package supports several additional features. While these does not have inherent Catalyst support, we do provide the `make_si_ode` function to simplify their use. Similarly to the previous functions, it takes a `ReactionSystem` and lists of measured quantities and known parameter values. The output is a [ode of the standard form supported by StructuralIdentifiability](https://docs.sciml.ai/StructuralIdentifiability/stable/tutorials/creating_ode/#Defining-the-model-using-@ODEmodel-macro). It can be created using the following syntax: ```example si1 si_ode = make_si_ode(goodwind_oscillator; measured_quantities=[:M]) nothing # hide @@ -132,7 +130,7 @@ E.g. if you run: ```example si2 find_identifiable_functions(rs; measured_quantities = [:X1, :X2]) ``` -we see that `Γ[1]` (`= X1 + X2`) is detected as an identifiable expression. If we want to disable this feature for any function, we can use the `remove_conserved = false` option: +we see that `Γ[1]` (`= X1(0) + X2(0)`) is detected as an identifiable expression. If we want to disable this feature for any function, we can use the `remove_conserved = false` option: ```example si2 find_identifiable_functions(rs; measured_quantities = [:X1, :X2], remove_conserved = false) ``` From 42a11c54d94431f5f33af3c7f3c39ee7bce2f534 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:03:15 -0500 Subject: [PATCH 59/80] Update ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl Co-authored-by: Sam Isaacson --- .../structural_identifiability_extension.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index cc4b2bc6d9..c41eb4ecad 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -8,7 +8,7 @@ Creates a ODE system of the form used within the StructuralIdentifiability.jl pa Arguments: - `rs::ReactionSystem`; The reaction system we wish to convert to an ODE. -- `measured_quantities=[]`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.s`, or the symbol `:x`). +- `measured_quantities=[]`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.x`, or the symbol `:x`). - `known_p = []`: List of parameters which values are known. - `ignore_no_measured_warn = false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. - `remove_conserved = true`: Whether to eliminate conservation laws when computing the ode (this can reduce runtime of identifiability analysis significantly). From 4c08e8af0562005e349f40fe51dd16cfab765e08 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:03:27 -0500 Subject: [PATCH 60/80] Update ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl Co-authored-by: Sam Isaacson --- .../structural_identifiability_extension.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index c41eb4ecad..0c35d8b5ec 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -9,7 +9,7 @@ Creates a ODE system of the form used within the StructuralIdentifiability.jl pa Arguments: - `rs::ReactionSystem`; The reaction system we wish to convert to an ODE. - `measured_quantities=[]`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.x`, or the symbol `:x`). -- `known_p = []`: List of parameters which values are known. +- `known_p = []`: List of parameters for which their values are assumed to be known. - `ignore_no_measured_warn = false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. - `remove_conserved = true`: Whether to eliminate conservation laws when computing the ode (this can reduce runtime of identifiability analysis significantly). From 20c2fd80b14ca051df12167b6fab1fbea2cfae0f Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:03:46 -0500 Subject: [PATCH 61/80] Update ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl Co-authored-by: Sam Isaacson --- .../structural_identifiability_extension.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 0c35d8b5ec..c039788817 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -43,7 +43,7 @@ assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = Applies StructuralIdentifiability.jl's `assess_local_identifiability` function to a Catalyst `ReactionSystem`. Internally it is converted ot a `ODESystem`, for which structural identifiability is computed. Arguments: -- `rs::ReactionSystem`; The reaction system we wish to compute structural identifiability for. +- `rs::ReactionSystem`; The reaction system for which we wish to compute structural identifiability of the associated reaction rate equation ODE model. - `measured_quantities=[]`: The quantities of the system we can measure. May either be equations (e.g. `x1 + x2`), or single species (e.g. the symbolic `x`, `rs.s`, or the symbol `:x`). - `known_p = []`: List of parameters which values are known. - `ignore_no_measured_warn = false`: If set to `true`, no warning is provided when the `measured_quantities` vector is empty. From 9b5b19cbf606f11de68948b9fae5ca79111f4bf2 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:04:27 -0500 Subject: [PATCH 62/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index e356b8b53d..9769b9cd0c 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -5,7 +5,7 @@ Identifiability can be divided into *structural* and *practical* identifiability Structural identifiability (which is what this tutorial considers) can be illustrated by the following differential equation: ${dx \over dt} = p1*p2*x(t)$ -where, however much data is collected on $x$, it is impossible to determine the distinct values of $p1$ and $p2$. Hence, these parameters are these are non-identifiable (however, their product, $p1*p2$, *is* identifiable). +where, however much data is collected on $x$, it is impossible to determine the distinct values of $p1$ and $p2$. Hence, these parameters are non-identifiable (however, their product, $p1*p2$, *is* identifiable). Catalyst contains a special extension for carrying out structural identifiability analysis using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. It also implements specialised routines to make these more efficient when applied to reaction network models. How to use these functions is described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). From b0f012bffbe43fd7f12bd554702b900215b750c2 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:04:57 -0500 Subject: [PATCH 63/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 9769b9cd0c..50ace48e11 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -143,7 +143,7 @@ rn = @reaction_network begin end assess_identifiability(rn; measured_quantities=[:X]) ``` -is currently not possible. Hopefully this will be a supported feature in the future. For now, these expression will have to be rewritten to not include such exponents. For some cases, e.g. `10^k` this is trivial. However, it is also possible generally (but more involved and often includes introducing additional variables). If you have a system where this is required, it is recommended to contact an expert. +is currently not possible. Hopefully this will be a supported feature in the future. For now, such expressions will have to be rewritten to not include such exponents. For some cases, e.g. `10^k` this is trivial. However, it is also possible generally (but more involved and often includes introducing additional variables). --- ## [Citation](@id structural_identifiability_citation) From 6575136cb884b8c87c0a65341ef8438b4b65cdf5 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:05:09 -0500 Subject: [PATCH 64/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 50ace48e11..de631a6d54 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -122,7 +122,7 @@ rs = @reaction_network begin (k1,k2), X1 <--> X2 end ``` -contain conservation laws (in this case $Γ = X1 + X2$, where $Γ = X1(0) + X2(0)$ is a constant). Because the presence of such conservation laws makes structural identifiability analysis prohibitively computationally expensive (for all but the simplest of cases), these are automatically eliminated by Catalyst (removing one ODE from the resulting ODE system for each conservation law). For the `assess_identifiability` and `assess_local_identifiability` functions, this will be unnoticed by the user. However, for the `find_identifiable_functions` and `make_si_ode` functions, this may result in one, or several, parameters on the form `Γ[i]` (where `i` is an integer) appearing in the produced expressions. These correspond to the conservation law constants and can be found through +contain conservation laws (in this case $Γ = X1 + X2$, where $Γ = X1(0) + X2(0)$ is a constant). Because the presence of such conservation laws makes structural identifiability analysis prohibitively computationally expensive (for all but the simplest of cases), these are automatically eliminated by Catalyst (removing one ODE from the resulting ODE system for each conservation law). For the `assess_identifiability` and `assess_local_identifiability` functions, this will be unnoticed by the user. However, for the `find_identifiable_functions` and `make_si_ode` functions, this may result in one, or several, parameters of the form `Γ[i]` (where `i` is an integer) appearing in the produced expressions. These correspond to the conservation law constants and can be found through ```example si2 conservedequations(rs) ``` From 7cc24b4e4a25ad43697329fd6f8d00a61b67c4fa Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:05:19 -0500 Subject: [PATCH 65/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index de631a6d54..65f2e66ff1 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -108,7 +108,7 @@ While the functionality described above covers the vast majority of analysis tha si_ode = make_si_ode(goodwind_oscillator; measured_quantities=[:M]) nothing # hide ``` -and then used as input to various StructuralIdentifiability functions. In the following example we uses StructuralIdentifiability's `print_for_DAISY` function, printing the model as an expression that can be used by the [DAISY](https://daisy.dei.unipd.it/) software for identifiability analysis[^3]. +and then used as input to various StructuralIdentifiability functions. In the following example we use StructuralIdentifiability's `print_for_DAISY` function, printing the model as an expression that can be used by the [DAISY](https://daisy.dei.unipd.it/) software for identifiability analysis[^3]. ```example si1 print_for_DAISY(si_ode) nothing # hide From bafc4510743202668807d464f6e1666599623e85 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:06:16 -0500 Subject: [PATCH 66/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 65f2e66ff1..7bd1cc36f6 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -9,7 +9,7 @@ where, however much data is collected on $x$, it is impossible to determine the Catalyst contains a special extension for carrying out structural identifiability analysis using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. It also implements specialised routines to make these more efficient when applied to reaction network models. How to use these functions is described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). -Structural identifiability can be divided into *local* and *global* identifiability. If a model quantity is locally identifiable, it means that its true value can be determined down to a finite-number of possible options. This also means that there is some limited region around the quantity's true value where this true value is the only possible value (and hence, within this region, the quantity is fully identifiable). Globally identifiable quantities' values, on the other hand, can be uniquely determined. Again, while identifiability can be confirmed structurally for a model, it does not necessarily mean that they are practically identifiable for some given data. +Structural identifiability can be divided into *local* and *global* identifiability. If a model quantity is locally identifiable, it means that its true value can be determined down to a finite-number of possible options. This also means that there is some limited region around the quantity's true value where this true value is the only possible value (and hence, within this region, the quantity is fully identifiable). Globally identifiable quantities' values, on the other hand, can be uniquely determined. Again, while identifiability can be confirmed structurally for a quantity, it does not necessarily mean that it is practically identifiable for some given data. Generally, there are three types of quantities for which identifiability can be assessed. - Parameters (e.g. $p1$ and $p2$). From c32484ba002d9d5651c242ff34399bced798f455 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:06:43 -0500 Subject: [PATCH 67/80] Update ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl Co-authored-by: Sam Isaacson --- .../structural_identifiability_extension.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index c039788817..50c791894b 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -40,7 +40,7 @@ end """ assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = [], known_p = [], remove_conserved = true, ignore_no_measured_warn=false, kwargs...) -Applies StructuralIdentifiability.jl's `assess_local_identifiability` function to a Catalyst `ReactionSystem`. Internally it is converted ot a `ODESystem`, for which structural identifiability is computed. +Applies StructuralIdentifiability.jl's `assess_local_identifiability` function to the reaction rate equation ODE model for the given Catalyst `ReactionSystem`. Automatically converts the `ReactionSystem` to an appropriate `ODESystem` and computes structural identifiability, Arguments: - `rs::ReactionSystem`; The reaction system for which we wish to compute structural identifiability of the associated reaction rate equation ODE model. From 120177caa7745971e49aabf5fc6d1691509d0242 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:07:02 -0500 Subject: [PATCH 68/80] Update ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl Co-authored-by: Sam Isaacson --- .../structural_identifiability_extension.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 50c791894b..3349ae0692 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -4,7 +4,7 @@ """ make_si_ode(rs::ReactionSystem; measured_quantities=observed(rs), known_p = [], ignore_no_measured_warn=false) -Creates a ODE system of the form used within the StructuralIdentifiability.jl package. The output system is compatible with all StructuralIdentifiability functions. +Creates a reaction rate equation ODE system of the form used within the StructuralIdentifiability.jl package. The output system is compatible with all StructuralIdentifiability functions. Arguments: - `rs::ReactionSystem`; The reaction system we wish to convert to an ODE. From 35c1bb1cc327e2880b5b17374aaa6ca69b678e71 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:08:26 -0500 Subject: [PATCH 69/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 7bd1cc36f6..f4d12ef926 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -55,7 +55,7 @@ Not only does this turn the previously non-identifiable `pₑ` (globally) identi To, in a similar manner, indicate that certain initial conditions are known is a work in progress. Hopefully the feature should be an available in the near future. ### Providing non-trivial measured quantities -Sometimes, we are not actually measuring species species, but rather some combinations of species (or possibly parameters). To account for this, `measured_quantities` accepts any algebraic expressions (and not just single species). To form such expressions, species and parameters have to first be `@unpack`'ed from the model. Say that we have a model where an enzyme ($E$) is converted between an active and inactive form, which in turns activates the production of a product, $P$: +Sometimes, ones may not have measurements of species, but rather some combinations of species (or possibly parameters). To account for this, `measured_quantities` accepts any algebraic expression (and not just single species). To form such expressions, species and parameters have to first be `@unpack`'ed from the model. Say that we have a model where an enzyme ($E$) is converted between an active and inactive form, which in turns activates the production of a product, $P$: ```example si2 using Catalyst, StructuralIdentifiability # hide enzyme_activation = @reaction_network begin From 97590de3c4026454d872477ad92ab2ba562bd7b0 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:09:06 -0500 Subject: [PATCH 70/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index f4d12ef926..4c03223dd3 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -37,7 +37,7 @@ goodwind_oscillator = @reaction_network begin end assess_identifiability(goodwind_oscillator; measured_quantities=[:M], loglevel=Logging.Error) ``` -From the output, we find that `E(t)`, `pₑ`, and `pₚ` (the trajectory of $E$, and the production rates of $E$ and $P$, respectively) are non-identifiable. Next, `dₑ` and `dₚ` (the degradation rates of $E$ and $P$, respectively) are locally identifiable. Finally, `P(t)`, `M(t)`, `pₘ`, and `dₘ` (the trajectories of `P` and `M`, and the production and degradation rate of `M`, respectively) are all globally identifiable. We note that we also imported the Logging.jl package, and provided the `loglevel=Logging.Error` input argument. StructuralIdentifiability functions generally provide a large number of output messages. Hence, we will use this argument (which requires the Logging package) throughout this tutorial to decrease the amount of printed text. +From the output, in the associated reaction rate equation ODE model we find that `E(t)`, `pₑ`, and `pₚ` (the trajectory of $E$, and the production rates of $E$ and $P$, respectively) are non-identifiable. Next, `dₑ` and `dₚ` (the degradation rates of $E$ and $P$, respectively) are locally identifiable. Finally, `P(t)`, `M(t)`, `pₘ`, and `dₘ` (the trajectories of `P` and `M`, and the production and degradation rate of `M`, respectively) are all globally identifiable. We note that we also imported the Logging.jl package, and provided the `loglevel=Logging.Error` input argument. StructuralIdentifiability functions generally provide a large number of output messages. Hence, we will use this argument (which requires the Logging package) throughout this tutorial to decrease the amount of printed text. Next, we also assess identifiability in the case where we can measure all three species concentrations: ```example si1 From 521673a3eebff29e3ba425aee1f2509765e982f4 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:09:40 -0500 Subject: [PATCH 71/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 4c03223dd3..4cc7aa6905 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -103,7 +103,7 @@ Again, these results are consistent with those produced by `assess_identifiabili The `find_identifiable_functions` functions tries to simplify its output functions to create nice expressions. The degree to which it does this can be adjusted using the `simplify` keywords. Using the `:weak`, `:standard` (default), and `:strong` arguments, increased simplification can be forced (at the expense of longer runtimes). ## Creating StructuralIdentifiability compatible ODE models from Catalyst `ReactionSystem`s -While the functionality described above covers the vast majority of analysis that user might want to perform, the StructuralIdentifiability package supports several additional features. While these does not have inherent Catalyst support, we do provide the `make_si_ode` function to simplify their use. Similarly to the previous functions, it takes a `ReactionSystem` and lists of measured quantities and known parameter values. The output is a [ode of the standard form supported by StructuralIdentifiability](https://docs.sciml.ai/StructuralIdentifiability/stable/tutorials/creating_ode/#Defining-the-model-using-@ODEmodel-macro). It can be created using the following syntax: +While the functionality described above covers the vast majority of analysis that user might want to perform, the StructuralIdentifiability package supports several additional features. While these does not have inherent Catalyst support, we do provide the `make_si_ode` function to simplify their use. Similar to the previous functions, it takes a `ReactionSystem`, lists of measured quantities, and known parameter values. The output is a [ODE of the standard form supported by StructuralIdentifiability](https://docs.sciml.ai/StructuralIdentifiability/stable/tutorials/creating_ode/#Defining-the-model-using-@ODEmodel-macro). It can be created using the following syntax: ```example si1 si_ode = make_si_ode(goodwind_oscillator; measured_quantities=[:M]) nothing # hide From d3d0282e357a2e23e4135217780e65e229724781 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:10:39 -0500 Subject: [PATCH 72/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 4cc7aa6905..e434a16256 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -7,7 +7,7 @@ Structural identifiability (which is what this tutorial considers) can be illust ${dx \over dt} = p1*p2*x(t)$ where, however much data is collected on $x$, it is impossible to determine the distinct values of $p1$ and $p2$. Hence, these parameters are non-identifiable (however, their product, $p1*p2$, *is* identifiable). -Catalyst contains a special extension for carrying out structural identifiability analysis using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. It also implements specialised routines to make these more efficient when applied to reaction network models. How to use these functions is described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). +Catalyst contains a special extension for carrying out structural identifiability analysis of generated reaction rate equation ODE models using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. This bypasses the need for users to convert `ReactionSystem`s to `ODESystem`s, provides a cleaner interface for calculating identifiability of reaction rate equation models, and implements specialised routines to speed up the identifiability calculations for the generated ODE models. How to use these functions is described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). Structural identifiability can be divided into *local* and *global* identifiability. If a model quantity is locally identifiable, it means that its true value can be determined down to a finite-number of possible options. This also means that there is some limited region around the quantity's true value where this true value is the only possible value (and hence, within this region, the quantity is fully identifiable). Globally identifiable quantities' values, on the other hand, can be uniquely determined. Again, while identifiability can be confirmed structurally for a quantity, it does not necessarily mean that it is practically identifiable for some given data. From aa2fc77f2dc01e5c6f25fefd39201848baf512f8 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:17:08 -0500 Subject: [PATCH 73/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index e434a16256..9cba507578 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -63,7 +63,7 @@ enzyme_activation = @reaction_network begin (Eₐ, d), 0 <-->P end ``` -and we can measure the total amount of $E$ ($=Eᵢ+Eₐ$), as well as the amount of $P$, we can use the following to assess identifiability: +If we can measure the total amount of $E$ ($=Eᵢ+Eₐ$), as well as the amount of $P$, we can use the following to assess identifiability: ```example si2 @unpack Eᵢ, Eₐ = enzyme_activation assess_identifiability(enzyme_activation; measured_quantities=[Eᵢ+Eₐ, :P], loglevel=Logging.Error) From 8619447311dd23abe08e77968e6ec03ec1d9d4d7 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:18:41 -0500 Subject: [PATCH 74/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 9cba507578..a566f9d812 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -87,7 +87,7 @@ nothing # hide giving a minimum bound of $99.9\%$ chance of correctness. In practise, the bounds used by StructuralIdentifiability are very conservative, which means that while the minimum guaranteed probability of correctness in the default case is $99\%$, in practise it is much higher. While increasing the value of `p` increases the certainty of correctness, it will also increase the time required to assess identifiability. ## Local identifiability analysis -Local identifiability can be assessed through the `assess_local_identifiability` function. While this is already determined by `assess_identifiability`, assessing local identifiability only have the advantage that it is easier to compute. Hence, there might be models where global identifiability analysis fails (or takes prohibitively long time), where instead `assess_local_identifiability` can be used. This functions takes the same inputs as `assess_identifiability` and returns, for each quantity, `true` if it is locally identifiable (or `false` if it is not). Here, for the Goodwind oscillator, we assesses it for local identifiability only: +Local identifiability can be assessed through the `assess_local_identifiability` function. While this is already determined by `assess_identifiability`, assessing local identifiability only has the advantage that it is easier to compute. Hence, there might be models where global identifiability analysis fails (or takes a prohibitively long time), where instead `assess_local_identifiability` can be used. This function takes the same inputs as `assess_identifiability` and returns, for each quantity, `true` if it is locally identifiable (or `false` if it is not). Here, for the Goodwind oscillator, we assesses it for local identifiability only: ```example si1 assess_local_identifiability(goodwind_oscillator; measured_quantities=[:M], loglevel=Logging.Error) ``` From bef5302c462abab44d7d9eff3a433100c52dd4f8 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:25:08 -0500 Subject: [PATCH 75/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index a566f9d812..2219af023c 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -98,7 +98,7 @@ Finally, StructuralIdentifiability provides the `find_identifiable_functions` fu ```example si1 find_identifiable_functions(goodwind_oscillator; measured_quantities=[:M], loglevel=Logging.Error) ``` -Again, these results are consistent with those produced by `assess_identifiability`. There, `pₑ` and `pₚ` where found to be globally identifiable. Here, they correspond directly to identifiable expressions. The remaining four parameters (`pₘ`, `dₘ`, `dₑ`, and `dₚ`) occurs as part of more complicated composite expressions. +Again, these results are consistent with those produced by `assess_identifiability`. There, `pₑ` and `pₚ` where found to be globally identifiable. Here, they correspond directly to identifiable expressions. The remaining four parameters (`pₘ`, `dₘ`, `dₑ`, and `dₚ`) occur as part of more complicated composite expressions. The `find_identifiable_functions` functions tries to simplify its output functions to create nice expressions. The degree to which it does this can be adjusted using the `simplify` keywords. Using the `:weak`, `:standard` (default), and `:strong` arguments, increased simplification can be forced (at the expense of longer runtimes). From a792559cfa0adeb16006c20b2e60a1033ad75956 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 30 Dec 2023 06:27:54 -0500 Subject: [PATCH 76/80] Update docs/src/inverse_problems/structural_identifiability.md Co-authored-by: Sam Isaacson --- docs/src/inverse_problems/structural_identifiability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 2219af023c..a15f5ebf0a 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -100,7 +100,7 @@ find_identifiable_functions(goodwind_oscillator; measured_quantities=[:M], logle ``` Again, these results are consistent with those produced by `assess_identifiability`. There, `pₑ` and `pₚ` where found to be globally identifiable. Here, they correspond directly to identifiable expressions. The remaining four parameters (`pₘ`, `dₘ`, `dₑ`, and `dₚ`) occur as part of more complicated composite expressions. -The `find_identifiable_functions` functions tries to simplify its output functions to create nice expressions. The degree to which it does this can be adjusted using the `simplify` keywords. Using the `:weak`, `:standard` (default), and `:strong` arguments, increased simplification can be forced (at the expense of longer runtimes). +`find_identifiable_functions` tries to simplify its output functions to create nice expressions. The degree to which it does this can be adjusted using the `simplify` keywords. Using the `:weak`, `:standard` (default), and `:strong` arguments, increased simplification can be forced (at the expense of longer runtime). ## Creating StructuralIdentifiability compatible ODE models from Catalyst `ReactionSystem`s While the functionality described above covers the vast majority of analysis that user might want to perform, the StructuralIdentifiability package supports several additional features. While these does not have inherent Catalyst support, we do provide the `make_si_ode` function to simplify their use. Similar to the previous functions, it takes a `ReactionSystem`, lists of measured quantities, and known parameter values. The output is a [ODE of the standard form supported by StructuralIdentifiability](https://docs.sciml.ai/StructuralIdentifiability/stable/tutorials/creating_ode/#Defining-the-model-using-@ODEmodel-macro). It can be created using the following syntax: From 283b0cdabd70b46cad08679e2839b94f7944dfd7 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 30 Dec 2023 12:28:12 +0100 Subject: [PATCH 77/80] up --- docs/src/inverse_problems/structural_identifiability.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index a15f5ebf0a..a8f935fcc8 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -22,7 +22,7 @@ StructuralIdentifiability currently assesses identifiability for the first two o ### Basic example -Local identifiability can be assessed using the `assess_identifiability` function. For each model quantity (parameters and variables), it will assess whether they are: +Global identifiability can be assessed using the `assess_identifiability` function. For each model quantity (parameters and variables), it will assess whether they are: - Globally identifiable. - Locally identifiable. - Unidentifiable. @@ -94,7 +94,7 @@ assess_local_identifiability(goodwind_oscillator; measured_quantities=[:M], logl We note that the results are consistent with those produced by `assess_identifiability` (with globally or locally identifiable quantities here all being assessed as at least locally identifiable). ## Finding identifiable functions -Finally, StructuralIdentifiability provides the `find_identifiable_functions` function. Rather than determining the identifiability of each parameter and initial condition of the model, it finds a minimal set of identifiable functions, such as any other identifiable expression of the model can be generated by these. Let us again consider the Goodwind oscillator, using the `find_identifiable_functions` function we find that identifiability can be reduced to five globally identifiable expressions: +Finally, StructuralIdentifiability provides the `find_identifiable_functions` function. Rather than determining the identifiability of each parameter and state of the model, it finds a set of identifiable functions, such as any other identifiable expression of the model can be generated by these. Let us again consider the Goodwind oscillator, using the `find_identifiable_functions` function we find that identifiability can be reduced to five globally identifiable expressions: ```example si1 find_identifiable_functions(goodwind_oscillator; measured_quantities=[:M], loglevel=Logging.Error) ``` From 22a8ae4f7864571c9be8d28695feec13161aad65 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 30 Dec 2023 13:00:21 +0100 Subject: [PATCH 78/80] test update --- .../structural_identifiability.md | 9 +++++--- test/extensions/structural_identifiability.jl | 22 +++++++++---------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index a8f935fcc8..b3562de440 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -7,7 +7,7 @@ Structural identifiability (which is what this tutorial considers) can be illust ${dx \over dt} = p1*p2*x(t)$ where, however much data is collected on $x$, it is impossible to determine the distinct values of $p1$ and $p2$. Hence, these parameters are non-identifiable (however, their product, $p1*p2$, *is* identifiable). -Catalyst contains a special extension for carrying out structural identifiability analysis of generated reaction rate equation ODE models using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. This bypasses the need for users to convert `ReactionSystem`s to `ODESystem`s, provides a cleaner interface for calculating identifiability of reaction rate equation models, and implements specialised routines to speed up the identifiability calculations for the generated ODE models. How to use these functions is described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). +Catalyst contains a special extension for carrying out structural identifiability analysis of generated reaction rate equation ODE models using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. It also implements specialised routines to make these more efficient when applied to reaction network models (e.g. by improving runtimes). How to use these functions is described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/). Structural identifiability can be divided into *local* and *global* identifiability. If a model quantity is locally identifiable, it means that its true value can be determined down to a finite-number of possible options. This also means that there is some limited region around the quantity's true value where this true value is the only possible value (and hence, within this region, the quantity is fully identifiable). Globally identifiable quantities' values, on the other hand, can be uniquely determined. Again, while identifiability can be confirmed structurally for a quantity, it does not necessarily mean that it is practically identifiable for some given data. @@ -18,6 +18,9 @@ Generally, there are three types of quantities for which identifiability can be StructuralIdentifiability currently assesses identifiability for the first two only (however, if $x(t)$ is identifiable, then $x(0)$ will be as well). +!!! note + Currently, the StructuralIdentifiability.jl extension only considers structural identifiability for the ODE generated by the reaction rate equation. It is possible that for the SDE model (generated by the chemical Langevin equation) and the jump model (generated by stochastic chemical kinetics) the identifiability of model quantities is different. + ## Global identifiability analysis ### Basic example @@ -37,7 +40,7 @@ goodwind_oscillator = @reaction_network begin end assess_identifiability(goodwind_oscillator; measured_quantities=[:M], loglevel=Logging.Error) ``` -From the output, in the associated reaction rate equation ODE model we find that `E(t)`, `pₑ`, and `pₚ` (the trajectory of $E$, and the production rates of $E$ and $P$, respectively) are non-identifiable. Next, `dₑ` and `dₚ` (the degradation rates of $E$ and $P$, respectively) are locally identifiable. Finally, `P(t)`, `M(t)`, `pₘ`, and `dₘ` (the trajectories of `P` and `M`, and the production and degradation rate of `M`, respectively) are all globally identifiable. We note that we also imported the Logging.jl package, and provided the `loglevel=Logging.Error` input argument. StructuralIdentifiability functions generally provide a large number of output messages. Hence, we will use this argument (which requires the Logging package) throughout this tutorial to decrease the amount of printed text. +From the output, we find that `E(t)`, `pₑ`, and `pₚ` (the trajectory of $E$, and the production rates of $E$ and $P$, respectively) are non-identifiable. Next, `dₑ` and `dₚ` (the degradation rates of $E$ and $P$, respectively) are locally identifiable. Finally, `P(t)`, `M(t)`, `pₘ`, and `dₘ` (the trajectories of `P` and `M`, and the production and degradation rate of `M`, respectively) are all globally identifiable. We note that we also imported the Logging.jl package, and provided the `loglevel=Logging.Error` input argument. StructuralIdentifiability functions generally provide a large number of output messages. Hence, we will use this argument (which requires the Logging package) throughout this tutorial to decrease the amount of printed text. Next, we also assess identifiability in the case where we can measure all three species concentrations: ```example si1 @@ -52,7 +55,7 @@ assess_identifiability(gwo; measured_quantities=[:M], known_p=[:pₑ], loglevel= ``` Not only does this turn the previously non-identifiable `pₑ` (globally) identifiable (which is obvious, given that its value is now known), but this additional information improve identifiability for several other network components. -To, in a similar manner, indicate that certain initial conditions are known is a work in progress. Hopefully the feature should be an available in the near future. +To, in a similar manner, indicate that certain initial conditions are known is a work in progress. Hopefully this feature should be an available in the near future. ### Providing non-trivial measured quantities Sometimes, ones may not have measurements of species, but rather some combinations of species (or possibly parameters). To account for this, `measured_quantities` accepts any algebraic expression (and not just single species). To form such expressions, species and parameters have to first be `@unpack`'ed from the model. Say that we have a model where an enzyme ($E$) is converted between an active and inactive form, which in turns activates the production of a product, $P$: diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index f5fb31449a..b42f1ef027 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -162,10 +162,10 @@ let @test length(ifs_1[2:end]) == length(ifs_2) == length(ifs_3) # In the first case, the conservation law parameter is also identifiable. # Checks output to manually checked correct answers. - @test isequal(collect(keys(gi_1)),[states(rs_catalyst); parameters(rs_catalyst)]) - @test isequal(collect(values(gi_1)),[:globally, :locally, :locally, :nonidentifiable, :nonidentifiable, :globally, :globally, :globally, :globally, :locally, :locally, :nonidentifiable, :locally, :globally]) - @test isequal(collect(keys(li_1)),[states(rs_catalyst); parameters(rs_catalyst)]) - @test isequal(collect(values(li_1)),[1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1]) + correct_gi = Pair.([states(rs_catalyst); parameters(rs_catalyst)], [:globally, :locally, :locally, :nonidentifiable, :nonidentifiable, :globally, :globally, :globally, :globally, :locally, :locally, :nonidentifiable, :locally, :globally]) + correct_li = Pair.([states(rs_catalyst); parameters(rs_catalyst)], [1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1]) + @test issetequal(gi_1, correct_gi) + @test issetequal(li_1, correct_li) end # Tests that various inputs types work. @@ -194,13 +194,13 @@ let # Tests using model.component style (have to make system complete first). gw_osc_complt = complete(goodwind_oscillator_catalyst) - make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M]) - make_si_ode(gw_osc_complt; known_p=[gw_osc_complt.pₑ]) - make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M], known_p=[gw_osc_complt.pₑ]) - make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M, gw_osc_complt.E], known_p=[gw_osc_complt.pₑ]) - make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M], known_p=[gw_osc_complt.pₑ, gw_osc_complt.pₚ]) - make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M], known_p = [:pₚ]) - make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M*gw_osc_complt.E]) + @test make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M]) isa ODE + @test make_si_ode(gw_osc_complt; known_p=[gw_osc_complt.pₑ]) isa ODE + @test make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M], known_p=[gw_osc_complt.pₑ]) isa ODE + @test make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M, gw_osc_complt.E], known_p=[gw_osc_complt.pₑ]) isa ODE + @test make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M], known_p=[gw_osc_complt.pₑ, gw_osc_complt.pₚ]) isa ODE + @test make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M], known_p = [:pₚ]) isa ODE + @test make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M*gw_osc_complt.E]) isa ODE end # Tests for hierarchical model with conservation laws at both top and internal levels. From 165fb8c3776ab4d0c7642111fe1393013e70a148 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 30 Dec 2023 14:29:13 +0100 Subject: [PATCH 79/80] use prob_thres --- .../inverse_problems/structural_identifiability.md | 6 +++--- .../structural_identifiability_extension.jl | 12 ++++++------ test/extensions/structural_identifiability.jl | 10 ++++++++++ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index b3562de440..23aeb13e82 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -82,12 +82,12 @@ nothing # hide ``` ### Probability of correctness -The identifiability methods used can, in theory, produce erroneous results. However, it is possible to adjust the lower bound for the probability of correctness using the argument `p` (by default set to `0.99`, that is, at least a $99\%$ chance of correctness). We can e.g. increase the bound through: +The identifiability methods used can, in theory, produce erroneous results. However, it is possible to adjust the lower bound for the probability of correctness using the argument `prob_threshold` (by default set to `0.99`, that is, at least a $99\%$ chance of correctness). We can e.g. increase the bound through: ```example si2 -assess_identifiability(goodwind_oscillator; measured_quantities=[:M], p=0.999, loglevel=Logging.Error) +assess_identifiability(goodwind_oscillator; measured_quantities=[:M], prob_threshold=0.999, loglevel=Logging.Error) nothing # hide ``` -giving a minimum bound of $99.9\%$ chance of correctness. In practise, the bounds used by StructuralIdentifiability are very conservative, which means that while the minimum guaranteed probability of correctness in the default case is $99\%$, in practise it is much higher. While increasing the value of `p` increases the certainty of correctness, it will also increase the time required to assess identifiability. +giving a minimum bound of $99.9\%$ chance of correctness. In practise, the bounds used by StructuralIdentifiability are very conservative, which means that while the minimum guaranteed probability of correctness in the default case is $99\%$, in practise it is much higher. While increasing the value of `prob_threshold` increases the certainty of correctness, it will also increase the time required to assess identifiability. ## Local identifiability analysis Local identifiability can be assessed through the `assess_local_identifiability` function. While this is already determined by `assess_identifiability`, assessing local identifiability only has the advantage that it is easier to compute. Hence, there might be models where global identifiability analysis fails (or takes a prohibitively long time), where instead `assess_local_identifiability` can be used. This function takes the same inputs as `assess_identifiability` and returns, for each quantity, `true` if it is locally identifiable (or `false` if it is not). Here, for the Goodwind oscillator, we assesses it for local identifiability only: diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl index 3349ae0692..7d99ecc59a 100644 --- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl +++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl @@ -64,14 +64,14 @@ Notes: """ function SI.assess_local_identifiability(rs::ReactionSystem, args...; measured_quantities = [], known_p = [], funcs_to_check = Vector(), remove_conserved = true, - ignore_no_measured_warn=false, kwargs...) + ignore_no_measured_warn=false, prob_threshold = 0.99, kwargs...) # Creates a ODESystem, list of measured quantities, and functions to check, of SI's preferred form. osys, conseqs, vars = make_osys(rs; remove_conserved) measured_quantities = make_measured_quantities(rs, measured_quantities, known_p, conseqs; ignore_no_measured_warn) funcs_to_check = make_ftc(funcs_to_check, conseqs, vars) # Computes identifiability and converts it to a easy to read form. - out = SI.assess_local_identifiability(osys, args...; measured_quantities, funcs_to_check, kwargs...) + out = SI.assess_local_identifiability(osys, args...; measured_quantities, funcs_to_check, p=prob_threshold, kwargs...) return make_output(out, funcs_to_check, reverse.(conseqs)) end @@ -102,14 +102,14 @@ Notes: """ function SI.assess_identifiability(rs::ReactionSystem, args...; measured_quantities = [], known_p = [], funcs_to_check = Vector(), remove_conserved = true, ignore_no_measured_warn=false, - kwargs...) + prob_threshold = 0.99, kwargs...) # Creates a ODESystem, list of measured quantities, and functions to check, of SI's preferred form. osys, conseqs, vars = make_osys(rs; remove_conserved) measured_quantities = make_measured_quantities(rs, measured_quantities, known_p, conseqs; ignore_no_measured_warn) funcs_to_check = make_ftc(funcs_to_check, conseqs, vars) # Computes identifiability and converts it to a easy to read form. - out = SI.assess_identifiability(osys, args...; measured_quantities, funcs_to_check, kwargs...) + out = SI.assess_identifiability(osys, args...; measured_quantities, funcs_to_check, p=prob_threshold, kwargs...) return make_output(out, funcs_to_check, reverse.(conseqs)) end @@ -140,13 +140,13 @@ Notes: """ function SI.find_identifiable_functions(rs::ReactionSystem, args...; measured_quantities = [], known_p = [], remove_conserved = true, ignore_no_measured_warn=false, - kwargs...) + prob_threshold = 0.99, kwargs...) # Creates a ODESystem, and list of measured quantities, of SI's preferred form. osys, conseqs = make_osys(rs; remove_conserved) measured_quantities = make_measured_quantities(rs, measured_quantities, known_p, conseqs; ignore_no_measured_warn) # Computes identifiable functions and converts it to a easy to read form. - out = SI.find_identifiable_functions(osys, args...; measured_quantities, kwargs...) + out = SI.find_identifiable_functions(osys, args...; measured_quantities, p=prob_threshold, kwargs...) return vector_subs(out, reverse.(conseqs)) end diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index b42f1ef027..524a54956d 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -203,6 +203,16 @@ let @test make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M*gw_osc_complt.E]) isa ODE end +# Check that `prob_threshold` alternative kwarg works. +let + rs = @reaction_network begin + p, X --> 0 + end + + assess_identifiability(rs_catalyst; measured_quantities=[rs.X], prob_thres=0.9) + assess_identifiability(rs_catalyst; measured_quantities=[rs.X], prob_thres=0.999) +end + # Tests for hierarchical model with conservation laws at both top and internal levels. let # Identifiability analysis for Catalyst model. From 90ab89e40bcec40891c99d6aa641754140467d4d Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 30 Dec 2023 14:31:23 +0100 Subject: [PATCH 80/80] test up --- test/extensions/structural_identifiability.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index 524a54956d..683203e134 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -208,9 +208,10 @@ let rs = @reaction_network begin p, X --> 0 end + @unpack X = rs - assess_identifiability(rs_catalyst; measured_quantities=[rs.X], prob_thres=0.9) - assess_identifiability(rs_catalyst; measured_quantities=[rs.X], prob_thres=0.999) + assess_identifiability(rs; measured_quantities=[X], prob_threshold=0.9) + assess_identifiability(rs; measured_quantities=[X], prob_threshold=0.999) end # Tests for hierarchical model with conservation laws at both top and internal levels.