GithubHelp home page GithubHelp logo

modypy / modypy Goto Github PK

View Code? Open in Web Editor NEW
3.0 3.0 1.0 2.11 MB

A framework for hierarchical modelling, simulation and analysis of dynamic systems

Home Page: https://modypy.org

License: BSD 2-Clause "Simplified" License

Python 100.00%
simulation simulation-modeling simulation-framework dynamical-systems python block-diagram linear-systems continuous-time discrete-time events

modypy's Introduction

PyPi Version

PyPI - Python Version

GitHub

Build

Documentation Build

Code Coverage

Code Quality Score

Code Grade

MoDyPy (rhymes with "modify") is a Python framework for Modelling dynamic systems in Python. The framework provides methods for describing continuous-time linear and non-linear systems in state-space representation. It was originally inspired by simupy developed by Ben Margolis, but has a completely different philosophy and architecture than simupy.

The basic components of a dynamic system in MoDyPy are states and signals. States represent the internal state of the system, and signals represent the values calculated based on the state. Ports can be connected to signals, so that reusable blocks with input and output ports can be easily built. For more details refer to the documentation or check out the video tutorial.

Main Features

  • Simple architecture based on states, signals and connectible ports
  • Enables hierarchical modelling
  • Allows the establishment of reusable building blocks
  • Simulator for linear and non-linear continuous- and discrete-time systems
  • Clock system to model periodic events and discrete-time components
  • Steady state determination and linearization
  • Library of standard blocks, including 6-degree-of-freedom rigid body motion
  • Tested for 100% statement and branch coverage

Installation

MoDyPy is available via the pip installer:

$ pip install modypy

To install the development version,

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

Examples

Simulation of a DC-motor with propeller

Simulation of a DC-motor with propeller

Check out the examples in the examples directory and the User's Guide. These include:

dcmotor.py

A simple example using a DC-motor driving a propeller and sampling the thrust using a zero-order hold.

rigidbody.py

Some rigid-body simulation using moments and forces showing an object moving in a circle with constant velocity and turn-rate.

bouncing_ball.py

An example modelling a bouncing ball, demonstrating the use of events and event-handler functions.

quadcopter_trim.py

A larger example showcasing the steady-state-determination and linearisation of complex systems, in this case for a quadrocopter frame with four DC-motors with propellers.

They can be run from the sources using, e.g.,

$ pip install matplotlib
$ python examples/bouncing_ball.py

Note that some of the examples require matplotlib to run and display the results.

Contributing

Contributions are welcome! Check out the GitHub Project Page for issues and ideas on how to add to the project.

Contributions must adhere to the following conditions:

  • New features must be accompanied by appropriate pytest tests (ensure 100% statement and branch coverage!)
  • New features should at least carry Python Docstrings for API documentation following the general style of the existing API documentation.
  • Use black with a line-length of 80 to format your code. We are successively moving the project to the black style.
  • Contributors must accept publishing their contribution under the licensing conditions laid out in the LICENSE file.

modypy's People

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

ralfgerlich

modypy's Issues

Have system_jacobian return a structure for more logical access

system_jacobian should return a structure with properly-named fields for the matrices for more convenient access.

For backwards-compatibility, we could overload the single_matrix parameter with a special value (e.g., "structure") to indicate that the caller wants the structure.

Make state, signal, event vectors/matrices more consistent to allow vectorization

In simulation, simple vectors are used to represent all states, signals and event values. In the simulation results, they are represented as matrices, with the row being the time-axis and the column being the state/signal/event axis. This is inconsistent, e.g., with the matrix algebra for LTI, and also makes vectorization of state derivative, signal and event functions more difficult.

For an LTI, the state derivative is represented as A x(t) + B u(t) , with the state x(t) and the input u(t). Thus, both the state and the input are expected to be column vectors. Considering a time series -- such as the result of a simulation -- it is most natural to represent the values of state and inputs at different times as columns of matrices X and U, so that the matrix of state derivatives would naturally be A X + B U.

Similarly, linear outputs can be represented as y(t) = C x(t) + D u(t), and for a time series, Y = C X + D U would still hold. The structure of the formula -- and the code representing the formula -- would not have to change.

NumPy supports vectorization and broadcasting in that manner, and, for example, most of the ODE solvers provided by SciPy also do. If we were to consistently represent state and input vectors as column vectors, we could make use of these features by default without any code-changes on the state derivative, signal and event functions being necessary.

Vectorization would be the default then, and for any function not vectorizable for any reason, the numpy.vectorize function could be used as an adaptor.

This would mostly require changes to the SimulatorResult class, but also would allow us to simplify the linearization implementation.

Document linearisation

Add a chapter to the user's guide explaining linearisation of a system around a steady state

Introduce generic wrapping of functions into signals

Generic functions (such as sin, cos, addition, etc.) are often used to simply map some input signal (or a tuple thereof) to an output. Defining these functions can become tedious:

import numpy as np
# ...

@signal_function
def sin_of_some_signal(state):
    return np.sin(some_signal(state))

@signal_function
def sum_of_two_signals(state):
    return one_signal(state)+another_signal(state)

However, Python allows us to do some dynamic shenanigans here to dynamically generate these signal objects (see, e.g., the generic func object in SQLAlchemy).

A simpler API could look like this:

import numpy as np
# ...

sin_of_some_signal = modypy.func.np.sin(some_signal)
sum_of_two_signals = modypy.func.np.add(one_signal, another_signal)

Introduce basic operators for signals

The Signal class should implement basic operators such as negation, addition, subtraction, multiplication and division so that simple expressions can be written. The operations shall be mapped so that for signals s1 and s2 and constant c the following holds:

  • (s1 op s2)(state)==s1(state) op s2(state)
  • (c op s1)(state)==c op s1(state)
  • (s1 op c)(state)==s1(state) op c

Implement more sophisticated strategies for detecting zero-crossings

when _find_active_events, you carefully handle the required "direction" of the event. However, when _find_event_time, it looks like the direction might be overlooked. If that is the case, then it is possible that _find_event_time returns a time that is earlier than the first valid time, since there may be a root that is earlier, but does not match the direction criteria. Is this possible? If not, then I would certainly appreciate help improving my understanding.

Root-finder setting for Zero-Crossing-Events in Simulator is being ignored

We introduced our own root-finder in the simulator for localisation of zero-crossing-events. This was needed to provide a proper implementation of the tolerance. However, the interface for selecting the root-finder was left in there by mistake.

We should add a warning exception in case anyone makes use of this interface, and update the documentation accordingly, but ignore the setting all in all.

We never really advertised this as a feature, so we might as well drop it.

Try to keep integrator instance if no reset of integration time occurred

Currently, we are creating a new instance of the integrator for each step. The reason for this is that on occurrence of an event we reset integration time, but we cannot expect the integrator to support this (specifically, the ODEsolvers do not).

However, if no event occurred (which will be the case most of the time) or only events without event handlers occurred (which would mean that the system state cannot have changed and integration results are still valid), there is no need to reset integration time. In that case, we could keep the current instance and save the overhead of recreating one for each integration step.

This ticket should be assessed together with #30: Ideally, solver_ivp would lead to better performance and better maintainability.

Describe the simulator main loop in the documentation

A description of the simulator main loop would further understanding of both the simulator and the semantic content of simulation models. Important aspects are:

  • cooperation with the underlying solver for differential equations
  • handling of zero-crossing events
  • handling of clocks
  • output of samples

Allow modelling clock-jitter

Digital control systems contain time-discrete parts due to the discrete nature of digital processing, using microprocessors and synchronous clocks. The actual implementation therefore is an instance of real-time computing, often involving scheduling of multiple periodic processes, including sensor acquisition, filtering, control update, monitoring, command-and-control, etc.

These tasks may interfere with each other, and due to execution times of individual tasks varying between individual activations, response times (the time between activation and conclusion of a task) may vary as well, leading to, e.g., the time of update of the control outputs to vary between cycles.

Thus, to analyse the impact of this jitter on control quality, it might be of interest to be able to model clocks jittering. The jitter would be expressed by varying the time of the next tick by a zero-mean offset around the ideal occurrence of the tick. The actual offset could be selected according to a number of different statistical models, including uniform distribution over a given interval.

Steady-State finding: Make constraints explicit

Currently, inputs and constraints for steady-state-determination and linearization need to be specified in the model. However, this is clumsy, and most specifically, expressing steady-state-constraints as OutputPort instances has the disadvantage that the model may need to re-established after finding a steady state if one wants to perform linearisation with output signals that differ from those signals which have been constrained.

Instead, we should make constraints explicit in the configuration of the steady-state-determination algorithm. A new interface could introduce a configuration object for the specification of the problem:

from modypy.steady_state import SteadyStateConfiguration, EqualityConstraint, MinSquareConstraint
from modypy.linearization import system_jacobian, JacobianConfiguration

system = ...
block = SomeBlock(system, ...)
input_1 = InputSignal(system, ...)
input_1.connect(block.some_input)
output_1 = OutputPort(system, ...)
output_1.connect(block.some_output)

# Create a configuration for the steady-state finder
steady_state_config = SteadyStateConfiguration(system)
# Add a constraint that forces the value of block.some_signal to be 10
steady_state_config.add_constraint(EqualityConstraint(signal=block.some_signal, value=10))
# Add a constraint to minimize the value of the square of a given signal
steady_state_config.add_constraint(MinSquareConstraint(signal=block.some_other_signal))
# Find the steady state given the constraints
result = find_steady_state(steady_state_config)
print("Value of input_1: %s" % result.inputs[input_1.input_slice])
print("Value of some_state: %s" % result.states[block.some_state.state_slice])
print("Value of some_signal: %s" % result.signals[block.some_signal.signal_slice])

# Find the jacobian at the given steady state
jac = system_jacobian(system=system, state=result.states, inputs=result.inputs, single_matrix=False)
print("some_state -> output_1: %s" % jac.output_matrix[output_1.output_slice, block.some_state.state_slice])

Investigate using `scipy.signal.sosfilt` for more efficient filtering

SciPy has a Cython implementation of the filtering algorithm, which allows applying a single filter to a possibly multi-dimensional time-series. In modypy.blocks.filters.IIRFilter, we re-implement that algorithm in Python, working on a single time step of a multi-dimensional filter.

Using the code from SciPy instead of our own might improve maintainability by reducing code-duplication. We should investigate whether using the SciPy code - in a wrapper allowing the application to a multi-dimensional filter - could also help performance, and based on that evidence decide whether to use the SciPy code.

Check whether Simulator can be simplified by using scipy.integrate.solve_ivp

solve_ivp is a frontend to the ODESolver classes that also allows event detection. Check whether that can be used to replace most of the code in Simulator.

One aspect to check is the way how event times are determined. We are using a specific time bounding model to avoid issues with advancing time. We need to see how solve_ivp performs here.

Also, the terminate attribute of the event functions could be set based on whether there are any listeners registered on the event.

Implement dead-band for zero-crossing events

Numerical inaccuraccies may lead to the value of an event function changing its sign just due to noise.

To avoid that, event values within a dead-band around zero should be considered to be zero. A sign change should only be considered when that clamped down value changes its sign.

The size of the dead-band should be configurable on the zero-crossing-event object,

Example for control-loop design using python-control

Add an example to the user's guide where a control loop is designed for a non-linear plant using pycontrol.

The steps would be:

  • establishment of the model
  • determination of the steady state
  • linearisation
  • simplification using python-control
  • eigenvalue placement using python-control
  • extension of the model by the controller
  • simulation of the controlled system

Replace signals by simple callables

Right now, signals are explicitly represented as objects and have indices assigned on the system level. In the theory of dynamic systems, signals are just functions of time, the system state and system inputs: y(t) = h(t, x(t), u(t))

Representing them as actual callables would have several advantages:

  • It would be a more natural way of defining signals in Python. One would only have to define a function (or any other callable) with appropriate parameter(s), and would be able to use that anywhere for a signal without having to introduce an object for it.
  • It would optimize resource usage, both in storage and in execution time. Currently, the evaluator and the simulation result object introduce arrays that accomodate the whole signal vector, and all signals are evaluated in every simulation step - even if they are actually not used. Case in point: The DC-motor and the propeller model provide several convenience signals -- current, power -- that might not get used in every model. Further, a future atmospheric model will provide many signals that would not be used in all models.
  • It would reduce overhead in much of the API implementation. Right now, there is a lot of complexity in handling signals, including index registration and a lot of indirection happening due to us supporting the whole dictionary access elements.
  • It would reduce complexity of the API. We would be able to drop the Signal and Port classes, and also simplify the System class.

The only advantage of representing them explicitly at system level is caching of their values in the evaluator as well as in the simulator result. However, a more pythonic way of implementing that would be to allow data objects to optionally provide caching, and to implement a decorator that allows a function to make use of this optional caching feature if available.

Another aspect to consider on a change is the use of ports for connecting signals. However, ports are actually only necessary to resolve circular dependencies, and there is another way of solving that problem. Consider, for example, the example with the propeller and the DC-engine. Here, the torque from the propeller must be fed back into the DC-engine as braking torque, while the DC-engine needs to provide the speed to the propeller. In both cases, place-holder properties can be declared in either model, which can afterwards be assigned the specific signal method of the respective other component:

prop = Propeller(system, ...)
dcmotor = DCMotor(system, braking_torque=prop.braking_torque, ...)
prop.speed_rps = dcmotor.speed_rps

Here, braking_torque and speed_rps would be methods on the respective objects that evaluate to the braking torque and speed in revolutions per second based on the data in the data object. The respective receptables on the other object (speed_rps on the Propeller object and braking_torque on the DCMotor object) would be initializable via keyword parameters to the constructor, and would default to None. This would automatically lead to an appropriate exception when using a non-connected port.

Note that in that case, the order of initialization would also not be relevant:

dcmotor = DCMotor(system, ...)
prop = Propeller(system, speed_rps = dcmotor.speed_rps, ...)
dcmotor.braking_torque = prop.braking_torque

Simplify standard blocks

Most of the standard blocks in modypy.blocks.linear do not need to be implemented as blocks. Instead, the better interface would be a function that returns a signal.

For example, the Sum block could be replaced by a sum function:

sum_signal = sum((signal_1, signal_2, signal_3))
diff_signal = sum((signal_1, signal_2), factors=(1, -1))

The Gain block could be replaced by a gain function:

amplified_signal = gain(input_signal, gain=10.0)

This would ease generation of systems tremendously.

Properly handle discrete-time only systems

The current situation: Whenever a system only contains discrete-time states, i.e. states with a zero derivative, the integrator will select a very small step (see select_initial_step in https://github.com/scipy/scipy/blob/v1.6.0/scipy/integrate/_ivp/common.py). This leads to the integrator taking a lot of time to advance between timesteps.

Note that there is no difference for mixed systems with at least one continuous state. In that case, the zero derivatives of discrete-time states do not influence the step selection, i.e. the step size selected is the same as it would be if only the continuous state was present.

So, to properly handle discrete-time only systems, we should avoid running the integrator for these and instead simply take a step to the time of the next event.

State, signal and event matrices are inconsistent with numpy broadcasting rules

In #21 we argued that the last index should be the one iterating over the time, but looking at it again, that's inconsistent - among others - with the numpy broadcasting rules. We might have been led to the wrong conclusion by focussing on matrix multiplication in LTI systems, which isn't appropriate anyway. Instead, we should be using the numpy.dot-function for these kinds of multiplications.

Numpy broadcasts by prepending additional axes. This means that an operation - such as a matrix multiplication - applied to a time series should have the time index at its first index.

The thing that we struggled with in the original issue #21 was that multiplying a matrix with a series of vectors (such as in the LTI-system implementation) would only work properly with numpy.matmul, if we had the time index being the last one instead of the first. However, in that case, numpy.dot would follow the proper rules:

If a is an N-D array and b is a 1-D array, it is a sum product over the last axis of a and b.

If we were to multiply a sequence of matrices with a sequence of matrices, numpy.matmul would be appropriate:

If either argument is N-D, N > 2, it is treated as a stack of matrices residing in the last two indexes and broadcast accordingly.

Also, having the time index run in the first dimension would be consistent with the dictionary access to SimulatorResult. Otherwise, signal(result[0])==signal(result)[...,0].

So, as unfortunate changing the convention again and breaking compatibility again might be, it's the more consistent choice.

Allow construction of LTIs from `scipy.signal.lti` objects.

SciPy already includes classes for LTIs in various formats (state-space, transfer-function, zero-pole-gain-form) and translation between them. Also, the way SciPy handles the different ways of specifying system matrices may be a bit more elegant than what we are currently doing with a lot of LoC. We only need to make sure that specifying scalars for A, B, C and D will give us scalar shapes for inputs, states and outputs as they do now.

The idea is that we should still be able to specify A, B, C and D directly, while also allowing StateSpace, TransferFunction and ZerosPolesGain objects to be passed. The latter two would be automatically transformed into state-space representation using the appropriate functions from scipy.signal.

Implement discrete-time IIR filter component

The filter component should apply a discrete-time IIR filter to an input signal or vector of input signals.

This should be usable with filters designed using scipy.signal, so we need to support transfer functions as well as zero-pole-gain and second-order-section form. Internally, we should prefer second-order-section form for numerical reasons. Transfer functions and zero-pole-gain form can be converted to second-order-section form using tf2sos and zpk2sos from scipy.signal.

A good choice for the API might be:

system = System()
filter_sos = scipy.signal.butter(2, 0.25, output='sos')
tick_source = Clock(...)
some_signal = ...
filtered_signal = IIRFilter(system, signal=some_signal, filter=filter_sos, format='sos')
filtered_signal.trigger.connect(tick_source)

Update for Python 3.10/3.11, drop 3.6/3.7

We should check that MoDyPy works with current Python versions.

  • LTS for 3.6 and 3.7 has ended
  • 3.10 and 3.11 are available as stable releases
  • 3.12 is in development

Investigate improved handling of Zeno behaviour

Zeno behaviour occurs when zero-crossing events (and associated changes of system state) occur at ever-decreasing time-distances from each other. This may lead to stalling the progress of simulation time.

So far, we detect excessive occurrence of zero-crossing events, and throw an exception if this occurs. However, there are strategies for resolving some types of Zeno behaviour. An overview is given in https://doi.org/10.3182/20080706-5-KR-1001.01346 (thanks @albert-mathews for pointing this out).

Implementation of these strategies in MoDyPy should be investigated.

Linearization: Make outputs explicit

Currently, to identify the outputs to be considered for linearization, we have to define OutputPort instances. This way, the configuration aspects of a single analysis aspect infiltrate the model. To avoid this, we might want to introduce an explicit configuration of outputs for the linearization algorithm. This could look like this:

from modypy.linearization import LinearizationConfiguration, system_jacobian

block = SomeBlock(system, ...)
input_1 = InputSignal(system, ...)
input_1.connect(block.some_input)

# Set up the configuration for linearization
config = LinearizationConfiguration(system=system, time=0, state=some_state, inputs=some_inputs)
# Set the default step size for numerical differentiation
config.default_step_size = 0.5
# Add an output
output_1 = config.add_output(block.some_output)
# Add another output with different step size
output_2 = config.add_output(block.some_other_output, step_size=0.1)

# Perform the linearisation
result = system_jacobian(config)

# Print the results
print("System Matrix: %s" % result.system_matrix)
print("Input Matrix: %s" % result.input_matrix)
print("Output Matrix for output_1: %s" % result.output_matrix[output_1.output_slice])
print("Feedthrough Matrix for output_1: %s" % result.feedthrough_matrix[output_1.output_slice])

Introduce an exception for simulation failure

Currently, we simply return a message from the step or run_until functions to indicate an error. We should instead introduce an exception to indicate simulation failure, so that we can focus on the normal execution flow. It would also make introduction of the iterator/generator interface simpler (see #22).

Replace the arrays from steady-state configuration

Currently, bounds on signals and states need to be specified in arrays in the steady-state configuration. A more natural way of doing this would be to introduce constraint objects for constraining individual signals or states. This would also allow the steady-state finding to become more independent of the internal vector representation of the system state.

Support steady-state determination for discrete-time states

For mixed systems, the steady state would be defined as the state where all derivatives are zero and no clock event would lead to a state change. Thus, for a steady state the following entities must become zero:

  • The derivative of the system state,
  • the difference of the system state before and after each clock event, and
  • the output signals.

Note:

  • Each clock event may change all the states of the system, so that the difference must be considered for all the states.
  • The order in which clock events are considered is irrelevant as for the steady state the occurrence of any event must not be observable.
  • Zero-crossing events do not need to be considered, as in steady state neither states nor inputs change, and therefore event functions - which are only dependent on these - will not change either.

However, the user would have to take care so that the steady state can be reached from the initial state specified without any zero-crossing events occurring.

Implement International Standard Atmosphere/ICAO Standard Atmosphere as a block

Input should be height, which should be configurable to be either geopotential or geometric height.

Outputs should be at least

  • pressure
  • density
  • temperature
  • dynamic viscosity
  • kinematic viscosity
  • speed of sound

All inputs and outputs should be in SI-units.

It seems, a good implementation of these standard atmospheres is available in the ambiance package.

Consistently handle scalar states, ports and signals

Signals and states currently by default have shape 1, which means that they actually represent a one-dimensional array with a single element. Because reshaping is currently not consistently implemented, this did not have much of an impact. However, once we properly implement reshaping when accessing states and signals, we will have scalar signals and states return matrices of size 1, which will lead to unexpected results when multiplying vectors and matrices with that, as it will lead to an additional dimension popping up in the result.

We should therefore introduce the special shape () for scalars, as it is used in numpy. The respective functions should then also return scalars instead of arrays. This might require some special-case handling, but it'll improve the API.

Add a Simulator option to suppress intermediate samples

At present, the Simulator will yield all intermediate samples generated by the ODE solver, in addition to any samples implied due to zero-crossing events or clock ticks. In some cases, these intermediate samples may not be desired, e.g., when generating regularly-spaced samples for an animation.

Thus, it would be useful to deactivate intermediate samples in order to only generate samples when at least a zero-crossing or clock event occurred.

Re-implement simulation using iterator concepts

Ideally, a simulator should just yield the simulation steps iteratively. For example, the following could run the simulation from time 0 to time 10 and print out intermediate results:

simulator = Simulator(system, start_time=0)
for item in simulator.run_until(time_boundary=10):
    print(item)

Also, we could decouple the Simulator and the SimulationResult classes. The latter could just accept an iterator and collect all the results:

simulator = Simulator(system, start_time=0)
result = SimulatorResult(simulator.run_until(time_boundary=10))

Introduce more general way of evaluating signals and states

Currently, there are several different ways of evaluating the value of a signal, a state or a state derivative. One is to use an Evaluator instance and call get_port_value, get_state_value or any of the other functions there. Another - in case of SimulationResult - is to access the signals or state property using signal_slice or state_slice. Yet another function is to use a DataProvider instance, and specifically use the states and signals dictionaries there.

This is all quite a mess, and we should get rid of that in favour of a more general and more generic way of accessing these. Why not make port and state instances callables:

system = ...;
dcm = State(system,shape=(3,3))
simulator = Simulator(...)
simulator.run()

# Print the whole trace of the dcm state
print("dcm output=%s" % dcm(simulator.result))
print("dcm derivative=%s" % dcm.derivative(simulator.result))

The respective objects would still have to implement some methods for dispatch, such as get_port_value or get_state_value, get_state_derivative or similar, but we would be able to make the interface more generic and hide all that mechanism behind it.

Note that in the example above, the whole result vector with shape (n,3,3) would be returned, where n is the number of time-points acquired during simulation. We could do similar things with states and signals for evaluation during simulation, and thereby allow vectorisation of calculations. However, that should only be done if the respective state or signal is marked to support this.

Otherwise, a derivative function could be written as follows:

system = ...;

def omega_dt(data):
    return -GRAVITY/LENGTH * np.sin(alpha(data))
    pass
...
# Create the alpha state
alpha = State(system,
              derivative_function=alpha_dt,
              initial_condition=ALPHA_0)

# Create the omega state
omega = State(system,
              derivative_function=omega_dt,
              derivative_vectorized=True,
              initial_condition=OMEGA_0)

Introduce an integrator element

Introduce a function integrator in modypy.blocks.linear that accepts a signal as input and returns a state signal that represents the integrated value of the input signal.

Introduce an element for band-limited white noise

The element should produce band-limited white noise with a given correlation time constant and a given expected variance of the output. This can be done using the following AR(1)-process:

x(t+dt) = delta(dt) * x(t) + e(t, dt)

with e(t,dt) ~ N(0,sigma(dt)^2), delta(dt)=exp(-t/tau) and sigma(dt)^2 = E<x(t)^2> * (1-delta(dt)^2), where tau is the correlation time constant.

Note that the limit of sigma(dt)^2 is zero for dt -> 0, so transformation into a derivative of a continuous-time state would degenerate this into a simple PT1-element without any noise. To implement this, we need to allow states to provide their own time-step function which may also depend on the step size dt.

The element could be implemented as an appropriate signal-source for e(t,dt) and a simple PT1-element as filter.

Introduce "Time" signal

Currently, the only way to access the time in a result set is via the time property, which requires to handle time in a specific manner.

By introducing a simple wrapper signal - aptly named time - we could introduce time as a first-class signal, which would allow us to use all the other features of signals - including those which we intend to introduce - to time as well.

Abort simulation on excessive zero-crossing events

Some systems - such as the bouncing ball example - exhibit Zeno-behaviour, where within a finite timeframe an infinite number of zero-crossing events can occur. In that case, simulation will never progress beyond this finite timeframe. This is difficult to overcome. One indicator is the occurrence of many consecutive zero-crossing-events in sequence.

Thus, if a given number of zero-crossing-events occur one after another, we should abort simulation with an appropriate message. The event limit should be set in the simulator.

Introduce a saturation element

In most real-life control systems, the values of control signals are limited in range. To emulate that, we need a saturation element, that clamps the input signal into given lower and upper limits. The element should support arbitrary array inputs.

Detect zero-crossing events that occur as the result of handling an event

Currently, zero-crossing-events are only detected if they occur in the continuous-time segment of simulation, i.e. when progressing from after one clock tick to before the next. However, changes in state on a clock tick might also change the sign of an event function.

Therefore, zero-crossing events should also be detected when they occur due to state changes on clock ticks.

Allow local caching of UIUC propeller data

Extend load_static_propeller by optional parameters for local caching:

load_static_propeller(
    "volume-1/data/apcsf_8x3.8_static_2777rd.txt",
    interp_options={
        "bounds_error": False,
        "fill_value": "extrapolate"},
    download=True,
    cache_dir="./prop_data/")

This should check whether the respective propeller data has already been downloaded into the directory ./prop_data, and if so, reads it from there. Otherwise the data shall be downloaded and stored at ./prop_data/volume-1/data/apcsf_8x3.8_static_2777rd.txt

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.