From f644bc2ac6987a8ce2933353bc878da3f0410b47 Mon Sep 17 00:00:00 2001 From: Datseris Date: Sun, 11 Feb 2024 18:28:54 +0000 Subject: [PATCH 01/10] fix missing tanh test --- test/runtests.jl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index a2d025b..3662263 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -25,6 +25,25 @@ using OrdinaryDiffEq return absorbed_shortwave - emitted_longwave end + # make a new type of process + struct TanhProcess <: Process + variable + driver_variable + left + right + scale + reference + end + function ProcessBasedModelling.rhs(p::TanhProcess) + x = p.driver_variable + (; left, right, scale, reference) = p + return tanh_expression(x, left, right, scale, reference) + end + function tanh_expression(T, left, right, scale, reference) + return left + (right - left)*(1 + tanh(2(T - reference)/(scale)))*0.5 + end + + processes = [ TanhProcess(α, T, 0.7, 0.289, 10.0, 274.5), TanhProcess(ε, T, 0.5, 0.41, 2.0, 288.0), From c830cb47ced6d664e746c361b9258a2e1c2c274c Mon Sep 17 00:00:00 2001 From: Datseris Date: Sun, 11 Feb 2024 18:31:26 +0000 Subject: [PATCH 02/10] finish docs of Process --- src/API.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/API.jl b/src/API.jl index 0d2a491..668cec6 100644 --- a/src/API.jl +++ b/src/API.jl @@ -1,6 +1,8 @@ """ A process subtype `p::Process` extends the functions: -- `lhs_variable(p)` which returns the variable the process describes (left-hand-side variable) +- `lhs_variable(p)` which returns the variable the process describes + (left-hand-side variable). There is a default implementation + `lhs_variable(p) = p.variable` if the field exists. - `rhs(p)` which is the right-hand-side expression, i.e., the "actual" process. - (optional) `timescale`, which defaults to [`NoTimeVariability`](@ref). - (optional) `lhs(p)` which returns the left-hand-side. Let `τ = timescale(p)`. @@ -18,6 +20,8 @@ abstract type Process end Singleton value that is the default output of the [`timescale`](@ref) function for variables that do not vary in time autonomously (i.e., no d/dt derivative). +Note that explicit time dependence of the form `x(t) = cos(t)` is still +`NoTimeVariability()`. """ struct NoTimeVariability end From a025d91f5743db324001be643d4e4dc4bb45fd7f Mon Sep 17 00:00:00 2001 From: Datseris Date: Sun, 11 Feb 2024 18:32:22 +0000 Subject: [PATCH 03/10] rename notimevar to no timeder --- docs/src/index.md | 2 +- src/API.jl | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index 3a88b29..ed96e4e 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -165,7 +165,7 @@ This API describes how you can implement your own `Process` subtype, if the [exi Process rhs timescale -NoTimeVariability +NoTimeDerivative ``` ## Utility functions diff --git a/src/API.jl b/src/API.jl index 668cec6..23c9335 100644 --- a/src/API.jl +++ b/src/API.jl @@ -4,10 +4,10 @@ A process subtype `p::Process` extends the functions: (left-hand-side variable). There is a default implementation `lhs_variable(p) = p.variable` if the field exists. - `rhs(p)` which is the right-hand-side expression, i.e., the "actual" process. -- (optional) `timescale`, which defaults to [`NoTimeVariability`](@ref). +- (optional) `timescale`, which defaults to [`NoTimeDerivative`](@ref). - (optional) `lhs(p)` which returns the left-hand-side. Let `τ = timescale(p)`. Then `lhs(p)` behavior depends on `τ` as follows: - - Just `lhs_variable(p)` if `τ == NoTimeVariability()`. + - Just `lhs_variable(p)` if `τ == NoTimeDerivative()`. - `Differential(t)(p)` if `τ == nothing`. - `τ_var*Differential(t)(p)` if `τ isa Union{Real, Num}`. If real, a new named parameter `τ_var` is created that has the prefix `:τ_` and then the @@ -16,14 +16,13 @@ A process subtype `p::Process` extends the functions: abstract type Process end """ - NoTimeVariability() + NoTimeDerivative() Singleton value that is the default output of the [`timescale`](@ref) function -for variables that do not vary in time autonomously (i.e., no d/dt derivative). -Note that explicit time dependence of the form `x(t) = cos(t)` is still -`NoTimeVariability()`. +for variables that do not vary in time autonomously, i.e., they have no d/dt derivative +and hence the concept of a "timescale" does not apply to them. """ -struct NoTimeVariability end +struct NoTimeDerivative end function lhs_variable(p::Process) if !hasfield(typeof(p), :variable) @@ -33,14 +32,14 @@ function lhs_variable(p::Process) end end -timescale(::Process) = NoTimeVariability() +timescale(::Process) = NoTimeDerivative() function lhs(p::Process) τ = timescale(p) v = lhs_variable(p) if isnothing(τ) # time variability exists but timescale is nonexistent (unity) return Differential(t)(v) - elseif τ isa NoTimeVariability || iszero(τ) # no time variability + elseif τ isa NoTimeDerivative || iszero(τ) # no time variability return v else # τ is either Num or Real τvar = new_derived_named_parameter(v, τ, "τ", false) From 2efe6556c129dce01c61234de4927254624b88c3 Mon Sep 17 00:00:00 2001 From: Datseris Date: Sun, 11 Feb 2024 18:36:26 +0000 Subject: [PATCH 04/10] discuss default lhs --- src/API.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/API.jl b/src/API.jl index 23c9335..b2f0c7e 100644 --- a/src/API.jl +++ b/src/API.jl @@ -6,12 +6,13 @@ A process subtype `p::Process` extends the functions: - `rhs(p)` which is the right-hand-side expression, i.e., the "actual" process. - (optional) `timescale`, which defaults to [`NoTimeDerivative`](@ref). - (optional) `lhs(p)` which returns the left-hand-side. Let `τ = timescale(p)`. - Then `lhs(p)` behavior depends on `τ` as follows: + Then default `lhs(p)` behavior depends on `τ` as follows: - Just `lhs_variable(p)` if `τ == NoTimeDerivative()`. - `Differential(t)(p)` if `τ == nothing`. - `τ_var*Differential(t)(p)` if `τ isa Union{Real, Num}`. If real, a new named parameter `τ_var` is created that has the prefix `:τ_` and then the lhs-variable name and has default value `τ`. Else if `Num`, `τ_var = τ` as given. + - Explicitly extend `lhs_variable` if the above do not suit you. """ abstract type Process end From 05ec0c3d193dfbbf66f525d5321805e2d3f3c1c8 Mon Sep 17 00:00:00 2001 From: Datseris Date: Sun, 11 Feb 2024 22:13:56 +0000 Subject: [PATCH 05/10] correct doc reference page --- docs/make.jl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index a9cdcfe..83c8630 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -10,11 +10,7 @@ Downloads.download( include("build_docs_with_style.jl") pages = [ - "Introduction" => "index.md", - "Overarching tutorial" => "tutorial.md", - "Contents" => "contents.md", - "Animations, GUIs, Visuals" => "visualizations.md", - "Contributor Guide" => "contributors_guide.md", + "Documentation" => "index.md", ] build_docs_with_style(pages, ProcessBasedModelling; From 4daf8a2c80f10f64590740bb45d9f78c9cd6d47d Mon Sep 17 00:00:00 2001 From: Datseris Date: Sun, 11 Feb 2024 23:06:33 +0000 Subject: [PATCH 06/10] add correct stuff in docstrings --- docs/src/index.md | 2 -- src/API.jl | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index ed96e4e..ffe85ba 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -163,8 +163,6 @@ This API describes how you can implement your own `Process` subtype, if the [exi ```@docs Process -rhs -timescale NoTimeDerivative ``` diff --git a/src/API.jl b/src/API.jl index b2f0c7e..6070ae6 100644 --- a/src/API.jl +++ b/src/API.jl @@ -1,5 +1,5 @@ """ -A process subtype `p::Process` extends the functions: +A process subtype `p::Process` extends the `ProcessBasedModelling` unexported functions: - `lhs_variable(p)` which returns the variable the process describes (left-hand-side variable). There is a default implementation `lhs_variable(p) = p.variable` if the field exists. @@ -19,7 +19,7 @@ abstract type Process end """ NoTimeDerivative() -Singleton value that is the default output of the [`timescale`](@ref) function +Singleton value that is the default output of the `timescale` function for variables that do not vary in time autonomously, i.e., they have no d/dt derivative and hence the concept of a "timescale" does not apply to them. """ From e8099e95baf2a7bcbfca9a03270f97eb7806190a Mon Sep 17 00:00:00 2001 From: Datseris Date: Mon, 12 Feb 2024 10:08:33 +0000 Subject: [PATCH 07/10] finish docs --- docs/src/index.md | 26 ++++++++++++++------------ src/API.jl | 32 +++++++++++++++++++++++++++++--- src/ProcessBasedModelling.jl | 1 - src/make.jl | 13 +++++++++++-- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index ffe85ba..aad1dc9 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -30,6 +30,7 @@ symbolically using ModelingToolkit.jl (**MTK**). We define using ModelingToolkit using OrdinaryDiffEq: Tsit5 +@variables t # independent variable @variables z(t) = 0.0 @variables x(t) # no default value @variables y(t) = 0.0 @@ -38,7 +39,6 @@ ProcessBasedModelling.jl (**PBM**) strongly recommends that all defined variable To make the equations we want, we can use MTK directly, and call ```@example MAIN -@variables t eqs = [ Differential(t)(z) ~ x^2 - z @@ -71,28 +71,26 @@ end Interestingly, the error is wrong. ``x`` is defined and has an equation, at least on the basis of our scientific reasoning. However ``y`` that ``x`` introduced does not have an equation. Moreover, in our experience these errors 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. -**PBM** resolves these problems and always gives accurate error messages. This is because on top of the variable map that MTK constructs automatically, **PBM** requires the user to implicitly create 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). +**PBM** resolves these problems and always gives accurate error messages. 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: ```@example MAIN processes = [ - ExpRelaxation(z, x^2), # introduces x variable + ExpRelaxation(z, x^2), # introduces x variable TimeDerivative(x, 0.1*y), # introduces y variable - y ~ z-x, # can be an equation because LHS is single variable + y ~ z - x, # can be an equation because LHS is single variable ] ``` -Internally, all of these - -which is the given to +which is then given to ```@example MAIN -model = processes_to_mtkmodel(processes) +model = processes_to_mtkmodel(processes; name = :example) equations(model) ``` -Notice that the resulting **MTK** model is not `structural_simplify`-ed, to allow composing it with other models. +Notice that the resulting **MTK** model is not `structural_simplify`-ed, to allow composing it with other models. By default `t` is taken as the independent variable. -Now, in contrast to before, if we "forgot" a process, **PBM** will react accordingly. For example, we forgot the 2nd process, then the construction will error informatively, telling us exactly which variable is missing, and because of which processes it is missing: +Now, in contrast to before, if we "forgot" a process, **PBM** will react accordingly. For example, if we forgot the 2nd process, then the construction will error informatively, telling us exactly which variable is missing, and because of which processes it is missing: ```@example MAIN try model = processes_to_mtkmodel(processes[[1, 3]]) @@ -101,7 +99,7 @@ catch e end ``` -If instead we "forgot" the ``y`` process, **PBM** will not error, but instead warn, and make ``y`` a named parameter: +If instead we "forgot" the ``y`` process, **PBM** will not error, but instead warn, and make ``y`` equal to a named parameter: ```@example MAIN model = processes_to_mtkmodel(processes[1:2]) equations(model) @@ -112,6 +110,7 @@ parameters(model) ``` Lastly, [`processes_to_mtkmodel`](@ref) also allows the concept of "default" processes, that can be used for introduced "process-less" variables. +Default processes like `processes` given as a 2nd argument to [`process_to_mtkmodel`](@ref). For example, ```@example MAIN @@ -163,7 +162,10 @@ This API describes how you can implement your own `Process` subtype, if the [exi ```@docs Process -NoTimeDerivative +ProcessBasedModelling.lhs_variable +ProcessBasedModelling.rhs +ProcessBasedModelling.NoTimeDerivative +ProcessBasedModelling.lhs ``` ## Utility functions diff --git a/src/API.jl b/src/API.jl index 6070ae6..b74800a 100644 --- a/src/API.jl +++ b/src/API.jl @@ -1,5 +1,6 @@ """ -A process subtype `p::Process` extends the `ProcessBasedModelling` unexported functions: +A process subtype `p::Process` extends the following unexported functions: + - `lhs_variable(p)` which returns the variable the process describes (left-hand-side variable). There is a default implementation `lhs_variable(p) = p.variable` if the field exists. @@ -19,12 +20,17 @@ abstract type Process end """ NoTimeDerivative() -Singleton value that is the default output of the `timescale` function +Singleton value that is the default output of the [`timescale`](@ref) function for variables that do not vary in time autonomously, i.e., they have no d/dt derivative and hence the concept of a "timescale" does not apply to them. """ struct NoTimeDerivative end +""" + ProcessBasedModelling.lhs_variable(p::Process) + +Return the variable (a single symbolic variable) corresponding to `p`. +""" function lhs_variable(p::Process) if !hasfield(typeof(p), :variable) error("`lhs_variable` not defined for process $(nameof(typeof(p))).") @@ -33,8 +39,20 @@ function lhs_variable(p::Process) end end +""" + ProcessBasedModelling.timescale(p::Process) + +Return the timescale associated with `p`. See [`Process`](@ref) for more. +""" timescale(::Process) = NoTimeDerivative() +""" + ProcessBasedModelling.lhs(p::Process) + +Return the left-hand-side of the equation that `p` represents as an `Expression`. +If [`timescale`](@ref) is implemented for `p`, typically `lhs` does not need to be as well. +See [`Process`](@ref) for more. +""" function lhs(p::Process) τ = timescale(p) v = lhs_variable(p) @@ -47,10 +65,18 @@ function lhs(p::Process) return τvar*Differential(t)(v) end end + +""" + ProcessBasedModelling.rhs(p::Process) + +Return the right-hand-side of the equation that `p` represents as an `Expression`. +See [`Process`](@ref) for more. +""" function rhs(p::Process) error("Right-hand side (`rhs`) is not defined for process $(nameof(typeof(p))).") end +# Extensions for `Equation`: rhs(e::Equation) = e.rhs lhs(e::Equation) = lhs_variable(e) function lhs_variable(e::Equation) @@ -58,7 +84,7 @@ function lhs_variable(e::Equation) # we first check whether `x` is a variable if !is_variable(x) throw(ArgumentError("In given equation $(e), the left-hand-side does "* - "not represent a variable.")) + "not represent a single variable.")) end return x end diff --git a/src/ProcessBasedModelling.jl b/src/ProcessBasedModelling.jl index a5dc1fc..b93fe39 100644 --- a/src/ProcessBasedModelling.jl +++ b/src/ProcessBasedModelling.jl @@ -30,7 +30,6 @@ include("processes_basic.jl") # TODO: Perhaps not don't export `t, rhs`? export t -export lhs_variable, rhs, timescale, NoTimeVariability export Process, ParameterProcess, TimeDerivative, ExpRelaxation export processes_to_mtkmodel export new_derived_named_parameter diff --git a/src/make.jl b/src/make.jl index 1f186fa..6e16905 100644 --- a/src/make.jl +++ b/src/make.jl @@ -18,8 +18,17 @@ but they don't themselves have an assigned process. It is expected that downstream packages that use ProcessBasedModelling.jl to make a field-specific library implement a 1-argument version of `processes_to_mtkmodel`, or provide a wrapper function for it, and add a default value for `default`. + +## Keyword arguments + +- `type = ODESystem`: the model type to make +- `name = nameof(type)`: the name of the model +- `independent = t`: the independent variable (default: `@variables t`). + `t` is also exported by ProcessBasedModelling.jl for convenience. """ -function processes_to_mtkmodel(_processes, _default = []; type = ODESystem, name = nameof(type)) +function processes_to_mtkmodel(_processes, _default = []; + type = ODESystem, name = nameof(type), independent = t + ) processes = expand_multi_processes(_processes) default = default_dict(_default) # Setup: obtain lhs-variables so we can track new variables that are not @@ -72,7 +81,7 @@ function processes_to_mtkmodel(_processes, _default = []; type = ODESystem, name end end end - sys = type(eqs, t; name) + sys = type(eqs, independent; name) return sys end # version without given processes From 21fa02eaa30403e3b5f469b34d0314119e0eb6c0 Mon Sep 17 00:00:00 2001 From: Datseris Date: Mon, 12 Feb 2024 10:50:54 +0000 Subject: [PATCH 08/10] GOOD EXPORTS --- docs/src/index.md | 1 + src/API.jl | 4 ++-- src/ProcessBasedModelling.jl | 11 ++--------- src/utils.jl | 9 ++++++--- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index aad1dc9..4269be2 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -171,6 +171,7 @@ ProcessBasedModelling.lhs ## Utility functions ```@docs +default_value has_variable new_derived_named_parameter ``` diff --git a/src/API.jl b/src/API.jl index b74800a..dc7bd89 100644 --- a/src/API.jl +++ b/src/API.jl @@ -7,7 +7,7 @@ A process subtype `p::Process` extends the following unexported functions: - `rhs(p)` which is the right-hand-side expression, i.e., the "actual" process. - (optional) `timescale`, which defaults to [`NoTimeDerivative`](@ref). - (optional) `lhs(p)` which returns the left-hand-side. Let `τ = timescale(p)`. - Then default `lhs(p)` behavior depends on `τ` as follows: + Then default `lhs(p)` behaviour depends on `τ` as follows: - Just `lhs_variable(p)` if `τ == NoTimeDerivative()`. - `Differential(t)(p)` if `τ == nothing`. - `τ_var*Differential(t)(p)` if `τ isa Union{Real, Num}`. If real, @@ -18,7 +18,7 @@ A process subtype `p::Process` extends the following unexported functions: abstract type Process end """ - NoTimeDerivative() + ProcessBasedModelling.NoTimeDerivative() Singleton value that is the default output of the [`timescale`](@ref) function for variables that do not vary in time autonomously, i.e., they have no d/dt derivative diff --git a/src/ProcessBasedModelling.jl b/src/ProcessBasedModelling.jl index b93fe39..8e18ab8 100644 --- a/src/ProcessBasedModelling.jl +++ b/src/ProcessBasedModelling.jl @@ -17,21 +17,14 @@ include("utils.jl") include("make.jl") include("processes_basic.jl") - -# TODO: In MAKE, make it so that if a variable does not have a process -# a constant process is created for it if it has a default value. -# Add a keyword `use_default` which would warn if no process but there is default -# and otherwise would error. # TODO: Make an "addition process" that adds to processes # It checks whether they target the same variable -# TODO: Package should compose with ODESystem -# so that component-based modelling can be utilized as well. - -# TODO: Perhaps not don't export `t, rhs`? +# TODO: Perhaps not don't export `t`? export t export Process, ParameterProcess, TimeDerivative, ExpRelaxation export processes_to_mtkmodel export new_derived_named_parameter +export hs_variable, default_value end diff --git a/src/utils.jl b/src/utils.jl index 92612bb..169f39a 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,6 +1,3 @@ -export print_system_info, has_variable, default_value -export new_derived_named_parameter, @named_parameters - """ has_variable(eq, var) @@ -13,6 +10,12 @@ function has_variable(eq::Equation, var) end has_variable(eqs, var) = any(eq -> has_variable(eq, var), eqs) +""" + 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. +""" default_value(x) = x default_value(x::Num) = default_value(x.val) function default_value(x::ModelingToolkit.SymbolicUtils.Symbolic) From e261d6ae298b0a3ef95fa7c582c1f340d17ba2f5 Mon Sep 17 00:00:00 2001 From: Datseris Date: Tue, 13 Feb 2024 22:14:31 +0000 Subject: [PATCH 09/10] note similarity with ODESystem construction --- docs/src/index.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index 4269be2..0357392 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -12,7 +12,7 @@ In ProcessBasedModelling.jl, each variable is governed by a "process". Conceptually this is just an equation that _defines_ the given variable. To couple the variable with the process it is governed by, a user either defines simple equations of the form "variable = expression", or creates an instance of [`Process`](@ref) if the left-hand-side of the equation needs to be anything more complex. In either case, the variable and the expression are both _symbolic expressions_ created via ModellingToolkit.jl (more specifically, via Symbolics.jl). -Once all the processes about the physical system are collected, they are given as a `Vector` to the [`processes_to_mtkmodel`](@ref) central function. This function also defines what quantifies as a "process" in more specificity. +Once all the processes about the physical system are collected, they are given as a `Vector` to the [`processes_to_mtkmodel`](@ref) central function, similarly to how one gives a `Vector` of `Equation`s to e.g., `ModelingToolkit.ODESystem`. This function also defines what quantifies as a "process" in more specificity. ## Example @@ -39,7 +39,6 @@ ProcessBasedModelling.jl (**PBM**) strongly recommends that all defined variable To make the equations we want, we can use MTK directly, and call ```@example MAIN - eqs = [ Differential(t)(z) ~ x^2 - z Differential(x) ~ 0.1y From e67ed1db8d480c495329e9732ca9ce79fc6c441e Mon Sep 17 00:00:00 2001 From: Datseris Date: Tue, 20 Feb 2024 12:47:39 +0000 Subject: [PATCH 10/10] ignore all errors to build docs --- docs/make.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index 83c8630..1d50a6f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -17,5 +17,5 @@ build_docs_with_style(pages, ProcessBasedModelling; authors = "George Datseris ", # We need to remove the cross references because we don't list here # the whole `DynamicalSystem` API... - warnonly = [:doctest, :missing_docs, :cross_references], + warnonly = true, )