[NEW] SimpleSim.jl: A minimalist Julia package for modular dynamical systems simulation

Hello everyone,

I’m very happy to announce my first larger Julia package SimpleSim.jl which is now available on the Julia package registry!

SimpleSim.jl aims at being a lightweight simulation engine 100% written in Julia with support for systems described by (continuous-time) ordinary differential equations or discrete-time processes.
Hence, the package can be used to simulate most physical and digital systems, which makes it perfect for robotics but also other areas.

Below, I will briefly describe how the package works for anyone only reading on Julia discourse.
If you want to learn more, please read the documentation. All features currently available are documented there in a lot more detail.

The package itself lives in this GitHub repo.

Overview of Features

  • Simulation/numerical solutions of ordinary differential equations
  • Integrated support for different ODE solvers (Euler, Heun, RK4, Adaptive RKF45)
  • Simulation of discrete processes
  • Simulation of models with both continous-time processes (ODEs) and discrete-time processes (“hybrid” models)
  • Zero-crossing detection for continuous-time system states
  • Random variable handling for repeatable results
  • Support for nested model structures

How it works

The main point of interaction with the package is the simulate function. Let’s say you have set up a model object my_model that contains all of the physics and processes you want to simulate (more on that later). Then, simulating that model can be done by running

data = simulate(my_model, T = 10 // 1)

where T denotes the total time the simulation will be run for.

The package does not export any types and only very few functions/macros. The whole “design process” of what a model could look like is left up to the user.
For example, if you want to simulate a continuous-time ODE system, SimpleSim.jl only expects your model to have the named fields p, fc and yc. The field p can contain model parameters, or it can be set to nothing if parameters aren’t needed.
fc defines the models behavior. It should contain a function taking the arguments (x, u, p, t) for the current state, input, parameters and time of the model, and returns the state derivative. yc takes the same arguments, but returns the current system output.

For example, if you want to simulate a falling object without air resistance, i.e. a double integrator of the gravitational constant, a complete model and simulation looks like this:

falling_object = (
    p = (g = 9.81,)
    fc = (z, u, p, t) -> [z[2], -p.g],
    yc = (z, u, p, t) -> z[1],
)
data = simulate(falling_object, T = 3 // 1, xc0 = [0, 0])

These few lines of code will create a valid SimpleSim.jl model and will simulate it for 3 seconds.
The resulting data object has the fields tcs, xcs and ycs which contain the time, state and output evolution of the system.

If you want to simulate a more complex system, it may make sense to define your own types. Just make sure your model provides the necessary named fields as interfaces for SimpleSim.jl.
If you are just using SimpleSim.jl to numerically solve an ODE, then defining a model as a NamedTuple (like above) works just fine.

Some Simple Examples

An extension of the “falling object” example from above is a ball bouncing down a flight of stairs. This example also makes use of SimpleSim.jl’s zero-crossing detection feature.
Bouncing Ball

An example of a discrete process would be the random walk below. This example also uses SimplSim.jl’s random variable handling.
Random Walk

I have written quite a few more examples, which you can find in the project repo. I will make sure to document these examples a bit better soon. Especially the more complex and nested models SimpleSim.jl is capable of handling are not very well documented at the moment.

Nested Models

A key feature of SimpleSim.jl’s is its support for nested models. For example, if you want to simulate a whole airplane, you don’t have to set up a single differential equation that describes every aspect of the aircraft’s behavior. Instead, you model each component of the system separately and define separate models. Then, you use a supermodel that “calls” your components and makes sure they receive the correct inputs. Nesting on multiple levels is also possible.

The support for nested models may be SimpleSim.jl’s most powerful feature. It allows you to implement models in a very similar way to how you would draw block diagrams. If you want to learn more about this, please read the documentation and take a look at the examples.

What I still want to improve

All core features are implemented and fully functional (I think). But of course there are still things that I would like to improve and you are more than welcome to contribute.
I have listed a few ideas for future work on the issues page on GitHub.

If you have read all the way until here, let me know what you think! :slight_smile:
Best,
Jannes

28 Likes

Nice! Thank you for sharing this, @janneshb. I’m going to share with a few folks that have been looking for something like this.

1 Like

One question I have is: What could ModelingToolkit and co learn from your package? I think ModelingToolkit still does not support hybrid systems well.

I don’t think ModelingToolkit supports hybrid systems. Its system types are always distinctly ODE/PDE/Discrete etc. (correct me if I’m wrong).
So, as you said, that’s already something SimpleSim has over ModelingToolkit.

But in my opinion the best things about SimpleSim are:

  • Nested models. You can implement a discrete or continuos-time model as a submodel of another continuous time model. This allows you to build very complex models in a very realistic way and SimpleSim will take care of all the annoying stuff, such as ensuring that discrete-time systems are only updated according to their defined sampling time. This feature is something that I’ve always missed and maybe the main reason I hadn’t uninstalled Matlab/Simulink until very recently.
  • It doesn’t do everything. Some of the existing packages can do a lot. Like ODEs, PDEs, stochastic systems, complex implicit nonlinear systems and so on. SimpleSim focuses solely on solving explicit ODEs and discrete-time processes. This may seem like a disadvantage, but I think it makes it a lot easier to get started with SimpleSim. Of course, this doesn’t mean that the package will never be extended to do more in the future.

ModelingToolkit does support hybrid systems but the support is very patchy and incomplete at the moment. Some of the functionality is located here ModelingToolkitSampledData.jl, but this is not yet released.

1 Like

Interesting, thanks for sharing.

Ultimately, I didn’t build SimpleSim to replace any of the existing packages and tools. If you are looking for something specific that’s available in DynamicalSystems.jl, ModelingToolkit.jl etc. then that’s what you should use.

But the reality is that most users won’t need a lot of the features offered by these packages and SimpleSim offers a lightweight alternative with a low entrance barrier, while still providing a powerful simulation engine with a broad spectrum of features.

6 Likes

I encourage anyone reading this to just play around with the package a little bit. It’s much easier to see the difference in “user experience” that way.

And definitely let me know what you think! :slight_smile:

2 Likes

Hi @janneshb , thanks for sharing your work for the community! Since you explicitly ask for feedback I will contribute my two cents (but I am sorry in advance as I typically challenge people when asked for feedback…).

An important note here is that DynamicalSystems.jl has nothing to do with modelling. It is a library that you use to analyze an already built model. It doesn’t help you build it. So in a sense it is entirely orthogonal with SimpleSim.jl. But…

I saw that in SimpleSim.jl you really started from scratch. Even re-wrote ODE solvers like RK4. It also appears to me that SimpleSim.jl made a deliberate choice to not conform the “state functions” f/g to the format that is currently established by SciML of f(u, p, t) with u state, p parameter, and t time. Why this choice was made I don’t understand, especially because such a choice is purely convention driven; any format is equally good as any other, and the only format that can be better is the one people already use. Which motivates the following…

Can a model created by SimpleSim.jl be casted into the standard form used by OrdinaryDiffEq.jl / DynamicalSystems.jl of a single state function f(u, p, t)? If not, that is hurting your users. You are missing out on really large tools for analyzing (not building and simulating) dynamical systems. Not only DynamicalSystems.jl but also the parameter sensitivity stuff in SciML among many others. Some of these stuff don’t even exist in other programming languages so they are a genuine Julia advantage and actual reason people switch to Julia in the first place. And there doesn’t appear to be any fundamental reason on why SimpleSim.jl should be missing out on all this stuff.

Generally speaking I think it is good that there are alternative simulation softwares in a programming language. I am just raising my concerns after the brief view I had of the documentation. And my concerns come because I think that there should be attempts to make everything compatible with each other to the extent possible and meaningful.


If I can give some feedback into the existing documentation, it would be the following:

First, would be to consider removing the sentence

SimpleSim.jl does not export any types.”

It is technically true, but me it sounds missleading. Sure, you don’t export a new type, but you expect all input to be of the exact type NamedTuple{...}{:fc, :yc, :p, ...} with all field names being exactly specified. To my eyes this has no difference to making a custom type with fields fc, yc, p and I would argue it is probably more safe to have a type in the first place since so much specificity is enforced.

Also, why do you need to have fc for continuous systems and fd for discrete, instead of using f for both? Same for xc0 and xd0?

Second to be more transparent in the introduction page. Why does the fields of the named tuple are named fd, yd instead of fd, gd since one function is f and the other is g. Why is u expected as an input to both functions? What even is u? It is not the state of the dynamical system, that’s x. What’s y? y is not used in the equation of x, so y is not even needed to be computed in this equation:

image

as you can solve for your state variable without y. In the whole introduction page it is never stated what u is. Is u a time forcing? If yes, why does the f function also depend on time t? Is y an observable of the dynamical system? If so, why would it be in the equations defining the dynamical system? If it is an observable it can be obtained after or during the system is simulated; not when it is defined.

As someone with a background in dynamical systems, I am actually confused about how I would write my dynamical system in the form expected by this library as it brings ambiguity with the multiple ways to introduce time dependence and the odd y equation that isn’t actually required to solve the system.


Lastly, I want to address the following because I’ve heard it before

… the reality is that most users won’t need a lot of the features offered by these packages and SimpleSim offers a lightweight alternative with a low entrance barrier …

This doesn’t stand up to me as clear cut as presented. Just because something has features people don’t need it isn’t a negative for them. What matters is that it has features they do need. If it has all the features they do need, all the extra features are a potential positive as in the future they may need much more features as their project evolves.

The entrance barrier is not something decided by the total number of features of a software. It is something dictated primarily by the documentation and design of a software. If the small and introductory subset of features is accessible by being designed intuitively and documented well in a dedicated self-contained page, then it shouldn’t matter at all if there is an entire universe of hard-to-understand advanced features in another documentation page. When I look at the tutorial for simulating the Lorenz-63 system with OrdinaryDiffEq.jl, there is nothing complex there. And there is also no ambiguity there either. Honestly, it is super trivial, despite OrdinaryDiffEq.jl genuinely being a massively complex library.

3 Likes

Hey @Datseris,
I must admit I got a bit excited when I saw that you commented on here; big fan of your work. So thanks for taking the time!

First, let me explain why I did a few things the way I did in a little bit more detail.

An important note here is that DynamicalSystems.jl has nothing to do with modelling.

Yes, SimpleSim.jl aims at providing a simulation framework, not an analysis framework for nonlinear systems like your DynamicalSystems.jl does. So the packages only have very little overlap.

It also appears to me that SimpleSim.jl made a deliberate choice to not conform the “state functions” f/g to the format that is currently established by SciML of f(u, p, t).

Indeed. SimpleSim separates states from inputs. This is very helpful, especially when designing control systems, or just systems with feedback in general. That was the main use case I had in mind for SimpleSim (mostly because that is my own area of research). Systems without the need for inputs can simply get passed nothing for the input at all times. Sometimes, we can just extend the idea of a state vector to also contain inputs, but if you frequently work with systems that need inputs, it’s helpful to take care of them explicitly.
I would actually say that the fact that SciML (etc.) don’t have explicit support for inputs built in is a huge disadvantage.

Can a model created by SimpleSim.jl be casted into the standard form used by OrdinaryDiffEq.jl / DynamicalSystems.jl of a single state function f(u, p, t) ?

Currently, no. Or rather, you would have to write the function to cast a SimpleSim model into OrdinaryDiffEq form yourself. This is a great idea for a feature, though. I will add that to the list.
One minor issue I can see with this, however, is that SimpleSim doesn’t export any types, neither concrete nor abstract. So, a feature like this would have to be a function to_standard_form(...) and cannot overload the convert function. But this is not a deal breaker.

I would argue it is probably more safe to have a type in the first place since so much specificity is enforced.

Leaving this up to the user was a very deliberate choice.
But maybe SimpleSim could export types CTModel, DTModel and HybridModel, also as guidance for people who don’t know the package well yet. It would still accept NamedTuple models or custom types with the same fieldnames, of course. I will think about this.

Also, why do you need to have fc for continuous systems and fd for discrete, instead of using f for both? Same for xc0 and xd0 ?

SimpleSim supports models with both CT and DT dynamics (“hybrid models”). Therefore, this distinction is important.

Why does the fields of the named tuple are named fd, yd instead of fd, gd since one function is f and the other is g .

This is a good point. yc should be named gc. The same is true for yd. This is something I will correct immediately, hoping that I’m still the only user of the package.

Why is u expected as an input to both functions? What even is u ? What’s y ?

x is the state, u is the input, y is the output.
The input function can be whatever you want and is passed to simulate as a (t) -> ... function.
If you don’t need it, SimpleSim will just call fc etc. with u = nothing. But if you do need an explicit input, it’s a very useful feature to have, in my opinion.

y is simply computed as a function of x, u, p and t. Again, if you don’t need an “output” of your system, you can simply pass (x, u, p, t) -> x for yc and get back the whole state vector. But it can be useful to have a separate output that isn’t the state vector itself; for example when simulating a sensor that “knows” about the true state but the output is some partial or noisy version of the state.
This is again very helpful when simulating real-world systems for controller synthesis.
Also, the yc function is very important when building nested models, as any submodels have to be called from within a parent model’s yc function. This is because yc preserves the current model’s state. The same is not true for fc functions.

As someone with a background in dynamical systems, I am actually confused about how I would write my dynamical system in the form expected by this library

Do you have a specific example in mind? y and u can be ignored, if you have no need for them.
For example, the forced Van-Der-Pol oscillator in the SimpleSim convention is shown below.

fc_vdp = (x, u, p, t) ->  [x[2], p.ε * (1 - x[1]^2) * x[2] - x[1] - u]
yc_vdp = (x, u, p, t) -> x
van_der_pol_oscillator = (
    p = (ε = 0.3),
    fc = fc_vdp,
    yc = yc_vdp,
)

Now, if you want to simulate the oscillator with a forcing u(t) = A sin(w*t) for 10 seconds, you can call simulate as follows.

A = 1.5
w = 1.0
forcing = (t) -> A * sin(w*t)
data = simulate(van_der_pol_oscillator, T = 10 // 1, xc0 = [0, 1], uc = forcing)

You could omit the input u and compute the forcing inside fc using t, I think this is what you meant. But having u as an explicit input enables other features, such as nested models.
Note that you can only omit uc or ud from simulate. The structure of fc and yc must include u.

The entrance barrier is not something decided by the total number of features of a software.

I agree. Currently, SimpleSim lacks some of the features of OrdinaryDiffEq and other packages. I just meant that the features it is lacking are probably not missed by the majority of users. But that may only reflect my view on the topic. I think the way to alleviate the lack of complex features is to build an interface to OrdinaryDiffEq and SciML in general. This comes back to one of your earlier comments.
Having an interface would allow simpl(er) systems to be simulated with SimpleSim without any additional dependencies, while complex systems could leverage the “best of both worlds”.

Thank you so much for taking the time to review SimpleSim. I really appreciate the detailed feedback and I’m sure some of it will make it into the package.

Best,
Jannes

4 Likes

Update:
I just released v0.1.4 introducing the breaking change renaming yc to gc and yd to gd, which is more consistent with the nomenclature used in the documentation.
Also this helps avoid confusion between functions (fc/gc/fd/gd) and data (xcs/ycs/xds/yds).

1 Like

I think you are missing an important point here, elaborated upon by @janneshb above, we are dealing with input-output systems here. ModelingToolkit also adds the input u and the output y where they are needed, for example: Input output · ModelingToolkit.jl

The particular format
io
is extremely common, you will find it

Also, you may be familiar with diagrams like this?
image
notice how these blocks have an input and an output, not only an internal state?

4 Likes

Hi @baggepinnen, thanks for your comment!
Happy to have some Julia legends united in the comment section :wink:

This all seems to depend on your specific background. For control systems it’s invaluable to have explicit inputs and outputs. In other fields they might be unnecessary.

As I said before, you can just leave the input unused, but it may be worth considering adding support for functions (x, p, t) -> ..., if that’s something a lot of people ask for.