@@ -729,6 +729,184 @@ function transformSqrtLinearEquation(
729729 return transformed ;
730730}
731731
732+ /**
733+ * Detect and solve nested sqrt equations of the form √(f(x, √x)) = a.
734+ *
735+ * Pattern 4: √(x + √x) = a (or similar with √x inside outer sqrt)
736+ * - Use substitution u = √x, so x = u²
737+ * - √(u² + u) = a becomes u² + u = a² (after squaring)
738+ * - Solve quadratic for u, then x = u² for valid u ≥ 0
739+ *
740+ * Returns the solutions for x, or null if pattern not detected.
741+ */
742+ function solveNestedSqrtEquation (
743+ expr : BoxedExpression ,
744+ variable : string
745+ ) : BoxedExpression [ ] | null {
746+ if ( expr . operator !== 'Add' ) return null ;
747+
748+ const ce = expr . engine ;
749+ const ops = expr . ops ;
750+ if ( ! ops || ops . length === 0 ) return null ;
751+
752+ // Find the outer sqrt term
753+ let outerSqrt : BoxedExpression | null = null ;
754+ let sqrtIndex = - 1 ;
755+
756+ for ( let i = 0 ; i < ops . length ; i ++ ) {
757+ if ( ops [ i ] . operator === 'Sqrt' ) {
758+ outerSqrt = ops [ i ] ;
759+ sqrtIndex = i ;
760+ break ;
761+ }
762+ }
763+
764+ if ( ! outerSqrt || sqrtIndex < 0 ) return null ;
765+
766+ // Get the argument of the outer sqrt
767+ const outerArg = outerSqrt . op1 ;
768+ if ( ! outerArg ) return null ;
769+
770+ // Check if the outer sqrt argument contains an inner √x (Sqrt of just the variable)
771+ // Pattern: √(... + √x + ...) or √(... + a*√x + ...)
772+ let hasInnerSqrtX = false ;
773+ let innerSqrtCoeff : BoxedExpression | null = null ;
774+
775+ if ( outerArg . operator === 'Add' && outerArg . ops ) {
776+ for ( const term of outerArg . ops ) {
777+ // Check for √x directly
778+ if (
779+ term . operator === 'Sqrt' &&
780+ term . op1 ?. symbol === variable
781+ ) {
782+ hasInnerSqrtX = true ;
783+ innerSqrtCoeff = ce . One ;
784+ break ;
785+ }
786+ // Check for Negate(Sqrt(x))
787+ if (
788+ term . operator === 'Negate' &&
789+ term . op1 ?. operator === 'Sqrt' &&
790+ term . op1 ?. op1 ?. symbol === variable
791+ ) {
792+ hasInnerSqrtX = true ;
793+ innerSqrtCoeff = ce . NegativeOne ;
794+ break ;
795+ }
796+ // Check for coefficient * √x
797+ if ( term . operator === 'Multiply' && term . ops ) {
798+ for ( const factor of term . ops ) {
799+ if (
800+ factor . operator === 'Sqrt' &&
801+ factor . op1 ?. symbol === variable
802+ ) {
803+ hasInnerSqrtX = true ;
804+ // Get coefficient (product of other factors)
805+ const otherFactors = term . ops . filter ( ( f ) => f !== factor ) ;
806+ innerSqrtCoeff =
807+ otherFactors . length === 1
808+ ? otherFactors [ 0 ]
809+ : ce . function ( 'Multiply' , otherFactors ) ;
810+ break ;
811+ }
812+ }
813+ if ( hasInnerSqrtX ) break ;
814+ }
815+ }
816+ }
817+
818+ if ( ! hasInnerSqrtX ) return null ;
819+
820+ // We have √(f(x, √x)) = a pattern
821+ // Collect the constant terms (non-sqrt parts of the Add expression)
822+ const nonSqrtTerms = ops . filter ( ( _ , i ) => i !== sqrtIndex ) ;
823+ if ( nonSqrtTerms . length === 0 ) return null ;
824+
825+ // a = -(sum of non-sqrt terms)
826+ let aExpr : BoxedExpression ;
827+ if ( nonSqrtTerms . length === 1 ) {
828+ aExpr = nonSqrtTerms [ 0 ] . neg ( ) ;
829+ } else {
830+ aExpr = ce . function ( 'Add' , nonSqrtTerms ) . neg ( ) ;
831+ }
832+
833+ // The constant should not contain the variable
834+ if ( aExpr . has ( variable ) ) return null ;
835+
836+ // Now we have: √(f(x, √x)) = a
837+ // Substitute u = √x, so x = u², √x = u
838+ // The outer arg f(x, √x) becomes f(u², u)
839+
840+ // Create a unique internal symbol for u (avoiding wildcard prefix _)
841+ // Use __internalU to avoid collision with user symbols
842+ const uSymbolName = '__internalU' ;
843+ const uSymbol = ce . symbol ( uSymbolName ) ;
844+
845+ // Substitute √x → u and x → u² in the outer sqrt argument
846+ // IMPORTANT: Must replace √x first, THEN x, otherwise √x becomes √(u²)
847+ const step1 = outerArg . replace (
848+ { match : [ 'Sqrt' , variable ] , replace : uSymbol } ,
849+ { recursive : true }
850+ ) ;
851+ const substitutedArg = step1 ?. subs ( { [ variable ] : ce . box ( [ 'Power' , uSymbolName , 2 ] ) } ) ;
852+
853+ if ( ! substitutedArg ) return null ;
854+
855+ // Now we have √(g(u)) = a where g(u) = substitutedArg
856+ // Square both sides: g(u) = a²
857+ // So g(u) - a² = 0
858+
859+ const aSquared = aExpr . mul ( aExpr ) ;
860+ const uEquation = substitutedArg . sub ( aSquared ) . simplify ( ) ;
861+
862+ // Solve for u
863+ ce . pushScope ( ) ;
864+ ce . declare ( uSymbolName , { type : 'real' } ) ;
865+
866+ const uSolutions = findUnivariateRoots ( uEquation , uSymbolName ) ;
867+
868+ ce . popScope ( ) ;
869+
870+ if ( uSolutions . length === 0 ) return null ;
871+
872+ // Convert u solutions back to x = u²
873+ // Only keep solutions where u ≥ 0 (since u = √x ≥ 0)
874+ const xSolutions : BoxedExpression [ ] = [ ] ;
875+
876+ for ( const uVal of uSolutions ) {
877+ // Check if u is real and non-negative (since u = √x ≥ 0)
878+ const uNumeric = uVal . N ( ) ;
879+
880+ // Use the expression's isNegative property for reliable checking
881+ if ( uNumeric . isNegative ) continue ; // Skip negative u values
882+
883+ // Also check numericValue for cases where isNegative might not be set
884+ const uNum = uNumeric . numericValue ;
885+ if ( uNum !== null ) {
886+ let uReal : number | null = null ;
887+ if ( typeof uNum === 'number' ) {
888+ uReal = uNum ;
889+ } else if ( typeof uNum === 'object' && 'decimal' in uNum ) {
890+ // BigNumericValue object - extract numeric value from decimal
891+ const decimal = ( uNum as any ) . decimal ;
892+ if ( decimal && typeof decimal . toNumber === 'function' ) {
893+ uReal = decimal . toNumber ( ) ;
894+ }
895+ } else if ( typeof uNum === 'object' && 're' in uNum ) {
896+ // Complex number object
897+ uReal = ( uNum as any ) . re ;
898+ }
899+ if ( uReal !== null && uReal < - 1e-10 ) continue ; // Skip negative u values
900+ }
901+
902+ // x = u²
903+ const xVal = uVal . mul ( uVal ) . simplify ( ) ;
904+ xSolutions . push ( xVal ) ;
905+ }
906+
907+ return xSolutions . length > 0 ? xSolutions : null ;
908+ }
909+
732910/**
733911 * Expression is a function of a single variable (`x`) or an Equality
734912 *
@@ -755,6 +933,14 @@ export function findUnivariateRoots(
755933 // Clear denominators to enable matching of expressions like F - 3x/h = 0
756934 expr = clearDenominators ( expr ) ;
757935
936+ // Try to solve nested sqrt equations: √(f(x, √x)) = a
937+ // This uses substitution u = √x, solves for u, then converts back to x = u²
938+ const nestedSqrtSolutions = solveNestedSqrtEquation ( expr , x ) ;
939+ if ( nestedSqrtSolutions !== null ) {
940+ // Validate and return the solutions
941+ return validateRoots ( originalExpr , x , nestedSqrtSolutions ) ;
942+ }
943+
758944 // Transform sqrt-linear equations: √(f(x)) = g(x) → f(x) - g(x)² = 0
759945 // This handles Pattern 2: √(ax+b) = cx+d by squaring both sides.
760946 // Must be done before pattern matching so quadratic formula can match.
0 commit comments