Skip to content

Commit e5a0bc7

Browse files
authored
Finish everything for tagging 0.1 (#1)
* fix missing tanh test * finish docs of Process * rename notimevar to no timeder * discuss default lhs * correct doc reference page * add correct stuff in docstrings * finish docs * GOOD EXPORTS * note similarity with ODESystem construction * ignore all errors to build docs
1 parent 2b8a844 commit e5a0bc7

File tree

7 files changed

+97
-48
lines changed

7 files changed

+97
-48
lines changed

docs/make.jl

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,12 @@ Downloads.download(
1010
include("build_docs_with_style.jl")
1111

1212
pages = [
13-
"Introduction" => "index.md",
14-
"Overarching tutorial" => "tutorial.md",
15-
"Contents" => "contents.md",
16-
"Animations, GUIs, Visuals" => "visualizations.md",
17-
"Contributor Guide" => "contributors_guide.md",
13+
"Documentation" => "index.md",
1814
]
1915

2016
build_docs_with_style(pages, ProcessBasedModelling;
2117
authors = "George Datseris <[email protected]>",
2218
# We need to remove the cross references because we don't list here
2319
# the whole `DynamicalSystem` API...
24-
warnonly = [:doctest, :missing_docs, :cross_references],
20+
warnonly = true,
2521
)

docs/src/index.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ In ProcessBasedModelling.jl, each variable is governed by a "process".
1212
Conceptually this is just an equation that _defines_ the given variable.
1313
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).
1414

15-
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.
15+
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.
1616

1717
## Example
1818

@@ -30,6 +30,7 @@ symbolically using ModelingToolkit.jl (**MTK**). We define
3030
using ModelingToolkit
3131
using OrdinaryDiffEq: Tsit5
3232
33+
@variables t # independent variable
3334
@variables z(t) = 0.0
3435
@variables x(t) # no default value
3536
@variables y(t) = 0.0
@@ -38,8 +39,6 @@ ProcessBasedModelling.jl (**PBM**) strongly recommends that all defined variable
3839

3940
To make the equations we want, we can use MTK directly, and call
4041
```@example MAIN
41-
@variables t
42-
4342
eqs = [
4443
Differential(t)(z) ~ x^2 - z
4544
Differential(x) ~ 0.1y
@@ -71,28 +70,26 @@ end
7170

7271
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.
7372

74-
**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).
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).
7574
Here is what the user defines to make the same system of equations:
7675

7776
```@example MAIN
7877
processes = [
79-
ExpRelaxation(z, x^2), # introduces x variable
78+
ExpRelaxation(z, x^2), # introduces x variable
8079
TimeDerivative(x, 0.1*y), # introduces y variable
81-
y ~ z-x, # can be an equation because LHS is single variable
80+
y ~ z - x, # can be an equation because LHS is single variable
8281
]
8382
```
8483

85-
Internally, all of these
86-
87-
which is the given to
84+
which is then given to
8885
```@example MAIN
89-
model = processes_to_mtkmodel(processes)
86+
model = processes_to_mtkmodel(processes; name = :example)
9087
equations(model)
9188
```
9289

93-
Notice that the resulting **MTK** model is not `structural_simplify`-ed, to allow composing it with other models.
90+
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.
9491

95-
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:
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:
9693
```@example MAIN
9794
try
9895
model = processes_to_mtkmodel(processes[[1, 3]])
@@ -101,7 +98,7 @@ catch e
10198
end
10299
```
103100

104-
If instead we "forgot" the ``y`` process, **PBM** will not error, but instead warn, and make ``y`` a named parameter:
101+
If instead we "forgot" the ``y`` process, **PBM** will not error, but instead warn, and make ``y`` equal to a named parameter:
105102
```@example MAIN
106103
model = processes_to_mtkmodel(processes[1:2])
107104
equations(model)
@@ -112,6 +109,7 @@ parameters(model)
112109
```
113110

114111
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).
115113
For example,
116114

117115
```@example MAIN
@@ -163,14 +161,16 @@ This API describes how you can implement your own `Process` subtype, if the [exi
163161

164162
```@docs
165163
Process
166-
rhs
167-
timescale
168-
NoTimeVariability
164+
ProcessBasedModelling.lhs_variable
165+
ProcessBasedModelling.rhs
166+
ProcessBasedModelling.NoTimeDerivative
167+
ProcessBasedModelling.lhs
169168
```
170169

171170
## Utility functions
172171

173172
```@docs
173+
default_value
174174
has_variable
175175
new_derived_named_parameter
176176
```

src/API.jl

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,36 @@
11
"""
2-
A process subtype `p::Process` extends the functions:
3-
- `lhs_variable(p)` which returns the variable the process describes (left-hand-side variable)
2+
A process subtype `p::Process` extends the following unexported functions:
3+
4+
- `lhs_variable(p)` which returns the variable the process describes
5+
(left-hand-side variable). There is a default implementation
6+
`lhs_variable(p) = p.variable` if the field exists.
47
- `rhs(p)` which is the right-hand-side expression, i.e., the "actual" process.
5-
- (optional) `timescale`, which defaults to [`NoTimeVariability`](@ref).
8+
- (optional) `timescale`, which defaults to [`NoTimeDerivative`](@ref).
69
- (optional) `lhs(p)` which returns the left-hand-side. Let `τ = timescale(p)`.
7-
Then `lhs(p)` behavior depends on `τ` as follows:
8-
- Just `lhs_variable(p)` if `τ == NoTimeVariability()`.
10+
Then default `lhs(p)` behaviour depends on `τ` as follows:
11+
- Just `lhs_variable(p)` if `τ == NoTimeDerivative()`.
912
- `Differential(t)(p)` if `τ == nothing`.
1013
- `τ_var*Differential(t)(p)` if `τ isa Union{Real, Num}`. If real,
1114
a new named parameter `τ_var` is created that has the prefix `:τ_` and then the
1215
lhs-variable name and has default value `τ`. Else if `Num`, `τ_var = τ` as given.
16+
- Explicitly extend `lhs_variable` if the above do not suit you.
1317
"""
1418
abstract type Process end
1519

1620
"""
17-
NoTimeVariability()
21+
ProcessBasedModelling.NoTimeDerivative()
1822
1923
Singleton value that is the default output of the [`timescale`](@ref) function
20-
for variables that do not vary in time autonomously (i.e., no d/dt derivative).
24+
for variables that do not vary in time autonomously, i.e., they have no d/dt derivative
25+
and hence the concept of a "timescale" does not apply to them.
26+
"""
27+
struct NoTimeDerivative end
28+
2129
"""
22-
struct NoTimeVariability end
30+
ProcessBasedModelling.lhs_variable(p::Process)
2331
32+
Return the variable (a single symbolic variable) corresponding to `p`.
33+
"""
2434
function lhs_variable(p::Process)
2535
if !hasfield(typeof(p), :variable)
2636
error("`lhs_variable` not defined for process $(nameof(typeof(p))).")
@@ -29,32 +39,52 @@ function lhs_variable(p::Process)
2939
end
3040
end
3141

32-
timescale(::Process) = NoTimeVariability()
42+
"""
43+
ProcessBasedModelling.timescale(p::Process)
44+
45+
Return the timescale associated with `p`. See [`Process`](@ref) for more.
46+
"""
47+
timescale(::Process) = NoTimeDerivative()
48+
49+
"""
50+
ProcessBasedModelling.lhs(p::Process)
3351
52+
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.
54+
See [`Process`](@ref) for more.
55+
"""
3456
function lhs(p::Process)
3557
τ = timescale(p)
3658
v = lhs_variable(p)
3759
if isnothing(τ) # time variability exists but timescale is nonexistent (unity)
3860
return Differential(t)(v)
39-
elseif τ isa NoTimeVariability || iszero(τ) # no time variability
61+
elseif τ isa NoTimeDerivative || iszero(τ) # no time variability
4062
return v
4163
else # τ is either Num or Real
4264
τvar = new_derived_named_parameter(v, τ, "τ", false)
4365
return τvar*Differential(t)(v)
4466
end
4567
end
68+
69+
"""
70+
ProcessBasedModelling.rhs(p::Process)
71+
72+
Return the right-hand-side of the equation that `p` represents as an `Expression`.
73+
See [`Process`](@ref) for more.
74+
"""
4675
function rhs(p::Process)
4776
error("Right-hand side (`rhs`) is not defined for process $(nameof(typeof(p))).")
4877
end
4978

79+
# Extensions for `Equation`:
5080
rhs(e::Equation) = e.rhs
5181
lhs(e::Equation) = lhs_variable(e)
5282
function lhs_variable(e::Equation)
5383
x = e.lhs
5484
# we first check whether `x` is a variable
5585
if !is_variable(x)
5686
throw(ArgumentError("In given equation $(e), the left-hand-side does "*
57-
"not represent a variable."))
87+
"not represent a single variable."))
5888
end
5989
return x
6090
end

src/ProcessBasedModelling.jl

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,14 @@ include("utils.jl")
1717
include("make.jl")
1818
include("processes_basic.jl")
1919

20-
21-
# TODO: In MAKE, make it so that if a variable does not have a process
22-
# a constant process is created for it if it has a default value.
23-
# Add a keyword `use_default` which would warn if no process but there is default
24-
# and otherwise would error.
2520
# TODO: Make an "addition process" that adds to processes
2621
# It checks whether they target the same variable
27-
# TODO: Package should compose with ODESystem
28-
# so that component-based modelling can be utilized as well.
29-
3022

31-
# TODO: Perhaps not don't export `t, rhs`?
23+
# TODO: Perhaps not don't export `t`?
3224
export t
33-
export lhs_variable, rhs, timescale, NoTimeVariability
3425
export Process, ParameterProcess, TimeDerivative, ExpRelaxation
3526
export processes_to_mtkmodel
3627
export new_derived_named_parameter
28+
export hs_variable, default_value
3729

3830
end

src/make.jl

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,17 @@ but they don't themselves have an assigned process.
1818
It is expected that downstream packages that use ProcessBasedModelling.jl to make a
1919
field-specific library implement a 1-argument version of `processes_to_mtkmodel`,
2020
or provide a wrapper function for it, and add a default value for `default`.
21+
22+
## Keyword arguments
23+
24+
- `type = ODESystem`: the model type to make
25+
- `name = nameof(type)`: the name of the model
26+
- `independent = t`: the independent variable (default: `@variables t`).
27+
`t` is also exported by ProcessBasedModelling.jl for convenience.
2128
"""
22-
function processes_to_mtkmodel(_processes, _default = []; type = ODESystem, name = nameof(type))
29+
function processes_to_mtkmodel(_processes, _default = [];
30+
type = ODESystem, name = nameof(type), independent = t
31+
)
2332
processes = expand_multi_processes(_processes)
2433
default = default_dict(_default)
2534
# 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
7281
end
7382
end
7483
end
75-
sys = type(eqs, t; name)
84+
sys = type(eqs, independent; name)
7685
return sys
7786
end
7887
# version without given processes

src/utils.jl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
export print_system_info, has_variable, default_value
2-
export new_derived_named_parameter, @named_parameters
3-
41
"""
52
has_variable(eq, var)
63
@@ -13,6 +10,12 @@ function has_variable(eq::Equation, var)
1310
end
1411
has_variable(eqs, var) = any(eq -> has_variable(eq, var), eqs)
1512

13+
"""
14+
default_value(x)
15+
16+
Return the default value of a symbolic variable `x` or `nothing`
17+
if it doesn't have any. Return `x` if `x` is not a symbolic variable.
18+
"""
1619
default_value(x) = x
1720
default_value(x::Num) = default_value(x.val)
1821
function default_value(x::ModelingToolkit.SymbolicUtils.Symbolic)

test/runtests.jl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,25 @@ using OrdinaryDiffEq
2525
return absorbed_shortwave - emitted_longwave
2626
end
2727

28+
# make a new type of process
29+
struct TanhProcess <: Process
30+
variable
31+
driver_variable
32+
left
33+
right
34+
scale
35+
reference
36+
end
37+
function ProcessBasedModelling.rhs(p::TanhProcess)
38+
x = p.driver_variable
39+
(; left, right, scale, reference) = p
40+
return tanh_expression(x, left, right, scale, reference)
41+
end
42+
function tanh_expression(T, left, right, scale, reference)
43+
return left + (right - left)*(1 + tanh(2(T - reference)/(scale)))*0.5
44+
end
45+
46+
2847
processes = [
2948
TanhProcess(α, T, 0.7, 0.289, 10.0, 274.5),
3049
TanhProcess(ε, T, 0.5, 0.41, 2.0, 288.0),

0 commit comments

Comments
 (0)