Right now I use my own “framework”. It separates dynamic components (which calculate state derivatives) and algebraic ones (just functions of their inputs, parameters and time). Then there are assemblies, which are constructed from arbitrarily many components (or other assemblies) and connections between them. The expose only specified inputs and outputs (to make the “interface” only as complex as necessary). Large/complex models can be constructed by nesting such assemblies. The overall-model function is then automatically built by recursively flattening all assemblies (or “subsystems”) bottom-up, building the “path” of each component for e.g. setting/getting its parameters or state/algebraic variables, building a dependency graph and applying a topological sort to determine execution order. (This is done twice; once for the dynamics, i.e. everything required to calculate all state derivatives, and once for all the algebraic variables, which can be calculated from the ODE solution afterwards.)
The overall-model code-generation handles indexing of state variables and resolving connections (i.e. constructing input tuples to the component functions). Right now, everything is passed around as tuples since the length and types of the signals are known at model-build time. Combined with the in-place form of the model function for DiffEq, no allocations happen so everything is really fast. One could even get the code for the overall model and generate a source file with it, then compile it into the system image (or even an executable) using PackageCompiler
to get statically compiled models/simulations (tried it, works).
I also implemented some macros to make construction of components and assemblies easier. And at that point I noticed that I’m re-doing stuff that’s already there multiple times (JuMP, DiffEq, Modia, …) and will probably be implemented in a much more coherent and thought-out way in the upcoming ModelingToolkit
. So I’d rather contribute there than contribute just another framework that does very similar things.
For example,
@dynamic :twoPT1 "two PT-1 elements in series" begin
dx[1] = (u[1]-x[1])/tau[1]
dx[2] = (x[1]-x[2])/tau[2]
@parameter :tau "PT-1 time constants" [1.0, 5.0]
end
creates a dynamic component, and
@assemble :my_assembly "just an assembly" begin
@components compA compB
@parameter :V̇ "constant volumetric flow (m³/s)" 1.3
@connect ( u -> compA[1:3], # u is the assembly's inputs, no indices means [:]
p.V̇ -> compA[end], # parameters can be used as input to components
compA -> compB,
compA[1] -> y[1], # y is the assembly's outputs
compB[2:3] -> y[2:3] )
end
constructs an assembly.