Introduction: This application demonstrates properties of transfer functions (including the polynomial characteristic equation, the pole/zero representation, the partial fraction expansion, and the time-domain representation), and the output when the system is applied to various inputs.
Directions:
Input or select a transfer function for a system, as a numerator and a denominator.
Input or select a transfer function for an input to view the application of the system on the input.
Change plot parameters. (Time scale adjusts the horizontal pixels in one second; resolution adjusts the sampling rate; decimal precision adjusts the mathematical precision of calculations.)
Position the mouse over a point along the root locus in the pole-zero plot to see the gain that will produce a pole at that location in the closed-loop equation of the system.
More Info:
This application accepts a transfer function of a system, specified as a numerator and denominator, and performs the following steps:
The application parses each expression into an arrangement of logical operators.
The application transforms each parsed expression into a single polynomial (numerator / denominator), normalizing the highest-order coefficients to 1 (and a gain, \( k \) ) and the lowest-order terms to \( s^0 \).
The application solves the numerator and denominator polynomials to identify roots that represent the zeros and poles.
The application verifies that the numerator and denominator represent a proper fraction (i.e., the numerator is a lower order than the denominator), or else performs long division to produce a non-fraction polynomial and a proper fraction, which are carried through the rest of the process.
The application expands the pole/zero representation into a sum of linear and quadratic partial fractions.
The application consolidates zeros and poles within a small distance to reduce errors from the polynomial solver approximation. (e.g., \( {1 \over (s + 1.01) (s + 1.00) } = {1 \over (s + 1.00) (s + 1.00) } \) )
The application coalesces identical zeros and poles into exponentials. (e.g., \( { 1 \over (s+1) (s+1) } = { 1 \over (s+1)^2 } \) )
The application applies an inverse Laplace transform to each partial fraction to produce a time-domain representation.
The application traces the root locus branches from each pole to each zero, keeping track of where branches diverge.
The application calculates the frequency response over a log range.
The application presents a time-domain plot representing the impulse response of the system.
The application presents an interactive pole-zero and root-locus plot of the transfer function.
The application generates Bode and Nyquist plots of the frequency response.
The application applies the transfer function to a selected input signal, and displays plots of the input signal and the output signal.
The application renders relevant equations via LaTeX.
The application updates its URL parameters with the system equation and the input equation. The URL can be copied-and-pasted, bookmarked, emailed, etc.; when loaded, the URL causes the application to present the same system, input, and output functions.
This application handles a decent range of typical transfer functions, but certainly not all. Some known limitations:
The application cannot handle non-integer exponents (e.g., \( F(s) = (s + 1)^{({3 \over 2})} \) ), nor exponents that are powers of \( s \) (e.g., \( F(s) = 12^s \) ).
While the application can handle transfer functions with complex poles and roots, it cannot handle transfer functions specified using complex values. (The parser is unable to cope with the possibility of non-conjugate complex values.) That is, the parser can handle \( F(s) = {1 \over s^2 + 1} \), which is equal to \( {1 \over (s + j) (s - j) } \), but cannot directly accept \( F(s) = {1 \over (s + j) (s - j) } \).
The application does not correctly plot some transfer functions that are expressed as improper fractions. Explanation:
\( F(s) = s \leftrightarrow f(t) = \delta'(t) = \begin{cases} \infty, t = 0^- \\ -\infty, t = 0^+ \\ 0, t \neq 0 \\ \end{cases} \)
Obviously, this \( f(0) \) is difficult to represent on a time-domain plot. This problem generally applies to all \( F(s) = s^n, n \geq 1 \rightarrow f(t) = \delta^{(n)}(t) \).
Development Notes:
In addition to demonstrating transfer functions, this application was developed as a library of transfer-function-related code, in order to serve as a base for other applications in the domain of control systems. (Techniques like systems analysis and compensation schemes are very powerful, but the writing more complex applications from scratch with these equations included is an overly daunting prospect!) The code features extensive comments to promote reuse.
Mathematically, the most challenging part of this project was figuring out how to handle quadratic partial fractions of the form: \( { (As + B) \over ((s^2 + \alpha^2) + \omega^2)^n }, n \geq 2 \). These types of factors arise from transfer functions such as \( { 1 \over (s^2 + s + 1)^2 } \), which is syntactically valid input that I wanted the application to handle.
The partial fraction decomposition of this function is: \( {As + B \over s^2 + s + 1} + {Cs + D \over (s^2 + s + 1)^2 } \) . While the inverse Laplace transform of the first fraction is elementary, the second is much more difficult - and higher-order powers even more so.
The solution was derived by examining the series of inverse Laplace transforms for a few specific values of \( n \). For instance:
For the record, for a denominator power of \( n \), the pattern involves applying a coefficient to each term of the series, of the general form:
\( { e^{-\alpha t} \over 2^{n-2} (n-1)! (\omega)^n } \sum\limits_{m = 1}^{n - 2} { (n + m)! \over m! (n - m)! 2^{n-m}} t^m (\sin(\omega t)^{(m)}) \)
(A full description of the algorithm can be found by examining the generate_weird_terms() function.) This series is efficiently computable for each term, and the application produces results that not only match WolframAlpha's results, but respond much faster.
Another aspect that required some finesse was the root-locus plot. This application determines the root locus by starting at each open-loop pole, and incrementally sampling the neighborhood of each coordinate to see where the angle condition holds and the magnitude increases. Each root locus branch is individually traced in this manner - but at a divergence point, the branch trace encounters multiple forks, and a branch is chosen arbitrarily. On the one hand, arbitrary selection is actually OK - if multiple branches meet at a certain point and then head off in different directions, it actually doesn't matter which branch each one takes. However, multiple traces sometimes ended up taking the same fork, leaving some forks mapped twice and other forks unmapped.
This problem was solved by detecting divergence points (i.e., coordinates where two or more significantly different angles will satisfy the conditions), and keeping a record of which angles have been taken at this point by previous branches - thereby forcing each branch to take a new fork. The solution is slightly clumsy, but it seems to do the trick for all tested cases.
Some difficulty arose with the identification of zeros in improper fractions, where the order of the numerator is the same as or greater than the order of the denominator. For these fractions, the application applies long division and produces a polynomial plus a proper fraction. However, this transformation may distort the appearance of zeros.
For example, a fraction such as \( {s \over s + 3} \) is identified as improper and factored via long division into \( -3 (-{1 \over 3} + {1 \over s + 3}) \). Although this function is numerically identical, the zero no longer appears in the numerator. That is, after long division, both the non-fraction polynomial and the proper fraction must be evaluated to determine the zeros.
The solution involved evaluating and retaining arrays of zeros and poles from the initial numerator and denominator before performing long division. These arrays, original_zeros and original_poles, are the basis for the pole/zero plot. (Note that although the poles do not change during long division, the cancellation of identical roots must be taken into account - e.g., \( { (s + 1) \over (s + 1) (s + 2)} \). This may change the number of poles as well as zeros, so both arrays must be generated and then evaluated to cancel identical roots.)
Much of this code is not structured in a functional programming style: many of the mathematical functions transform the inputs. I got tired of doing stuff like this:
var result = calculate(a, b, c);
a = result[0]; b = result[1]; c = result[2];
function calculate(a, b, c) { ... return [a, b, c]; };
...instead of simply:
calculate(a, b, c);
The polynomial solver was adapted from this library by Mikola Lysenko. It uses a Durand-Kerner iterative approximation technique, which chooses random values and gradually coalesces them into the actual polynomial roots. This approximation may fail in some cases (unfortunately selected initial values may result in runaway calculation rather than coalescence); may not be entirely deterministic (small variance may occur depending on the initial random values, but should only be apparent for very high-order transfer functions); and is neither the fastest nor most popular polynomial solver. For the purposes of this application, it's probably good enough.
Release history:
v1.0: Initial release.
v1.1: Added frequency response, Bode plots, and Nyquist plots.
v1.2: Corrected some issues with improper fractions (thanks, Brian!); improved factoring.
v1.3: Corrected some issues with root locus branches and evaluating simple functions like \( {s \over 1} \).