An input-output-system is characterized by a set of equations. These equations can be either first order ODEs or explicit algebraic equations.

\[\begin{aligned} \dot{\mathbf x}(t) &= f(\mathbf x(t), \mathbf y(t), \mathbf i(t), p)\\ \mathbf y(t) &= g(\mathbf x(t), \mathbf y(t), \mathbf i(t), p) \end{aligned}\]

such system contains of

  • states ($x$ and $y$) and
  • parameters ($i$, $p$).

States are determined by the given equations (i.e. the equations describe how the states change). Parameters are externally given. For IO systems we define subgroups

  • states
    • internal states (istates) which are meant for internal use
    • output states (outputs) which might be used as inputs for other systems
  • parameters
    • internal parameters (iparams) which are typically constant and
    • inputs (inputs) which can be connected to the outputs of other systems.


The base type for the BlockSystems is AbstractIOSystem with the has two concrete implementations.

An IOBlock consists of a set of equations, a set of inputs and outputs and a name.

struct IOBlock <: AbstractIOSystem

A basic IOSystem which consists of a single ODESystem.

  • name::Symbol

  • inputs::Vector{SymbolicUtils.Symbolic}

  • iparams::Vector{SymbolicUtils.Symbolic}

  • istates::Vector{SymbolicUtils.Symbolic}

  • outputs::Vector{SymbolicUtils.Symbolic}

  • system::ODESystem

  • removed_states::Vector{SymbolicUtils.Symbolic}

  • removed_eqs::Vector{Equation}

IOBlock(eqs, inputs, outputs; name, iv, warn, rem_eqs)

Construct a new IOBlock for the given arguments.

using BlockSystems, ModelingToolkit
@parameters t i(t)
@variables x(t) o(t)
D = Differential(t)

iob = IOBlock([D(x) ~ i, o ~ x], [i], [o], name=:iob)

An IOSystem consists of multiple AbstractIOSystems and the connections between them.

struct IOSystem <: AbstractIOSystem

A composite IOSystem which consists of multiple AbstractIOSystem which are connected via a vector of namespaced pairs (subsys1.out =>

An IOSystem contains maps how to promote the namespaced variables of the subsystem to the new scope subsys1₊x(t) => x(t) subsys1₊y(t) => subsys1₊y(t) subsys2₊y(t) => subsys2₊y(t)

  • name::Symbol

  • inputs::Vector{SymbolicUtils.Symbolic}

  • iparams::Vector{SymbolicUtils.Symbolic}

  • istates::Vector{SymbolicUtils.Symbolic}

  • outputs::Vector{SymbolicUtils.Symbolic}

  • removed_states::Vector{SymbolicUtils.Symbolic}

  • connections::Vector{Pair{SymbolicUtils.Symbolic, SymbolicUtils.Symbolic}}

  • namespace_map::Dict{SymbolicUtils.Symbolic, SymbolicUtils.Symbolic}

  • systems::Vector{AbstractIOSystem}


Construct a new IOSystem from various subsystems. Arguments:

  • cons:

    The connections in the form sub1.output => sub2.input. It is also possible to use simple algebraic equations such as sub1.o1 + sub2.o2 => sub3.input.

    If :autocon, BlockSystems will automaticially create connections based on matchin output and input names. This requires the output name to be unique. The same output may be connected to several inputs with the same name.

  • io_systems: Vector of subsystems

  • namespace_map: Provide collection of custom namespace promotions / renamings e.g. sub1.input => voltage. Variables without entry in the map will be promoted automatically. Automatic promotion means that the sub-namespace is removed whenever it is possible without naming conflicts. The map may contain inputs, outputs, istates, iparams and removed_states. The rhs of the map can be provided as as Symbol: sub1.input => :newname.

  • Specify outputs of composite system:

    outputs=:all (default): All of the subsystem outputs will become system outputs.

    outputs=:remaining: Outputs, which have been used in connections won't become outputs of the composite system.

    outputs=[sub1.o, sub2.o]: only specified outputs will become outputs of composite sytem. All other sub-outputs will become internal states of the connected system (and might be optimized away in connect_system).

  • name: namespace

  • autopromote=true: enable/disable automatic promotion of variable names to system namespace

  • globalp=Symbol[]: List of symbols, which represent system-wide parameters (iparams or inputs). All occurences in subsystems will be promoted to the system namespace (i.e. if globalp=[:K], each component block which has some parameter :K will be represented by the same parameter rather than) blk1.K and blk2.K.



Each IOSystem can be transformed into an IOBlock. At this step the actual manipulation of the equations happen.


Recursively transform IOSystems to IOBlocks.

  • substitute inputs with connected outputs
  • try to eliminate equations for internal states which are not used to calculate the specified outputs of the system.
  • try to eliminate explicit algebraic equations (i.e. outputs of internal blocks) by substituting each occurrence with their rhs. Explicit algebraic states which are marked as system outputs won't be removed.


  • ios: system to connect
  • verbose=false: toggle verbosity (show equations at different steps)
  • remove_superflous_states=true: toggle whether the system should try to get rid of unused states
  • substitute_algebraic_states=true: toggle whether the algorithm tries to get rid of explicit algebraic equations
  • substitute_derivatives=true: toggle whether to expand all derivatives and try to substitute them
  • simplify_eqs=true: toggle simplification of all equations at the end

There are also other transformations available

replace_vars(blk::IOBlock, p::Dict; warn=WARN[])
replace_vars(blk::IOBlock, p::Pair; warn=WARN[])
replace_vars(blk::IOBlock; warn=WARN[], v1=val1, v2=val2)

Replace variables, either rename them by giving a new Symbol or replace them by actual numerical values (only possible for inputs and iparams). Returns new IOBlock.

Keys of dict can be either Symbols or the Symbolic subtypes. I.e. blk.u => 1.0 is as valid as :u => 1.0.

replace_vars(blk; π = 3.14, foo = :bar) # set blk.π to number and rename foo
replace_vars(blk, Dict(:π => 3.14, :foo = :bar))
replace_vars(blk, blk.π => 3.14)
set_input(blk::IOBlock, p::Pair; verbose=false)

Close an input of blk. Given as an pair of (input=>substitution). The input may be given as an symbol (i.e. :a) or symbolic (i.e. blk.a). The substitution term can be either a numer or a term of parameters (which will become internal parameters).

remove_superfluous_states(iob::IOBlock; verbose=false, warn=WARN[])

This function removes equations from block, which are not used in order to generate the outputs. It looks for equations which have no path to the outputs equations in the dependency graph. Returns a new IOBlock.

The removed equations will be not available as removed equations of the new IOBlock

TODO: Maybe we should try to reduce the inputs to.

substitute_algebraic_states(iob::IOBlock; verbose=false, warn=WARN[])

Reduces the number of equations by substituting explicit algebraic equations. Returns a new IOBlock with the reduced equations. The removed eqs are stored together with the previous removed_eqs in the new IOBlock. Won't reduce algebraic states which are labeled as output.

substitute_derivatives(iob::IOBlock; verbose=false, warn=WARN[])

Expand all derivatives in the RHS of the system. Try to substitute in the lhs with their definition.


D(o) ~ 1 + D(i)   =>  D(o) ~ 2 + a
D(i) ~ 1 + a          D(i) ~ 1 + a

Process happens in multiple steps:

  • try to find explicit equation for differential
  • if none found try to recursively substitute inside differential with known algebraic states
  • expand derivatives and try again to substitute with known differentials

Function building


Generate callable functions for an AbstractIOSystem. An IOSystem will be transformed to an IOBlock first. At this level there is no more distinction between internal states and outputs: states=(istates ∪ outputs).


  • ios: the system to build the function


  • type=:auto: :ode or :static, determines the output of the function
  • f_states: define states=(istates ∪ outputs) which should appear first
  • f_inputs: define inputs which should appear first
  • f_params: define parameters which should appear first
  • f_rem_states: define removed states algebraic state order
  • expression=Val{false}: toggle expression and callable function output
  • warn=WARN[]: toggle warnings for missing f_* parameters

Returns an named tuple with the fields

  • for type=:ode:
    • f_ip in-place function f(dstates, states, inputs, params, iv)
    • f_oop out-of-place function f(states, inputs, params, iv) => dstates
  • for type=:static:
    • f_ip in-place function f(states, inputs, params, iv)
    • f_oop out-of-place function f(inputs, params, iv) => states
  • always:
  • massm mass matrix of the system (nothing if :static)
  • states symbols of states (in order)
  • inputs symbols of inputs (in order)
  • params symbols of parameters (in order)
  • rem_states symbols of removed states (in order)
  • g_ip, g_oop functions g((opt. out), states, inputs, params, iv) to calculate the removed states (substituted expl. algebraic equations). nothing if empty.

Block specifications

Sometimes it is useful to define a the input/output structure for a block.

struct BlockSpec
BlockSpec(in::Vector, out::Vector; in_strict=true, out_strict=false)

Block specification, defines which inputs/outputs an AbstractIOSystem should have. Contains two vectors of Symbols. Can be initialized with Vectors of Symbols, Num or <:Symbolic.

If strict=true the in/outputs must equal the specification. If strict=false the block must contain the in/outputs from the specification.

Object is functor: call (::BlockSpec)(ios) to check whether ios fulfills specification. See also fulfills.

iob = IOBlock(...)
spec = BlockSpec([:uᵣ, :uᵢ], [:iᵣ, :iᵢ])
fulfills(iob, spec)