Skip to content

Commit 7203e4a

Browse files
authored
Finish docs! (#2)
* typos * correct incorrect differential in docs * linebreaks * fix docs problems * add convert_to_parameters macro * fix missing exports * correct names in index * finish readme
1 parent e5a0bc7 commit 7203e4a

File tree

8 files changed

+86
-51
lines changed

8 files changed

+86
-51
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
# ProcessBasedModelling.jl
22

3-
[![docsdev](https://img.shields.io/badge/docs-dev-lightblue.svg)](https://juliadynamics.github.io/ProcessBasedModelling,jl/dev/)
3+
[![docsdev](https://img.shields.io/badge/docs-dev-lightblue.svg)](https://juliadynamics.github.io/ProcessBasedModelling.jl/dev/)
44
[![docsstable](https://img.shields.io/badge/docs-stable-blue.svg)](https://juliadynamics.github.io/ProcessBasedModelling.jl/stable/)
55
[![CI](https://github.com/JuliaDynamics/ProcessBasedModelling.jl/workflows/CI/badge.svg)](https://github.com/JuliaDynamics/ProcessBasedModelling.jl/actions?query=workflow%3ACI)
66
[![codecov](https://codecov.io/gh/JuliaDynamics/ProcessBasedModelling.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliaDynamics/ProcessBasedModelling.jl)
77
[![Package Downloads](https://shields.io/endpoint?url=https://pkgs.genieframework.com/api/v1/badge/ProcessBasedModelling)](https://pkgs.genieframework.com?packages=ProcessBasedModelling)
88

99
ProcessBasedModelling.jl is an extension to [ModelingToolkit.jl](https://docs.sciml.ai/ModelingToolkit/stable/) (MTK) for building a model of equations using symbolic expressions.
1010
It is an alternative framework to MTK's [native component-based modelling](https://docs.sciml.ai/ModelingToolkit/stable/tutorials/acausal_components/), but, instead of components, there are "processes".
11-
This approach is useful in the modelling of physical/biological/whatever systems, where each variable corresponds to particular physical concept or observable and there are few (or any) duplicate variables to make the definition of MTK "factories" worthwhile, while, there plenty of different physical representations, or _processes_ to represent the given physical concept.
12-
In many fields this approach parallels modelling reasoning line of the researcher more closely than the "components" approach.
11+
This approach is useful in the modelling of physical/biological/whatever systems, where each variable corresponds to a particular physical concept or observable and there are few (or none) duplicate variables to make the definition of MTK "factories" worthwhile.
12+
On the other hand, there plenty of different physical representations, or _processes_ to represent a given physical concept.
13+
In many scientific fields this approach parallels the modelling reasoning of the researcher more closely than the "components" approach.
1314

1415
Beyond this reasoning style, the biggest strength of ProcessBasedModelling.jl is the **informative errors and automation** it provides regarding incorrect/incomplete equations. When building the MTK model via ProcessBasedModelling.jl the user provides a vector of "processes": equations or custom types that have a well defined and single left-hand-side variable.
1516
This allows ProcessBasedModelling.jl to:

docs/make.jl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,5 @@ pages = [
1515

1616
build_docs_with_style(pages, ProcessBasedModelling;
1717
authors = "George Datseris <[email protected]>",
18-
# We need to remove the cross references because we don't list here
19-
# the whole `DynamicalSystem` API...
2018
warnonly = true,
2119
)

docs/src/index.md

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ To make the equations we want, we can use MTK directly, and call
4141
```@example MAIN
4242
eqs = [
4343
Differential(t)(z) ~ x^2 - z
44-
Differential(x) ~ 0.1y
44+
Differential(t)(x) ~ 0.1y
4545
y ~ z - x
4646
]
4747
@@ -50,30 +50,32 @@ model = ODESystem(eqs, t; name = :example)
5050
equations(model)
5151
```
5252

53-
All good. Now, if we missed the process for one variable (because of our own error/sloppyness/very-large-codebase), MTK will not throw an error at model construction,
53+
All good. Now, if we missed the process for one variable (because of our own error/sloppyness/very-large-codebase), MTK will throw an error when we try to _structurally simplify_ the model (a step necessary before solving the ODE problem):
5454

5555
```@example MAIN
5656
model = ODESystem(eqs[1:2], t; name = :example)
57-
model = structural_simplify(model)
58-
equations(model)
59-
```
60-
61-
only at the construction of the "problem" (here the `ODEProblem`)
62-
63-
```@example MAIN
6457
try
65-
prob = ODEProblem(model)
58+
model = structural_simplify(model)
6659
catch e
6760
return e.msg
6861
end
6962
```
7063

71-
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.
64+
As you can see, the error message is unhelpful even with such a trivial system of equations,
65+
as all variables are reported as "potentially missing".
66+
At least on the basis of our scientific reasoning however, both ``x, z`` have an equation.
67+
It is ``y`` that ``x`` introduced that does not have an equation.
68+
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.
69+
70+
**PBM** resolves these problems and always gives accurate error messages when it comes to
71+
the construction of the system of equations.
72+
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).
7273

73-
**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).
74-
Here is what the user defines to make the same system of equations:
74+
Here is what the user defines to make the same system of equations via **PBM**:
7575

7676
```@example MAIN
77+
using ProcessBasedModelling
78+
7779
processes = [
7880
ExpRelaxation(z, x^2), # introduces x variable
7981
TimeDerivative(x, 0.1*y), # introduces y variable
@@ -82,14 +84,17 @@ processes = [
8284
```
8385

8486
which is then given to
87+
8588
```@example MAIN
8689
model = processes_to_mtkmodel(processes; name = :example)
8790
equations(model)
8891
```
8992

9093
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.
9194

92-
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:
95+
Now, in contrast to before, if we "forgot" a process, **PBM** will react accordingly.
96+
For example, if we forgot the 2nd process, then the construction will error informatively,
97+
telling us exactly which variable is missing, and because of which processes it is missing:
9398
```@example MAIN
9499
try
95100
model = processes_to_mtkmodel(processes[[1, 3]])
@@ -109,7 +114,7 @@ parameters(model)
109114
```
110115

111116
Lastly, [`processes_to_mtkmodel`](@ref) also allows the concept of "default" processes, that can be used for introduced "process-less" variables.
112-
Default processes like `processes` given as a 2nd argument to [`process_to_mtkmodel`](@ref).
117+
Default processes are like `processes` and given as a 2nd argument to [`process_to_mtkmodel`](@ref).
113118
For example,
114119

115120
```@example MAIN
@@ -119,14 +124,14 @@ equations(model)
119124

120125
does not throw any warnings as it obtained a process for ``y`` from the given default processes.
121126

122-
### Special handling of timescales
127+
## Special handling of timescales
123128

124129
In dynamical systems modelling the timescale associated with a process is a special parameter. That is why, if a timescale is given for either the [`TimeDerivative`](@ref) or [`ExpRelaxation`](@ref) processes, it is converted to a named `@parameter` by default:
125130

126131
```@example MAIN
127132
processes = [
128-
ExpRelaxation(z, x^2, 2.0), # third argument is the timescale
129-
TimeDerivative(x, 0.1*y, 0.5),
133+
ExpRelaxation(z, x^2, 2.0), # third argument is the timescale
134+
TimeDerivative(x, 0.1*y, 0.5), # third argument is the timescale
130135
y ~ z-x,
131136
]
132137
@@ -163,6 +168,7 @@ This API describes how you can implement your own `Process` subtype, if the [exi
163168
Process
164169
ProcessBasedModelling.lhs_variable
165170
ProcessBasedModelling.rhs
171+
ProcessBasedModelling.timescale
166172
ProcessBasedModelling.NoTimeDerivative
167173
ProcessBasedModelling.lhs
168174
```
@@ -173,4 +179,5 @@ ProcessBasedModelling.lhs
173179
default_value
174180
has_variable
175181
new_derived_named_parameter
182+
@convert_to_parameters
176183
```

src/API.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ A process subtype `p::Process` extends the following unexported functions:
55
(left-hand-side variable). There is a default implementation
66
`lhs_variable(p) = p.variable` if the field exists.
77
- `rhs(p)` which is the right-hand-side expression, i.e., the "actual" process.
8-
- (optional) `timescale`, which defaults to [`NoTimeDerivative`](@ref).
8+
- (optional) `timescale(p)`, which defaults to [`NoTimeDerivative`](@ref).
99
- (optional) `lhs(p)` which returns the left-hand-side. Let `τ = timescale(p)`.
1010
Then default `lhs(p)` behaviour depends on `τ` as follows:
1111
- Just `lhs_variable(p)` if `τ == NoTimeDerivative()`.
@@ -20,7 +20,7 @@ abstract type Process end
2020
"""
2121
ProcessBasedModelling.NoTimeDerivative()
2222
23-
Singleton value that is the default output of the [`timescale`](@ref) function
23+
Singleton value that is the default output of the `timescale` function
2424
for variables that do not vary in time autonomously, i.e., they have no d/dt derivative
2525
and hence the concept of a "timescale" does not apply to them.
2626
"""
@@ -50,7 +50,7 @@ timescale(::Process) = NoTimeDerivative()
5050
ProcessBasedModelling.lhs(p::Process)
5151
5252
Return the left-hand-side of the equation that `p` represents as an `Expression`.
53-
If [`timescale`](@ref) is implemented for `p`, typically `lhs` does not need to be as well.
53+
If `timescale` is implemented for `p`, typically `lhs` does not need to be as well.
5454
See [`Process`](@ref) for more.
5555
"""
5656
function lhs(p::Process)

src/ProcessBasedModelling.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export t
2525
export Process, ParameterProcess, TimeDerivative, ExpRelaxation
2626
export processes_to_mtkmodel
2727
export new_derived_named_parameter
28-
export hs_variable, default_value
28+
export has_variable, default_value
29+
export @convert_to_parameters
2930

3031
end

src/processes_basic.jl

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
The simplest process which equates a given `variable` to a constant value
55
that is encapsulated in a parameter. If `value isa Real`, then
6-
hence, a named parameter with the name of `variable` and `_0` appended is created.
6+
a named parameter with the name of `variable` and `_0` appended is created.
77
Else, if `valua isa Num` then it is taken as the paremeter directly.
88
99
Example:
@@ -58,15 +58,16 @@ given `expression`, with timescale `τ`. It creates the equation:
5858
τn*Differential(t)(variable) ~ expression - variable
5959
```
6060
Where `τn` is a new named `@parameter` with the value of `τ`
61-
and name `τ_(\$(variable))`. If instead `τ` is `nothing`, then 1 is used in its place.
61+
and name `τ_(\$(variable))`. If instead `τ` is `nothing`, then 1 is used in its place
62+
(this is the default behavior).
6263
If `iszero(τ)`, then the equation `variable ~ expression` is created instead.
6364
6465
The convenience function
6566
```julia
6667
ExpRelaxation(process, τ)
6768
```
6869
allows converting an existing process (or equation) into an exponential relaxation
69-
by using the `rhs` as the `expression` in the equation above.
70+
by using the `rhs(process)` as the `expression` in the equation above.
7071
"""
7172
struct ExpRelaxation <: Process
7273
variable
@@ -77,4 +78,7 @@ ExpRelaxation(v, e) = ExpRelaxation(v, e, nothing)
7778
ExpRelaxation(proc::Union{Process,Equation}, τ) = ExpRelaxation(lhs_variable(proc), rhs(proc), τ)
7879

7980
timescale(e::ExpRelaxation) = e.timescale
80-
rhs(e::ExpRelaxation) = iszero(e.timescale) ? e.expression : e.expression - e.variable
81+
function rhs(e::ExpRelaxation)
82+
dt = isnothing(e.timescale) || iszero(e.timescale)
83+
dt ? e.expression : e.expression - e.variable
84+
end

src/utils.jl

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -68,28 +68,38 @@ function new_derived_named_parameter(newstring::String, value::Real)
6868
return first(dummy)
6969
end
7070

71+
# Macro thanks to Jonas Isensee,
72+
# https://discourse.julialang.org/t/metaprogramming-macro-calling-another-macro-making-named-variables/109621/6
7173
"""
72-
@named_parameters vars...
74+
@convert_to_parameters vars...
7375
74-
Convert all Julia variables `vars` into `@parameters` with name the same as `vars`
75-
and default value the same as the value of `vars`.
76+
Convert all variables `vars` into `@parameters` with name the same as `vars`
77+
and default value the same as the value of `vars`. Example:
78+
79+
```
80+
julia> A, B = 0.5, 0.5
81+
(0.5, 0.5)
82+
83+
julia> @convert_to_parameters A B
84+
2-element Vector{Num}:
85+
A
86+
B
87+
88+
julia> typeof(A) # `A` is not a number anymore!
89+
Num
90+
91+
julia> default_value(A)
92+
0.5
7693
"""
77-
macro named_parameters(vars...)
78-
return quote
79-
out = []
80-
for v in vars
81-
res = (ModelingToolkit.toparam)((Symbolics.wrap)((Symbolics.SymbolicUtils.setmetadata)((Symbolics.setdefaultval)((Sym){Real}($(QuoteNode(v))), $(esc(v))), Symbolics.VariableSource, (:parameters, $(QuoteNode(v))))))
82-
push!(out, res)
83-
end
84-
$(out...)
94+
macro convert_to_parameters(vars...)
95+
expr = Expr(:block)
96+
for var in vars
97+
binding = esc(var)
98+
varname = QuoteNode(var)
99+
push!(expr.args,
100+
:($binding = (ModelingToolkit.toparam)((Symbolics.wrap)((SymbolicUtils.setmetadata)((Symbolics.setdefaultval)((Sym){Real}($varname), $binding), Symbolics.VariableSource, (:parameters, $varname)))))
101+
)
85102
end
103+
push!(expr.args, Expr(:vect, esc.(vars)...))
104+
return expr
86105
end
87-
88-
# This may help:
89-
90-
# julia> @macroexpand @parameters A=0.1 B=0.1
91-
# quote
92-
# A = (ModelingToolkit.toparam)((Symbolics.wrap)((SymbolicUtils.setmetadata)((Symbolics.setdefaultval)((Sym){Real}(:A), 0.1), Symbolics.VariableSource, (:parameters, :A))))
93-
# B = (ModelingToolkit.toparam)((Symbolics.wrap)((SymbolicUtils.setmetadata)((Symbolics.setdefaultval)((Sym){Real}(:B), 0.1), Symbolics.VariableSource, (:parameters, :B))))
94-
# [A, B]
95-
# end

test/runtests.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,17 @@ end
112112
@testset "extending default processes" begin
113113
# API not yet finished on this one
114114
end
115+
116+
@testset "utility functions" begin
117+
@variables x(t) = 0.5
118+
p = new_derived_named_parameter(x, 0.2, "t")
119+
@test ModelingToolkit.getname(p) == :x_t
120+
@test default_value(p) == 0.2
121+
p = new_derived_named_parameter(x, p, "lala")
122+
@test ModelingToolkit.getname(p) == :x_t
123+
124+
A, B = 0.5, 0.5
125+
@convert_to_parameters A B
126+
@test A isa Num
127+
@test default_value(A) == 0.5
128+
end

0 commit comments

Comments
 (0)