--- Final Solver (used for performance test) ---
import numpy as np
from scipy.linalg import expm
from scipy.signal import lfilter


def _tf2ss_direct(num, den):
    """Controller canonical form, mirroring scipy.signal.tf2ss."""
    num = np.atleast_1d(num).astype(float)
    den = np.atleast_1d(den).astype(float)
    # Normalize by den[0]
    if den[0] != 1.0:
        num = num / den[0]
        den = den / den[0]
    n = len(den) - 1
    # Pad num to length n+1
    if len(num) < n + 1:
        num = np.concatenate([np.zeros(n + 1 - len(num)), num])
    # D term: if num and den same order, D = num[0]; else 0
    if len(num) == len(den):
        D = num[0]
        num = num[1:]
    else:
        D = 0.0
    if n == 0:
        return np.zeros((0, 0)), np.zeros(0), np.zeros(0), D
    # A: companion matrix (controller canonical form, scipy convention)
    A = np.zeros((n, n))
    A[0, :] = -den[1:][::-1]   # row 0: -den_n, -den_{n-1}, ..., -den_1
    for i in range(1, n):
        A[i, i-1] = 1.0
    # B: [1, 0, ..., 0]
    B = np.zeros(n)
    B[0] = 1.0
    # C: num coefficients reversed (num_{n-1}, ..., num_0)... check scipy
    # In scipy's tf2ss, C is built as:
    #   C = num[:,1:] if num has the right shape, with reshape
    # Specifically C[0, :] = num[1:][::-1]? Let me check.
    # From scipy source: C = num[:, 1:].reshape(...). Actually:
    #   C = np.atleast_2d(num)[:, 1:]  -> shape (1, n)
    # But this gives C = [num_1, num_2, ..., num_n] (in order, not reversed).
    # Wait, scipy pads num with leading zeros, so num is [0, ..., num_0, num_1, ...]
    # of length n+1. After [:, 1:], C = [num_0, num_1, ..., num_{n-1}]? No, the leading zero is at index 0.
    # Actually: if original num has length < n+1, scipy pads to n+1 with leading zeros.
    # Then num (padded) = [0, 0, ..., num_orig[0], num_orig[1], ...].
    # D = num[0] = 0 (the leading zero) if len(num_orig) < n+1. ✓
    # If len(num_orig) == n+1, no padding, D = num_orig[0].
    # C = num[1:] (after dropping D index 0).
    # For padded case, C = [0, 0, ..., num_orig[0], num_orig[1], ...] (length n).
    # For unpadded case (len(num_orig) == n+1), C = [num_orig[1], num_orig[2], ..., num_orig[n]] (length n).
    # In our code above, we already separated D and padded num to length n+1 then dropped index 0.
    # Wait let me redo this carefully.
    # Final num (after padding and dropping D): we want it to be length n.
    # If len(num_orig) <= n: pad to length n with leading zeros. No D term.
    # If len(num_orig) == n+1: D = num_orig[0], rest is length n.
    # C = num (length n, in original order).
    # Hmm but in scipy, the order is reversed for the companion form. Let me check.
    # Actually looking at scipy.signal.tf2ss source:
    #   A = np.zeros((ni, ni))
    #   for i in range(ni):
    #       A[i, 0] = -den[i+1]
    #   for i in range(1, ni):
    #       A[i, i-1+1] = 1.0   ... hmm actually need to recheck
    # Let me just call tf2ss and match its output.
    raise NotImplementedError("verifying convention")
--- End Solver ---
Running performance test...
============================= test session starts ==============================
platform linux -- Python 3.12.13, pytest-9.0.2, pluggy-1.6.0
rootdir: /tests
plugins: anyio-4.13.0, jaxtyping-0.3.9
collected 3 items

../tests/test_outputs.py .EE

==================================== ERRORS ====================================
____________________ ERROR at setup of test_solver_validity ____________________

    @pytest.fixture(scope="module")
    def solver_instance() -> Any:
        """Loads the user's 'Solver' class from solver.py."""
        if not SOLVER_PATH.exists():
            pytest.fail(f"Solver file not found at {SOLVER_PATH}")
    
        spec = importlib.util.spec_from_file_location("solver", SOLVER_PATH)
        solver_module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(solver_module)
>       return solver_module.Solver()
               ^^^^^^^^^^^^^^^^^^^^
E       AttributeError: module 'solver' has no attribute 'Solver'

/tests/test_outputs.py:48: AttributeError
____________________ ERROR at setup of test_solver_speedup _____________________

    @pytest.fixture(scope="module")
    def solver_instance() -> Any:
        """Loads the user's 'Solver' class from solver.py."""
        if not SOLVER_PATH.exists():
            pytest.fail(f"Solver file not found at {SOLVER_PATH}")
    
        spec = importlib.util.spec_from_file_location("solver", SOLVER_PATH)
        solver_module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(solver_module)
>       return solver_module.Solver()
               ^^^^^^^^^^^^^^^^^^^^
E       AttributeError: module 'solver' has no attribute 'Solver'

/tests/test_outputs.py:48: AttributeError
==================================== PASSES ====================================
=========================== short test summary info ============================
PASSED ../tests/test_outputs.py::test_solver_exists
ERROR ../tests/test_outputs.py::test_solver_validity - AttributeError: module...
ERROR ../tests/test_outputs.py::test_solver_speedup - AttributeError: module ...
========================= 1 passed, 2 errors in 0.87s ==========================
