Source code for ahkab.time_functions

# -*- coding: iso-8859-1 -*-
# time_functions.py
# Time functions for independent sources
# Copyright 2006 Giuseppe Venturini

# This file is part of the ahkab simulator.
#
# Ahkab is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 2 of the License.
#
# Ahkab is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License v2
# along with ahkab.  If not, see <http://www.gnu.org/licenses/>.
"""
This module contains several basic time functions.

The classes that are found in module are useful to provide a time-varying
characteristic to independent sources.

Notice that the time functions are not restricted to those provided here, the
user is welcome to provide his own.
Implementing a custom time function is easy and common practice, as long as you
are interfacing to the simulator through Python. Please see the dedicated section
:ref:`define-custom-time-functions` below.

Classes defined in this module
------------------------------

.. autosummary::
    pulse
    pwl
    sin
    exp
    sffm
    am

Supplying a time function to an independent source
--------------------------------------------------

Providing a time-dependent characteristic to an independent source is very
simple and probably best explained with an example.

Let's say we wish to define a sinusoidal voltage source with no offset,
amplitude 5V and 1kHz frequency.

It is done in two steps:

* first we define the time function with the built-in class
  :class:`ahkab.time_functions.sin`:

    .. code-block:: python

        sin1k = time_functions.sin(vo=0, va=5, freq=1e3)

* Then we define the voltage source and we assign the time function to it:

    .. code-block:: python

        cir.add_vsource('V1', 'n1', cir.gnd, 1, function=mys)

In the example above, the sine wave is assigned to a voltage source ``'V1'``,
that gets added to a circuit ``cir`` (not shown).

.. _define-custom-time-functions:

Defining custom time functions
------------------------------

Defining a custom time function is easy, all you need is either:

* A function that takes a ``float`` (the time) and returns the function
  value,
* An instance with a ``__call__(self, time)`` method. This solution
  allows having internal parameters, typically set through the constructor.

In both cases, in time-based simulations, the simulator will call the object at
every time step, supplying a single parameter, the simulation time (``time`` in
the following, of type ``float``).

In turn, the simulator expects to receive as return value a ``float``,
corresponding to the value of the time-dependent function at the time specified
by the ``time`` variable.

If the time-dependent function is used to define the characteristics of a
voltage source (:class:`VSource`), its return value has to be expressed in Volt.
In the case of a current source (:class:`ISource`), the return value is to be
expressed in Ampere.

The standard notation applies.

As an example, we'll define a custom time-dependent voltage source, having a
:math:`\\mathrm{sinc}(ft)` characteristic. In this example, :math:`f` has a
value of 10kHz.

First we define the time function, in this case we'll do that through the Python
``lambda`` construct.

.. code-block:: python

    mys = lambda t: 1 if not t else math.sin(math.pi*1e4*t)/(math.pi*1e4*t)

Then, we define the circuit -- a very simple one in this case -- and assign our
``mys`` function to ``V1``. In the following circuit, we simply apply the
voltage from ``V1`` to a resistor ``R1``.

.. code-block:: python

    import ahkab
    cir = ahkab.Circuit('Test custom time functions')
    cir.add_resistor('R1', 'n1', cir.gnd, 1e3)
    cir.add_vsource('V1', 'n1', cir.gnd, 1, function=mys)
    tr = ahkab.new_tran(0, 1e-3, 1e-5, x0=None)
    r = ahkab.run(cir, tr)['tran']

Plotting ``Vn1`` and the expected result (:math:`\\mathrm{sinc}(ft)`) we
get:

.. plot::

    import math
    import numpy as np
    import pylab
    import ahkab
    cir = ahkab.Circuit('Test custom time functions')
    cir.add_resistor('R1', 'n1', cir.gnd, 1e3)
    mys = lambda t: 1 if not t else math.sin(math.pi*1e4*t)/(math.pi*1e4*t)
    cir.add_vsource('V1', 'n1', cir.gnd, 1, function=mys)
    tr = ahkab.new_tran(0, 1e-3, 1e-5, x0=None)
    r = ahkab.run(cir, tr)['tran']
    t = r.get_x()
    pylab.hold(True)
    pylab.plot(t, r['vn1'], 'o', ms=3, label='V(n1) (simulation)')
    npsin1k = np.frompyfunc(mys, 1, 1)
    pylab.plot(t, npsin1k(t), label='sinc(ft) (theory)')
    pylab.legend()
    pylab.xlabel('t [s]')
    pylab.ylabel('Voltage [V]')


Module reference
----------------
"""

from __future__ import (unicode_literals, absolute_import,
                        division, print_function)

import math

from scipy.interpolate import InterpolatedUnivariateSpline

time_fun_specs = {'sin': { #VO VA FREQ TD THETA
    'tokens': ({
               'label': 'vo',
               'pos': 0,
               'type': float,
               'needed': True,
               'dest': 'vo',
               'default': None
               },
               {
               'label': 'va',
               'pos': 1,
               'type': float,
               'needed': True,
               'dest': 'va',
               'default': None
               },
               {
               'label': 'freq',
               'pos': 2,
               'type': float,
               'needed': True,
               'dest': 'freq',
               'default': None
               },
               {
               'label': 'td',
               'pos': 3,
               'type': float,
               'needed': False,
               'dest': 'td',
               'default': 0.
               },
               {
               'label': 'theta',
               'pos': 4,
               'type': float,
               'needed': False,
               'dest': 'theta',
               'default': 0
               }
               )
        },'exp': { #EXP(V1 V2 TD1 TAU1 TD2 TAU2)
    'tokens': ({
               'label': 'v1',
               'pos': 0,
               'type': float,
               'needed': True,
               'dest': 'v1',
               'default': None
               },
               {
               'label': 'v2',
               'pos': 1,
               'type': float,
               'needed': True,
               'dest': 'v2',
               'default': None
               },
               {
               'label': 'td1',
               'pos': 2,
               'type': float,
               'needed': False,
               'dest': 'td1',
               'default': 0.
               },
               {
               'label': 'tau1',
               'pos': 3,
               'type': float,
               'needed': True,
               'dest': 'tau1',
               'default': None
               },
               {
               'label': 'td2',
               'pos': 4,
               'type': float,
               'needed': False,
               'dest': 'td2',
               'default': float('inf')
               },
               {
               'label': 'tau2',
               'pos': 5,
               'type': float,
               'needed': False,
               'dest': 'tau2',
               'default': float('inf')
               }
               )
        },'pulse': { #PULSE(V1 V2 TD TR TF PW PER)
    'tokens': ({
               'label': 'v1',
               'pos': 0,
               'type': float,
               'needed': True,
               'dest': 'v1',
               'default': None
               },
               {
               'label': 'v2',
               'pos': 1,
               'type': float,
               'needed': True,
               'dest': 'v2',
               'default': None
               },
               {
               'label': 'td',
               'pos': 2,
               'type': float,
               'needed': False,
               'dest': 'td',
               'default': 0.
               },
               {
               'label': 'tr',
               'pos': 3,
               'type': float,
               'needed': True,
               'dest': 'tr',
               'default': None
               },
               {
               'label': 'tf',
               'pos': 4,
               'type': float,
               'needed': True,
               'dest': 'tf',
               'default': None
               },
               {
               'label': 'pw',
               'pos': 5,
               'type': float,
               'needed': True,
               'dest': 'pw',
               'default': None
               },
               {
               'label': 'per',
               'pos': 6,
               'type': float,
               'needed': True,
               'dest': 'per',
               'default': None
               })
        }, 'sffm': { ## SFFM(VO VA FC MDI FS TD)
    'tokens': ({
               'label': 'vo',
               'pos': 0,
               'type': float,
               'needed': True,
               'dest': 'vo',
               'default': None
               },
               {
               'label': 'va',
               'pos': 1,
               'type': float,
               'needed': True,
               'dest': 'va',
               'default': None
               },
               {
               'label': 'fc',
               'pos': 2,
               'type': float,
               'needed': False,
               'dest': 'fc',
               'default': None
               },
               {
               'label': 'mdi',
               'pos': 3,
               'type': float,
               'needed': True,
               'dest': 'mdi',
               'default': None
               },
               {
               'label': 'fs',
               'pos': 4,
               'type': float,
               'needed': True,
               'dest': 'fs',
               'default': None
               },
               {
               'label': 'td',
               'pos': 5,
               'type': float,
               'needed': False,
               'dest': 'td',
               'default': 0.
               })
        }, 'am': { #AM(sa oc fm fc [td])
    'tokens': ({
               'label': 'sa',
               'pos': 0,
               'type': float,
               'needed': True,
               'dest': 'sa',
               'default': None
               },
               {
               'label': 'oc',
               'pos': 1,
               'type': float,
               'needed': True,
               'dest': 'oc',
               'default': None
               },
               {
               'label': 'fm',
               'pos': 2,
               'type': float,
               'needed': True,
               'dest': 'fm',
               'default': None
               },
               {
               'label': 'fc',
               'pos': 3,
               'type': float,
               'needed': True,
               'dest': 'fc',
               'default': None
               },
               {
               'label': 'td',
               'pos': 4,
               'type': float,
               'needed': False,
               'dest': 'td',
               'default': None
               })
        }
}

#
# Functions for time dependent sources  #
#

[docs]class pulse(object): """Square wave aka pulse function .. image:: images/elem/pulse.svg **Parameters:** v1 : float Square wave low value. v2 : float Square wave high value. td : float Delay time to the first ramp, in seconds. Negative values are considered as zero. tr : float Rise time in seconds, from the low value ``v1`` to the pulse high value ``v2``. tf : float Fall time in seconds, from the pulse high value ``v2`` to the low value ``v1``. pw : float Pulse width in seconds. per : float Periodicity interval in seconds. """ # PULSE(V1 V2 TD TR TF PW PER) def __init__(self, v1, v2, td, tr, pw, tf, per): self.v1 = v1 self.v2 = v2 self.td = max(td, 0.0) self.per = per self.tr = tr self.tf = tf self.pw = pw self._type = "V" def __call__(self, time): """Evaluate the pulse function at the given time.""" if time is None: time = 0 time = time - self.per * int(time / self.per) if time < self.td: return self.v1 elif time < self.td + self.tr: return self.v1 + ((self.v2 - self.v1) / (self.tr)) * (time - self.td) elif time < self.td + self.tr + self.pw: return self.v2 elif time < self.td + self.tr + self.pw + self.tf: return self.v2 + ((self.v1 - self.v2) / (self.tf)) * (time - (self.td + self.tr + self.pw)) else: return self.v1 def __str__(self): return "type=pulse " + \ self._type.lower() + "1=" + str(self.v1) + " " + \ self._type.lower() + "2=" + str(self.v2) + \ " td=" + str(self.td) + " per=" + str(self.per) + \ " tr=" + str(self.tr) + " tf=" + str(self.tf) + \ " pw=" + str(self.pw)
[docs]class sin(object): """Sine wave .. image:: images/elem/sin.svg Mathematically, the sine wave function is defined as: * :math:`t < t_d`: .. math:: f(t) = v_o + v_a \\sin\\left(\\pi \\phi/180 \\right) * :math:`t \\ge t_d`: .. math:: f(t) = v_o + v_a \\exp\\left[-(t - t_d)\,\\theta \\right] \\sin\\left[2 \\pi f (t - t_d) + \\pi \\phi/180\\right] **Parameters:** vo : float Offset value. va : float Amplitude. freq : float Sine frequency in Hz. td : float, optional time delay before beginning the sinusoidal time variation, in seconds. Defaults to 0. theta : float optional damping factor in 1/s. Defaults to 0 (no damping). phi : float, optional Phase delay in degrees. Defaults to 0 (no phase delay). .. note:: This implementation is consistent with the SPICE simulator, other simulators use different formulae. """ # SIN(VO VA FREQ TD THETA) def __init__(self, vo, va, freq, td=0., theta=0., phi=0.): self.vo = vo self.va = va self.freq = freq self.td = td self.theta = theta self.phi = phi self._type = "V" def __call__(self, time): """Evaluate the sine function at the given time.""" if time is None: time = 0 if time < self.td: return self.vo + self.va*math.sin(math.pi*self.phi/180.) else: return self.vo + self.va * math.exp((self.td - time)*self.theta) \ * math.sin(2*math.pi*self.freq*(time - self.td) + \ math.pi*self.phi/180.) def __str__(self): return "type=sin " + \ self._type.lower() + "o=" + str(self.vo) + " " + \ self._type.lower() + "a=" + str(self.va) + \ " freq=" + str(self.freq) + " theta=" + str(self.theta) + \ " td=" + str(self.td)
[docs]class exp(object): """Exponential wave .. image:: images/elem/exp.svg Mathematically, it is described by the equations: * :math:`0 \\le t < TD1`: .. math:: f(t) = V1 * :math:`TD1 < t < TD2` .. math:: f(t) = V1+(V2-V1) \\cdot \\left[1-\\exp \\left(-\\frac{t-TD1}{TAU1}\\right)\\right] * :math:`t > TD2` .. math:: f(t) = V1+(V2-V1) \\cdot \\left[1-\\exp \\left(-\\frac{t-TD1}{TAU1}\\right)\\right]+(V1-V2) \\cdot \\left[1-\\exp \\left(-\\frac{t-TD2}{TAU2}\\right)\\right] **Parameters:** v1 : float Initial value. v2 : float Pulsed value. td1 : float Rise delay time in seconds. td2 : float Fall delay time in seconds. tau1 : float Rise time constant in seconds. tau2 : float Fall time constant in seconds. """ # EXP(V1 V2 TD1 TAU1 TD2 TAU2) def __init__(self, v1, v2, td1, tau1, td2, tau2): self.v1 = v1 self.v2 = v2 self.td1 = td1 self.tau1 = tau1 self.td2 = td2 self.tau2 = tau2 self._type = "V" def __call__(self, time): """Evaluate the exponential function at the given time.""" if time is None: time = 0 if time < self.td1: return self.v1 elif time < self.td2: return self.v1 + (self.v2 - self.v1) * \ (1 - math.exp(-1*(time - self.td1)/self.tau1)) else: return self.v1 + (self.v2 - self.v1) * \ (1 - math.exp(-1*(time - self.td1)/self.tau1)) + \ (self.v1 - self.v2)*(1 - math.exp(-1*(time - self.td2)/self.tau2)) def __str__(self): return "type=exp " + \ self._type.lower() + "1=" + str(self.v1) + " " + \ self._type.lower() + "2=" + str(self.v2) + \ " td1=" + str(self.td1) + " td2=" + str(self.td2) + \ " tau1=" + str(self.tau1) + " tau2=" + str(self.tau2)
[docs]class sffm(object): """Single-Frequency FM (SFFM) waveform .. image:: images/elem/fm.svg Mathematically, it is described by the equations: * :math:`0 \\le t \\le t_D`: .. math:: f(t) = V_O * :math:`t > t_D` .. math:: f(t) = V_O + V_A \\cdot \\sin \\left[2\\pi f_C (t - t_D) + MDI \\sin \\left[2 \\pi f_S (t - t_D) \\right] \\right] **Parameters:** vo : float Offset in Volt or Ampere. va : float Amplitude in Volt or Ampere. fc : float Carrier frequency in Hz. mdi : float Modulation index. fs : float Signal frequency in HZ. td : float Time delay before the signal begins, in seconds. """ # SFFM(VO VA FC MDI FS) def __init__(self, vo, va, fc, mdi, fs, td): self.vo = vo self.va = va self.fc = fc self.mdi = mdi self.fs = fs self.td = td self._type = "V" def __call__(self, time): """Evaluate the SFFM function at the given time.""" if time is None: time = 0 if time <= self.td: return self.vo else: return self.vo + self.va*math.sin(2*math.pi*self.fc*(time - self.td) + self.mdi*math.sin(2*math.pi*self.fs* (time - self.td)) ) def __str__(self): return "type=sffm vo=%g va=%g fc=%g mdi=%g fs=%g td=%g" % \ (self.vo, self.va, self.fc, self.mdi, self.fs, self.td)
[docs]class am(object): """Amplitude Modulated (AM) waveform .. image:: images/elem/am.svg Mathematically, it is described by the equations: * :math:`0 \\le t \\le t_D`: .. math:: f(t) = O * :math:`t > t_D` .. math:: f(t) = SA \\cdot \\left[OC + \\sin \\left[2\\pi f_m (t - t_D) \\right] \\right] \\cdot \\sin \\left[2 \\pi f_c (t - t_D) \\right] **Parameters:** sa : float Signal amplitude in Volt or Ampere. fc : float Carrier frequency in Hertz. fm : float Modulation frequency in Hertz. oc : float Offset constant, setting the absolute magnitude of the modulation. td : float Time delay before the signal begins, in seconds. """ # AM(sa oc fm fc <td>) def __init__(self, sa, fc, fm, oc, td): self.sa = sa self.fc = fc self.fm = fm self.oc = oc self.td = td self._type = "V" def __call__(self, time): """Evaluate the AM function at the given time.""" if time is None: time = 0 if time <= self.td: return 0. else: return self.sa*(self.oc + math.sin(2*math.pi*self.fm* (time - self.td)))* \ math.sin(2*math.pi*self.fc*(time - self.td)) def __str__(self): return "type=am sa=%g oc=%g fm=%g fc=%g td=%g" % \ (self.sa, self.oc, self.fm, self.fc, self.td)
[docs]class pwl(object): """Piece-Wise Linear (PWL) waveform .. image:: images/elem/pwl.svg A piece-wise linear waveform is defined by a sequence of points :math:`(x_i, y_i)`. Please supply the abscissa values :math:`\\{x\\}_i` in the vector ``x``, the ordinate values :math:`\\{y\\}_i` in the vector ``y``, separately. **Parameters:** x : sequence-like The abscissa values of the interpolation points. y : sequence-like The ordinate values of the interpolation points. repeat : boolean, optional Whether the waveform should be repeated after its end. If set to ``True``, ``repeat_time`` also needs to be set to define when the repetition begins. Defaults to ``False``. repeat_time : float, optional In case the waveform is set to be repeated, setting the ``repeat`` flag above, the parameter, defined in seconds, set the first time instant at which the waveform repetition happens. td : float, optional Time delay before the signal begins, in seconds. Defaults to zero. **Example:** The following code:: import ahkab import numpy as np import pylab as plt # vs = (x1, y1, x2, y2, x3, y3 ...) vs = (60e-9, 0, 120e-9, 0, 130e-9, 5, 170e-9, 5, 180e-9, 0) x, y = vs[::2], vs[1::2] fun = ahkab.time_functions.pwl(x, y, repeat=1, repeat_time=60e-9, td=0) myg = np.frompyfunc(fun, 1, 1) t = np.linspace(0, 5e-7, 2000) plt.plot(t, myg(t), lw=3) plt.xlabel('Time [s]'); plt.ylabel('Arbitrary units []') Produces: .. plot:: import ahkab import numpy as np import pylab as plt vs = (60e-9, 0, 120e-9, 0, 130e-9, 5, 170e-9, 5, 180e-9, 0) x, y = vs[::2], vs[1::2] fun = ahkab.time_functions.pwl(x, y, repeat=1, repeat_time=60e-9, td=0) myg = np.frompyfunc(fun, 1, 1) t = np.linspace(0, 5e-7, 2000) plt.figure(figsize=(6, 3)); plt.grid() plt.plot(t, myg(t), lw=3) plt.xlabel('Time [s]'); plt.ylabel('Arbitrary units []') plt.tight_layout() """ def __init__(self, x, y, repeat=False, repeat_time=0, td=0): self.x = x self.y = y self.repeat = repeat self.repeat_time = repeat_time if self.repeat_time == max(x): self.repeat_time = 0 self.td = td self._type = "V" self._f = InterpolatedUnivariateSpline(self.x, self.y, k=1) def __call__(self, time): """Evaluate the PWL function at the given time.""" time = self._normalize_time(time) return self._f(time) def _normalize_time(self, time): if time is None: time = 0 if time <= self.td: time = 0 elif time > self.td: time = time - self.td if self.repeat: if time > max(self.x): time = (time - max(self.x)) % \ (max(self.x) - self.repeat_time) + \ self.repeat_time else: pass return time def __str__(self): pwl_str = "type=pwl" tv = " " for x, y in zip(self.x, self.y): tv += "%g %g " pwl_str += tv if self.td: pwl_str += "td=%g " % self.td if self.repeat: pwl_str = "RPT=%g " % self.repeat_time return pwl_str[:-1]