33import numpy .typing as npt
44from typing import Any
55from scipy .linalg import lu_factor , lu_solve
6- from collections import namedtuple
76from types import SimpleNamespace
87
98
@@ -200,6 +199,7 @@ def __init__(
200199 self .cond_lu_thresh = cond_lu_thresh
201200 self .cond_lstsq_thresh = cond_lstsq_thresh
202201 self .eps = 2.0 * np .finfo (float ).eps
202+ self .max_condition_number = 1.0 / np .finfo (float ).eps
203203
204204 assert np .all (
205205 self ._constraints (self .guess ) < self .eps
@@ -598,8 +598,10 @@ def _termination_info(
598598 n_it : int ,
599599 max_iterations : int ,
600600 violated_constraints : list [int ],
601+ condition_number : float ,
601602 ) -> tuple [int , str , bool ]:
602- if converged :
603+ singular_jacobian = condition_number > self .max_condition_number
604+ if converged and not singular_jacobian :
603605 return (
604606 True ,
605607 0 ,
@@ -611,7 +613,7 @@ def _termination_info(
611613 1 ,
612614 f"The function is too non-linear for lower lambda bound ({ lmda_bounds [0 ]} )" ,
613615 )
614- if persistent_bound_violation :
616+ if persistent_bound_violation and not singular_jacobian :
615617 return (
616618 False ,
617619 2 ,
@@ -622,6 +624,30 @@ def _termination_info(
622624 )
623625 if n_it == max_iterations :
624626 return False , 3 , f"The solver reached max_iterations ({ max_iterations } )"
627+ if singular_jacobian and not converged :
628+ return (
629+ False ,
630+ 4 ,
631+ (
632+ f"The system was effectively singular (cond={ condition_number :.2e} ) "
633+ f"and tried to leave the feasible region at iteration { n_it } by "
634+ "crossing the constraints with the following indices: "
635+ f"{ [i for i , lmda in violated_constraints ]} .\n "
636+ "You may want to try a different initial guess "
637+ "or check the Jacobian implementation. "
638+ "It may be that the problem is ill-posed."
639+ ),
640+ )
641+ if singular_jacobian and converged :
642+ return (
643+ True ,
644+ 5 ,
645+ (
646+ f"The solver successfully found a root after { n_it } iterations,"
647+ f"but the system is effectively singular (cond={ condition_number :.2e} ). "
648+ "You should check the problem formulation before trusting this result."
649+ ),
650+ )
625651 raise Exception ("Unknown termination of solver" )
626652
627653 def solve (self ) -> Solution :
@@ -645,10 +671,12 @@ def solve(self) -> Solution:
645671 * 1 -- Failure: lambda reached its minimum bound
646672 * 2 -- Failure: descent direction violates constraints
647673 * 3 -- Failure: maximum iterations reached
674+ * 4 -- Failure: singular system and violated constraints
675+ * 5 -- Successful convergence but singular system
648676
649677 * **text** (str) -- Human-readable termination description.
650678 * **n_it** (int) -- Number of Newton iterations performed.
651- * **iterates** (namedtuple , optional) -- Present if
679+ * **iterates** (Iterates instance , optional) -- Present if
652680 ``store_iterates=True``. Contains iteration history:
653681
654682 * **x** (list[np.ndarray]) -- Solution for each iteration.
@@ -751,14 +779,16 @@ def solve(self) -> Solution:
751779 sol .iterates .append (sol .x , sol .F , lmda )
752780
753781 # Final adjustment for constraints
754- if converged and not persistent_bound_violation :
755- sol .x = x_j + dxbar_j
756- if not np .all (self ._constraints (sol .x ) <= 0.0 ):
757- sol .x -= dxbar_j
758-
759- sol .F = self .F (sol .x )
760- sol .F_norm = np .linalg .norm (sol .F , ord = 2 )
761- sol .J = self .J (sol .x )
782+ if condition_number < self .max_condition_number :
783+ if converged and not persistent_bound_violation :
784+ sol .x = x_j + dxbar_j
785+ if not np .all (self ._constraints (sol .x ) <= 0.0 ):
786+ sol .x -= dxbar_j
787+
788+ sol .F = self .F (sol .x )
789+ sol .F_norm = np .linalg .norm (sol .F , ord = 2 )
790+ sol .J = self .J (sol .x )
791+ condition_number = np .linalg .cond (sol .J )
762792
763793 sol .success , sol .code , sol .text = self ._termination_info (
764794 converged ,
@@ -768,6 +798,7 @@ def solve(self) -> Solution:
768798 sol .n_it ,
769799 self .max_iterations ,
770800 locals ().get ("violated_constraints" , []),
801+ condition_number ,
771802 )
772803 return sol
773804
0 commit comments