pwl_writer.py

#

by Victor SabiĆ” P. Carpes

Tested on:

  • python 3.6.6
    • numpy 1.19.5
    • matplotlib 3.3.4
  • python 3.11.1
    • numpy 1.25.0
    • matplotlib 3.7.1

Type stubs for older numpy versions for mypy checking can be found here.


Index


Package Summary

This package defines a class PWL to generate objects that represent time dependent signals x(t) that need to be coded in a PWL file. Those objects are built using little components such as rectangular pulses and sawtooth pulses that can be chained together.

The motivations for this package are the nuisances of writing PWL files by hand. To properly explain this, let’s discuss how PWL files work.

PWL stands for piecewise linear. A PWL file is a way to represent a time dependent signal (referenced by x(t) from now on) for simulation softwares such as LTspice and Cadence Virtuoso. In it, points of the form (t, x) are stored. During simulation, those points are interpolated with first degree polynomials. This poses 2 problems:

  1. Due to the linear interpolation, the resulting signal is continuous. This tends to be desirable, but if the intention is moddeling, for example, rectangular pulses, each transition will need 2 points with very close time coordinates to approximate a discontinuous transition. This can get extremely tedious to code out by hand.

  2. Each point has an absolute time coordinate with respect to the origin. If the desired signal is for example a series of rectangular pulses with certain durations and for some reason the duration of the first pulse is changed, all the following points will need to have their time coordinates changed as well.

This package solves both problems by providing an abstraction layer. They are solved by the 2 following features:

  1. A minimal timestep is defined at the creation of the PWL object that is used to automatically generate all the needed transitions for any discontinous transition.

  2. The signal is built using small building blocks (such as rectangular pulse and exponential transition) called events that are defined in terms of durations. That is to say, time is treated in a differential fashion. The time coordinates from a given event are all with respect to the final instant of the previous event. For example, let’s assume we want to model a rectangular pulse with amplitude 1 and duration 1 second followed by a downtime at zero for 10 seconds and then another rectangular pulse with the same duration and amplitude. If we change the duration of the first pulse to 2 seconds, the downtime and second pulse will be both delayed by the 1 second but retain their durations.

Another advantage of using this package is not a feature per se but more a consequence of using a programing language. That advantage is simply that all those events can be added inside for loops, if clauses and functions, allowing for greater flexibility. For example, let’s assume we want to control a system that can be in the following states:

  • Idle
  • Mode 1
  • Mode 2

For each state, various control signals need to be at specific values. We could create a PWL object for each control signal and define 3 functions that apply all the needed values for the control signals for each state. If we nedded the system to be at mode 1 for 3 seconds, idle for 1 second and at mode 2 for 5 seconds, we could write something like the following:

   mode1_state(3)
   idle_state(1)
   mode2_state(5)
__all__ = ['PrecisionError', 'PWL']

from warnings import warn
from numbers import Real
from typing import Callable, Dict, List, Optional
import numpy as np

try:
    import matplotlib.pyplot as plt  # type: ignore
except ImportError:
    _has_matplotlib = False
    warn("Matplotlib package not found. Optional features deactivated.", ImportWarning)
else:
    _has_matplotlib = True
#

#

Precision Related Exception

#

PrecisionError exception class

class PrecisionError(Exception):
#

This class defines an exception meant to be raised when any type of rounding or loss of precision that causes the time coordinates of a PWL object to not be strictly increasing.

#

#

PWL Class

#

PWL class

class PWL():
#

This class defines an object that represnts a time dependent signal x(t). Those objects can operated on by methods to build, event by event, the desired signal as described on the package introduction.

    __dict_of_objects: Dict[str, 'PWL'] = {}
#

#

Initializer

#

Dunder method __init__ of PWL class

    def __init__(self, t_step: float, name: Optional[str] = None, verbose: bool = False) -> None:
#

Summary

Initializer for the PWL class.

Arguments

  • t_step (float) : Default timestep for all operations. Should be strictly positive.
  • name (str, optional) : Name of the PWL object used for verbose output printing. Should not be empty. If not set, automatically generates a name based on already taken names.
  • verbose (bool, optional) : Flag indicating if verbose output should be printed. If not set, defaults to False.

Raises

  • TypeError : Raised if any of the arguments has an invalid type.
  • ValueError : Raised if t_step is not strictly positive or name is empty.
        if name is None:
            i: int = 0
            while f"pwl_{i}" in PWL.__dict_of_objects:
                i += 1
            name = f"pwl_{i}"

        if not isinstance(t_step, Real):
            raise TypeError(
                f"Argument 't_step' should be a real number but has type '{type(t_step).__name__}'.")
        if not isinstance(name, str):
            raise TypeError(
                f"Argument 'name' should either be a string but has type '{type(name).__name__}'.")
        if not isinstance(verbose, bool):
            raise TypeError(
                f"Argument 'verbose' should be a boolean but has type '{type(verbose).__name__}'.")

        if t_step <= 0:
            raise ValueError(
                f"Argument 't_step' should be strictly positive but has value of {t_step}.")
        if not name:
            raise ValueError("Argument 'name' should not be empty.")

        self._t_list: List[float] = []
        self._x_list: List[float] = []
        self._t_step: float = t_step
        self._name: str = name
        self._verbose: bool = verbose

        if name in PWL. __dict_of_objects:
            raise ValueError(f"Name '{name}' already in use.")

        PWL. __dict_of_objects[name] = self
#

#

String Representation

#

Dunder method __str__ of PWL class

    def __str__(self) -> str:
#

Summary

String representation of PWL instances in the form [name]: PWL object with [# of points] and duration of [total time duration] seconds.

Returns

  • str
        duration = 0 if len(self._t_list) == 0 else max(self._t_list)

        return f"{self.name}: PWL object with {len(self._t_list)} points and duration of {duration} seconds"
#

#

Length Calculator

#

Dunder method __len__ of PWL class

    def __len__(self) -> int:
#

Summary

Length of PWL instances defined as the number of (t, x) points they contain.

Returns

  • int
        return len(self._t_list)
#

#

Time Coordinates

#

t_list property of PWL class

    @property
    def t_list(self) -> List[float]:
#

Type

  • list[float]

Summary

Read only property containing all the time coordinates of a PWL object.

Raises

  • AttributeError : Raised if assignment was attempetd.
        return self._t_list
#

#

Dependent Coordinates

#

x_list property of PWL class

    @property
    def x_list(self) -> List[float]:
#

Type

  • list[float]

Summary

Read only property containing all the dependent coordinates of a PWL object.

Raises

  • AttributeError : Raised if assignment was attempetd.
        return self._x_list
#

#

Default Timestep

#

t_step property of PWL class

    @property
    def t_step(self) -> float:
#

Type

  • float

Summary

Property defining the default timestep of a PWL object.

Raises

  • TypeError : Raised if the assigned value is not a real number.
  • ValueError : Raised if the assigned value is not strictly positive.
        return self._t_step
#
    @t_step.setter
    def t_step(self, new_t_step: float) -> None:
        if not isinstance(new_t_step, float):
            raise TypeError(
                f"Property 't_step' should be a real number but an object of type '{type(new_t_step).__name__}' was assigned to it.")
        if new_t_step <= 0:
            raise ValueError(
                f"Propety 't_step' should be strictly positive but a value of {new_t_step} was assigned to it.")

        self._t_step = new_t_step
#

#

Name

#

name property of PWL class

    @property
    def name(self) -> str:
#

Type

  • str

Summary

Property defining the name of a PWL object for verbose output printing.

Raises

  • TypeError : Raised if the assigned value is not a string.
  • ValueError : Raised if the assigned value is an empty string or already in use.
        return self._name
#
    @name.setter
    def name(self, new_name: str) -> None:
        if not isinstance(new_name, str):
            raise TypeError(
                f"Property 'name' should be a string but an object of type '{type(new_name).__name__}' was assigned to it.")
        if not new_name:
            raise ValueError(
                "An empty string cannot be assigned to the 'name' property.")

        if new_name in PWL. __dict_of_objects:
            raise ValueError(f"Name '{new_name}' already in use.")

        PWL. __dict_of_objects.pop(self._name)
        PWL. __dict_of_objects[new_name] = self
        self._name = new_name
#

#

Verbose Flag

#

verbose property of PWL class

    @property
    def verbose(self) -> bool:
#

Type

  • bool

Summary

Property defining if verbose output should be printed or not.

Raises

  • TypeError : Raised if the assigned value is not a boolean.
        return self._verbose
#
    @verbose.setter
    def verbose(self, new_verbose: bool) -> None:

        if not isinstance(new_verbose, bool):
            raise TypeError(
                f"Attribute 'verbose' should be a boolean but an object of type '{type(new_verbose).__name__}' was assigned to it.")
        self._verbose = new_verbose
#

#

Last Value Holder

#

hold method of PWL class

    def hold(self, duration: float) -> None:
#

Summary

Method that holds the last value from the previous event for a given duration.

If the PWL object is empty, adds the point (0, 0) and holds that.

Parameters

  • duration (float) : Duration to hold the last value for. Should be strictly positive.

Raises

  • TypeError : Raised if duration is not a real number.
  • ValueError : Raised if duration is not strictly positive.
  • PrecisionError : Raised if computational noise causes the time coordinates to not be strictly increasing.
        if not isinstance(duration, Real):
            raise TypeError(
                f"Argument 'duration' should be a real number but has type '{type(duration).__name__}'.")

        if duration <= 0:
            raise ValueError(
                f"Argument 'duration' should be strictly positive but has value of {duration}.")

        if self._verbose:
            print(f"{self._name}: Adding hold with duration of {duration}.")

        if len(self._t_list) == len(self._x_list) == 0:
            if self._verbose:
                print("    Empty PWL object. Adding initial (0, 0) point.")
            self._add(0, 0)

        last_t = self._t_list[-1]
        last_x = self._x_list[-1]

        self._add(last_t+duration, last_x)
#

#

Linear Transition

#

lin_transition method of PWL class

    def lin_transition(self, target: float, duration: float) -> None:
#

Summary

Method that generates a linear transition from the last value of the previous event to a given target with a given duration.

If the PWL object is empty, adds the point (0, 0) and transitions from that.

Arguments

  • target (float) : Value to transition to.
  • duration (float) : Duration of the transition. Should be strictly positive.

Raises

  • TypeError : Raised if either target or duration` is not a real number.
  • ValueError : Raised if duration is not strictly positive.
  • PrecisionError : Raised if computational noise causes the time coordinates to not be strictly increasing.
        if not isinstance(target, Real):
            raise TypeError(
                f"Argument 'target' should be a real number but has type '{type(target).__name__}'.")
        if not isinstance(duration, Real):
            raise TypeError(
                f"Argument 'duration' should be a real number but has type '{type(duration).__name__}'.")

        if duration <= 0:
            raise ValueError(
                f"Argument 'duration' should be strictly positive but has value of {duration}.")

        self._lin_transition(target, duration, 0)
#

#

Rectangular Pulse

#

rect_pulse method of PWL class

    def rect_pulse(self, value: float, duration: float, t_step: Optional[float] = None) -> None:
#

Summary

Method that generates a rectangular pulse with given amplitude and duration.

If duration is less than or equal to t_step (self.t_step if t_step is not set), substitutes the pulse by a linear transition from the last value of the previous event to value with duration t_step (self.t_step if t_step is not set).

Arguments

  • value (float) : Amplitude of the pulse.
  • duration (float) : Duration of the pulse. Should be strictly positive.
  • t_step (float, optional) : Transition time for the discontinuity. Should be strictly positive. If not set, uses self.t_step.

Raises

  • TypeError : Raised if either value, durationort_step` is not a real number.
  • ValueError : Raised if either duration or t_step is not strictly positive.
  • PrecisionError : Raised if computational noise causes the time coordinates to not be strictly increasing.

See Also

        if t_step is None:
            t_step = self._t_step

        if not isinstance(value, Real):
            raise TypeError(
                f"Argument 'value' should be a real number but has type '{type(value).__name__}'.")
        if not isinstance(duration, Real):
            raise TypeError(
                f"Argument 'duration' should be a real number but has type '{type(duration).__name__}'.")
        if not isinstance(t_step, Real):
            raise TypeError(
                f"Argument 't_step' should either be a real number but has type '{type(t_step).__name__}'.")

        if duration <= 0:
            raise ValueError(
                f"Argument 'duration' should be strictly positive but has value of {duration}.")
        if t_step <= 0:
            raise ValueError(
                f"Argument 't_step' should be strictly positive but has value of {t_step}.")

        if self._verbose:
            print(f"{self._name}: Adding rectangular pulse with value of {value}, duration of {duration} and time step of {t_step}.")

        if duration <= t_step:
            if self._verbose:
                print(
                    f"{self._name}: Duration of {duration} is less than or equal to time step of {t_step}. Converting to linear transition.")
            self._lin_transition(value, t_step, 1)
            return

        if len(self._t_list) == len(self._x_list) == 0:
            self._add(0, value)
            last_t = 0
        else:
            last_t = self._t_list[-1]
            self._add(last_t+t_step, value)

        self._add(last_t+duration, value)
#

#

Sawtooth Pulse

#

sawtooth_pulse method of PWL class

    def sawtooth_pulse(self, start: float, end: float, duration: float, t_step: Optional[float] = None) -> None:
#

Summary

Method that generates a sawtooth pulse with given starting and ending amplitudes and duration.

If duration is less than or equal to t_step (self.t_step if t_step is not set), substitutes the pulse by a linear transition from the last value of the previous event to end with duration t_step (self.t_step if t_step is not set).

Arguments

  • start (float) : Amplitude at the start of the pulse.
  • end (float) : Amplitude at the end of the pulse.
  • duration (float) : Duration of the pulse. Should be strictly positive.
  • t_step (float, optional) : Transition time for the discontinuity. Should be strictly positive. If not set, uses self.t_step.

Raises

  • TypeError : Raised if either start, end, durationort_step` is not a real number.
  • ValueError : Raised if either duration or t_step is not strictly positive.
  • PrecisionError : Raised if computational noise causes the time coordinates to not be strictly increasing.

See Also

        if t_step is None:
            t_step = self._t_step

        if not isinstance(start, Real):
            raise TypeError(
                f"Argument 'start' should be a real number but has type '{type(start).__name__}'.")
        if not isinstance(end, Real):
            raise TypeError(
                f"Argument 'end' should be a real number but has type '{type(end).__name__}'.")
        if not isinstance(duration, Real):
            raise TypeError(
                f"Argument 'duration' should be a real number but has type '{type(duration).__name__}'.")
        if not isinstance(t_step, Real):
            raise TypeError(
                f"Argument 't_step' should either be a real number but has type '{type(t_step).__name__}'.")

        if duration <= 0:
            raise ValueError(
                f"Argument 'duration' should be strictly positive but has value of {duration}.")
        if t_step <= 0:
            raise ValueError(
                f"Argument 't_step' should be strictly positive but has value of {t_step}.")

        if self._verbose:
            print(f"{self._name}: Adding sawtoth pulse from {start} to {end} with duration of {duration} and time step of {t_step}.")

        if duration <= t_step:
            if self._verbose:
                print(
                    f"{self._name}: Duration of {duration} is less than or equal to time step of {t_step}. Converting to linear transition.")
            self._lin_transition(end, t_step, 1)
            return

        if len(self._t_list) == len(self._x_list) == 0:
            self._add(0, start)
            last_t = 0
        else:
            last_t = self._t_list[-1]
            self._add(last_t+t_step, start)

        self._add(last_t+duration, end)
#

#

Exponential Transition

#

exp_transition method of PWL class

    def exp_transition(self, target: float, duration: float, tau: float, t_step: Optional[float] = None) -> None:
#

Summary

Method that generates an exponential transition from the last value of the previous event to a given target with a given duration.

If the PWL object is empty, adds the point (0, 0) and transitions from that.

Let’s call x0 the last value of the previous event and t0 the instant when the transition begins. The transition will follow thw following form:

f(t) = A + B*exp(-t/tau)

The constants A and B are chosen such that the following conditions are met:

f(t0) = x0
f(t0 + duration) = target

The sign of tau defines if f(t) diverges or converges when t goes to positive infinity.

If duration is less than or equal to t_step (self.t_step if t_step is not set), substitutes the pulse by a linear transition from the last value of the previous event to target with duration t_step (self.t_step if t_step is not set).

Arguments

  • target (float) : Value to transition to.
  • duration (float) : Duration of the transition. Should be strictly positive.
  • tau (float) : Time constant of the exponential. SHould be non zero.
  • t_step (float, optional) : Timestep between consecutive points inside the transition. Should be strictly positive. If not set, uses self.t_step.

Raises

  • TypeError : Raised if either target, duration, tauort_step` is not a real number.
  • ValueError : Raised if either duration or t_step is not strictly positive or tau is equal to zero.
  • PrecisionError : Raised if computational noise causes the time coordinates to not be strictly increasing.

See Also

        if t_step is None:
            t_step = self._t_step

        if not isinstance(target, Real):
            raise TypeError(
                f"Argument 'target' should be a real number but has type '{type(target).__name__}'.")
        if not isinstance(duration, Real):
            raise TypeError(
                f"Argument 'duration' should be a real number but has type '{type(duration).__name__}'.")
        if not isinstance(tau, Real):
            raise TypeError(
                f"Argument 'tau' should be a real number but has type '{type(tau).__name__}'.")
        if not isinstance(t_step, Real):
            raise TypeError(
                f"Argument 't_step' should either be a real number but has type '{type(t_step).__name__}'.")

        if duration <= 0:
            raise ValueError(
                f"Argument 'duration' should be strictly positive but has value of {duration}.")
        if tau == 0:
            raise ValueError("Argument 'tau' should be non zero.")
        if t_step <= 0:
            raise ValueError(
                f"Argument 't_step' should be strictly positive but has value of {t_step}.")

        if self._verbose:
            print(f"{self._name}: Adding exponential transition with target of {target}, time constant of {tau}, duration of {duration} and time step of {t_step}.")

        if duration <= t_step:
            if self._verbose:
                print(
                    f"    Duration of {duration} is less than or equal to time step of {t_step}. Converting to linear transition.")
            self._lin_transition(target, t_step, 2)
            return

        if len(self._t_list) == len(self._x_list) == 0:
            if self._verbose:
                print("    Empty PWL object. Adding initial (0, 0) point.")
            self._add(0, 0)

        last_t = self._t_list[-1]
        last_x = self._x_list[-1]

        f = _exp_transition_func(tau=tau, t1=last_t, t2=last_t +
                                 duration, f1=last_x, f2=target)

        for t in np.arange(last_t+t_step, last_t+duration, t_step):
            self._add(t, f(t))

        self._add(last_t+duration, target)
#

#

Half Sine Transition

#

sin_transition method of PWL class

    def sin_transition(self, target: float, duration: float, t_step: Optional[float] = None) -> None:
#

Summary

Method that generates a half sine transition from the last value of the previous event to a given target with a given duration.

If the PWL object is empty, adds the point (0, 0) and transitions from that.

Let’s call x0 the last value of the previous event and t0 the instant when the transition begins. The transition will follow thw following form:

f(t) = A + B*sin(w*t - phi)

The constants A, B, w and phi are chosen shuch that the following conditions are met:

f(t0) = x0
f(t0 + duration) = target
f'(t0) = f'(t0 + duration) = 0

Due to the periodic nature of sine, inifinite solutions for f(t) that satisfy those conditions exist. The only monotonic solution is chosen. That is to say, the wavelength of the chopse solution is equal to 2*duration.

If duration is less than or equal to t_step (self.t_step if t_step is not set), substitutes the pulse by a linear transition from the last value of the previous event to target with duration t_step (self.t_step if t_step is not set).

Arguments

  • target (float) : Value to transition to.
  • duration (float) : Duration of the transition. Should be strictly positive.
  • t_step (float, optional) : Timestep between consecutive points inside the transition. Should be strictly positive. If not set, uses self.t_step.

Raises

  • TypeError : Raised if either target, duration or t_step is not a real number.
  • ValueError : Raised if either duration or t_step is not strictly positive.
  • PrecisionError : Raised if computational noise causes the time coordinates to not be strictly increasing.

See Also

        if t_step is None:
            t_step = self._t_step

        if not isinstance(target, Real):
            raise TypeError(
                f"Argument 'target' should be a real number but has type '{type(target).__name__}'.")
        if not isinstance(duration, Real):
            raise TypeError(
                f"Argument 'duration' should be a real number but has type '{type(duration).__name__}'.")
        if not isinstance(t_step, Real):
            raise TypeError(
                f"Argument 't_step' should either be a real number but has type '{type(t_step).__name__}'.")

        if duration <= 0:
            raise ValueError(
                f"Argument 'duration' should be strictly positive but has value of {duration}.")
        if t_step <= 0:
            raise ValueError(
                f"Argument 't_step' should be strictly positive but has value of {t_step}.")

        if self._verbose:
            print(f"{self._name}: Adding sinusoidal transition with target of {target}, duration of {duration} and time step of {t_step}.")

        if duration <= t_step:
            if self._verbose:
                print(
                    f"    Duration of {duration} is less than or equal to time step of {t_step}. Converting to linear transition.")
            self._lin_transition(target, t_step, n=2)
            return

        if len(self._t_list) == len(self._x_list) == 0:
            if self._verbose:
                print("    Empty PWL object. Adding initial (0, 0) point.")
            self._add(0, 0)

        last_t = self._t_list[-1]
        last_x = self._x_list[-1]

        f = _sin_transition_func(
            t1=last_t, t2=last_t+duration, f1=last_x, f2=target)

        for t in np.arange(last_t+t_step, last_t+duration, t_step):
            self._add(t, f(t))

        self._add(last_t+duration, target)
#

#

Smoothstep Transition

#

smoothstep_transition method of PWL class

    def smoothstep_transition(self, target: float, duration: float, t_step: Optional[float] = None) -> None:
#

Summary

Method that generates a smoothstep transition from the last value of the previous event to a given target with a given duration.

If the PWL object is empty, adds the point (0, 0) and transitions from that.

Let’s call x0 the last value of the previous event and t0 the instant when the transition begins. The transition will follow thw following form:

f(t) = A + B*t + C*t^2 + D*t^3

The constants A, B, C and D are chosen shuch that the following conditions are met:

f(t0) = x0
f(t0 + duration) = target
f'(t0) = f'(t0 + duration) = 0

If duration is less than or equal to t_step (self.t_step if t_step is not set), substitutes the pulse by a linear transition from the last value of the previous event to target with duration t_step (self.t_step if t_step is not set).

Arguments

  • target (float) : Value to transition to.
  • duration (float) : Duration of the transition. Should be strictly positive.
  • t_step (float, optional) : Timestep between consecutive points inside the transition. Should be strictly positive. If not set, uses self.t_step.

Raises

  • TypeError : Raised if either target, duration or t_step is not a real number.
  • ValueError : Raised if either duration or t_step is not strictly positive.
  • PrecisionError : Raised if computational noise causes the time coordinates to not be strictly increasing.

See Also

        if t_step is None:
            t_step = self._t_step

        if not isinstance(target, Real):
            raise TypeError(
                f"Argument 'target' should be a real number but has type '{type(target).__name__}'.")
        if not isinstance(duration, Real):
            raise TypeError(
                f"Argument 'duration' should be a real number but has type '{type(duration).__name__}'.")
        if not isinstance(t_step, Real):
            raise TypeError(
                f"Argument 't_step' should either be a real number but has type '{type(t_step).__name__}'.")

        if duration <= 0:
            raise ValueError(
                f"Argument 'duration' should be strictly positive but has value of {duration}.")
        if t_step <= 0:
            raise ValueError(
                f"Argument 't_step' should be strictly positive but has value of {t_step}.")

        if self._verbose:
            print(f"{self._name}: Adding smoothstep transition with target of {target}, duration of {duration} and time step of {t_step}.")

        if duration <= t_step:
            if self._verbose:
                print(
                    f"    Duration of {duration} is less than or equal to time step of {t_step}. Converting to linear transition.")
            self._lin_transition(target, t_step, n=2)
            return

        if len(self._t_list) == len(self._x_list) == 0:
            if self._verbose:
                print("    Empty PWL object. Adding initial (0, 0) point.")
            self._add(0, 0)

        last_t = self._t_list[-1]
        last_x = self._x_list[-1]

        f = _smoothstep_transition_func(
            t1=last_t, t2=last_t+duration, f1=last_x, f2=target)

        for t in np.arange(last_t+t_step, last_t+duration, t_step):
            self._add(t, f(t))

        self._add(last_t+duration, target)
#

#

PWL File Writer

#

write method of PWL class

    def write(self, filename: Optional[str] = None, precision: int = 10) -> None:
#

Summary

Method that takes a PWL object and writes a PWL file with it’s (t, x) coordinates in scientific notation.

If the specified file already exists, overwrites it.

Arguments

  • filename (str, optional) : Name of file to be created. If not set, uses self.name if an added .txt extension.
  • precision (int, optional) : Number of significant figures used when writing the PWL file. Should be strictly positive. If not set, defaults to 10.

Raises

  • TypeError : Raised if filename is not a string or precision is not an integer.
  • ValueError : Raised if precision is not strictly positive.
  • PrecisionError : Raised if precision is such that the rounding causes the time coordinates to not be strictly increasing.
        if filename is None:
            filename = f'{self._name}.txt'

        if not isinstance(filename, str):
            raise TypeError(
                f"Argument 'filename' should be a string but has type '{type(filename).__name__}'.")
        if not isinstance(precision, int):
            raise TypeError(
                f"Argument 'precision' should be an integer but has type '{type(precision).__name__}'.")

        if precision <= 0:
            raise ValueError(
                f"Argument 'precision' should be strictly positive but has value of {precision}.")

        if self._verbose:
            print(f"{self._name}: Writing PWL file to {filename}.")

        t_list = self._t_list
        x_list = self._x_list

        with open(filename, "w") as file:
            ti_str = np.format_float_scientific(
                t_list[0], precision-1, unique=False, sign=False)
            xi_str = np.format_float_scientific(
                x_list[0], precision-1, unique=False, sign=True)
            file.write(f"{ti_str}    {xi_str}\n")
            last_t = ti_str
            for ti, xi in zip(t_list[1:], x_list[1:]):
                ti_str = np.format_float_scientific(
                    ti, precision-1, unique=False, sign=False)
                xi_str = np.format_float_scientific(
                    xi, precision-1, unique=False, sign=True)
                if ti_str == last_t:
                    raise PrecisionError(
                        "The chosen precision level caused the written time coordinates to not be strictly increasing.")
                file.write(
                    f"{ti_str}    {xi_str}\n")
                last_t = ti_str
#

#

PWL Plotter

#

plot class method of PWL class

    @classmethod
    def plot(cls, merge: bool = False) -> None:
#

Optional feature: Requires matplotlib

Summary

Class method that takes all instances of the PWL class and plots them on the same time axis.

Arguments

  • merge (bool, optional) : Flag indicating if all signals should be ploted on the same strip or separeted. If not set, defaults to False.

Raises

  • TypeError : Raised if merge is not a boolean.
  • ImportError : Raised if the matplotlib package is not installed.
        if not _has_matplotlib:
            raise ImportError(
                "Optional features are deactivated. Install matplotlib to use.")

        if not isinstance(merge, bool):
            raise TypeError(
                f"Argument 'merge' should be a boolean but has type '{type(merge).__name__}'.")

        dict_of_objects = cls.__dict_of_objects

        if not dict_of_objects:
            return None

        if merge:
            fig, axs = plt.subplots(nrows=1, sharex=True, squeeze=False)
            axs = np.repeat(axs, len(dict_of_objects))
        else:
            fig, axs = plt.subplots(
                nrows=len(dict_of_objects), sharex=True, squeeze=False)
            axs = axs.flatten()
        x_max: float = 0

        for key, ax in zip(dict_of_objects, axs):
            pwl = dict_of_objects[key]
            x_list = pwl.t_list
            x_max = max(x_max, max(x_list))
            y_list = pwl.x_list
            label = pwl.name
            ax.plot(x_list, y_list)
            ax.set_ylabel(label)

        axs[0].set_xlim(xmin=0, xmax=x_max)
        plt.show()
#

Private Methods and Functions

#

From this point forward, all listed methods and functions are not intended to be used by the user. Brief descriptions will be provided, but documentation will be kept to a minimum.

#

#

PWL Point Adder

#

_add private method of PWL class

    def _add(self, t: float, x: float) -> None:
#

Private method that adds a (t, x) point to a PWL object of any size.

        if len(self._t_list) >= 1 and t <= self._t_list[-1]:
            raise PrecisionError(
                f"Internal Python rounding caused the time coordinates to not be strictly increasing when adding points to {self._name}.")

        if len(self._t_list) == len(self._x_list) < 2:
            self._t_list.append(t)
            self._x_list.append(x)
        else:
            self._colinear_eliminator(x, t)
#

#

Colinear Points Eliminator

#

_colinear_eliminator private method of PWL class

    def _colinear_eliminator(self, x: float, t: float) -> None:
#

Private method that adds a (t, x) point to a PWL object with 2 or more points without consecutive colinear points.

        t_n_1 = self._t_list[-1]
        t_n_2 = self._t_list[-2]

        x_n_1 = self._x_list[-1]
        x_n_2 = self._x_list[-2]

        last_m = (x_n_1 - x_n_2)/(t_n_1 - t_n_2)
        new_m = (x - x_n_1)/(t - t_n_1)

        if last_m == new_m:
            self._t_list[-1] = t
            self._x_list[-1] = x
        else:
            self._t_list.append(t)
            self._x_list.append(x)
#

#

Nested Linear Transition

#

_lin_transition private method of PWL class

    def _lin_transition(self, target: float, duration: float, n: int) -> None:
#

Private method to generate a linear transition. The difference between this method and the public linear transition method is that this method prints indented verbose output when called from within any of the public methods that revert to a linear transition in certain conditions.

        if self._verbose:
            if n == 0:
                print(
                    f"{self._name}: Adding linear transition with target of {target} and duration of {duration}.")
            else:
                print(
                    n*"    "+f"Adding linear transition with target of {target} and duration of {duration}.")

        if len(self._t_list) == len(self._x_list) == 0:
            if self._verbose:
                print((n+1)*"    "+"Empty PWL object. Adding initial (0, 0) point.")
            self._add(0, 0)

        last_t = self._t_list[-1]
        self._add(last_t+duration, target)
#

#

Exponential Function Generator

#

_exp_transition_func private function

def _exp_transition_func(tau: float, t1: float, f1: float, t2: float, f2: float) -> Callable[[float], float]:
#

Private function that generates an exponential function passing trough 2 fixed points.

    A: float = (f1*np.exp(t1/tau) - f2*np.exp(t2/tau)) / \
        (np.exp(t1/tau) - np.exp(t2/tau))
    B: float = (f1 - f2)/(np.exp(-t1/tau) - np.exp(-t2/tau))
#
    def f(t: float) -> float:
        result: float = A + B*np.exp(-t/tau)
        return result

    return f
#

#

Sinusoidal Function Generator

#

_sin_transition_func private function

def _sin_transition_func(t1: float, f1: float, t2: float, f2: float) -> Callable[[float], float]:
#

Private function that generates a sinusoidal function passing trough 2 fixed points.

    fm: float = (f1+f2)/2
    tm: float = (t1+t2)/2
    T: float = 2*(t2-t1)
    w: float = 2*np.pi/T
    phi: float = w*tm
    A: float = f2-fm
#
    def f(t: float) -> float:
        result: float = fm + A*np.sin(w*t - phi)
        return result

    return f
#

#

Smoothstep Function Generator

#

_smoothstep_transition_func private function

def _smoothstep_transition_func(t1: float, f1: float, t2: float, f2: float) -> Callable[[float], float]:
#

Private function that generates a smoothstep function passing trough 2 fixed points.

    Am = np.array([[1, t1, t1**2, t1**3],
                   [1, t2, t2**2, t2**3],
                   [0, 1, 2*t1, 3*t1**2],
                   [0, 1, 2*t2, 3*t2**2]])
    Bm = np.array([f1, f2, 0, 0])
    A, B, C, D = np.linalg.solve(Am, Bm)
#
    def f(t: float) -> float:
        result: float = A + B*t + C*t**2 + D*t**3
        return result

    return f
#

if __name__ == "__main__":
    pwl0 = PWL(0.001)
    pwl1 = PWL(0.001)

    pwl0.hold(1)
    pwl1.hold(1)

    pwl0.sin_transition(1, 1)
    pwl1.sin_transition(-1, 1)

    pwl0.hold(1)
    pwl1.hold(1)

    pwl0.sin_transition(0, 1)

    PWL.plot(merge=True)