Problem: LTI System Frequency Domain Analysis and Visualization

The problem aims to create a Python toolkit for linear time-invariant (LTI) system analysis, integrating coefficient loading (complex/sparse handling), frequency array generation (distorted/fractional phase), response computation (stability/zero-pole cancellation), and enhanced Bode/Nyquist plotting (diagnostics) to enable robust system analysis and visualization.

Background: ### Industry Background Knowledge
The provided code implements a production-grade pipeline for analyzing Linear Time-Invariant (LTI) systems, a foundational framework in control engineering, signal processing, telecommunications, and aerospace. Each function addresses critical industry requirements for robustness, numerical stability, and real-world applicability:

1. **Coefficient Loading (`load_coefficients`)**
   - **Mathematical Foundations**: Parses complex number notations ("i"/"j"), validates against NaN/Inf values, and converts to Compressed Sparse Row (CSR) matrices when ≥90% of coefficients are zero (via SciPy).
   - **Industry Context**: Complex coefficients model phase-shifted systems like RF filters and communication channels. Sparse matrices optimize memory for high-order systems (e.g., large-scale power grids, MIMO systems) where most coefficients are zero. Input validation ensures resilience against malformed real-world data (sensor readings, configuration files).

2. **Frequency Array Generation (`generate_frequency_array`)**
   - **Mathematical Foundations**: Log-spaced frequency sampling (spanning multiple decades), Mittag-Leffler function approximation for fractional-order phase correction, adaptive log-domain Gaussian noise, and quadratic distortion.
   - **Industry Context**: Log spacing is standard for Bode plots, the primary tool for analyzing system stability and frequency response. Fractional-order phase correction supports modeling of complex processes (battery dynamics, viscoelastic materials, biological systems). Added noise simulates real-world measurement artifacts to test system robustness.

3. **Frequency Response Calculation (`compute_frequency_response`)**
   - **Mathematical Foundations**: Horner's method for dense polynomial evaluation, sparse matrix polynomial evaluation, zero-pole cancellation to mitigate floating-point errors, unstable pole detection (real part > ε), and batched computation.
   - **Industry Context**: Zero-pole cancellation ensures numerical stability for high-precision control systems. Unstable pole detection warns engineers of systems that could exhibit dangerous behavior (robotic arms, industrial processes). Batched computation handles large frequency arrays common in 5G signal testing and high-resolution spectrum analysis.

4. **Bode Plotting (`plot_bode`)**
   - **Mathematical Foundations**: Logarithmic magnitude conversion (20log₁₀), phase unwrapping, linear interpolation for non-uniform frequency spacing, and magnitude clipping to avoid log(0) errors.
   - **Industry Context**: Bode plots are the standard tool for analyzing gain margin, phase margin (key stability metrics), and frequency characteristics of filters, amplifiers, and control loops. Interpolation ensures smooth plots even with irregular sampling from real sensors.

5. **Nyquist Plotting (`plot_nyquist`)**
   - **Mathematical Foundations**: Wiener filter for noise reduction, cubic spline interpolation for adaptive sampling, winding number calculation (Nyquist criterion), and symmetry error quantification.
   - **Industry Context**: Nyquist plots determine system stability using the Nyquist criterion, critical for designing feedback control systems (aerospace flight control, industrial automation). Adaptive sampling improves accuracy in regions of rapid curve change (e.g., resonance peaks). Noise reduction handles measurement artifacts from sensors or test equipment.

### Required Import Statements
import numpy as np
from typing import Union, Tuple
from scipy.sparse import csr_matrix, isspmatrix
from scipy.signal import wiener
from scipy.interpolate import interp1d
from numpy.linalg import norm
import matplotlib.pyplot as plt
import warnings

Input/Output: The problem receives two string inputs (num_line, denum_line) for whitespace-separated numerator and denominator coefficients (supporting complex notation like "1+2i"), and three numerical inputs (start_exp: float, stop_exp: float, num_points: positive int) defining the frequency array’s log-domain range (10^start_exp to 10^stop_exp) and resolution. It processes the coefficient strings into dense or sparse arrays (validating complex values, NaNs, and Infs), generates a complex frequency array w via log-domain linear sequencing with quadratic distortion and adaptive noise, computes the LTI system’s complex frequency response H (handling sparse/dense data, zero-pole cancellation, and unstable pole checks), then outputs the processed coefficient arrays, frequency array w, response array H, a Bode plot (magnitude/phase subplots with log x-axis, start/end markers) as matplotlib Figure and Axes, and a Nyquist plot (refined curve, stability winding number, symmetry error annotations) as matplotlib Figure and Axes.

Required dependencies:
import numpy as np
import matplotlib.pyplot as plt
from scipy.sparse import csr_matrix, isspmatrix
from scipy.signal import wiener
from scipy.interpolate import interp1d
from scipy.linalg import norm
import warnings

Sub-steps:
step_number: 1
step_description_prompt: Please write a Python function named load_coefficients. It takes two string inputs, parses them into complex coefficients, checks for invalid values (NaN/Inf), converts to sparse matrices if over 90% zeros (using scipy if available), and returns a tuple of processed arrays or csr matrices. Handle input type and conversion errors.
function_header: def load_coefficients(num_line, denum_line):
test_cases: for num_str, den_str, exp_num, exp_den in [
    ("1 2", "3 4 5", [1.0, 2.0], [3.0, 4.0, 5.0]),
    ("1.5", "1 0.5 0", [1.5], [1.0, 0.5, 0.0]),
    ("", "1", [], [1.0])
]:
    num, den = load_coefficients(num_str, den_str)
    if isinstance(num, np.ndarray):
        num = num.tolist()
    if isinstance(den, np.ndarray):
        den = den.tolist()
    assert num == exp_num and den == exp_den

step_number: 2
step_description_prompt: Please write a Python function named generate_frequency_array that takes start_exp, stop_exp, num_points. It first validates input types and values, then creates a high-precision log-domain linear sequence. Next, it applies quadratic distortion and adaptive log-domain Gaussian noise. Then compute real part via stable log calculation, add fractional-order phase correction using Mittag-Leffler approximation, combine into complex array, clip overflow values, and return as complex128 array.
function_header: def generate_frequency_array(start_exp=-2, stop_exp=2, num_points=1000):
test_cases: for start, stop, pts, expected_shape in [
    (-2, 2, 1000, (1000,)),
    (0, 1, 10, (10,)),
    (-3, 3, 5, (5,))
]:
    w = generate_frequency_array(start, stop, pts)
    assert w.shape == expected_shape
    w0_mag = abs(w[0])
    w0_real = w[0].real
    expected_start = 10**start
    w_last_mag = abs(w[-1])
    w_last_real = w[-1].real
    expected_stop = 10**stop
    w0_mag_rounded = round(float(w0_mag), 4)
    w0_real_rounded = round(float(w0_real), 4)
    w_last_mag_rounded = round(float(w_last_mag), 4)
    w_last_real_rounded = round(float(w_last_real), 4)
    expected_start_rounded = round(float(expected_start), 4)
    expected_stop_rounded = round(float(expected_stop), 4)
    assert np.isfinite(w0_mag_rounded) and np.isfinite(w0_real_rounded), \
        f"Start frequency values must be finite: w0_mag={w0_mag_rounded}, w0_real={w0_real_rounded}"
    assert np.isfinite(w_last_mag_rounded) and np.isfinite(w_last_real_rounded), \
        f"Stop frequency values must be finite: w_last_mag={w_last_mag_rounded}, w_last_real={w_last_real_rounded}"

step_number: 3
step_description_prompt: Please write a Python function named compute_frequency_response that calculates the frequency response of an LTI system. It should handle sparse/dense numerator/denominator polynomials, perform zero-pole cancellation for numerical stability, check for unstable poles, and compute responses in batches to optimize memory usage. The function takes num, denum, and w as inputs and returns the complex frequency response array.
function_header: def compute_frequency_response(num, denum, w):
test_cases: for num, den, w in [
    ([1], [1, 1], np.array([0, 1, 10])),
    ([1], [1, 2, 1], np.array([0.1, 1, 10])),
    ([1, 1], [1, 1], np.array([0.5, 2, 5]))
]:
    H = compute_frequency_response(num, den, w)
    assert isinstance(H, np.ndarray), "compute_frequency_response should return numpy array"
    assert H.shape == w.shape, "H should have same shape as w"
    assert np.issubdtype(H.dtype, np.complexfloating), "H should be complex-valued"
    assert np.all(np.isfinite(H)), "H should contain finite values"
    assert len(H) == len(w), "H and w should have same length"


step_number: 4
step_description_prompt: Please write a Python code to generate a function named plot_bode that takes w (1D array of positive frequencies) and H (1D array of complex values). Validate inputs, interpolate H if w isn't evenly log-spaced, compute magnitude (dB) and unwrapped phase (degrees), plot both in subplots with log x-axis, add start/end markers, grids, legends, return fig and axes.
function_header: def plot_bode(w, H):
test_cases: # Test case 1: Standard frequency range
w1 = np.logspace(-1, 1, 100)
H1 = 1/(1j*w1 + 1)
assert np.all(w1 > 0), "All frequencies must be positive"
assert len(w1) == len(H1), "w and H must have same length"
assert np.all(np.isfinite(H1)), "H must be finite"
fig1, ax_mag1, ax_phase1 = plot_bode(w1, H1)
assert isinstance(fig1, plt.Figure), "plot_bode should return a Figure object"
assert isinstance(ax_mag1, plt.Axes), "plot_bode should return Axes objects"
assert isinstance(ax_phase1, plt.Axes), "plot_bode should return Axes objects"
# Calculate and verify numerical results
abs_H1 = np.abs(H1)
mag_db1 = 20 * np.log10(np.clip(abs_H1, 1e-20, None))
phase_deg1 = np.rad2deg(np.unwrap(np.angle(H1)))
result1_mag_start = round(float(mag_db1[0]), 4)
result1_mag_end = round(float(mag_db1[-1]), 4)
result1_phase_start = round(float(phase_deg1[0]), 4)
result1_phase_end = round(float(phase_deg1[-1]), 4)
expected1_mag_start = result1_mag_start
expected1_mag_end = result1_mag_end
expected1_phase_start = result1_phase_start
expected1_phase_end = result1_phase_end
assert result1_mag_start == expected1_mag_start, f"Test 1 failed: mag_start = {result1_mag_start}, expected {expected1_mag_start}"
assert result1_mag_end == expected1_mag_end, f"Test 1 failed: mag_end = {result1_mag_end}, expected {expected1_mag_end}"
assert result1_phase_start == expected1_phase_start, f"Test 1 failed: phase_start = {result1_phase_start}, expected {expected1_phase_start}"
assert result1_phase_end == expected1_phase_end, f"Test 1 failed: phase_end = {result1_phase_end}, expected {expected1_phase_end}"
plt.close(fig1)

# Test case 2: Wide frequency range
w2 = np.logspace(-2, 2, 50)
H2 = 1/(1j*w2 + 1)
assert np.all(w2 > 0), "All frequencies must be positive"
assert len(w2) == len(H2), "w and H must have same length"
assert np.all(np.isfinite(H2)), "H must be finite"
fig2, ax_mag2, ax_phase2 = plot_bode(w2, H2)
assert isinstance(fig2, plt.Figure), "plot_bode should return a Figure object"
assert isinstance(ax_mag2, plt.Axes), "plot_bode should return Axes objects"
assert isinstance(ax_phase2, plt.Axes), "plot_bode should return Axes objects"
# Calculate and verify numerical results
abs_H2 = np.abs(H2)
mag_db2 = 20 * np.log10(np.clip(abs_H2, 1e-20, None))
phase_deg2 = np.rad2deg(np.unwrap(np.angle(H2)))
result2_mag_start = round(float(mag_db2[0]), 4)
result2_mag_end = round(float(mag_db2[-1]), 4)
result2_phase_start = round(float(phase_deg2[0]), 4)
result2_phase_end = round(float(phase_deg2[-1]), 4)
expected2_mag_start = result2_mag_start
expected2_mag_end = result2_mag_end
expected2_phase_start = result2_phase_start
expected2_phase_end = result2_phase_end
assert result2_mag_start == expected2_mag_start, f"Test 2 failed: mag_start = {result2_mag_start}, expected {expected2_mag_start}"
assert result2_mag_end == expected2_mag_end, f"Test 2 failed: mag_end = {result2_mag_end}, expected {expected2_mag_end}"
assert result2_phase_start == expected2_phase_start, f"Test 2 failed: phase_start = {result2_phase_start}, expected {expected2_phase_start}"
assert result2_phase_end == expected2_phase_end, f"Test 2 failed: phase_end = {result2_phase_end}, expected {expected2_phase_end}"
plt.close(fig2)

# Test case 3: High frequency range
w3 = np.logspace(0, 3, 80)
H3 = 1/(1j*w3 + 1)
assert np.all(w3 > 0), "All frequencies must be positive"
assert len(w3) == len(H3), "w and H must have same length"
assert np.all(np.isfinite(H3)), "H must be finite"
fig3, ax_mag3, ax_phase3 = plot_bode(w3, H3)
assert isinstance(fig3, plt.Figure), "plot_bode should return a Figure object"
assert isinstance(ax_mag3, plt.Axes), "plot_bode should return Axes objects"
assert isinstance(ax_phase3, plt.Axes), "plot_bode should return Axes objects"
# Calculate and verify numerical results
abs_H3 = np.abs(H3)
mag_db3 = 20 * np.log10(np.clip(abs_H3, 1e-20, None))
phase_deg3 = np.rad2deg(np.unwrap(np.angle(H3)))
result3_mag_start = round(float(mag_db3[0]), 4)
result3_mag_end = round(float(mag_db3[-1]), 4)
result3_phase_start = round(float(phase_deg3[0]), 4)
result3_phase_end = round(float(phase_deg3[-1]), 4)
expected3_mag_start = result3_mag_start
expected3_mag_end = result3_mag_end
expected3_phase_start = result3_phase_start
expected3_phase_end = result3_phase_end
assert result3_mag_start == expected3_mag_start, f"Test 3 failed: mag_start = {result3_mag_start}, expected {expected3_mag_start}"
assert result3_mag_end == expected3_mag_end, f"Test 3 failed: mag_end = {result3_mag_end}, expected {expected3_mag_end}"
assert result3_phase_start == expected3_phase_start, f"Test 3 failed: phase_start = {result3_phase_start}, expected {expected3_phase_start}"
assert result3_phase_end == expected3_phase_end, f"Test 3 failed: phase_end = {result3_phase_end}, expected {expected3_phase_end}"
plt.close(fig3)

step_number: 5
step_description_prompt: Please write a Python code to generate a function named plot_nyquist. The function takes a 1D complex numpy array H, does input validation, uses Wiener filter for noise reduction, applies gradient-based adaptive sampling with cubic interpolation to refine the curve, calculates winding number for stability and symmetry error, then plots with refined curve, smoothed points, start/end markers, mirror curve, critical point, and diagnostic annotations.
function_header: def plot_nyquist(H):
test_cases: import warnings

warnings.filterwarnings('ignore', category=RuntimeWarning, message='divide by zero encountered in divide')

# Test case 1: Normal case with standard frequency range
w1 = np.logspace(-2, 2, 50)
H1 = 1/(1j*w1 + 1)
assert np.all(np.isfinite(H1)), "Test 1 failed: H1 contains NaN or Inf values"
assert len(H1) >= 2, "Test 1 failed: H1 must have at least 2 points for plot_nyquist"
assert H1.ndim == 1, "Test 1 failed: H1 must be 1-dimensional"
assert np.issubdtype(H1.dtype, np.complexfloating), "Test 1 failed: H1 must be complex-valued"
assert np.all(np.abs(H1) <= 1.0 + 1e-10), "Test 1 failed: H1 magnitude exceeds theoretical maximum"

fig1, ax1 = plot_nyquist(H1)
assert isinstance(fig1, plt.Figure), "Test 1 failed: plot_nyquist should return a Figure object"
assert isinstance(ax1, plt.Axes), "Test 1 failed: plot_nyquist should return an Axes object"
# Calculate and verify numerical results
result1_real_start = round(float(H1.real[0]), 4)
result1_real_end = round(float(H1.real[-1]), 4)
result1_imag_start = round(float(H1.imag[0]), 4)
result1_imag_end = round(float(H1.imag[-1]), 4)
crit_point = -1.0 + 0.0j
distances1 = np.abs(H1 - crit_point)
result1_min_distance = round(float(np.min(distances1)), 4)
expected1_real_start = result1_real_start
expected1_real_end = result1_real_end
expected1_imag_start = result1_imag_start
expected1_imag_end = result1_imag_end
expected1_min_distance = result1_min_distance
assert result1_real_start == expected1_real_start, f"Test 1 failed: real_start = {result1_real_start}, expected {expected1_real_start}"
assert result1_real_end == expected1_real_end, f"Test 1 failed: real_end = {result1_real_end}, expected {expected1_real_end}"
assert result1_imag_start == expected1_imag_start, f"Test 1 failed: imag_start = {result1_imag_start}, expected {expected1_imag_start}"
assert result1_imag_end == expected1_imag_end, f"Test 1 failed: imag_end = {result1_imag_end}, expected {expected1_imag_end}"
assert result1_min_distance == expected1_min_distance, f"Test 1 failed: min_distance = {result1_min_distance}, expected {expected1_min_distance}"
plt.close(fig1)

# Test case 2: Very small frequency range near zero (numerical stability challenge)
w2 = np.logspace(-6, -3, 30)
H2 = 1/(1j*w2 + 1)
assert np.all(np.isfinite(H2)), "Test 2 failed: H2 contains NaN or Inf values"
assert len(H2) >= 2, "Test 2 failed: H2 must have at least 2 points"
assert np.all(np.abs(np.abs(H2) - 1.0) < 1e-2), "Test 2 failed: H2 magnitude unstable near zero frequency"
assert np.all(np.abs(H2.real - 1.0) < 1e-2), "Test 2 failed: H2 real part unstable near zero"

fig2, ax2 = plot_nyquist(H2)
assert isinstance(fig2, plt.Figure), "Test 2 failed: plot_nyquist should return a Figure object"
assert isinstance(ax2, plt.Axes), "Test 2 failed: plot_nyquist should return an Axes object"
assert ax2.get_xlabel() != "", "Test 2 failed: Axes should have x-axis label"
assert ax2.get_ylabel() != "", "Test 2 failed: Axes should have y-axis label"
# Calculate and verify numerical results
result2_real_start = round(float(H2.real[0]), 4)
result2_real_end = round(float(H2.real[-1]), 4)
result2_imag_start = round(float(H2.imag[0]), 4)
result2_imag_end = round(float(H2.imag[-1]), 4)
distances2 = np.abs(H2 - crit_point)
result2_min_distance = round(float(np.min(distances2)), 4)
expected2_real_start = result2_real_start
expected2_real_end = result2_real_end
expected2_imag_start = result2_imag_start
expected2_imag_end = result2_imag_end
expected2_min_distance = result2_min_distance
assert result2_real_start == expected2_real_start, f"Test 2 failed: real_start = {result2_real_start}, expected {expected2_real_start}"
assert result2_real_end == expected2_real_end, f"Test 2 failed: real_end = {result2_real_end}, expected {expected2_real_end}"
assert result2_imag_start == expected2_imag_start, f"Test 2 failed: imag_start = {result2_imag_start}, expected {expected2_imag_start}"
assert result2_imag_end == expected2_imag_end, f"Test 2 failed: imag_end = {result2_imag_end}, expected {expected2_imag_end}"
assert result2_min_distance == expected2_min_distance, f"Test 2 failed: min_distance = {result2_min_distance}, expected {expected2_min_distance}"
plt.close(fig2)

# Test case 3: Very large frequency range (potential overflow/underflow)
w3 = np.logspace(0, 6, 40)
H3 = 1/(1j*w3 + 1)
assert np.all(np.isfinite(H3)), "Test 3 failed: H3 contains NaN or Inf values, possible overflow"
assert len(H3) >= 2, "Test 3 failed: H3 must have at least 2 points"
assert np.all(np.abs(H3) <= 1.0 + 1e-10), "Test 3 failed: H3 magnitude exceeds theoretical maximum"
high_freq_mask = w3 > 1e4
if np.any(high_freq_mask):
    expected_bound = 1.0 / w3[high_freq_mask]
    assert np.all(np.abs(H3[high_freq_mask]) < expected_bound * 2.0), "Test 3 failed: High frequency response not following 1/w decay"

fig3, ax3 = plot_nyquist(H3)
assert isinstance(fig3, plt.Figure), "Test 3 failed: plot_nyquist should return a Figure object"
assert isinstance(ax3, plt.Axes), "Test 3 failed: plot_nyquist should return an Axes object"
assert len(fig3.axes) > 0, "Test 3 failed: Figure should contain at least one axes"
assert fig3.axes[0] == ax3, "Test 3 failed: Returned axes should match figure axes"
# Calculate and verify numerical results
result3_real_start = round(float(H3.real[0]), 4)
result3_real_end = round(float(H3.real[-1]), 4)
result3_imag_start = round(float(H3.imag[0]), 4)
result3_imag_end = round(float(H3.imag[-1]), 4)
distances3 = np.abs(H3 - crit_point)
result3_min_distance = round(float(np.min(distances3)), 4)
expected3_real_start = result3_real_start
expected3_real_end = result3_real_end
expected3_imag_start = result3_imag_start
expected3_imag_end = result3_imag_end
expected3_min_distance = result3_min_distance
assert result3_real_start == expected3_real_start, f"Test 3 failed: real_start = {result3_real_start}, expected {expected3_real_start}"
assert result3_imag_start == expected3_imag_start, f"Test 3 failed: imag_start = {result3_imag_start}, expected {expected3_imag_start}"
assert result3_real_end == expected3_real_end, f"Test 3 failed: real_end = {result3_real_end}, expected {expected3_real_end}"
assert result3_imag_end == expected3_imag_end, f"Test 3 failed: imag_end = {result3_imag_end}, expected {expected3_imag_end}"
assert result3_min_distance == expected3_min_distance, f"Test 3 failed: min_distance = {result3_min_distance}, expected {expected3_min_distance}"
plt.close(fig3)

General solution:

General tests: