@@ -332,6 +332,187 @@ export const UNIVARIATE_ROOTS: Rule[] = [
332332 useVariations : true ,
333333 condition : filter ,
334334 } ,
335+
336+ //
337+ // Trigonometric equations
338+ //
339+ // Note: These return principal values only. For general solutions,
340+ // add 2πn for sin/cos or πn for tan (where n ∈ ℤ).
341+ //
342+
343+ // a·sin(x) + b = 0 => x = arcsin(-b/a)
344+ // Valid when -1 ≤ -b/a ≤ 1
345+ {
346+ match : [ 'Add' , [ 'Multiply' , '__a' , [ 'Sin' , '_x' ] ] , '__b' ] ,
347+ replace : [ 'Arcsin' , [ 'Divide' , [ 'Negate' , '__b' ] , '__a' ] ] ,
348+ useVariations : true ,
349+ condition : ( sub ) => {
350+ if ( ! filter ( sub ) ) return false ;
351+ // Check that -b/a is in [-1, 1] for real solutions
352+ const a = sub . __a ;
353+ const b = sub . __b ;
354+ if ( ! a || a . is ( 0 ) ) return false ;
355+ const ratio = b . div ( a ) . neg ( ) ;
356+ const val = ratio . numericValue ;
357+ if ( val === null ) return true ; // Allow symbolic ratios
358+ if ( typeof val === 'number' ) return Math . abs ( val ) <= 1 ;
359+ return true ;
360+ } ,
361+ } ,
362+
363+ // Second solution for sin: x = π - arcsin(-b/a)
364+ {
365+ match : [ 'Add' , [ 'Multiply' , '__a' , [ 'Sin' , '_x' ] ] , '__b' ] ,
366+ replace : [ 'Subtract' , 'Pi' , [ 'Arcsin' , [ 'Divide' , [ 'Negate' , '__b' ] , '__a' ] ] ] ,
367+ useVariations : true ,
368+ condition : ( sub ) => {
369+ if ( ! filter ( sub ) ) return false ;
370+ const a = sub . __a ;
371+ const b = sub . __b ;
372+ if ( ! a || a . is ( 0 ) ) return false ;
373+ const ratio = b . div ( a ) . neg ( ) ;
374+ const val = ratio . numericValue ;
375+ if ( val === null ) return true ;
376+ if ( typeof val === 'number' ) return Math . abs ( val ) <= 1 ;
377+ return true ;
378+ } ,
379+ } ,
380+
381+ // sin(x) + b = 0 => x = arcsin(-b) (when a = 1)
382+ {
383+ match : [ 'Add' , [ 'Sin' , '_x' ] , '__b' ] ,
384+ replace : [ 'Arcsin' , [ 'Negate' , '__b' ] ] ,
385+ useVariations : true ,
386+ condition : ( sub ) => {
387+ if ( ! filter ( sub ) ) return false ;
388+ const b = sub . __b ;
389+ const val = b . numericValue ;
390+ if ( val === null ) return true ;
391+ if ( typeof val === 'number' ) return Math . abs ( val ) <= 1 ;
392+ return true ;
393+ } ,
394+ } ,
395+
396+ // Second solution for sin(x) + b = 0: x = π - arcsin(-b)
397+ {
398+ match : [ 'Add' , [ 'Sin' , '_x' ] , '__b' ] ,
399+ replace : [ 'Subtract' , 'Pi' , [ 'Arcsin' , [ 'Negate' , '__b' ] ] ] ,
400+ useVariations : true ,
401+ condition : ( sub ) => {
402+ if ( ! filter ( sub ) ) return false ;
403+ const b = sub . __b ;
404+ const val = b . numericValue ;
405+ if ( val === null ) return true ;
406+ if ( typeof val === 'number' ) return Math . abs ( val ) <= 1 ;
407+ return true ;
408+ } ,
409+ } ,
410+
411+ // a·cos(x) + b = 0 => x = arccos(-b/a)
412+ // Valid when -1 ≤ -b/a ≤ 1
413+ {
414+ match : [ 'Add' , [ 'Multiply' , '__a' , [ 'Cos' , '_x' ] ] , '__b' ] ,
415+ replace : [ 'Arccos' , [ 'Divide' , [ 'Negate' , '__b' ] , '__a' ] ] ,
416+ useVariations : true ,
417+ condition : ( sub ) => {
418+ if ( ! filter ( sub ) ) return false ;
419+ const a = sub . __a ;
420+ const b = sub . __b ;
421+ if ( ! a || a . is ( 0 ) ) return false ;
422+ const ratio = b . div ( a ) . neg ( ) ;
423+ const val = ratio . numericValue ;
424+ if ( val === null ) return true ;
425+ if ( typeof val === 'number' ) return Math . abs ( val ) <= 1 ;
426+ return true ;
427+ } ,
428+ } ,
429+
430+ // Second solution for cos: x = -arccos(-b/a) (since cos(-x) = cos(x))
431+ {
432+ match : [ 'Add' , [ 'Multiply' , '__a' , [ 'Cos' , '_x' ] ] , '__b' ] ,
433+ replace : [ 'Negate' , [ 'Arccos' , [ 'Divide' , [ 'Negate' , '__b' ] , '__a' ] ] ] ,
434+ useVariations : true ,
435+ condition : ( sub ) => {
436+ if ( ! filter ( sub ) ) return false ;
437+ const a = sub . __a ;
438+ const b = sub . __b ;
439+ if ( ! a || a . is ( 0 ) ) return false ;
440+ const ratio = b . div ( a ) . neg ( ) ;
441+ const val = ratio . numericValue ;
442+ if ( val === null ) return true ;
443+ if ( typeof val === 'number' ) return Math . abs ( val ) <= 1 ;
444+ return true ;
445+ } ,
446+ } ,
447+
448+ // cos(x) + b = 0 => x = arccos(-b) (when a = 1)
449+ {
450+ match : [ 'Add' , [ 'Cos' , '_x' ] , '__b' ] ,
451+ replace : [ 'Arccos' , [ 'Negate' , '__b' ] ] ,
452+ useVariations : true ,
453+ condition : ( sub ) => {
454+ if ( ! filter ( sub ) ) return false ;
455+ const b = sub . __b ;
456+ const val = b . numericValue ;
457+ if ( val === null ) return true ;
458+ if ( typeof val === 'number' ) return Math . abs ( val ) <= 1 ;
459+ return true ;
460+ } ,
461+ } ,
462+
463+ // Second solution for cos(x) + b = 0: x = -arccos(-b)
464+ {
465+ match : [ 'Add' , [ 'Cos' , '_x' ] , '__b' ] ,
466+ replace : [ 'Negate' , [ 'Arccos' , [ 'Negate' , '__b' ] ] ] ,
467+ useVariations : true ,
468+ condition : ( sub ) => {
469+ if ( ! filter ( sub ) ) return false ;
470+ const b = sub . __b ;
471+ const val = b . numericValue ;
472+ if ( val === null ) return true ;
473+ if ( typeof val === 'number' ) return Math . abs ( val ) <= 1 ;
474+ return true ;
475+ } ,
476+ } ,
477+
478+ // a·tan(x) + b = 0 => x = arctan(-b/a)
479+ // Tan has no domain restriction for the ratio
480+ {
481+ match : [ 'Add' , [ 'Multiply' , '__a' , [ 'Tan' , '_x' ] ] , '__b' ] ,
482+ replace : [ 'Arctan' , [ 'Divide' , [ 'Negate' , '__b' ] , '__a' ] ] ,
483+ useVariations : true ,
484+ condition : ( sub ) => {
485+ if ( ! filter ( sub ) ) return false ;
486+ return ! sub . __a . is ( 0 ) ;
487+ } ,
488+ } ,
489+
490+ // tan(x) + b = 0 => x = arctan(-b) (when a = 1)
491+ {
492+ match : [ 'Add' , [ 'Tan' , '_x' ] , '__b' ] ,
493+ replace : [ 'Arctan' , [ 'Negate' , '__b' ] ] ,
494+ useVariations : true ,
495+ condition : filter ,
496+ } ,
497+
498+ // a·cot(x) + b = 0 => x = arccot(-b/a)
499+ {
500+ match : [ 'Add' , [ 'Multiply' , '__a' , [ 'Cot' , '_x' ] ] , '__b' ] ,
501+ replace : [ 'Arccot' , [ 'Divide' , [ 'Negate' , '__b' ] , '__a' ] ] ,
502+ useVariations : true ,
503+ condition : ( sub ) => {
504+ if ( ! filter ( sub ) ) return false ;
505+ return ! sub . __a . is ( 0 ) ;
506+ } ,
507+ } ,
508+
509+ // cot(x) + b = 0 => x = arccot(-b)
510+ {
511+ match : [ 'Add' , [ 'Cot' , '_x' ] , '__b' ] ,
512+ replace : [ 'Arccot' , [ 'Negate' , '__b' ] ] ,
513+ useVariations : true ,
514+ condition : filter ,
515+ } ,
335516] ;
336517
337518/**
@@ -626,7 +807,7 @@ function validateRoots(
626807 x : string ,
627808 roots : ReadonlyArray < BoxedExpression >
628809) : BoxedExpression [ ] {
629- return roots . filter ( ( root ) => {
810+ const validRoots = roots . filter ( ( root ) => {
630811 // Evaluate the expression at the root
631812 const value = expr . subs ( { [ x ] : root } ) . canonical . evaluate ( ) ;
632813 if ( value === null ) return false ;
@@ -638,4 +819,15 @@ function validateRoots(
638819 // The former accounts for tolerance, the latter does not
639820 return value . isEqual ( 0 ) ;
640821 } ) ;
822+
823+ // Deduplicate roots (e.g., arccos(1) and -arccos(1) both equal 0)
824+ const uniqueRoots : BoxedExpression [ ] = [ ] ;
825+ for ( const root of validRoots ) {
826+ const isDuplicate = uniqueRoots . some (
827+ ( existing ) => existing . isSame ( root ) || existing . isEqual ( root )
828+ ) ;
829+ if ( ! isDuplicate ) uniqueRoots . push ( root ) ;
830+ }
831+
832+ return uniqueRoots ;
641833}
0 commit comments