Source code for bdsim.blocks.sources
"""
Source blocks:
- have outputs but no inputs
- have no state variables
- are a subclass of ``SourceBlock`` |rarr| ``Block``
"""
import numpy as np
import math
from bdsim.components import SourceBlock
# ------------------------------------------------------------------------ #
[docs]class Constant(SourceBlock):
"""
:blockname:`CONSTANT`
.. table::
:align: left
+--------+---------+---------+
| inputs | outputs | states |
+--------+---------+---------+
| 0 | 1 | 0 |
+--------+---------+---------+
| | float, | |
| | A(N,) | |
+--------+---------+---------+
"""
nin = 0
nout = 1
[docs] def __init__(self, value=None, **blockargs):
"""
Constant value.
:param value: the constant, defaults to None
:type value: any, optional
:param blockargs: |BlockOptions|
:return: a CONSTANT block
:rtype: Constant instance
This block has only one output port, but the value can be any
Python type, for example float, list or Numpy ndarray.
"""
super().__init__(**blockargs)
if isinstance(value, (tuple, list)):
value = np.array(value)
self.value = value
self.add_param('value')
def output(self, t=None):
return [self.value]
# ------------------------------------------------------------------------ #
[docs]class Time(SourceBlock):
"""
:blockname:`TIME`
.. table::
:align: left
+--------+---------+---------+
| inputs | outputs | states |
+--------+---------+---------+
| 0 | 1 | 0 |
+--------+---------+---------+
| | float | |
+--------+---------+---------+
"""
nin = 0
nout = 1
[docs] def __init__(self, value=None, **blockargs):
"""
Simulation time.
:param blockargs: |BlockOptions|
:type blockargs: dict
:return: a TIME block
:rtype: Time instance
The block has only one output port which is the current simulation time.
"""
super().__init__(**blockargs)
def output(self, t=None):
return [t]
# ------------------------------------------------------------------------ #
[docs]class WaveForm(SourceBlock):
"""
:blockname:`WAVEFORM`
.. table::
:align: left
+--------+---------+---------+
| inputs | outputs | states |
+--------+---------+---------+
| 0 | 1 | 0 |
+--------+---------+---------+
| | float | |
+--------+---------+---------+
"""
nin = 0
nout = 1
[docs] def __init__(self, wave='square',
freq=1, unit='Hz', phase=0, amplitude=1, offset=0,
min=None, max=None, duty=0.5,
**blockargs):
"""
Waveform as function of time.
:param wave: type of waveform to generate, one of: 'sine', 'square', 'triangle', defaults to 'square'
:type wave: str, optional
:param freq: frequency, defaults to 1
:type freq: float, optional
:param unit: frequency unit, one of: 'rad/s', 'Hz', defaults to 'Hz'
:type unit: str, optional
:param amplitude: amplitude, defaults to 1
:type amplitude: float, optional
:param offset: signal offset, defaults to 0
:type offset: float, optional
:param phase: Initial phase of signal in the range [0,1], defaults to 0
:type phase: float, optional
:param min: minimum value, defaults to 0
:type min: float, optional
:param max: maximum value, defaults to 1
:type max: float, optional
:param duty: duty cycle for square wave in range [0,1], defaults to 0.5
:type duty: float, optional
:param blockargs: |BlockOptions|
:type blockargs: dict
:return: a WAVEFORM block
:rtype: WaveForm instance
Create a waveform generator block.
Examples::
WAVEFORM(wave='sine', freq=2) # 2Hz sine wave varying from -1 to 1
WAVEFORM(wave='square', freq=2, unit='rad/s') # 2rad/s square wave varying from -1 to 1
The minimum and maximum values of the waveform are given by default in
terms of amplitude and offset. The signals are symmetric about the offset
value. For example::
WAVEFORM(wave='sine') varies between -1 and +1
WAVEFORM(wave='sine', amplitude=2) varies between -2 and +2
WAVEFORM(wave='sine', offset=1) varies between 0 and +2
WAVEFORM(wave='sine', amplitude=2, offset=1) varies between -1 and +3
Alternatively we can specify the minimum and maximum values which override
amplitude and offset::
WAVEFORM(wave='triangle', min=0, max=5) varies between 0 and +5
At time 0 the sine and triangle wave are zero and increasing, and the
square wave has its first rise. We can specify a phase shift with
a number in the range [0,1] where 1 corresponds to one cycle.
.. note:: For discontinuous signals (square, triangle) the block declares
events for every discontinuity.
:seealso :meth:`declare_events`
"""
super().__init__(**blockargs)
assert 0<duty<1, 'duty must be in range [0,1]'
if wave in ('square', 'triangle', 'sine'):
self.wave = wave
else:
raise ValueError('bad waveform')
if unit == 'Hz':
self.freq = freq
elif unit == 'rad/s':
self.freq = freq / (2 * math.pi)
else:
raise ValueError('bad unit')
if 0 <= phase <= 1:
self.phase = phase
else:
raise ValueError('phase out of range')
if max is not None and min is not None:
amplitude = (max - min) / 2
offset = (max + min) / 2
self.min = min
self.mablock = max
if 0 <= duty <= 1:
self.duty = duty
else:
raise ValueError('duty out of range')
self.amplitude = amplitude
self.offset = offset
def start(self, state=None):
if self.wave == 'square':
t1 = self.phase / self.freq
t2 = (self.duty + self.phase) / self.freq
elif self.wave == 'triangle':
t1 = (0.25 + self.phase) / self.freq
t2 = (0.75 + self.phase) / self.freq
else:
return
# t1 < t2
T = 1.0 / self.freq
while t1 < self.bd.simstate.T:
self.bd.simstate.declare_event(self, t1)
self.bd.simstate.declare_event(self, t2)
t1 += T
t2 += T
def output(self, t=None):
T = 1.0 / self.freq
phase = (t * self.freq - self.phase ) % 1.0
# define all signals in the range -1 to 1
if self.wave == 'square':
if phase < self.duty:
out = 1
else:
out = -1
elif self.wave == 'triangle':
if phase < 0.25:
out = phase * 4
elif phase < 0.75:
out = 1 - 4 * (phase - 0.25)
else:
out = -1 + 4 * (phase - 0.75)
elif self.wave == 'sine':
out = math.sin(phase*2*math.pi)
else:
raise ValueError('bad option for signal')
out = out * self.amplitude + self.offset
#print('waveform = ', out)
return [out]
# ------------------------------------------------------------------------ #
[docs]class Piecewise(SourceBlock):
"""
:blockname:`PIECEWISE`
.. table::
:align: left
+--------+---------+---------+
| inputs | outputs | states |
+--------+---------+---------+
| 0 | 1 | 0 |
+--------+---------+---------+
| | float | |
+--------+---------+---------+
"""
nin = 0
nout = 1
[docs] def __init__(self, *seq, **blockargs):
"""
Piecewise constant signal.
:param seq: sequence of time, value pairs
:type seq: list of 2-tuples
:param blockargs: |BlockOptions|
:type blockargs: dict
:return: a PIECEWISE block
:rtype: Piecewise instance
Outputs a piecewise constant function of time. This is described as
a series of 2-tuples (time, value). The output value is taken from the
active tuple, that is, the latest one in the list whose time is no greater
than simulation time.
.. note::
- The tuples must be order by monotonically increasing time.
- There is no default initial value, the list should contain
a tuple with time zero otherwise the output will be undefined.
.. note:: The block declares an event for the start of each segment.
:seealso: :meth:`declare_events`
"""
super().__init__(**blockargs)
self.t = [ x[0] for x in seq]
self.y = [ x[1] for x in seq]
def start(self, state=None):
for t in self.t:
state.declare_event(self, t)
def output(self, t):
i = sum([ 1 if t >= _t else 0 for _t in self.t]) - 1
out = self.y[i]
#print(out)
return [out]
# ------------------------------------------------------------------------ #
[docs]class Step(SourceBlock):
"""
:blockname:`STEP`
.. table::
:align: left
+--------+---------+---------+
| inputs | outputs | states |
+--------+---------+---------+
| 0 | 1 | 0 |
+--------+---------+---------+
| | float | |
+--------+---------+---------+
"""
nin = 0
nout = 1
[docs] def __init__(self, T=1,
off=0, on=1,
**blockargs):
"""
Step signal.
:param T: time of step, defaults to 1
:type T: float, optional
:param off: initial value, defaults to 0
:type off: float, optional
:param on: final value, defaults to 1
:type on: float, optional
:param blockargs: |BlockOptions|
:type blockargs: dict
:return: a STEP block
:rtype: Step
Output a step signal that transitions from the value ``off`` to ``on``
when time equals ``T``.
.. note:: The block declares an event for the step time.
:seealso: :meth:`declare_events`
"""
super().__init__(**blockargs)
self.T = T
self.off = off
self.on = on
def start(self, state=None):
state.declare_event(self, self.T)
def output(self, t=None):
if t >= self.T:
out = self.on
else:
out = self.off
#print(out)
return [out]
# ------------------------------------------------------------------------ #
[docs]class Ramp(SourceBlock):
"""
:blockname:`RAMP`
.. table::
:align: left
+--------+---------+---------+
| inputs | outputs | states |
+--------+---------+---------+
| 0 | 1 | 0 |
+--------+---------+---------+
| | float | |
+--------+---------+---------+
"""
nin = 0
nout = 1
[docs] def __init__(self, T=1,
off=0, slope=1,
**blockargs):
"""
Ramp signal.
:param T: time of ramp start, defaults to 1
:type T: float, optional
:param off: initial value, defaults to 0
:type off: float, optional
:param slope: gradient of slope, defaults to 1
:type slope: float, optional
:param blockargs: |BlockOptions|
:type blockargs: dict
:return: a RAMP block
:rtype: Ramp
Output a ramp signal that starts increasing from the value ``off``
when time equals ``T`` linearly with time, with a gradient of ``slope``.
.. note:: The block declares an event for the ramp start time.
:seealso: :method:`declare_event`
"""
super().__init__(**blockargs)
self.T = T
self.off = off
self.slope = slope
def start(self, state=None):
state.declare_event(self, self.T)
def output(self, t=None):
if t >= self.T:
out = self.off + self.slope * (t - self.T)
else:
out = self.off
#print(out)
return [out]
if __name__ == "__main__":
import pathlib
import os.path
exec(open(os.path.join(pathlib.Path(__file__).parent.absolute(), "test_sources.py")).read())