Fix numerical stability for unstable systems (issue #67) - #68
Merged
Conversation
added 2 commits
September 1, 2026 21:46
- kernels.py: Fix underdamped kernel bounds to exclude -1.0 singularity, handle zeta <= -1 with exponential growth - kernels.py: Add ExponentialDecayKernel for first-order delay dynamics - transforms.py: Add _safe_convolve with time-domain fallback; add NaN/Inf handling in transform_inputs - _system_id.py: Fix _savgol_coeffs_cache logic; add ridge regularization (1e-8) to OLS, equality-constrained, and active-set QP - model.py: Add NaN check in _fit_and_score to return -1.0 instead of NaN R² - lti.py: Fix lti_from_underdamped time vector spacing for control.impulse_response - tests: Update underdamped kernel defaults test for widened bounds
dantzert
force-pushed
the
fix/unstable-systems-67
branch
from
September 1, 2026 21:46
45452ac to
094a66e
Compare
added 18 commits
September 1, 2026 23:18
- kernels.py: Add is_unstable property and to_lti() method for explicit LTI system construction - transforms.py: Use LTI simulation for unstable kernels instead of convolution - kernels.py: Fix overdamped kernel numerical stability with difference of exponentials - lti.py: Compute impulse response analytically for underdamped case to avoid control library time vector issues - kernels.py: Widen UnderdampedOscillatorKernel bounds (zeta [-0.99, 5.0]), add overdamped/critical handling, handle zeta <= -1 - kernels.py: Add ExponentialDecayKernel and ExponentialKernel with to_lti() - _system_id.py: Ridge regularization (1e-8) for SVD convergence - model.py: NaN check in _fit_and_score() - transforms.py: Add _safe_convolve with oaconvolve fallback; NaN/Inf handling - tests: Update underdamped kernel defaults test for widened bounds All 67 tests pass.
…nel optimization - transforms.py: Add input scaling in _safe_convolve to prevent overflow in convolution for large inputs - train.py: For unstable kernels, optimize using NSE (full system simulation accuracy) instead of immediate SINDy R² - This implements the full approach: Bayesian optimizer now optimizes intervening LTI parameters for full system prediction accuracy (NSE) instead of just immediate SINDy R² All 67 tests pass.
- model.py: Add _simulate_with_divergence_handling() for step-by-step simulation with divergence detection - model.py: Fix _error_result method indentation bug - This allows unstable models to be simulated without throwing exceptions, enabling NSE computation All 67 tests pass.
…en bounds - train.py: Add eigenvalue magnitude penalty in objective function for unstable kernels to prevent extreme poles - kernels.py: Tighten UnderdampedOscillatorKernel bounds (zeta [-0.9, 5.0], omega_n [0.001, 20.0]) to prevent extreme growth rates - Now successfully identifies true unstable pole at ~4.347 (0.009% error) instead of extreme 550k All 67 tests pass.
…tation - Even with max_transforms=1, delay dynamics create spurious unstable eigenvalues - The delay-model architecture fundamentally creates spurious unstable modes - True system has 1 unstable pole (~4.35), identified model has 3+ unstable eigenvalues - Fundamental architectural limitation of delay-model for unstable systems All 67 tests pass.
…able systems - Constrained all delay kernels to stable dynamics (zeta > 0 for underdamped, lambda < 0 for exponential) - Even with stable delay dynamics and max_transforms=1, the SINDy coupling creates spurious unstable eigenvalues (e.g., 19951, 7701±19824j) - True system has 1 unstable pole (~4.35), but delay-model creates multiple spurious unstable eigenvalues - These spurious modes are uncontrollable, causing LQR to fail - Fundamental architectural limitation of delay-model for unstable systems All 67 tests pass.
- kernels.py: Add CanonicalLTIKernel with controllable canonical form (A, B, C, D) - lti.py: Add support for canonical_lti kernel in lti_from_kernel - train.py: Pass max_states parameter to canonical LTI kernel - The kernel uses controllable canonical form with 2n+1 parameters for n states - Default initialization is stable (eigenvalues inside unit circle) All 67 tests pass.
- Add DirectLTISystem kernel with controllable canonical form - Implement direct_lti mode in lti_system_gen that bypasses delay-model architecture - Direct LTI optimization optimizes A,B,C,D matrices directly instead of kernel parameters - Uses controllable canonical form with 2n+1 parameters for n states - Direct LTI mode bypasses delay-model architecture entirely All 67 tests pass.
- Add DirectLTISystem kernel with controllable canonical form - Implement direct_lti mode in lti_system_gen that bypasses delay-model architecture - Direct LTI optimization optimizes A,B,C,D matrices directly instead of kernel parameters - Uses controllable canonical form with 2n+1 parameters for n states - Direct LTI mode bypasses delay-model architecture entirely All 67 tests pass.
- Add DecoupledLTISystem kernel with controllable canonical form - Implement decoupled_lti mode in lti_system_gen that bypasses delay-model architecture - Direct LTI optimization optimizes A,B,C,D matrices directly instead of kernel parameters - Uses controllable canonical form with 2n+1 parameters for n states - Direct LTI mode bypasses delay-model architecture entirely All 67 tests pass.
dantzert
added a commit
that referenced
this pull request
Sep 3, 2026
- Add ExponentialDecayKernel, ExponentialKernel, CanonicalLTIKernel, DirectLTISystem, DecoupledLTISystem kernels - Add decoupled_lti_train() for direct LTI optimization bypassing delay-model architecture - Add _safe_convolve with FFT/time-domain fallback for growing signals - Add _transform_unstable_kernel for explicit LTI simulation - Add ridge regularization (1e-8) to OLS solves in _system_id.py - Add divergence handling in model.py and train.py - Use NSE instead of R² for unstable kernel optimization - Update tests for new kernel bounds and add spring pushcart test Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
dantzert
added a commit
that referenced
this pull request
Sep 3, 2026
- Add ExponentialDecayKernel, ExponentialKernel, CanonicalLTIKernel, DirectLTISystem, DecoupledLTISystem kernels - Add decoupled_lti_train() for direct LTI optimization bypassing delay-model architecture - Add _safe_convolve with FFT/time-domain fallback for growing signals - Add _transform_unstable_kernel for explicit LTI simulation - Add ridge regularization (1e-8) to OLS solves in _system_id.py - Add divergence handling in model.py and train.py - Use NSE instead of R² for unstable kernel optimization - Update tests for new kernel bounds and add spring pushcart test Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
dantzert
added a commit
that referenced
this pull request
Sep 3, 2026
- Add ExponentialDecayKernel, ExponentialKernel, CanonicalLTIKernel, DirectLTISystem, DecoupledLTISystem kernels - Add decoupled_lti_train() for direct LTI optimization bypassing delay-model architecture - Add _safe_convolve with FFT/time-domain fallback for growing signals - Add _transform_unstable_kernel for explicit LTI simulation - Add ridge regularization (1e-8) to OLS solves in _system_id.py - Add divergence handling in model.py and train.py - Use NSE instead of R² for unstable kernel optimization - Update tests for new kernel bounds and add spring pushcart test Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
dantzert
added a commit
that referenced
this pull request
Sep 4, 2026
- Add ExponentialDecayKernel, ExponentialKernel, CanonicalLTIKernel, DirectLTISystem, DecoupledLTISystem kernels - Add decoupled_lti_train() for direct LTI optimization bypassing delay-model architecture - Add _safe_convolve with FFT/time-domain fallback for growing signals - Add _transform_unstable_kernel for explicit LTI simulation - Add ridge regularization (1e-8) to OLS solves in _system_id.py - Add divergence handling in model.py and train.py - Use NSE instead of R² for unstable kernel optimization - Update tests for new kernel bounds and add spring pushcart test Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
dantzert
added a commit
that referenced
this pull request
Sep 4, 2026
- Add ExponentialDecayKernel, ExponentialKernel, CanonicalLTIKernel, DirectLTISystem, DecoupledLTISystem kernels - Add decoupled_lti_train() for direct LTI optimization bypassing delay-model architecture - Add _safe_convolve with FFT/time-domain fallback for growing signals - Add _transform_unstable_kernel for explicit LTI simulation - Add ridge regularization (1e-8) to OLS solves in _system_id.py - Add divergence handling in model.py and train.py - Use NSE instead of R² for unstable kernel optimization - Update tests for new kernel bounds and add spring pushcart test Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
dantzert
added a commit
that referenced
this pull request
Sep 4, 2026
- Add ExponentialDecayKernel, ExponentialKernel, CanonicalLTIKernel, DirectLTISystem, DecoupledLTISystem kernels - Add decoupled_lti_train() for direct LTI optimization bypassing delay-model architecture - Add _safe_convolve with FFT/time-domain fallback for growing signals - Add _transform_unstable_kernel for explicit LTI simulation - Add ridge regularization (1e-8) to OLS solves in _system_id.py - Add divergence handling in model.py and train.py - Use NSE instead of R² for unstable kernel optimization - Update tests for new kernel bounds and add spring pushcart test Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
dantzert
added a commit
that referenced
this pull request
Sep 4, 2026
- Add ExponentialDecayKernel, ExponentialKernel, CanonicalLTIKernel, DirectLTISystem, DecoupledLTISystem kernels - Add decoupled_lti_train() for direct LTI optimization bypassing delay-model architecture - Add _safe_convolve with FFT/time-domain fallback for growing signals - Add _transform_unstable_kernel for explicit LTI simulation - Add ridge regularization (1e-8) to OLS solves in _system_id.py - Add divergence handling in model.py and train.py - Use NSE instead of R² for unstable kernel optimization - Update tests for new kernel bounds and add spring pushcart test Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
- Add ExponentialDecayKernel, ExponentialKernel, CanonicalLTIKernel, DirectLTISystem, DecoupledLTISystem kernels - Add decoupled_lti_train() for direct LTI optimization bypassing delay-model architecture - Add _safe_convolve with FFT/time-domain fallback for growing signals - Add _transform_unstable_kernel for explicit LTI simulation - Add ridge regularization (1e-8) to OLS solves in _system_id.py - Add divergence handling in model.py and train.py - Use NSE instead of R² for unstable kernel optimization - Update tests for new kernel bounds and add spring pushcart test Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
…ck-riu - Add test_observer_compensator_stabilizes_unstable_system regression test - Fix mypy type issues with type: ignore comments - Fix black formatting in tests/test_modpods.py - Fix ruff linting issues - Ensure all 69 tests pass Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
dantzert
force-pushed
the
fix/unstable-systems-67
branch
from
September 5, 2026 02:43
faedc75 to
37829ab
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR addresses the three numerical stability issues identified in issue #67 for training modpods on unstable systems (spring pushcart experiment with growing oscillations), implements a canonical LTI kernel with controllable canonical form, and adds a direct LTI optimization mode that bypasses the delay-model architecture entirely.
Changes
1. transforms.py: FFT overflow fallback for growing oscillations
_safe_convolve()function that tries FFT convolution first, then falls back to time-domainoaconvolvefor growing signals (zeta < 0)_safe_convolveto prevent overflow in convolution for large inputs (scales forcing and kernel values before convolution, unscales result)TransformCache.get()andtransform_inputs()to use_safe_convolve()transform_inputs()to replace non-finite values with large finite values2. _system_id.py: Ridge regularization for SVD convergence
_savgol_coeffs_cachelogic bug_solve()_solve_equality_constrained()ridge_lambdaparameter to_active_set_qp()for regularized active-set QP3. kernels.py: New kernels with explicit LTI support
UnderdampedOscillatorKernelbounds: zeta [0.001, 5.0], omega_n [0.001, 20.0] (excluded -1.0 singularity, tightened omega_n upper bound)UnderdampedOscillatorKernel.kernel_fn()is_unstableproperty andis_unstable_params()method to all kernelsto_lti()method to create intervening LTI system for explicit simulationExponentialDecayKernel(name: "exponential_decay") for first-order delay dynamicsExponentialKernel(name: "exponential") for unnormalized growth/decayExponentialGrowthKernel(named "exponential_growth") with rate < 0 bounds for stable delaysDirectLTISystemkernel with controllable canonical formkernel_fnwith eigenvalue check and overflow handlingDecoupledLTISystemkernel for direct LTI optimization4. lti.py: Support for canonical LTI and direct LTI kernels
canonical_ltikernel inlti_from_kernel()decoupled_ltikernel inlti_from_kernel()max_statesparameter tolti_system_genanddelay_io_train()to_lti()methodto_lti()method to get A, B, C, D matrices5. train.py: Support for direct LTI kernel
max_statesparameter todelay_io_train()direct_ltias a valid kernel optiondecoupled_lti_train()function for direct LTI optimizationDecoupledLTITrainerclass that optimizes A, B, C, D directly5. lti.py: Analytical impulse response for underdamped case
control.impulse_response()call which had time vector spacing issues5. model.py: Divergence handling for unstable system simulation
_simulate_with_divergence_handling()for step-by-step simulation with divergence detection_error_resultmethod indentation bug6. model.py: NaN handling in model fitting
_fit_and_score()to return -1.0 instead of NaN R²7. kernels.py: Numerical stability improvements
UnderdampedOscillatorKernelbounds: zeta [0.001, 5.0], omega_n [0.001, 20.0] (excluded -1.0 singularity, tightened omega_n upper bound)UnderdampedOscillatorKernel.kernel_fn()ExponentialDecayKernel(name: "exponential_decay") for first-order delay dynamicsExponentialKernel(name: "exponential") for unnormalized growth/decayExponentialGrowthKernel(named "exponential_growth") with rate < 0 bounds for stable delays6. train.py: NSE-based optimization for unstable kernels
_create_objective()to detect unstable kernels viais_unstable_params()final_run=Trueand returns NSE (Nash-Sutcliffe Efficiency) from full system simulation7. transforms.py: FFT overflow fallback for growing oscillations
_safe_convolve()function that tries FFT convolution first, then falls back to time-domainoaconvolvefor growing signals (zeta < 0)TransformCache.get()andtransform_inputs()to use_safe_convolve()transform_inputs()to replace non-finite values with large finite values8. _system_id.py: Ridge regularization for SVD convergence
_savgol_coeffs_cachelogic bug_solve()_solve_equality_constrained()ridge_lambdaparameter to_active_set_qp()for regularized active-set QP7. model.py: Divergence handling for unstable system simulation
_simulate_with_divergence_handling()for step-by-step simulation with divergence detection_error_resultmethod indentation bug8. model.py: NaN handling in model fitting
_fit_and_score()to return -1.0 instead of NaN R²8. kernels.py: Numerical stability improvements
UnderdampedOscillatorKernelbounds: zeta [0.001, 5.0], omega_n [0.001, 20.0] (excluded -1.0 singularity, tightened omega_n upper bound)UnderdampedOscillatorKernel.kernel_fn()ExponentialDecayKernel(name: "exponential_decay") for first-order delay dynamicsExponentialKernel(name: "exponential") for unnormalized growth/decayExponentialGrowthKernel(named "exponential_growth") with rate < 0 bounds for stable delays8. transforms.py: FFT overflow fallback for growing oscillations
_safe_convolve()function that tries FFT convolution first, then falls back to time-domainoaconvolvefor growing signals (zeta < 0)TransformCache.get()andtransform_inputs()to use_safe_convolve()transform_inputs()to replace non-finite values with large finite values9. _system_id.py: Ridge regularization for SVD convergence
_savgol_coeffs_cachelogic bug_solve()_solve_equality_constrained()ridge_lambdaparameter to_active_set_qp()for regularized active-set QP10. model.py: Divergence handling for unstable system simulation
_simulate_with_divergence_handling()for step-by-step simulation with divergence detection_error_resultmethod indentation bug11. model.py: NaN handling in model fitting
_fit_and_score()to return -1.0 instead of NaN R²12. Tests updated
test_underdamped_kernel_defaultsto reflect widened bounds (zeta > 0)test_exponential_growth_kernel_defaultsandtest_exponential_growth_kernel_increasingfor negative rate boundsTesting
All 67 existing tests pass.
Results
The spring pushcart experiment now successfully identifies the true unstable pole at ~4.347 (0.009% error), a dramatic improvement from the previous extreme value of ~550,000. The optimization now correctly identifies the true unstable pole instead of diverging to extreme values.
Fundamental Limitation (Beyond Scope of #67)
Despite correctly identifying the true unstable pole, the spring pushcart experiment does not achieve stabilization due to a fundamental limitation of the delay-model architecture:
The delay-model architecture (SINDy + convolution kernels) creates spurious unstable modes from the delay-state coupling.
Root cause: The delay-model architecture (SINDy + convolution kernels) creates an augmented state space where delay states are coupled with original states via the SINDy A matrix. Even with a directly optimized intervening LTI system, the SINDy coupling between delay states and original states creates spurious unstable eigenvalues that don't exist in the true system.
These spurious unstable eigenvalues:
This is a fundamental architectural limitation of the delay-model approach for unstable systems. The delay-model assumes BIBO-stable impulse responses and cannot accurately identify unstable poles without also creating spurious unstable delay modes.
Achieving stabilization would require:
These are beyond the scope of the numerical stability fixes in issue #67.
Testing
All 67 existing tests pass.