Model or dataset
simupy/simupy avatar
simupy/simupy

simupy: dynamical system simulation with the equations left in code

A framework for modeling and simulating dynamical systems

649 stars82 forksPythonBSD-2-Clause

At a glance

What is it?
SimuPy is a BSD-2-Clause Python framework for simulating interconnected dynamical systems, where a model is an object with state, input and output equations and can be written directly as SymPy expressions that are compiled to callables. It suits engineers and students who want a Simulink-style block diagram without a licence, and it is a numerically intensive library rather than a general modelling environment.
Who is it for?
simupy fits an engineer or graduate student who wants to write a dynamical system model as equations, connect several systems into a block diagram and integrate the result in Python, without a licence fee and without learning a graphical tool.
Can I use it commercially?
Yes. BSD-2-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 151 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A model is an object, not a diagram

SimuPy is a framework for simulating interconnected dynamical system models, providing an open source Python-based tool for model- and system-based design and simulation workflows.

The design decision that sets it apart from a numerical library is that a model is an object with a defined interface, documented in the API documentation. That interface has state equations, input equations and output equations, and a system is anything that implements it. Models can also be constructed using symbolic expressions, and a helper class does the compilation work.

The worked example in the documentation is a three state system with one input:

python
    from sympy.physics.mechanics import dynamicsymbols
    from sympy.tensor.array import Array
    from simupy.systems.symbolic import DynamicalSystem

    x = x1, x2, x3 = Array(dynamicsymbols('x1:4'))
    u = dynamicsymbols('u')
    sys = DynamicalSystem(Array([-x1+x2-x3, -x1*x2-x2+u, -x1+u]), x, u)

The array of three expressions is the state derivative as a function of the three states and the input, and passing it to DynamicalSystem automatically creates callable functions for the state equations, the output equations and the jacobians. That last part is the useful bit. A Jacobian computed symbolically and then compiled is not a convenience, it is a numerically different object from one obtained by finite differences, and for a stiff system or a long integration it matters.

The code generation backend is configurable. By default it uses a wrapper for sympy.lambdify, and you can change it by passing the code_generator function and additional keyword arguments in a code_generator_args dictionary. The defaults for future systems are module level variables:

python
   import simupy.systems.symbolic
   simupy.systems.symbolic.DEFAULT_CODE_GENERATOR = your_code_generator_function
   simupy.systems.symbolic.DEFAULT_CODE_GENERATOR_ARGS = {'extra_arg': value}

So the generated code is not locked to SymPy's lambdify, and a team that wants Numba, JAX or a custom kernel can plug it in.

Block diagrams and the feedback loop

Interconnection is the point of the framework. A block diagram is built from systems and connections, and a controller is created as a helper class. The documentation gives a linear feedback controller:

python
   from simupy.systems import LTISystem
   ctrl = LTISystem([[1.73992128, 0.99212953,  -2.98819041]])

The gains in that example come from the infinite horizon LQR based on the system linearized about the origin, which tells you the controller is being used the way an engineer would use it in practice, with the gain matrix computed offline and the simulation only closing the loop.

The block diagram is then constructed and the two directions connected:

python
   from simupy.block_diagram import BlockDiagram
   BD = BlockDiagram(sys, ctrl)
   BD.connect(sys, ctrl) # connect the current state to the feedback controller
   BD.connect(ctrl, sys) # connect the controlled input to the system

Two connect calls and a loop exists. That is the whole point of the block diagram abstraction, and it is what makes a multi-system model writable in a dozen lines rather than as a tangle of signal routing inside a simulation function.

Initial conditions for systems with a non-zero dimensional state are set on the system, and they default to zeros of the appropriate dimension:

python
   sys.initial_condition = [5, -3, 1]
   res = BD.simulate(10)

The simulate call takes the final time. The result is a SimulationResult instance with array attributes t, x, y and e holding time, state, output and event values for each integrator time step. The first axis indexes the time step, and for x, y and e the second axis indexes the individual signal components, ordered first by the order each system was added to the block diagram, then by the system state and output specification. That ordering rule is worth internalising, because it is how you map a column of the result array back to a physical quantity once the model has more than a handful of signals.

The solver, the result arrays and the event axis

The integration backend is SciPy. The default solver for the initial-valued problem is scipy.integrate.ode, and the default solver is dopri5 with dense output. A different integrator_class and integrator_options can be used as long as the class supports a subset of the scipy.integrate.ode API, and the defaults for future simulations can be changed following the same pattern as the symbolic code generator options.

That API constraint is worth reading carefully. It is not the modern scipy.integrate.solve_ivp interface, and it is not a general odeint contract either. The project pins itself to a specific shape of SciPy API, and the dopri5 choice is a good one for non-stiff problems, but a team with a stiff model will want to check what is available in that API before committing.

The dense output default matters for event handling. Events are found by zero-crossings of the event_equation_function output, and the e array holds event values for each integrator time step. With dense output the solver can locate the crossing between steps rather than only detecting a sign change at a step boundary, which is what you need when a discontinuity has consequences for the state.

SwitchedSystem is the class used to construct systems with discontinuities, and it is built on that event machinery. A bouncing ball, a relay or a controller with a saturation limit are all cases where the continuous model has to be interrupted and restarted, and SimuPy treats that as a first-class concept rather than something you bolt on with a conditional inside your derivative function.

The helper library around the core

A set of utilities exists to construct and manipulate systems and simulation results, and they are worth knowing because they cover the parts of simulation work that are not the equations.

process_vector_args and lambdify_with_vector_args in simupy.utils.symbolic are helpers for code generation using sympy.lambdify. callable_from_trajectory in simupy.utils is a wrapper for making polynomial spline interpolators using scipy.interpolate.splprep, which is how you turn a reference trajectory into something a system can consume. simupy.matrices includes tools for constructing vector systems using matrix expressions and re-wrapping the results into matrix form.

For controllers and other stateless elements, SystemFromCallable is a helper for converting a function to a state-less system, typically a controller, that can then be simulated. MemorylessSystem and LTISystem are subclasses that create these types of systems more quickly, with LTISystem taking a state space matrix, a control matrix, an output matrix and a feedthrough matrix.

The dependency structure is worth noting for anyone deciding whether to install it. The setup.py install_requires is numpy>=1.11.3 and scipy>=0.18.1, and SymPy is not required at all, because much of the functionality works without it. SymPy is an extra, listed under symbolic, and also under doc and examples. The documentation says the same: installation does not require SymPy, while the examples use matplotlib to visualise the results. The core install is therefore NumPy and SciPy, and the symbolic convenience is opt-in.

Installing the released version or the development tree

The installation section distinguishes two things that are easy to confuse. Version 1.0 of SimuPy is pip installable:

bash
   $ pip install simupy

However, the latest features and examples are only available in the development version, and to install that:

bash
  $ git clone https://github.com/simupy/simupy.git 
  $ pip install -e simupy

That warning is the most practically important line in the README for an evaluator. If you pip install simupy and then follow the README's example code and it does not match, the likely explanation is that you have the 1.0 release and the documentation describes the development tree. The editable install from a clone is what the project actually wants you to use if you want to follow the documentation.

The tested versions are stated as Python >= 3.6, NumPy >= 1.11, SciPy >= 0.18 and SymPy >= 1.0, with the note that tests on Travis may run with newer versions. The setup.py raises the floor to python_requires='>=3' and pins numpy>=1.11.3 and scipy>=0.18.1, which is more conservative than the README's prose.

Testing uses pytest, the examples use matplotlib, and the documents are built with Sphinx version 1.6.3, which is a very old pin. The examples subdirectory contains worked problems including bouncing_ball.py, discrete_lti.py, linearcontrol_nolinearsys.py, pid.py, riccati_system.py, simple_saturation.py and vanderpol.py, and those files are the fastest way to see what the framework does with a problem you already recognise.

Where SimuPy is the wrong tool

The first case is a team that wants a visual modelling environment. The repository topics include simulink, and SimuPy is the open source answer to that category of tool, but it is an answer for people who prefer code. There is no block diagram editor, no parameter GUI, and no way to hand a model to a colleague who does not write Python.

The second case is hardware. There is no real-time execution, no communication with a controller or a plant, and no code generation target for an embedded device in the description of the project. A control engineer who needs a model that runs on a target, or that talks to a bench, is outside what this framework covers.

The third case is domain component libraries. SimuPy gives you the machinery to define a system and connect it, but not the models. A project needs its own equations, or its own validated component models, written out by hand. A framework that ships batteries-included physics models saves more time than one that only saves the plumbing, and the plumbing is what SimuPy provides.

The alternative worth comparing is not another Python simulation package, it is a different modelling style entirely. SimuPy is explicit and code-first: you write the derivative function and the framework integrates it. A system-level simulation tool that solves the model in a data-flow description instead lets you describe components and their signal flow and generates the equations for you, at the cost of a learning curve and a dependency on a specific tool. The difference in approach is between writing the mathematics and describing the structure. For a researcher who wants to test an equation, the explicit style is faster. For a team documenting a physical system that already exists as a block diagram, the description-first style saves more effort than it costs.

Maintenance and the state of the package

The licence is BSD 2-clause Simplified, as recorded in setup.py and with the LICENSE file at the repository root. For a scientific package that is the ideal choice, since it carries no copyleft obligation and no attribution burden beyond keeping the notice, so a commercial project or a course can vendor it without legal friction. The classifiers confirm the intent with Intended Audience entries for Education and Science/Research, and topic entries for Physics and Mathematics.

The maintenance picture is the main thing to weigh. The repository is not archived, and the last push was on 2026-05-25, which is a little over five months before the present date. There are no GitHub releases, so version tracking runs through the CHANGELOG.rst file in the repository root, and the version number itself lives in simupy/version.py, which setup.py reads by executing that file at build time rather than by parsing the manifest.

The build system is a plain setup.py with setuptools, and the repository has a MANIFEST.in, a requirements.txt with the two core dependencies, separate requirements-test.txt and requirements-doc.txt files, and a .flake8 configuration. The CI is Travis, with a .travis.yml file, and there is a paper/ directory at the top level, which usually means a journal or conference publication describing the framework, and that paper is often the better starting point than the README for understanding the modelling approach.

The docs are hosted on Read the Docs at simupy.readthedocs.io, and the API documentation has its own page. The contributing guide asks for an issue before a pull request, tests with any new feature or fix, and PEP8 compliance, which is a small and reasonable set of expectations for a project of this size.

Editorial conclusion

simupy fits an engineer or graduate student who wants to write a dynamical system model as equations, connect several systems into a block diagram and integrate the result in Python, without a licence fee and without learning a graphical tool. It is the wrong choice for control engineers who need a visual modelling environment with hardware-in-the-loop support, for anyone who needs validated component libraries for specific physical domains, or for a team that needs an actively maintained package, since the last push was on 2026-05-25 and the project has no GitHub releases. Before adopting it, install the development version with pip install -e simupy, because the README states the latest features and examples are only available there, and confirm your SciPy and NumPy versions sit inside the tested ranges before you rely on a long integration run.

Frequently asked questions

How do I install SimuPy?

Version 1.0 is pip installable with pip install simupy, but the documentation warns that the latest features and examples are only available in the development version, installed by cloning the repository and running pip install -e simupy. SymPy is an optional extra, and much of the functionality works without it.

Which Python and library versions does SimuPy support?

The project states it has been tested locally against Python >= 3.6, NumPy >= 1.11, SciPy >= 0.18 and SymPy >= 1.0, with the note that tests on Travis may run with newer versions. The core install requires numpy>=1.11.3 and scipy>=0.18.1.

What solver does SimuPy use for integration?

It uses scipy.integrate.ode as the default solver for the initial-valued problem, defaulting to the dopri5 solver with dense output. A different integrator_class and integrator_options can be used as long as they support a subset of the scipy.integrate.ode API.

How do I build a block diagram of interconnected systems in SimuPy?

Create a BlockDiagram with the systems, then use connect to wire signals between them, for example connecting the system output to the controller and the controller output back to the system. Initial conditions are set on the individual system and default to zeros, and the diagram is run with its simulate method.

What licence is SimuPy released under?

SimuPy is licensed under the BSD 2-clause Simplified licence, with the licence file at the repository root. The repository is not archived, the last push was on 2026-05-25, and the project has no GitHub releases.

Official sources

  1. Issues
  2. License: BSD-2-Clause
  3. README
  4. simupy/simupy on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/simupy-simupy.svg)](https://hysenlabs.com/projects/simupy-simupy)

Was this useful?