From 8c1ed5939a3b8e437ef967dbdcbae03e1b88d385 Mon Sep 17 00:00:00 2001 From: Sasha Date: Wed, 19 Aug 2026 18:34:34 +0200 Subject: [PATCH 1/4] add observation field --- src/StructuralIdentifiability.jl | 3 +- src/observation_field.jl | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/observation_field.jl diff --git a/src/StructuralIdentifiability.jl b/src/StructuralIdentifiability.jl index 1a3d4d74b..fcdb9df8b 100644 --- a/src/StructuralIdentifiability.jl +++ b/src/StructuralIdentifiability.jl @@ -93,7 +93,7 @@ export assess_local_identifiability, assess_identifiability export set_parameter_values, rename_variables # extra functionality -export find_ioequations, find_identifiable_functions +export find_ioequations, find_identifiable_functions, observation_field # exporting to other formats export print_for_maple, print_for_DAISY, print_for_COMBOS, print_for_GenSSI @@ -126,6 +126,7 @@ include("states.jl") include("output_saturation.jl") include("global_identifiability.jl") include("identifiable_functions.jl") +include("observation_field.jl") include("parametrizations.jl") include("lincomp.jl") include("pb_representation.jl") diff --git a/src/observation_field.jl b/src/observation_field.jl new file mode 100644 index 000000000..5749737a2 --- /dev/null +++ b/src/observation_field.jl @@ -0,0 +1,48 @@ +""" + observation_field(ode::ODE; options...) + +Find simple generators of the observation field of the given ODE system. + +## Options + +This functions takes the following optional arguments: +- `known_ic`: a list of functions whose initial conditions are assumed to be known, + then the returned identifiable functions will be functions of parameters and + initial conditions, not states (this is an experimental functionality). +- `prob_threshold`: A float in the range from 0 to 1, the probability of correctness. Default + is `0.99`. +- `seed`: The rng seed. Default value is `42`. +- `loglevel` - the minimal level of log messages to display (`Logging.Info` by default) + +## Example + +```jldoctest +using StructuralIdentifiability + +ode = @ODEmodel( + x0'(t) = -(a01 + a21) * x0(t) + a12 * x1(t), + x1'(t) = a21 * x0(t) - a12 * x1(t), + y(t) = x0(t) +); + +observation_field(ode) + +# Output + +4-element Vector{AbstractAlgebra.Generic.FracFieldElem{Nemo.QQMPolyRingElem}}: + x0(t) + a01 + a12 + a21 + a01*a12 + x0(t)*a12 + x1(t)*a12 +``` + +""" +function observation_field( + ode::ODE{T}; + known_ic::Vector{<:ExtendedFraction{T}} = Vector{ExtendedFraction{T}}(), + prob_threshold::Float64 = 0.99, + seed = 42, + loglevel = Logging.Info, + ) where {T <: MPolyRingElem{QQFieldElem}} + return find_identifiable_functions(ode, with_states = true, known_ic = known_ic, prob_threshold = prob_threshold, seed = seed, loglevel = loglevel) +end From 24c8b3a6ebd8b6074063bca8410937a3e3192c4c Mon Sep 17 00:00:00 2001 From: Sasha Date: Wed, 19 Aug 2026 19:39:34 +0200 Subject: [PATCH 2/4] Add observation_field --- docs/src/assets/Project.toml | 4 ++-- docs/src/identifiability/identifiability.md | 4 ++++ docs/src/index.md | 4 ++-- docs/src/tutorials/identifiable_functions.md | 13 ++++++++++++- src/identifiable_functions.jl | 8 +------- src/observation_field.jl | 19 ++++++++----------- test/bodies/observation_field.jl | 9 +++++++++ 7 files changed, 38 insertions(+), 23 deletions(-) create mode 100644 test/bodies/observation_field.jl diff --git a/docs/src/assets/Project.toml b/docs/src/assets/Project.toml index 30e21dee0..359116e04 100644 --- a/docs/src/assets/Project.toml +++ b/docs/src/assets/Project.toml @@ -1,11 +1,11 @@ [deps] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" StructuralIdentifiability = "220ca800-aa68-49bb-acd8-6037fa93a544" +ModelingToolkitBase = "7771a370-6774-4173-bd38-47e70ca0b839" [compat] BenchmarkTools = "1.3" Documenter = "0.27, 1" -ModelingToolkit = "9" +ModelingToolkitBase = "1.1" StructuralIdentifiability = "0.5" diff --git a/docs/src/identifiability/identifiability.md b/docs/src/identifiability/identifiability.md index cb172fd15..1c8752c73 100644 --- a/docs/src/identifiability/identifiability.md +++ b/docs/src/identifiability/identifiability.md @@ -17,3 +17,7 @@ assess_local_identifiability ```@docs find_identifiable_functions ``` + +```@docs +observation_field +``` diff --git a/docs/src/index.md b/docs/src/index.md index baddd7a3b..bbc9e2c61 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -28,13 +28,13 @@ Pkg.add("StructuralIdentifiability") ## Feature Summary -`StructuralIdentifiability.jl` can assess local and global identifiability of ODE models. In addition to these straightforward identifiability queries on individual parameters, the package can distinguish between single- and multi-experiment identifiability. +`StructuralIdentifiability.jl` can assess local and global identifiability of ODE models. In addition to these straightforward identifiability queries on individual parameters, the package can distinguish between single- and multi-experiment identifiability, and can compute identifiable functions of parameters and states. ## Feature List - Local identifiability checks - Global identifiability checks - - Assessment of identifiable functions of parameters and states + - Identifiable functions of parameters and states (a.k.a. observation field) - Model reparametrization (experimental) ## External tutorials diff --git a/docs/src/tutorials/identifiable_functions.md b/docs/src/tutorials/identifiable_functions.md index c161af2a5..a19245015 100644 --- a/docs/src/tutorials/identifiable_functions.md +++ b/docs/src/tutorials/identifiable_functions.md @@ -1,4 +1,4 @@ -# Globally Identifiable Functions +# Globally Identifiable Functions (a.k.a. Observation Field) In addition to assessing identifiability of given functions of parameters or states, `StructuralIdentifiability.jl` provides the function `find_identifiable_functions` which finds a set of identifiable functions such that any other @@ -30,6 +30,17 @@ of numbers `{p1, p3}` is uniquely determined since `p1 + p3` and `p1 * p3` are k Furthermore, we see that, for fixed input and output, `p2` and `p4` can take infinitely many values but knowing one of them, we would also be able to determine the other. +!!! note "Observation field" + The field of identifiable functions involving both parameters and states is also called the **observation field**. It can be computed using + `observation_field`: + + ```julia + observation_field(LLW1987) + ``` + + This is equivalent to calling + `find_identifiable_functions(LLW1987, with_states = true)`. + Moreover, we can find generators of all identifiable functions in parameters and states: ```@example funcs diff --git a/src/identifiable_functions.jl b/src/identifiable_functions.jl index 2dde62697..89d594f51 100644 --- a/src/identifiable_functions.jl +++ b/src/identifiable_functions.jl @@ -27,21 +27,15 @@ This functions takes the following optional arguments: ## Example -```jldoctest +```jldoctest; setup = :(using Logging; Logging.disable_logging(Logging.Info);) julia> using StructuralIdentifiability -julia> using Logging - -julia> Logging.disable_logging(Logging.Info); - julia> ode = @ODEmodel( x0'(t) = -(a01 + a21) * x0(t) + a12 * x1(t), x1'(t) = a21 * x0(t) - a12 * x1(t), y(t) = x0(t) ); -julia> Logging.disable_logging(Logging.BelowMinLevel); - julia> identifiable_functions = find_identifiable_functions(ode; loglevel = Logging.Error); julia> length(identifiable_functions) diff --git a/src/observation_field.jl b/src/observation_field.jl index 5749737a2..e46fb4bd6 100644 --- a/src/observation_field.jl +++ b/src/observation_field.jl @@ -16,19 +16,16 @@ This functions takes the following optional arguments: ## Example -```jldoctest -using StructuralIdentifiability +```jldoctest; setup = :(using Logging; Logging.disable_logging(Logging.Info);) +julia> using StructuralIdentifiability -ode = @ODEmodel( - x0'(t) = -(a01 + a21) * x0(t) + a12 * x1(t), - x1'(t) = a21 * x0(t) - a12 * x1(t), - y(t) = x0(t) -); - -observation_field(ode) - -# Output +julia> ode = @ODEmodel( + x0'(t) = -(a01 + a21) * x0(t) + a12 * x1(t), + x1'(t) = a21 * x0(t) - a12 * x1(t), + y(t) = x0(t) + ); +julia> observation_field(ode; loglevel=Logging.Warn) 4-element Vector{AbstractAlgebra.Generic.FracFieldElem{Nemo.QQMPolyRingElem}}: x0(t) a01 + a12 + a21 diff --git a/test/bodies/observation_field.jl b/test/bodies/observation_field.jl new file mode 100644 index 000000000..7b1b03470 --- /dev/null +++ b/test/bodies/observation_field.jl @@ -0,0 +1,9 @@ +include(joinpath(@__DIR__, "..", "shared", "test_setup.jl")) + +ode = @ODEmodel( + x1'(t) = -(k21 + V_M / (K_M + x1(t))) * x1(t) + k12 * x2(t) + b1 * u(t), + x2'(t) = k21 * x1(t) - (k02 + k12) * x2(t), + y(t) = c * x1(t) +) + +@test observation_field(ode) == find_identifiable_functions(ode, with_states = true) From 3c3404b581fcba7916f01092bc901ab098aaa152 Mon Sep 17 00:00:00 2001 From: Sasha Date: Fri, 21 Aug 2026 20:13:12 +0200 Subject: [PATCH 3/4] update readme --- README.md | 4 ++-- src/observation_field.jl | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index abecb955b..8ad7d97b0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ## About -`StructuralIdentifiability.jl` is a Julia package for assessing structural parameter identifiability of parametric ODE models, both local and global. +`StructuralIdentifiability.jl` is a Julia package for assessing structural identifiability and observability of parametric ODE models, both local and global. This includes computation of identifiable functions of states and parameters. The package also offers functionality to assess local identifiability in discrete-time models. For an introduction to structural identifiability, we refer to [[3]](#review). @@ -122,7 +122,7 @@ will return ``` which are exactly the identifiable functions we have found before. Furthermore, by specifying `with_states = true`, one can compute the generating set for -all identifiable functions of parameters and states (in other words, all observable functions): +all identifiable functions of parameters and states (in other words, all observable functions, also known as the observation field): ```julia find_identifiable_functions(ode, with_states = true) diff --git a/src/observation_field.jl b/src/observation_field.jl index e46fb4bd6..bfe6629a2 100644 --- a/src/observation_field.jl +++ b/src/observation_field.jl @@ -12,6 +12,9 @@ This functions takes the following optional arguments: - `prob_threshold`: A float in the range from 0 to 1, the probability of correctness. Default is `0.99`. - `seed`: The rng seed. Default value is `42`. +- `cmp`: A comparator for rational functions. For two rational functions `f1` + and `f2`, `cmp(f1, f2)` should be `true` if `f1` is simpler than `f2`. + By default, `StructuralIdentifiability.default_cmp(ode)` is used. - `loglevel` - the minimal level of log messages to display (`Logging.Info` by default) ## Example @@ -27,9 +30,9 @@ julia> ode = @ODEmodel( julia> observation_field(ode; loglevel=Logging.Warn) 4-element Vector{AbstractAlgebra.Generic.FracFieldElem{Nemo.QQMPolyRingElem}}: - x0(t) a01 + a12 + a21 a01*a12 + x0(t) x0(t)*a12 + x1(t)*a12 ``` @@ -39,7 +42,8 @@ function observation_field( known_ic::Vector{<:ExtendedFraction{T}} = Vector{ExtendedFraction{T}}(), prob_threshold::Float64 = 0.99, seed = 42, + cmp = default_cmp(ode), loglevel = Logging.Info, ) where {T <: MPolyRingElem{QQFieldElem}} - return find_identifiable_functions(ode, with_states = true, known_ic = known_ic, prob_threshold = prob_threshold, seed = seed, loglevel = loglevel) + return find_identifiable_functions(ode, with_states = true, known_ic = known_ic, prob_threshold = prob_threshold, seed = seed, loglevel = loglevel, cmp = cmp) end From 3968235aa821fc4aaa0b48c2f742d8cbc4c63da4 Mon Sep 17 00:00:00 2001 From: Sasha Date: Fri, 21 Aug 2026 20:29:04 +0200 Subject: [PATCH 4/4] minor fix and typo --- docs/src/tutorials/identifiable_functions.md | 2 +- src/observation_field.jl | 2 +- test/bodies/observation_field.jl | 14 ++++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/src/tutorials/identifiable_functions.md b/docs/src/tutorials/identifiable_functions.md index 44498e3a6..0598675bc 100644 --- a/docs/src/tutorials/identifiable_functions.md +++ b/docs/src/tutorials/identifiable_functions.md @@ -32,7 +32,7 @@ knowing one of them, we would also be able to determine the other. !!! note "Observation field" The field of identifiable functions involving both parameters and states is also called the **observation field**. It can be computed using - `observation_field`: + [`observation_field`](@ref): ```julia observation_field(LLW1987) diff --git a/src/observation_field.jl b/src/observation_field.jl index bfe6629a2..a4cda7923 100644 --- a/src/observation_field.jl +++ b/src/observation_field.jl @@ -5,7 +5,7 @@ Find simple generators of the observation field of the given ODE system. ## Options -This functions takes the following optional arguments: +This function takes the following optional arguments: - `known_ic`: a list of functions whose initial conditions are assumed to be known, then the returned identifiable functions will be functions of parameters and initial conditions, not states (this is an experimental functionality). diff --git a/test/bodies/observation_field.jl b/test/bodies/observation_field.jl index 7b1b03470..0e86a127f 100644 --- a/test/bodies/observation_field.jl +++ b/test/bodies/observation_field.jl @@ -1,9 +1,11 @@ include(joinpath(@__DIR__, "..", "shared", "test_setup.jl")) -ode = @ODEmodel( - x1'(t) = -(k21 + V_M / (K_M + x1(t))) * x1(t) + k12 * x2(t) + b1 * u(t), - x2'(t) = k21 * x1(t) - (k02 + k12) * x2(t), - y(t) = c * x1(t) -) +@testset "Identifiable functions of parameters" begin + ode = @ODEmodel( + x1'(t) = -(k21 + V_M / (K_M + x1(t))) * x1(t) + k12 * x2(t) + b1 * u(t), + x2'(t) = k21 * x1(t) - (k02 + k12) * x2(t), + y(t) = c * x1(t) + ) -@test observation_field(ode) == find_identifiable_functions(ode, with_states = true) + @test observation_field(ode) == find_identifiable_functions(ode, with_states = true) +end