Model Objects

For creating dynamic/static models, harold offers basically two options: A State() and a Transfer() object for representing systems with state space and transfer matrices. The initialization of these objects are pretty straightforward.

G = State([[1,2],[3,4]],[[1],[0]],[1,2],0)
H = Transfer([1,2,3],[7,5,3])

Note that, in Python, the calling order of the arguments are not fixed. You can also use the following

G = State(d=0,c=[1,2],[[1,2],[3,4]],[[1],[0]])
H = Transfer([1,2,3],[7,5,3])

As obvious to everyone who used this syntax even in matlab’s convenient bracket semicolon notation, creating these 4 individual matrices first just to pass to the function becomes increasingly annoying. Instead a matrix slicer is available in harold:

M = np.array([[1,2,1],[3,4,0],[1,2,0]])
G = State(*matrix_slice(M,(2,2))

to simply give matrix and the shape of the upper left block. Also, for strictly proper State representations (representations with \(D=0\)) adding zero blocks everytime is also redundant. Hence you can skip the fourth item and \(D=0\) will be assumed with the correct size.

In harold, there are quite a few handy shortcuts of this nature and if your favorite feature is not implemented please have your say.

Creating models

State() models

As shown above, the model creation is a straightforward enumeration of the involved A,B,C,D matrices in the arguments. For discrete time models, you simply add a fifth argument, if you have not omitted any (which is useful in some context but will be documented later), or explicitly mentioning dt=<sampling period> as an argument. Example:

G = State(-1,1,1,0,0.2)
G = State([[1,0],[-2,-5]],[[1],[0]],[0,1],0,dt=0.01)
    G = State(-1,1,1,dt=0.01) # D is omitted hence requires explicit dt=

To create a static system, a gain matrix(scalar) but still having a state representation, then it is possible to just provide a gain matrix

G = State(1)
G = State(np.ones((5,3))

If there is also the discrete time property that is needed to be specified, then dt=<sampling period> should be provided explicitly as harold would be confused about the second argument being the B element.

G = State(1,0.001)    # Will lead to error
G = State(1,dt=0.001) # Will work

The class methods and properties are listed below but probably something that might not be obvious is the following discretization detail If the model is discretized we can also check

G.SamplingPeriod  # returns the sampling period
G.SamplingSet     # returns 'Z' for discrete-time, 'R' otherwise
G.DiscretizedWith # returns the discretization method if applicable

These make sure that the discretization remembers how it got there in the first place if harold is used. Or if the model is already given as a discrete time model, the method can be set such that undiscretize can use the correct method.

class harold.State(a, b=None, c=None, d=None, dt=False)

State() is the one of two main system classes in harold (together with Transfer() ).

A State object can be instantiated in a straightforward manner by entering 2D arrays, floats, 1D arrays for row vectors and so on.:

>>>> G = State([[0,1],[-4,-5]],[[0],[1]],[[1,0]],1)

However, the preferred way is to make everything a numpy array. That would skip many compatibility checks. Once created the shape of the system matrices cannot be changed. But compatible sized arrays can be supplied and it will recalculate the pole/zero locations etc. properties automatically.

The Sampling Period can be given as a last argument or a keyword with ‘dt’ key or changed later with the property access.:

>>>> G = State([[0,1],[-4,-5]],[[0],[1]],[[1,0]],[1],0.5)
>>>> G.SamplingSet
'Z'
>>>> G.SamplingPeriod
0.5
>>>> F = State(1,2,3,4)
>>>> F.SamplingSet
'R'
>>>> F.SamplingPeriod = 0.5
>>>> F.SamplingSet
'Z'
>>>> F.SamplingPeriod
0.5

Setting SamplingPeriod property to ‘False’ value to the will make the system continous time again and relevant properties are reset to continuous-time properties.

Warning: Unlike matlab or other tools, a discrete time system needs a specified sampling period (and possibly a discretization method if applicable) because a model without a sampling period doesn’t make sense for analysis. If you don’t care, then make up a number, say, a million, since you don’t care.

SamplingSet

If this property is called G.SamplingSet then returns the set Z or R for discrete and continous models respectively. This is a read only property and cannot be set. Instead an appropriate setting should be given to the SamplingPeriod property.

NumberOfStates

A read only property that holds the number of states.

NumberOfInputs

A read only property that holds the number of inputs.

NumberOfOutputs

A read only property that holds the number of outputs.

shape

A read only property that holds the shape of the system as a tuple such that the result is (# of inputs , # of outputs).

matrices

A read only property that returns the model matrices.

a

If this property is called G.a then returns the matrix data. Alternatively, if this property is set then the provided value is first validated with the existing system shape and number of states.

b

If this property is called G.b then returns the matrix data. Alternatively, if this property is set then the provided value is first validated with the existing system shape and number of states.

c

If this property is called G.c then returns the matrix data. Alternatively, if this property is set then the provided value is first validated with the existing system shape and number of states.

d

If this property is called G.a then returns the matrix data. Alternatively, if this property is set then the provided value is first validated with the existing system shape.

SamplingPeriod

If this property is called G.SamplingPeriod then returns the sampling period data. If this property is set to False, the model is assumed to be a continuous model. Otherwise, a discrete time model is assumed. Upon changing this value, relevant system properties are recalculated.

DiscretizedWith

This property is used internally to keep track of (if applicable) the original method used for discretization. It is used by the undiscretize() function to reach back to the continous model that would hopefully minimize the discretization errors. It is also possible to manually set this property such that undiscretize uses the provided method.

DiscretizationMatrix

This matrix denoted with \(Q\) is internally used to represent the upper linear fractional transformation of the operation \(\frac{1}{s} I = \frac{1}{z} I \star Q\). For example, the typical tustin, forward/backward difference methods can be represented with

\[\begin{split}Q = \begin{bmatrix} I & \sqrt{T}I \\ \sqrt{T}I & \alpha TI \end{bmatrix}\end{split}\]

then for different \(\alpha\) values corresponds to the transformation given below:

\(\alpha\) method
\(0\) backward difference (euler)
\(0.5\) tustin
\(1\) forward difference (euler)

This operation is usually given with a Riemann sum argument however for control theoretical purposes a proper mapping argument immediately suggests a more precise control over the domain the left half plane is mapped to. For this reason, a discretization matrix option is provided to the user.

The available methods (and their aliases) can be accessed via the internal _KnownDiscretizationMethods variable.

Note

The common discretization techniques can be selected with a keyword argument and this matrix business can safely be avoided. This is a rather technical issue and it is best to be used sparingly. For the experts, I have to note that the transformation is currently not tested for well-posedness.

Note

SciPy actually uses a variant of this LFT representation as given in the paper of Zhang et al.

PrewarpFrequency

If the discretization method is tustin then a frequency warping correction might be required the match of the discrete time system response at the frequency band of interest. Via this property, the prewarp frequency can be provided.

static validate_arguments(a, b, c, d, verbose=False)

An internal command to validate whether given arguments to a State() instance are valid and compatible.

It also checks if the lists are 2D numpy.array’able entries.

Transfer() models

As mentioned previously, numpy array syntax is strange and a bit verbose. Hence, it makes it difficult to type every time np.array or some other alias for creation of an array tobe used in the transfer representation definitions. Hence, harold actually goes a long way to make sense what is entered for the Transfer() initialization.

First, it can understand even if the user enters a scalar or a Python list, instead of numpy arrays. It will be checked and converted if the input is sensible.

G = Transfer(1,[1,2,3])
G = Transfer(1,[[1,2,3]])

The second example might confuse the user since it will spit out a transfer representation of a \(1\times 3\) static gain.

What happens is that when the parses find a list of lists, it assumes that the user is trying to create a MIMO object. Thus, it changes its context to make sense with the missing information. It first checks that numerator has a single element and thus assumes that this is a common numerator. Then it parses the denominator and finds only scalars thus creates the static gain of fractions one, one half, and one third.

Discrete time models are handled similarly.

After the initialization of the models, we can see the model properties

G.NumberOfOutputs # returns the number of rows of numerator
G.NumberOfOutputs # returns the number of cols of numerator
G.shape           # returns a tuple of (# of outs,# of ins)

G.polynomials     # returns the polynomials ala tfdata
G.poles           # returns the poles
G.zeros           # returns the zeros
G.num,G.den       # returns the individual polynomials

If the model is discretized we can also check

G.SamplingPeriod  # returns the sampling period
G.SamplingSet     # returns 'Z' for discrete-time, 'R' otherwise
G.DiscretizedWith # returns the discretization method if applicable
class harold.Transfer(num, den=None, dt=False)

Transfer is the one of two main system classes in harold (together with State()).

Main types of instantiation of this class depends on whether the user wants to create a Single Input/Single Output system (SISO) or a Multiple Input/Multiple Output system (MIMO) model.

For SISO system creation, 1D lists or 1D numpy arrays are expected, e.g.,:

>>>> G = Transfer(1,[1,2,1])

For MIMO systems, depending on the shared denominators, there are two distinct ways of entering a MIMO transfer function:

1. Entering “list of lists of lists” such that every element of the inner lists are numpy array-able (explicitly checked) for numerator and entering a 1D list or 1D numpy array for denominator (and similarly for numerator):

>>>> G = Transfer([[[1,3,2],[1,3]],[[1],[1,0]]],[1,4,5,2])
>>>> G.shape
(2,2)

2. Entering the denominator also as a list of lists for individual entries as a bracket nightmare (thanks to Python’s nonnative support for arrays and tedious array syntax):

 >>>> G = Transfer([
          [ [1,3,2], [1,3] ],
          [   [1]  , [1,0] ]
        ],# end of num
        [
           [ [1,2,1] ,  [1,3,3]  ],
           [ [1,0,0] , [1,2,3,4] ]
        ])
>>>> G.shape
(2,2)

There is a very involved validator and if you would like to know why or how this input is handled ,provide the same numerator and denominator to the static method below with ‘verbose=True’ keyword argument, e.g.

>>>> n , d , shape , is_it_static = Transfer.validate_arguments(
          [1,3,2], # common numerator
          [[[1,2,1],[1,3,3]],[[1,0,0],[1,2,3,4]]],# explicit den
          verbose=True # print the logic it followed
          )

would give information about the context together with the regularized numerator, denominator, resulting system shape and boolean whether or not the system has dynamics.

However, the preferred way is to make everything a numpy array inside the list of lists. That would skip many compatibility checks. Once created the shape of the numerator and denominator cannot be changed. But compatible sized arrays can be supplied and it will recalculate the pole/zero locations etc. properties automatically.

The Sampling Period can be given as a last argument or a keyword with ‘dt’ key or changed later with the property access.:

>>>> G = Transfer([1],[1,4,4],0.5)
>>>> G.SamplingSet
'Z'
>>>> G.SamplingPeriod
0.5
>>>> F = Transfer([1],[1,2])
>>>> F.SamplingSet
'R'
>>>> F.SamplingPeriod = 0.5
>>>> F.SamplingSet
'Z'
>>>> F.SamplingPeriod
0.5

Providing ‘False’ value to the SamplingPeriod property will make the system continous time again and relevant properties are reset to CT properties.

Warning

Unlike matlab or other tools, a discrete time system needs a specified sampling period (and possibly a discretization method if applicable) because a model without a sampling period doesn’t make sense for analysis. If you don’t care, then make up a number, say, a million, since you don’t care.

SamplingSet

If this property is called G.SamplingSet then returns the set Z or R for discrete and continous models respectively. This is a read only property and cannot be set. Instead an appropriate setting should be given to the SamplingPeriod property.

NumberOfInputs

A read only property that holds the number of inputs.

NumberOfOutputs

A read only property that holds the number of outputs.

shape

A read only property that holds the shape of the system as a tuple such that the result is (# of inputs , # of outputs).

polynomials

A read only property that returns the model numerator and the denominator as the outputs.

SamplingPeriod

If this property is called G.SamplingPeriod then returns the sampling period data. If this property is set to False, the model is assumed to be a continuous model. Otherwise, a discrete time model is assumed. Upon changing this value, relevant system properties are recalculated.

num

If this property is called G.num then returns the numerator data. Alternatively, if this property is set then the provided value is first validated with the existing denominator shape and causality.

den

If this property is called G.den then returns the numerator data. Alternatively, if this property is set then the provided value is first validated with the existing numerator shape and causality.

DiscretizedWith

This property is used internally to keep track of (if applicable) the original method used for discretization. It is used by the undiscretize() function to reach back to the continous model that would hopefully minimize the discretization errors. It is also possible to manually set this property such that undiscretize uses the provided method.

DiscretizationMatrix

This matrix denoted with \(Q\) is internally used to represent the upper linear fractional transformation of the operation \(\frac{1}{s} I = \frac{1}{z} I \star Q\). For example, the typical tustin, forward/backward difference methods can be represented with

\[\begin{split}Q = \begin{bmatrix} I & \sqrt{T}I \\ \sqrt{T}I & \alpha TI \end{bmatrix}\end{split}\]

then for different \(\alpha\) values corresponds to the transformation given below:

\(\alpha\) method
\(0\) backward difference (euler)
\(0.5\) tustin
\(1\) forward difference (euler)

This operation is usually given with a Riemann sum argument however for control theoretical purposes a proper mapping argument immediately suggests a more precise control over the domain the left half plane is mapped to. For this reason, a discretization matrix option is provided to the user.

The available methods (and their aliases) can be accessed via the internal _KnownDiscretizationMethods variable.

Note

The common discretization techniques can be selected with a keyword argument and this matrix business can safely be avoided. This is a rather technical issue and it is best to be used sparingly. For the experts, I have to note that the transformation is currently not tested for well-posedness.

Note

SciPy actually uses a variant of this LFT representation as given in the paper of Zhang et al.

PrewarpFrequency

If the discretization method is tustin then a frequency warping correction might be required the match of the discrete time system response at the frequency band of interest. Via this property, the prewarp frequency can be provided.

static validate_arguments(num, den, verbose=False)

A helper function to validate whether given arguments to an Transfer instance are valid and compatible for instantiation.

Since there are many cases that might lead to a valid Transfer instance, Pythonic “try,except” machinery is not very helpful to check every possibility and equally challenging to branch off. A few examples of such issues that needs to be addressed is static gain, single entry for a MIMO system with common denominators and so on.

Thus, this function provides a front-end to the laborious size and type checking which would make the Transfer object itself seemingly compatible with duck-typing while keeping the nasty branching implementation internal.

The resulting output is compatible with the main harold Transfer class convention such that

  • If the recognized context is MIMO the resulting outputs are list of lists with numpy arrays being the polynomial coefficient entries.
  • If the recognized context is SISO the entries are numpy arrays with any list structure is stripped off.
Parameters:
  • num

    The polynomial coefficient containers. Either of them can be (not both) None to assume that the context will be derived from the other for static gains. Otherwise both are expected to be one of np.array, int , float , list , list of lists of lists or numpy arrays.

    For MIMO context, element numbers and causality checks are performed such that numerator list of list has internal arrays that have less than or equal to the internal arrays of the respective denominator entries.

    For SISO context, causality check is performed between numerator and denominator arrays.

  • den – Same as num
  • verbose (boolean) – A boolean switch to print out what this method thinksabout the argument context.
Returns:

  • num (List of lists or numpy array (MIMO/SISO))
  • den (List of lists or numpy array (MIMO/SISO))
  • shape (2-tuple) – Returns the recognized shape of the system
  • Gain_flag (Boolean) – Returns True if the system is recognized as a static gain False otherwise (for both SISO and MIMO)

Model Arithmetic

Both Transfer and State instances support basic model arithmetic. You can add/multiply/subtract models that are compatible (division is a completely different story hence omitted). Again, harold tries its best to explain what went wrong. Let’s take the same discrete time SISO system and set another random MIMO model H with 3 states:

G = State(-1,1,1,dt=0.01)
    H = State(*matrix_slice(np.random.rand(5,6),(3,3)))
    G*H

    TypeError: The sampling periods don't match so I cannot multiply these
    systems. If you still want to multiply them asif they are compatible,
    carry the data to a compatible system model and then multiply.

Even after making G a continous time system

G.SamplingPeriod = 0.
G*H

IndexError: Multiplication of systems requires their shape to match but
the system shapes I got are (1, 1) vs. (2, 3)

Notice the system sizes are repeated in the error message hence we don’t need to constantly check which part is the culprit for both systems.

For Transfer models, another useful property is recognition of common poles when doing simple addition/subtraction. For example,

G = Transfer([1,1],[1,2])
H = Transfer([1],[1,3,2])

F = G+H
F.polynomials #check num,den
(array([[ 1.,  2.,  2.]]), array([[ 1.,  3.,  2.]]))

As you can see the cancellations are performed at the computations such that the model order does not increase artificially.

Note

This is currently not the case for State instances that is to say the state matrices are directly augmented without any cancellation checks. This is probably going to change in the future.

Context Discovery

For both State() and Transfer() argument parsing, harold can tell what happened during the context discovery. For that, there is a validate_arguments() class method for each class. This will return the regularized version of the input arguments and also include a flag in case the resulting system is a static gain:

Transfer.validate_arguments(1,[[1,2,3]],verbose=1)

will print out the following for the example we discussed above

========================================
Handling numerator
========================================
I found only a float
========================================
Handling denominator
========================================
I found a list
I found a list that has only lists
Every row has consistent number of elements
==================================================
Handling raw entries are done.
Now checking the SISO/MIMO context and regularization.
==================================================
One of the MIMO flags are true
Denominator is MIMO, Numerator is something else
Denominator is MIMO, Numerator is SISO
In the MIMO context and proper entries, I've found
scalar denominator entries hence flagging as a static gain.
Out[7]:
([[array([[ 1.]]), array([[ 1.]]), array([[ 1.]])]],
 [[array([[ 1.]]), array([[ 2.]]), array([[ 3.]])]],
 (1, 3),
 True)

As seen from the resulting arrays, the numerator is now three numpy float arrays containing the common entry. Both the numerator and denominator are converted to list of lists.

This method can also be used to verify whether a certain input is a valid argument for creating model objects hence the name.

Same class method is also available for the State() class.