Repository navigation
Inconsistent evaluation of tf/ss systems with pole at the origin #532
Description
Activity
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]])+1 for consistency.
nanlooks 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)
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.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?
That's inf by convention which is the value of the limit, NaN is suitable for cases like 0/0 etc.
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 + nanjSo basically the real and imaginary parts of the numerator are evaluated independently and
1 / 0returnsinfwhile0 / 0returnsnan. These should happen automatically for theTransferFunctionclass andStateSpaceobjects 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 warningThoughts?
- linked a pull request that will close this issueFix inconsistencies in frequency response at poles #542
on Feb 11, 2021
Systems that have a pole at the origin return inconsistent values:
I think all of these should return
inf(orarray(inf)).If you have a pole and zero at the origin, you get
nanor 1: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.