Title: Unexpected Phase Jumps in Bode Plot of Third-Order Systems with Zeros and Poles · Issue #1179 · python-control/python-control · GitHub
Open Graph Title: Unexpected Phase Jumps in Bode Plot of Third-Order Systems with Zeros and Poles · Issue #1179 · python-control/python-control
X Title: Unexpected Phase Jumps in Bode Plot of Third-Order Systems with Zeros and Poles · Issue #1179 · python-control/python-control
Description: Version of the control package: 0.10.1 I would like to plot a Bode plot of two third-order systems. One system has three poles and no zeros. The other system also has three poles and two zeros. The phase response is step-like due to the ...
Open Graph Description: Version of the control package: 0.10.1 I would like to plot a Bode plot of two third-order systems. One system has three poles and no zeros. The other system also has three poles and two zeros. The...
X Description: Version of the control package: 0.10.1 I would like to plot a Bode plot of two third-order systems. One system has three poles and no zeros. The other system also has three poles and two zeros. The...
Opengraph URL: https://github.com/python-control/python-control/issues/1179
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Unexpected Phase Jumps in Bode Plot of Third-Order Systems with Zeros and Poles","articleBody":"_**Version of the control package: 0.10.1**_\n\nI would like to plot a Bode plot of two third-order systems.\nOne system has three poles and no zeros. The other system also has three poles and two zeros.\nThe phase response is step-like due to the lack of damping.\nI would have expected the following phase response and verified it with Matlab:\nSystem 1:\nStarts at -90° due to the pole at 0. Constant response up to the resonance frequency, then jumps to -270°. I also get this response with the control package.\n\nSystem 2:\nStarts at -90° due to the pole at 0. Constant response up to the frequency of the double zero, then jumps to +90°. Constant curve up to the double pole position and jump to -90°. However, with the control package, I get jumps to -270° and then -450°. The -270° is equivalent to the +90°, but the “direction of rotation,” i.e., how you get there, is not correct. \nThe code documenting the transfer functions and the rest of the code can be found below. Is it possible to achieve the expected behavior?\n\n**Matlab output:**\n\u003cimg width=\"863\" height=\"633\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/670320a6-9e5b-4dea-aabe-82e8b6e27ba5\" /\u003e\n\n**Python control output:**\n\u003cimg width=\"630\" height=\"475\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/9542e5fb-619f-496f-b153-f81a34c722e0\" /\u003e\n\n```python\nimport numpy as np\nfrom scipy import signal\nimport plotly.graph_objects as go\nimport sympy as sp\nfrom plotly.subplots import make_subplots\nimport matplotlib.pyplot as plt\nimport control\n\n# Parameters\nLafe_val = 3.1e-3\nLg_val = 2e-3\nCf_val = 3.3e-6\n\n# Symbolic Definition\ns = sp.symbols('s')\nLafe, Lg, Cf = sp.symbols('Lafe Lg Cf')\nG_g = 1 / (Lafe * Lg * Cf * s**3 + (Lafe + Lg) * s)\nG_c = (1 + Lg * Cf * s**2) / (Lafe * Lg * Cf * s**3 + (Lafe + Lg) * s)\n\n# Display symbolic transfer functions\nprint(\"Symbolic transfer function Gg(s):\")\nsp.pprint(G_g)\nprint(\"Symbolic transfer function Gc(s):\")\nsp.pprint(G_c)\n\n## Transfer Function 1\n\n# Calculate poles and zeros\n# Extract numerator and denominator\nnum_g_sym, den_g_sym = sp.fraction(G_g)\n\nzeros_g = sp.solve(num_g_sym, s)\npoles_g = sp.solve(den_g_sym, s)\n\n# Substitute parameter values\nG_g_num = G_g.subs({Lafe: Lafe_val, Lg: Lg_val, Cf: Cf_val})\nzeros_g_num = [z.subs({Lafe: Lafe_val, Lg: Lg_val, Cf: Cf_val}) for z in zeros_g]\npoles_g_num = [p.subs({Lafe: Lafe_val, Lg: Lg_val, Cf: Cf_val}) for p in poles_g]\n\n# Display zeros\nprint(\"\\nZeros of the numerator:\")\nif zeros_g:\n for z_sym, z_num in zip(zeros_g, zeros_g_num):\n print(f\" s = {z_sym} ≈ {sp.N(z_num, 4)}\")\nelse:\n print(\" No zeros (numerator is constant).\")\n\n# Display poles\nprint(\"\\nPoles of the denominator:\")\nif poles_g:\n for p_sym, p_num in zip(poles_g, poles_g_num):\n print(f\" s = {p_sym} ≈ {sp.N(p_num, 4)}\")\nelse:\n print(\" No poles (denominator is constant).\")\n\n# Extract numerator and denominator\nnum_g, den_g = sp.fraction(G_g_num)\n\n# Calculate coefficients\nnum_coeffs_g = sp.Poly(num_g, s).all_coeffs()\nden_coeffs_g = sp.Poly(den_g, s).all_coeffs()\n\n# Convert to float \nnum_coeffs_g = [float(c) for c in num_coeffs_g]\nden_coeffs_g = [float(c) for c in den_coeffs_g]\n\n# Transfer function\nsystem_g = control.TransferFunction(num_coeffs_g, den_coeffs_g)\n\n## Transfer Function 2\n\n# Calculate poles and zeros\n# Extract numerator and denominator\nnum_c_sym, den_c_sym = sp.fraction(G_c)\n\nzeros_c = sp.solve(num_c_sym, s)\npoles_c = sp.solve(den_c_sym, s)\n\n# Substitute parameter values\nG_c_num = G_c.subs({Lafe: Lafe_val, Lg: Lg_val, Cf: Cf_val})\nzeros_c_num = [z.subs({Lafe: Lafe_val, Lg: Lg_val, Cf: Cf_val}) for z in zeros_c]\npoles_c_num = [p.subs({Lafe: Lafe_val, Lg: Lg_val, Cf: Cf_val}) for p in poles_c]\n\n# Display zeros\nprint(\"\\nZeros of the numerator:\")\nif zeros_c:\n for z_sym, z_num in zip(zeros_c, zeros_c_num):\n print(f\" s = {z_sym} ≈ {sp.N(z_num, 4)}\")\nelse:\n print(\" No zeros (numerator is constant).\")\n\n# Display poles\nprint(\"\\nPoles of the denominator:\")\nif poles_c:\n for p_sym, p_num in zip(poles_c, poles_c_num):\n print(f\" s = {p_sym} ≈ {sp.N(p_num, 4)}\")\nelse:\n print(\" No poles (denominator is constant).\")\n\n# Extract numerator and denominator\nnum_c, den_c = sp.fraction(G_c_num)\n\n# Calculate coefficients\nnum_coeffs_c = sp.Poly(num_c, s).all_coeffs()\nden_coeffs_c = sp.Poly(den_c, s).all_coeffs()\n\n# Convert to float \nnum_coeffs_c = [float(c) for c in num_coeffs_c]\nden_coeffs_c = [float(c) for c in den_coeffs_c]\n\n# Transfer function\nsystem_c = control.TransferFunction(num_coeffs_c, den_coeffs_c)\n\n# Frequency range\nfrequencies_rad_s = np.logspace(2, 5, num=500)\nfrequencies_pole = np.linspace(0.99 * float(sp.Abs(poles_c_num[1])), 1.01 * float(sp.Abs(poles_c_num[1])), 100)\nfrequencies_zero = np.linspace(0.99 * float(sp.Abs(zeros_c_num[1])), 1.01 * float(sp.Abs(zeros_c_num[1])), 100)\ncombined_frequencies = np.sort(np.unique(np.concatenate((\n frequencies_rad_s,\n frequencies_pole,\n frequencies_zero,\n [float(sp.Abs(poles_c_num[1]))],\n [float(sp.Abs(zeros_c_num[1]))]\n))))\n\n# Plot Bode diagram\ncontrol.bode_plot([system_g, system_c], combined_frequencies, dB=True, Hz=False, deg=True)\nplt.show()\n\n``` \n","author":{"url":"https://github.com/Kreste-111","@type":"Person","name":"Kreste-111"},"datePublished":"2025-08-05T07:54:49.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":1},"url":"https://github.com/1179/python-control/issues/1179"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:9ffb38d2-0f03-3229-73d5-274c0ae7045c |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | D8EA:2DF2F1:3694598:4CBA8E3:69792CED |
| html-safe-nonce | c6109041d531aee1178b2829ca0705f5743758ffc7271fc227b94416d2137adc |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJEOEVBOjJERjJGMTozNjk0NTk4OjRDQkE4RTM6Njk3OTJDRUQiLCJ2aXNpdG9yX2lkIjoiMzg4MjUwNDYzMTgwMzc4NDQzMCIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 585bf8f4920b26444459d2da1f29c99e53468d02beab9ac28a98e4b93e43f296 |
| hovercard-subject-tag | issue:3292019836 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/python-control/python-control/1179/issue_layout |
| twitter:image | https://opengraph.githubassets.com/1d01cb4d611c00bfeac9c1f2d97361c62a0401659adc7746b62657d4e2a00532/python-control/python-control/issues/1179 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/1d01cb4d611c00bfeac9c1f2d97361c62a0401659adc7746b62657d4e2a00532/python-control/python-control/issues/1179 |
| og:image:alt | Version of the control package: 0.10.1 I would like to plot a Bode plot of two third-order systems. One system has three poles and no zeros. The other system also has three poles and two zeros. The... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | Kreste-111 |
| hostname | github.com |
| expected-hostname | github.com |
| None | 15417fb272585d09f7c894f2a869ba34cc4c7ded898af9dccaa85542b9adb711 |
| turbo-cache-control | no-preview |
| go-import | github.com/python-control/python-control git https://github.com/python-control/python-control.git |
| octolytics-dimension-user_id | 2285872 |
| octolytics-dimension-user_login | python-control |
| octolytics-dimension-repository_id | 22791752 |
| octolytics-dimension-repository_nwo | python-control/python-control |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 22791752 |
| octolytics-dimension-repository_network_root_nwo | python-control/python-control |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 535f4866a7b2e3d762bd599bffe50a3cfb0702b2 |
| ui-target | canary-1 |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width