ProcessBasedModelling
— ModuleProcessBasedModelling.jl
ProcessBasedModelling.jl is an extension to ModelingToolkit.jl (MTK) for building a model of equations using symbolic expressions. It is an alternative framework to MTK's native component-based modelling, but, instead of components, there are "processes". 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. On the other hand, there plenty of different physical representations, or processes to represent a given physical concept. In many scientific fields this approach parallels the modelling reasoning of the researcher more closely than the "components" approach.
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. This allows ProcessBasedModelling.jl to:
- Iterate over the processes and collect new variables that have been introduced by a provided process but do not themselves have a process assigned to them.
- For these collected "process-less" variables:
- If there is a default process defined, incorporate this one into the model
- If there is no default process but the variable has a default value, equate the variable to a parameter that has the same default value and throw an informative warning.
- Else, throw an informative error saying exactly which originally provided variable introduced this new "process-less" variable.
- Throw an informative error if a variable has two processes assigned to it (by mistake).
In our experience, and as we also highlight explicitly in the online documentation, this approach typically yields simpler, less ambiguous and more targeted warning or error messages than the native MTK one's, leading to faster identification and resolution of the problems with the composed equations.
ProcessBasedModelling.jl is particularly suited for developing a model about a physical/biological/whatever system and being able to try various physical "rules" (couplings, feedbacks, mechanisms, ...) for a given physical observable efficiently. This means switching arbitrarily between different processes that correspond to the same variable. Hence, the target application of ProcessBasedModelling.jl is to be a framework to develop field-specific libraries that offer predefined processes without themselves relying on the existence of context-specific predefined components. An example usage is in EnergyBalanceModels.jl.
Besides the informative errors, ProcessBasedModelling.jl also
- Provides a couple of common process subtypes out of the box to accelerate development of field-specific libraries.
- Makes named MTK variables and parameters automatically, corresponding to parameters introduced by the by-default provided processes. This typically leads to intuitive names without being explicitly coded, while being possible to opt-out.
- Provides some utility functions for further building field-specific libraries.
See the documentation online for details on how to use this package as well as examples highlighting its usefulness.
These docs assume that you have some basic familiarity with ModelingToolkit.jl. If you don't going through the introductory tutorial of ModelingToolkit.jl should be enough to get you started!
Like ModelingToolkit.jl, ProcessBasedModelling.jl also exports t
as the independent variable representing time. However, instead of the default t
of ModelingToolkit.jl, here t
is unitless. Do t = ModelingToolkit.t
to obtain the unitful version of t
.
Usage
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
if the left-hand-side of the equation needs to be anything more complex (or, simply if you want to utilize the conveniences of predefined processes). 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
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
Let's say we want to build the system of equations
\[\dot{z} = x^2 - z \\ +
ProcessBasedModelling
— ModuleProcessBasedModelling.jl
ProcessBasedModelling.jl is an extension to ModelingToolkit.jl (MTK) for building a model of equations using symbolic expressions. It is an alternative framework to MTK's native component-based modelling, but, instead of components, there are "processes". 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. On the other hand, there plenty of different physical representations, or processes to represent a given physical concept. In many scientific fields this approach parallels the modelling reasoning of the researcher more closely than the "components" approach.
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. This allows ProcessBasedModelling.jl to:
- Iterate over the processes and collect new variables that have been introduced by a provided process but do not themselves have a process assigned to them.
- For these collected "process-less" variables:
- If there is a default process defined, incorporate this one into the model
- If there is no default process but the variable has a default value, equate the variable to a parameter that has the same default value and throw an informative warning.
- Else, throw an informative error saying exactly which originally provided variable introduced this new "process-less" variable.
- Throw an informative error if a variable has two processes assigned to it (by mistake).
In our experience, and as we also highlight explicitly in the online documentation, this approach typically yields simpler, less ambiguous and more targeted warning or error messages than the native MTK one's, leading to faster identification and resolution of the problems with the composed equations.
ProcessBasedModelling.jl is particularly suited for developing a model about a physical/biological/whatever system and being able to try various physical "rules" (couplings, feedbacks, mechanisms, ...) for a given physical observable efficiently. This means switching arbitrarily between different processes that correspond to the same variable. Hence, the target application of ProcessBasedModelling.jl is to be a framework to develop field-specific libraries that offer predefined processes without themselves relying on the existence of context-specific predefined components. An example usage is in EnergyBalanceModels.jl.
Besides the informative errors, ProcessBasedModelling.jl also
- Provides a couple of common process subtypes out of the box to accelerate development of field-specific libraries.
- Makes named MTK variables and parameters automatically, corresponding to parameters introduced by the by-default provided processes. This typically leads to intuitive names without being explicitly coded, while being possible to opt-out.
- Provides some utility functions for further building field-specific libraries.
See the documentation online for details on how to use this package as well as examples highlighting its usefulness.
These docs assume that you have some basic familiarity with ModelingToolkit.jl. If you don't going through the introductory tutorial of ModelingToolkit.jl should be enough to get you started!
Like ModelingToolkit.jl, ProcessBasedModelling.jl also exports t
as the independent variable representing time. However, instead of the default t
of ModelingToolkit.jl, here t
is unitless. Do t = ModelingToolkit.t
to obtain the unitful version of t
.
Usage
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
if the left-hand-side of the equation needs to be anything more complex (or, simply if you want to utilize the conveniences of predefined processes). 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
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
Let's say we want to build the system of equations
\[\dot{z} = x^2 - z \\ \dot{x} = 0.1y \\ y = z - x\]
symbolically using ModelingToolkit.jl (MTK). We define
using ModelingToolkit
using OrdinaryDiffEq: Tsit5
@@ -77,12 +77,12 @@
model = processes_to_mtkmodel(processes)
equations(model)
\[ \begin{align} -z_{\tau} \frac{\mathrm{d} z\left( t \right)}{\mathrm{d}t} =& - z\left( t \right) + \left( x\left( t \right) \right)^{2} \\ -x_{\tau} \frac{\mathrm{d} x\left( t \right)}{\mathrm{d}t} =& 0.1 y\left( t \right) \\ +\frac{\mathrm{d} z\left( t \right)}{\mathrm{d}t} \tau_{z} =& - z\left( t \right) + \left( x\left( t \right) \right)^{2} \\ +\frac{\mathrm{d} x\left( t \right)}{\mathrm{d}t} \tau_{x} =& 0.1 y\left( t \right) \\ y\left( t \right) =& - x\left( t \right) + z\left( t \right) \end{align} \]
parameters(model)
2-element Vector{SymbolicUtils.BasicSymbolic{Real}}:
- z_τ
- x_τ
This special handling is also why each process can declare a timescale via the ProcessBasedModelling.timescale
function that one can optionally extend (although in our experience the default behaviour covers almost all cases).
Main API function
ProcessBasedModelling.processes_to_mtkmodel
— Functionprocesses_to_mtkmodel(processes::Vector, default::Vector = []; kw...)
Construct a ModelingToolkit.jl model/system using the provided processes
and default
processes. The model/system is not structurally simplified.
processes
is a vector whose elements can be:
- Any instance of a subtype of
Process
. - An
Equation
which is of the formvariable ~ expression
withvariable
a single variable resulting from an@variables
call. - A vector of the above two, which is then expanded. This allows the convenience of functions representing a physical process that may require many equations to be defined.
default
is a vector that can contain the first two possibilities only as it contains default processes that may be assigned to variables introduced in processes
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 makename = nameof(type)
: the name of the modelindependent = t
: the independent variable (default:@variables t
).t
is also exported by ProcessBasedModelling.jl for convenience.
Predefined Process
subtypes
ProcessBasedModelling.ParameterProcess
— TypeParameterProcess(variable, value = default_value(variable)) <: Process
The simplest process which equates a given variable
to a constant value that is encapsulated in a parameter. If value isa Real
, then a named parameter with the name of variable
and _0
appended is created. Else, if valua isa Num
then it is taken as the paremeter directly.
Example:
@variables T(t) = 0.5
-proc = ParameterProcess(T)
will create the equation T ~ T_0
, where T_0
is a @parameter
with default value 0.5
.
ProcessBasedModelling.TimeDerivative
— TypeTimeDerivative(variable, expression [, τ])
The second simplest process that equates the time derivative of the variable
to the given expression
while providing some conveniences over manually constructing an Equation
.
It creates the equation τ_$(variable) Differential(t)(variable) ~ expression
by constructing a new @parameter
with default value τ
(if τ
is already a @parameter
, it is used as-is). If τ
is not given, then 1 is used at its place and no parameter is created.
Note that if iszero(τ)
, then the process variable ~ expression
is created.
ProcessBasedModelling.ExpRelaxation
— TypeExpRelaxation(variable, expression [, τ]) <: Process
A common process for creating an exponential relaxation of variable
towards the given expression
, with timescale τ
. It creates the equation:
τn*Differential(t)(variable) ~ expression - variable
Where τn
is a new named @parameter
with the value of τ
and name τ_($(variable))
. If instead τ
is nothing
, then 1 is used in its place (this is the default behavior). If iszero(τ)
, then the equation variable ~ expression
is created instead.
The convenience function
ExpRelaxation(process, τ)
allows converting an existing process (or equation) into an exponential relaxation by using the rhs(process)
as the expression
in the equation above.
ProcessBasedModelling.AdditionProcess
— TypeAdditionProcess(process, added)
A convenience process for adding added
to the rhs
of the given process
. added
can be a Process
or Equation
, in which case it is checked that the lhs_variable
matches. Otherwise, it can be an arbitrary expression.
Process
API
This API describes how you can implement your own Process
subtype, if the existing predefined subtypes don't fit your bill!
ProcessBasedModelling.Process
— TypeProcess
A new process must subtype Process
and can be used in processes_to_mtkmodel
. The type must extend the following functions from the module ProcessBasedModelling
:
lhs_variable(p)
which returns the variable the process describes (left-hand-side variable). There is a default implementationlhs_variable(p) = p.variable
if the field exists.rhs(p)
which is the right-hand-side expression, i.e., the "actual" process.- (optional)
timescale(p)
, which defaults toNoTimeDerivative
. - (optional)
lhs(p)
which returns the left-hand-side. Letτ = timescale(p)
. Then defaultlhs(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, a new named parameterτ_var
is created that has the prefix:τ_
and then the lhs-variable name and has default valueτ
. Else ifNum
,τ_var = τ
as given.- Explicitly extend
lhs_variable
if the above do not suit you.
- Just
ProcessBasedModelling.lhs_variable
— FunctionProcessBasedModelling.lhs_variable(p::Process)
Return the variable (a single symbolic variable) corresponding to p
.
ProcessBasedModelling.rhs
— FunctionProcessBasedModelling.rhs(p::Process)
Return the right-hand-side of the equation that p
represents as an Expression
. See Process
for more.
ProcessBasedModelling.timescale
— FunctionProcessBasedModelling.timescale(p::Process)
Return the timescale associated with p
. See Process
for more.
ProcessBasedModelling.NoTimeDerivative
— TypeProcessBasedModelling.NoTimeDerivative()
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.
ProcessBasedModelling.lhs
— FunctionProcessBasedModelling.lhs(p::Process)
Return the left-hand-side of the equation that p
represents as an Expression
. If timescale
is implemented for p
, typically lhs
does not need to be as well. See Process
for more.
Utility functions
ProcessBasedModelling.default_value
— Functiondefault_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.
ProcessBasedModelling.has_variable
— Functionhas_variable(eq, var)
Return true
if variable var
exists in the equation(s) eq
, false
otherwise. Function works irrespectively if var
is an @variable
or @parameter
.
ProcessBasedModelling.new_derived_named_parameter
— Functionnew_derived_named_parameter(variable, value, extra::String, prefix = true)
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 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.
For example,
@variables x(t)
-p = new_derived_named_parameter(x, 0.5, "τ")
Now p
will be a parameter with name :τ_x
and default value 0.5
.
ProcessBasedModelling.@convert_to_parameters
— Macro@convert_to_parameters vars...
Convert all variables vars
into @parameters
with name the same as vars
and default value the same as the value of vars
. The macro leaves unaltered inputs that are of type Num
, assumming they are already parameters. It also replaces LiteralParameter
inputs with its literal values. This macro is extremely useful to convert e.g., keyword arguments into named parameters, while also allowing the user to give custom parameter names.
Example:
``` julia> A, B = 0.5, 0.5 (0.5, 0.5)
julia> C = first(@parameters X = 0.5)
julia> @converttoparameters A B C 3-element Vector{Num}: A B X
julia> typeof(A) # A
is not a number anymore! Num
julia> default_value(A) 0.5
julia> C # the binding C
still corresponds to parameter named :X
! X
ProcessBasedModelling.LiteralParameter
— TypeLiteralParameter(p)
A wrapper around a value p
to indicate to new_derived_named_parameter
or @convert_to_parameters
to not convert the given parameter p
into a named @parameters
instance, but rather keep it as a numeric literal in the generated equations.
Settings
This document was generated with Documenter.jl version 1.2.1 on Monday 26 February 2024. Using Julia version 1.10.1.
This special handling is also why each process can declare a timescale via the ProcessBasedModelling.timescale
function that one can optionally extend (although in our experience the default behaviour covers almost all cases).
Main API function
ProcessBasedModelling.processes_to_mtkmodel
— Functionprocesses_to_mtkmodel(processes::Vector, default::Vector = []; kw...)
Construct a ModelingToolkit.jl model/system using the provided processes
and default
processes. The model/system is not structurally simplified.
processes
is a vector whose elements can be:
- Any instance of a subtype of
Process
. - An
Equation
which is of the formvariable ~ expression
withvariable
a single variable resulting from an@variables
call. - A vector of the above two, which is then expanded. This allows the convenience of functions representing a physical process that may require many equations to be defined.
default
is a vector that can contain the first two possibilities only as it contains default processes that may be assigned to variables introduced in processes
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 makename = nameof(type)
: the name of the modelindependent = t
: the independent variable (default:@variables t
).t
is also exported by ProcessBasedModelling.jl for convenience.
Predefined Process
subtypes
ProcessBasedModelling.ParameterProcess
— TypeParameterProcess(variable, value = default_value(variable)) <: Process
The simplest process which equates a given variable
to a constant value that is encapsulated in a parameter. If value isa Real
, then a named parameter with the name of variable
and _0
appended is created. Else, if valua isa Num
then it is taken as the paremeter directly.
Example:
@variables T(t) = 0.5
+proc = ParameterProcess(T)
will create the equation T ~ T_0
, where T_0
is a @parameter
with default value 0.5
.
ProcessBasedModelling.TimeDerivative
— TypeTimeDerivative(variable, expression [, τ])
The second simplest process that equates the time derivative of the variable
to the given expression
while providing some conveniences over manually constructing an Equation
.
It creates the equation τ_$(variable) Differential(t)(variable) ~ expression
by constructing a new @parameter
with default value τ
(if τ
is already a @parameter
, it is used as-is). If τ
is not given, then 1 is used at its place and no parameter is created.
Note that if iszero(τ)
, then the process variable ~ expression
is created.
ProcessBasedModelling.ExpRelaxation
— TypeExpRelaxation(variable, expression [, τ]) <: Process
A common process for creating an exponential relaxation of variable
towards the given expression
, with timescale τ
. It creates the equation:
τn*Differential(t)(variable) ~ expression - variable
Where τn
is a new named @parameter
with the value of τ
and name τ_($(variable))
. If instead τ
is nothing
, then 1 is used in its place (this is the default behavior). If iszero(τ)
, then the equation variable ~ expression
is created instead.
The convenience function
ExpRelaxation(process, τ)
allows converting an existing process (or equation) into an exponential relaxation by using the rhs(process)
as the expression
in the equation above.
ProcessBasedModelling.AdditionProcess
— TypeAdditionProcess(process, added)
A convenience process for adding added
to the rhs
of the given process
. added
can be a Process
or Equation
, in which case it is checked that the lhs_variable
matches. Otherwise, it can be an arbitrary expression.
Process
API
This API describes how you can implement your own Process
subtype, if the existing predefined subtypes don't fit your bill!
ProcessBasedModelling.Process
— TypeProcess
A new process must subtype Process
and can be used in processes_to_mtkmodel
. The type must extend the following functions from the module ProcessBasedModelling
:
lhs_variable(p)
which returns the variable the process describes (left-hand-side variable). There is a default implementationlhs_variable(p) = p.variable
if the field exists.rhs(p)
which is the right-hand-side expression, i.e., the "actual" process.- (optional)
timescale(p)
, which defaults toNoTimeDerivative
. - (optional)
lhs(p)
which returns the left-hand-side. Letτ = timescale(p)
. Then defaultlhs(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, a new named parameterτ_var
is created that has the prefix:τ_
and then the lhs-variable name and has default valueτ
. Else ifNum
,τ_var = τ
as given.- Explicitly extend
lhs_variable
if the above do not suit you.
- Just
ProcessBasedModelling.lhs_variable
— FunctionProcessBasedModelling.lhs_variable(p::Process)
Return the variable (a single symbolic variable) corresponding to p
.
ProcessBasedModelling.rhs
— FunctionProcessBasedModelling.rhs(p::Process)
Return the right-hand-side of the equation that p
represents as an Expression
. See Process
for more.
ProcessBasedModelling.timescale
— FunctionProcessBasedModelling.timescale(p::Process)
Return the timescale associated with p
. See Process
for more.
ProcessBasedModelling.NoTimeDerivative
— TypeProcessBasedModelling.NoTimeDerivative()
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.
ProcessBasedModelling.lhs
— FunctionProcessBasedModelling.lhs(p::Process)
Return the left-hand-side of the equation that p
represents as an Expression
. If timescale
is implemented for p
, typically lhs
does not need to be as well. See Process
for more.
Utility functions
ProcessBasedModelling.default_value
— Functiondefault_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.
ProcessBasedModelling.has_variable
— Functionhas_variable(eq, var)
Return true
if variable var
exists in the equation(s) eq
, false
otherwise. Function works irrespectively if var
is an @variable
or @parameter
.
ProcessBasedModelling.new_derived_named_parameter
— Functionnew_derived_named_parameter(variable, value, extra::String; kw...)
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.
For example,
@variables x(t)
+p = new_derived_named_parameter(x, 0.5, "τ")
Now p
will be a parameter with name :τ_x
and default value 0.5
.
ProcessBasedModelling.@convert_to_parameters
— Macro@convert_to_parameters vars...
Convert all variables vars
into @parameters
with name the same as vars
and default value the same as the value of vars
. The macro leaves unaltered inputs that are of type Num
, assumming they are already parameters. It also replaces LiteralParameter
inputs with its literal values. This macro is extremely useful to convert e.g., keyword arguments into named parameters, while also allowing the user to give custom parameter names.
Example:
``` julia> A, B = 0.5, 0.5 (0.5, 0.5)
julia> C = first(@parameters X = 0.5)
julia> @converttoparameters A B C 3-element Vector{Num}: A B X
julia> typeof(A) # A
is not a number anymore! Num
julia> default_value(A) 0.5
julia> C # the binding C
still corresponds to parameter named :X
! X
ProcessBasedModelling.LiteralParameter
— TypeLiteralParameter(p)
A wrapper around a value p
to indicate to new_derived_named_parameter
or @convert_to_parameters
to not convert the given parameter p
into a named @parameters
instance, but rather keep it as a numeric literal in the generated equations.