I would like to propose the deprecation of the “linear time invariant system base class” scipy.signal._ltisys.LinearTimeInvariant and its children. This post was motivated by the current discussions about deprecations for SciPy 2.0. Issue #6137 (opened in 2016) provides some further insights on this topic.
The object-oriented design of the classes in file scipy/signal/_ltisys.py relies
heavily on inheritance, i.e.:
class LinearTimeInvariant:
"""Linear time invariant system base class. """
class lti(LinearTimeInvariant):
"""Continuous-time linear time invariant system base class. """
class dlti(LinearTimeInvariant):
"""Discrete-time linear time invariant system base class. """
class TransferFunction(LinearTimeInvariant):
"""Linear Time Invariant system class in transfer function form. """
class TransferFunctionContinuous(TransferFunction, lti):
"""Continuous-time Linear Time Invariant system in transfer function form. """
class TransferFunctionDiscrete(TransferFunction, dlti):
"""Discrete-time Linear Time Invariant system in transfer function form. """
class ZerosPolesGain(LinearTimeInvariant):
"""Linear Time Invariant system class in zeros, poles, gain form. """
class ZerosPolesGainContinuous(ZerosPolesGain, lti):
"""Continuous-time Linear Time Invariant system in zeros, poles, gain form. """
class ZerosPolesGainDiscrete(ZerosPolesGain, dlti):
"""Discrete-time Linear Time Invariant system in zeros, poles, gain form. """
class StateSpace(LinearTimeInvariant):
"""Linear Time Invariant system in state-space form. """
class StateSpaceContinuous(StateSpace, lti):
"""Continuous-time Linear Time Invariant system in state-space form """
class StateSpaceDiscrete(StateSpace, dlti):
"""Discrete-time Linear Time Invariant system in state-space form """
The inheritance reduces some code duplication at the price of adding significant code
complexity and maintenance costs. E.g.:
- The classes
LinearTimeInvariant,lti,dlti,TransferFunction,TransferFunctionContinuous,TransferFunctionDiscrete,ZerosPolesGain,ZerosPolesGainContinuous, andZerosPolesGainDiscretedo not contain algorithms. Their methods just wrap other public functions. - Instantiating a
StateSpaceproduces either an instance ofStateSpaceContinuousorStateSpaceDiscretedepending on the parameters. The same holds forZerosPolesGain. This does not mix too well with the Python typing system and the new Array API backends. - The functionality is inconsistent. E.g.,
StateSpaceimplements a multiplication operator (concatenation), butTransferFunctionandZerosPolesGaindo not. - The functions
lsim,dlsim,impulse,dimpulse,dfreqresp,freqresp,step, anddstepaccept instances from the classes above, but do not automatically distinguish between continuous-time and discrete-time systems.
Progress of new features and bug fixing of these classes is quite slow. In my opinion, a key reason for the few new contributions is the relatively steep learning curve for understanding the code.
Further reasons for not keeping these classes:
- In theory these classes allow easily switching system representations. In practice, numerics frequently foil this. IMO, it is better to make the user call explicit conversion functions, which ideally would document numerical pitfalls.
- It would not be too hard to implement system composability functionality by adding
+,-,*,/, and@operators, where applicable. This would make these classes much more versatile. But again, the numerics of almost stable systems are a rich source for potential issues. - Practical reasoning about continuous-time systems is quite different from reasoning about discrete-time systems. Hence, I think that mixing discrete-time and continuous-time systems in the same class is a little bit confusing from a user’s perspective: A newcomer might only be well-versed in one of them.
For that reason I would propose deprecating the classes listed above. Furthermore,
to depecreate that the functions lsim, dlsim, impulse, dimpulse, step, dstep, dfreqresp, freqresp, step, dstep, ss2tf, ss2zpk, cont2discrete, and place_poles accept instances of those classes as an argument.
Outlook
Having a modernized set of LTI classes is a worthwhile goal for SciPy, IMO.
If such an ambitious project is pursued, the following features would be desirable:
- There would have to be well-documented mechanisms to deal with the most common numerical problems. E.g., detecting and eliminating (almost) equal zeros and poles in transfer functions or detect (almost) unstable state space systems.
- The functionality of the functions
lsim,dlsim,impulse,dimpulse,dfreqresp,freqresp,step,dstep,ss2tf,ss2zpk,cont2discrete,place_polesshould become class methods. - System composability by adding
+,-,*,/, and@operators, where applicable. Also, adding functionality for designing (linear) controllers like PID or LQG would be a boon. - It would be great to have a pluggable arbitrary-precision floating-point arithmetic backend like mpmath. E.g., this would allow calculating eigenvalues with arbitrary
accuracy.
I would love to read feedback!