diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e965a3d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +ProcessBasedModelling.jl follows semver 2.0. +Changelog is kept with respect to v1 release. \ No newline at end of file diff --git a/docs/Project.toml b/docs/Project.toml index ed92fe2..077d0f9 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -5,4 +5,3 @@ DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244" DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8" DynamicalSystems = "61744808-ddfa-5f27-97ff-6e42cc95d634" ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" -OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" diff --git a/docs/src/index.md b/docs/src/index.md index 3e5c6e9..adcbba5 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -33,7 +33,6 @@ symbolically using ModelingToolkit.jl (**MTK**). We define ```@example MAIN using ModelingToolkit -using OrdinaryDiffEq: Tsit5 @variables t # independent variable _without_ units @variables z(t) = 0.0 @@ -78,9 +77,10 @@ The error message is unhelpful as all variables are reported as "potentially mis At least on the basis of our scientific reasoning however, both ``x, z`` have an equation. It is ``y`` that ``x`` introduced that does not have an equation. Moreover, in our experience these error messages become increasingly less useful when a model has many equations and/or variables, as many variables get cited as "missing" from the variable map even when only one should be. +This makes it difficult to quickly find out where the "mistake" happened in the equations. -**PBM** resolves these problems and always gives accurate error messages when it comes to -the construction of the system of equations. +**PBM** resolves these problems and always gives accurate error messages when +it comes to the construction of the system of equations. This is because on top of the variable map that MTK constructs automatically, **PBM** requires the user to implicitly provide a map of variables to processes that govern said variables. **PBM** creates the map automatically, the only thing the user has to do is to define the equations in terms of what [`processes_to_mtkmodel`](@ref) wants (which are either [`Process`](@ref)es or `Equation`s as above). Here is what the user defines to make the same system of equations via **PBM**: @@ -199,12 +199,17 @@ ProcessBasedModelling.NoTimeDerivative ProcessBasedModelling.lhs ``` -## Utility functions +## Automatic named parameters ```@docs -default_value -has_variable new_derived_named_parameter @convert_to_parameters LiteralParameter ``` + +## Utility functions + +```@docs +default_value +has_symbolic_var +``` diff --git a/src/ProcessBasedModelling.jl b/src/ProcessBasedModelling.jl index 0c233d5..be92da7 100644 --- a/src/ProcessBasedModelling.jl +++ b/src/ProcessBasedModelling.jl @@ -25,8 +25,9 @@ export t export Process, ParameterProcess, TimeDerivative, ExpRelaxation, AdditionProcess export processes_to_mtkmodel export new_derived_named_parameter -export has_variable, default_value +export has_symbolic_var, default_value export @convert_to_parameters, LiteralParameter -export lhs_variable, rhs, lhs +# export lhs_variable, rhs, lhs # I am not sure whether these should be exported. +export all_equations end diff --git a/src/make.jl b/src/make.jl index c568494..136545a 100644 --- a/src/make.jl +++ b/src/make.jl @@ -33,8 +33,8 @@ function processes_to_mtkmodel(_processes, _default = []; processes = expand_multi_processes(_processes) default = default_dict(_default) # Setup: obtain lhs-variables so we can track new variables that are not - # in this vector - lhs_vars = map(lhs_variable, processes) + # in this vector. The vector has to be of type `Num` + lhs_vars = Num[lhs_variable(p) for p in processes] ensure_unique_vars(lhs_vars) # First pass: generate equations from given processes # and collect variables without equations diff --git a/src/processes_basic.jl b/src/processes_basic.jl index fb4b195..80541e4 100644 --- a/src/processes_basic.jl +++ b/src/processes_basic.jl @@ -17,7 +17,7 @@ struct ParameterProcess <: Process variable value function ParameterProcess(variable, value) - var = new_derived_named_parameter(variable, value, "0") + var = new_derived_named_parameter(variable, value, "0"; prefix = false) return new(variable, var) end end diff --git a/src/utils.jl b/src/utils.jl index 43f2e92..6bffc53 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -14,22 +14,48 @@ _literalvalue(x) = x _literalvalue(p::LiteralParameter) = p.p """ - has_variable(eq, var) + has_symbolic_var(eqs, var) -Return `true` if variable `var` exists in the equation(s) `eq`, `false` otherwise. -Function works irrespectively if `var` is an `@variable` or `@parameter`. +Return `true` if symbolic variable `var` exists in the equation(s) `eq`, `false` otherwise. +This works for either `@parameters` or `@variables`. +If `var` is a `Symbol` isntead of a `Num`, all variables are converted to their names +and equality is checked on the basis of the name only. + + has_symbolic_var(model, var) + +When given a MTK model (such as `ODESystem`) search in _all_ the equations of the system, +including observed variables. """ -function has_variable(eq::Equation, var) +function has_symbolic_var(eq::Equation, var) vars = get_variables(eq) + return _has_thing(var, vars) +end +has_symbolic_var(eqs::Vector{Equation}, var) = any(eq -> has_symbolic_var(eq, var), eqs) +has_symbolic_var(mtk, var) = has_symbolic_var(all_equations(mtk), var) + +function _has_thing(var::Num, vars) return any(isequal(var), vars) end -has_variable(eqs, var) = any(eq -> has_variable(eq, var), eqs) +function _has_thing(var::Symbol, vars) + vars = ModelingToolkit.getname.(vars) + var = ModelingToolkit.getname(var) + return any(isequal(var), vars) +end + +""" + all_equations(model) + +Equivalent with `vcat(equations(model), observed(model))`. +""" +all_equations(model) = vcat(equations(model), observed(model)) """ default_value(x) Return the default value of a symbolic variable `x` or `nothing` if it doesn't have any. Return `x` if `x` is not a symbolic variable. +The difference with `ModelingToolkit.getdefault` is that this function will +not error on the absence of a default value. """ default_value(x) = x default_value(x::Num) = default_value(x.val) @@ -64,11 +90,14 @@ end If `value isa Num` return `value`. If `value isa LiteralParameter`, replace it with its literal value. -Otherwise, create a new MTK `@parameter` -whose name is created from `variable` (which could also be just a `Symbol`) by adding the `extra` string. -If the keyword `prefix == false` the `extra` is added at the end after a `_`. Otherwise -it is added at the start, then a `_` and then the variable name. -The keyword `connector = "_"` is what connects the `extra` with the name. +Otherwise, create a new MTK `@parameter` whose name is created from `variable` +(which could also be just a `Symbol`) by adding the `extra` string. + +**Keywords**: + +- `prefix = true`: whether the `extra` is added at the start or the end, connecting + with the with the `connector`. +- `connector = "_"`: what to use to connect `extra` with the name. For example, @@ -83,10 +112,10 @@ new_derived_named_parameter(v, value::Num, extra::String; kw...) = value new_derived_named_parameter(v, value::LiteralParameter, extra::String; kw...) = value.p function new_derived_named_parameter(v, value::Real, extra; connector = "_", prefix = true) n = string(ModelingToolkit.getname(v)) - newstring = if !(prefix) - n*connector*extra - else + newstring = if prefix extra*connector*n + else + n*connector*extra end new_derived_named_parameter(newstring, value) end diff --git a/test/runtests.jl b/test/runtests.jl index adcc5e8..8d9dd41 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -55,7 +55,7 @@ using OrdinaryDiffEq sys = structural_simplify(sys) @test length(unknowns(sys)) == 1 - @test has_variable(equations(sys), T) + @test has_symbolic_var(equations(sys), T) u0s = [[300.0], [100.0]] ufs = [] @@ -81,7 +81,7 @@ using OrdinaryDiffEq sys = structural_simplify(sys) @test length(unknowns(sys)) == 1 - @test has_variable(equations(sys), T) + @test has_symbolic_var(equations(sys), T) end @@ -160,12 +160,9 @@ end @test p == 0.5 end - - @testset "new named var" - end -@testset "default processes" begin +@testset "default processes, has_thing" begin @variables x(t) = 0.5 @variables y(t) = 0.5 @variables z(t) = 0.5 @@ -174,15 +171,19 @@ end processes = [ TimeDerivative(x, x^2, 1.2), ParameterProcess(y), - ExpRelaxation(z, x^2), + ExpRelaxation(z, x^2, 0.5), AdditionProcess(ParameterProcess(w), x^2), AdditionProcess(TimeDerivative(q, x^2, 1.2), ExpRelaxation(q, x^2)) ] mtk = processes_to_mtkmodel(processes) - eqs = equations(mtk) - @test has_variable(eqs, x) - @test has_variable(eqs, y) - @test has_variable(eqs, z) - @test has_variable(eqs, w) - @test has_variable(eqs, q) + mtk = structural_simplify(mtk) + eqs = all_equations(mtk) + @test has_symbolic_var(eqs, x) + @test has_symbolic_var(eqs, y) + @test has_symbolic_var(eqs, :z) + @test has_symbolic_var(eqs, mtk.w) + @test has_symbolic_var(eqs, q) + @test has_symbolic_var(mtk, q) + @test has_symbolic_var(eqs, mtk.τ_z) + @test has_symbolic_var(eqs, :w_0) end \ No newline at end of file