Skip to content

Inconsistent evaluation of tf/ss systems with pole at the origin #532

Description

@murrayrm

Systems that have a pole at the origin return inconsistent values:

import control as ct
sys_tf = ct.tf([1], [1, 0])
sys_ss = ct.tf2ss(sys_tf)

sys_tf(0)        # returns (inf+0j) with divide by zero warning
sys_tf(0j)       # returns (inf+nanj) with divide by zero warning
sys_ss(0)        # LinAlgError: Singular matrix
sys_tf.dcgain()  # returns array(inf)
sys_ss.dcgain()  # returns array(nan)

I think all of these should return inf (or array(inf)).

If you have a pole and zero at the origin, you get nan or 1:

sys_tf = ct.tf([1, 0], [1, 0])
sys_ss = ct.tf2ss(sys_tf)

sys_tf(0)        # returns (nan+0j) with invalid value warning
sys_ss(0)        # returns (1+0j) [state space system is empty]
sys_tf.dcgain()  # returns array(nan)
sys_ss.dcgain()  # returns (1+0j) [state space system is empty]

I think all of these cases should produce nan.

I have a partial fix in a branch, but want to see what people think the "right" answer is before committing it.

Activity

  1. roryyorke commented on Feb 6, 2021

    @roryyorke
    Contributor

    Would this apply to evaluating a system at any s-value where it has a pole?

    For the second set of examples, for TFs one objection is that \lim_{s->0} (s/s) = 1. For a Bode or Nyquist plot, for example, wouldn't one want this limit, and not a NaN ?

    For state-space objects, why would you want a static gain to evaluate to NaN?

    Separarately for state-space, the MIMO case might be tricky: consider a diagonal system with g_11 = 1/(s+1), g_22 = 1/s; would evaluating this at s = 0 be expected to give [[1, 0], [0, inf]] ? I wonder how easy this will be to arrange:

    n [37]: np.eye(2) @ np.diag([1, np.inf]) @ np.eye(2)
    <ipython-input-37-7a3bfddc9905>:1: RuntimeWarning: invalid value encountered in matmul
      np.eye(2) @ np.diag([1, np.inf]) @ np.eye(2)
    Out[37]: 
    array([[nan, nan],
           [nan, inf]])
    
  2. bnavigator commented on Feb 6, 2021

    @bnavigator
    Contributor

    +1 for consistency. nan looks like a good choice to me.

    Although one could argue that the transfer function should make a distinction between true marginal stability cases and cases where the pole/zero at 0 cancel each other. (Just as @roryyorke pointed out just now)

  3. ilayn commented on Feb 6, 2021

    @ilayn

    I think point-wise evaluation at 0 is not the correct way to compute the limit but using the explicit expression D - C @ solve(A, B) for the limit should be preferred (and the discrete version obviously). If A is not invertible then it should return inf with correct shape. NaN doesn't seem to be a meaningful return value for the gain.

  4. bnavigator commented on Feb 6, 2021

    @bnavigator
    Contributor

    For state-space objects, why would you want a static gain to evaluate to NaN?

    If the system is not asymptotically stable, is there a static gain?

  5. ilayn commented on Feb 6, 2021

    @ilayn

    That's inf by convention which is the value of the limit, NaN is suitable for cases like 0/0 etc.

  6. murrayrm commented on Feb 8, 2021

    @murrayrm
    MemberAuthor

    I thought it might be interesting to see what Python and NumPy do with various cases:

    Expression                                             Returns
    ----------                                             --------
    1 / 0                                                  ZeroDivisionError
    1j / 0j                                                ZeroDivisionError
    np.array(1) / np.array(0)                              inf
    np.array(0) / np.array(0)                              nan
    np.array(1j) / np.array(0j)                            nan + infj
    np.array(1) / np.array(0j)                             inf + nanj
    np.array(1 + 0j) / np.array(0j)                        inf + nanj
    np.array(1j) / np.array(0)                             nan + infj
    np.array(0j) / np.array(0j)                            nan + nanj
    np.array(0) / np.array(0j)                             nan + nanj
    np.polyval([1, 0], 1j) / np.polyval([1, 0], 0j)        nan + infj
    np.polyval([1, 1], 1j) / np.polyval([1, 0], 0j)        inf + infj
    np.polyval([1, 0], 1j) / np.polyval([1, 0, 0], 0j)     nan + infj
    np.polyval([1, 0], 0j) / np.polyval([1, 0], 0j)        nan + nanj
    np.polyval([1, 1], 0j) / np.polyval([1, 0], 0j)        inf + nanj
    

    So basically the real and imaginary parts of the numerator are evaluated independently and 1 / 0 returns inf while 0 / 0 returns nan. These should happen automatically for the TransferFunction class and StateSpace objects should be consistent.

    Regarding unstable systems: while it is true that H(jw) is not well defined in terms of giving the magnitude and phase of the output for a sinusoidal input, it is still the case that we want to evaluate transfer functions at these values. For example, the loop transfer function can be unstable while the closed loop system is stable.

    Specific proposal:

    sys_tf = ct.tf([1], [1, 0])
    sys_ss = ct.tf2ss(sys_tf)
    
    sys_tf(0)        # return (inf+0j) with divide by zero warning
    sys_tf(0j)       # return (inf+nanj) with divide by zero warning
    sys_ss(0)        # return (inf+0j) with singular matrix warning
    sys_ss(0j)       # return (inf+nanjj) with singular matrix warning
    sys_tf.dcgain()  # return array(inf+nanj) with divide by zero warning
    sys_ss.dcgain()  # return array(inf+nanj) with singular matrix warning
    

    Thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions