@@ -1607,7 +1607,11 @@ impl Simplifier { |
| 1607 | 1607 | } |
| 1608 | 1608 | |
| 1609 | 1609 | // Collect like terms (simplified version) |
| 1610 | | - let result_terms = Self::collect_like_terms(non_numeric); |
| 1610 | + let mut result_terms = Self::collect_like_terms(non_numeric); |
| 1611 | + |
| 1612 | + // Apply trig identities: sin^2 + cos^2 = 1, etc. |
| 1613 | + Self::try_pythagorean_identity(&mut result_terms); |
| 1614 | + Self::try_pythagorean_complement(&mut result_terms); |
| 1611 | 1615 | |
| 1612 | 1616 | let mut final_terms = result_terms; |
| 1613 | 1617 | if has_numeric && numeric_sum != 0.0 { |
@@ -1752,6 +1756,9 @@ impl Simplifier { |
| 1752 | 1756 | } |
| 1753 | 1757 | } |
| 1754 | 1758 | |
| 1759 | + // Cancel common symbolic factors (x^3 / x^2 -> x) |
| 1760 | + Self::cancel_common_factors(&mut non_numeric); |
| 1761 | + |
| 1755 | 1762 | // Simplify the numeric part |
| 1756 | 1763 | let mut final_factors = non_numeric; |
| 1757 | 1764 | |
@@ -1782,10 +1789,259 @@ impl Simplifier { |
| 1782 | 1789 | } else if final_factors.len() == 1 { |
| 1783 | 1790 | final_factors.into_iter().next().unwrap() |
| 1784 | 1791 | } else { |
| 1792 | + // Try double-angle: 2*sin(x)*cos(x) -> sin(2x) |
| 1793 | + if let Some(result) = Self::try_double_angle(&final_factors) { |
| 1794 | + return result; |
| 1795 | + } |
| 1785 | 1796 | Expr::Mul(final_factors) |
| 1786 | 1797 | } |
| 1787 | 1798 | } |
| 1788 | 1799 | |
| 1800 | + /// Apply Pythagorean trig identities to a list of additive terms (mutated in place). |
| 1801 | + /// sin(A)^2 + cos(A)^2 → 1, with coefficient matching. |
| 1802 | + fn try_pythagorean_identity(terms: &mut Vec<Expr>) -> bool { |
| 1803 | + // Find pairs of sin(A)^2 and cos(A)^2 with matching coefficients |
| 1804 | + let mut changed = false; |
| 1805 | + 'outer: loop { |
| 1806 | + for i in 0..terms.len() { |
| 1807 | + let (coef_i, func_i, arg_i) = match Self::extract_trig_squared(&terms[i]) { |
| 1808 | + Some(t) => t, |
| 1809 | + None => continue, |
| 1810 | + }; |
| 1811 | + for j in (i + 1)..terms.len() { |
| 1812 | + let (coef_j, func_j, arg_j) = match Self::extract_trig_squared(&terms[j]) { |
| 1813 | + Some(t) => t, |
| 1814 | + None => continue, |
| 1815 | + }; |
| 1816 | + // Need sin^2 + cos^2 (or cos^2 + sin^2) with same arg and coefficient |
| 1817 | + if arg_i.to_string() == arg_j.to_string() |
| 1818 | + && (coef_i - coef_j).abs() < 1e-12 |
| 1819 | + && ((func_i == "sin" && func_j == "cos") |
| 1820 | + || (func_i == "cos" && func_j == "sin")) |
| 1821 | + { |
| 1822 | + terms.remove(j); |
| 1823 | + terms.remove(i); |
| 1824 | + if (coef_i - 1.0).abs() < 1e-12 { |
| 1825 | + terms.push(Expr::Integer(1)); |
| 1826 | + } else if coef_i.fract() == 0.0 { |
| 1827 | + terms.push(Expr::Integer(coef_i as i64)); |
| 1828 | + } else { |
| 1829 | + terms.push(Expr::Float(coef_i)); |
| 1830 | + } |
| 1831 | + changed = true; |
| 1832 | + continue 'outer; |
| 1833 | + } |
| 1834 | + } |
| 1835 | + } |
| 1836 | + break; |
| 1837 | + } |
| 1838 | + changed |
| 1839 | + } |
| 1840 | + |
| 1841 | + /// Apply 1 - sin^2(x) → cos^2(x) and 1 - cos^2(x) → sin^2(x) |
| 1842 | + fn try_pythagorean_complement(terms: &mut Vec<Expr>) -> bool { |
| 1843 | + let mut changed = false; |
| 1844 | + 'outer: loop { |
| 1845 | + // Find a numeric constant and a negated trig squared |
| 1846 | + let mut const_idx = None; |
| 1847 | + let mut const_val = 0.0; |
| 1848 | + for (i, t) in terms.iter().enumerate() { |
| 1849 | + match t { |
| 1850 | + Expr::Integer(n) => { |
| 1851 | + const_idx = Some(i); |
| 1852 | + const_val = *n as f64; |
| 1853 | + } |
| 1854 | + Expr::Float(f) => { |
| 1855 | + const_idx = Some(i); |
| 1856 | + const_val = *f; |
| 1857 | + } |
| 1858 | + _ => {} |
| 1859 | + } |
| 1860 | + } |
| 1861 | + let ci = match const_idx { |
| 1862 | + Some(i) if const_val != 0.0 => i, |
| 1863 | + _ => break, |
| 1864 | + }; |
| 1865 | + |
| 1866 | + for j in 0..terms.len() { |
| 1867 | + if j == ci { |
| 1868 | + continue; |
| 1869 | + } |
| 1870 | + // Check for -coef * sin^2(A) or -coef * cos^2(A) |
| 1871 | + let (coef, func_name, arg) = match Self::extract_trig_squared(&terms[j]) { |
| 1872 | + Some((c, f, a)) if c < 0.0 => (c, f, a), |
| 1873 | + _ => continue, |
| 1874 | + }; |
| 1875 | + let neg_coef = -coef; // positive version of the coefficient |
| 1876 | + if (neg_coef - const_val).abs() < 1e-12 { |
| 1877 | + let complement = if func_name == "sin" { "cos" } else { "sin" }; |
| 1878 | + terms.remove(j.max(ci)); |
| 1879 | + terms.remove(j.min(ci)); |
| 1880 | + let replacement = if (neg_coef - 1.0).abs() < 1e-12 { |
| 1881 | + Expr::pow( |
| 1882 | + Expr::func(complement, vec![arg]), |
| 1883 | + Expr::Integer(2), |
| 1884 | + ) |
| 1885 | + } else { |
| 1886 | + Expr::mul(vec![ |
| 1887 | + if neg_coef.fract() == 0.0 { |
| 1888 | + Expr::Integer(neg_coef as i64) |
| 1889 | + } else { |
| 1890 | + Expr::Float(neg_coef) |
| 1891 | + }, |
| 1892 | + Expr::pow( |
| 1893 | + Expr::func(complement, vec![arg]), |
| 1894 | + Expr::Integer(2), |
| 1895 | + ), |
| 1896 | + ]) |
| 1897 | + }; |
| 1898 | + terms.push(replacement); |
| 1899 | + changed = true; |
| 1900 | + continue 'outer; |
| 1901 | + } |
| 1902 | + } |
| 1903 | + break; |
| 1904 | + } |
| 1905 | + changed |
| 1906 | + } |
| 1907 | + |
| 1908 | + /// Extract (coefficient, "sin"|"cos", argument) from a term like coef*sin(A)^2 |
| 1909 | + fn extract_trig_squared(expr: &Expr) -> Option<(f64, &'static str, Expr)> { |
| 1910 | + let (coef, base) = Self::extract_coefficient(expr); |
| 1911 | + match &base { |
| 1912 | + Expr::Pow(inner, exp) if **exp == Expr::Integer(2) => { |
| 1913 | + if let Expr::Func(name, args) = &**inner { |
| 1914 | + if args.len() == 1 { |
| 1915 | + let func_name = match name.as_str() { |
| 1916 | + "sin" => "sin", |
| 1917 | + "cos" => "cos", |
| 1918 | + _ => return None, |
| 1919 | + }; |
| 1920 | + return Some((coef, func_name, args[0].clone())); |
| 1921 | + } |
| 1922 | + } |
| 1923 | + None |
| 1924 | + } |
| 1925 | + _ => None, |
| 1926 | + } |
| 1927 | + } |
| 1928 | + |
| 1929 | + /// Detect 2*sin(x)*cos(x) → sin(2*x) in a Mul node |
| 1930 | + fn try_double_angle(factors: &[Expr]) -> Option<Expr> { |
| 1931 | + if factors.len() < 2 { |
| 1932 | + return None; |
| 1933 | + } |
| 1934 | + let (overall_coef, non_numeric): (f64, Vec<&Expr>) = { |
| 1935 | + let mut c = 1.0; |
| 1936 | + let mut rest = Vec::new(); |
| 1937 | + for f in factors { |
| 1938 | + match f { |
| 1939 | + Expr::Integer(n) => c *= *n as f64, |
| 1940 | + Expr::Float(x) => c *= x, |
| 1941 | + other => rest.push(other), |
| 1942 | + } |
| 1943 | + } |
| 1944 | + (c, rest) |
| 1945 | + }; |
| 1946 | + |
| 1947 | + if non_numeric.len() != 2 { |
| 1948 | + return None; |
| 1949 | + } |
| 1950 | + // Need sin(A) and cos(A) |
| 1951 | + let (sin_arg, cos_arg) = match (non_numeric[0], non_numeric[1]) { |
| 1952 | + (Expr::Func(n1, a1), Expr::Func(n2, a2)) |
| 1953 | + if n1 == "sin" && n2 == "cos" && a1.len() == 1 && a2.len() == 1 => |
| 1954 | + { |
| 1955 | + (&a1[0], &a2[0]) |
| 1956 | + } |
| 1957 | + (Expr::Func(n1, a1), Expr::Func(n2, a2)) |
| 1958 | + if n1 == "cos" && n2 == "sin" && a1.len() == 1 && a2.len() == 1 => |
| 1959 | + { |
| 1960 | + (&a2[0], &a1[0]) |
| 1961 | + } |
| 1962 | + _ => return None, |
| 1963 | + }; |
| 1964 | + |
| 1965 | + if sin_arg.to_string() != cos_arg.to_string() { |
| 1966 | + return None; |
| 1967 | + } |
| 1968 | + |
| 1969 | + // 2*sin(A)*cos(A) = sin(2A) |
| 1970 | + // overall_coef * sin(A)*cos(A) = (overall_coef/2) * sin(2A) |
| 1971 | + let remaining_coef = overall_coef / 2.0; |
| 1972 | + let double_arg = Simplifier::simplify(&Expr::mul(vec![Expr::Integer(2), sin_arg.clone()])); |
| 1973 | + let sin_2a = Expr::func("sin", vec![double_arg]); |
| 1974 | + |
| 1975 | + if (remaining_coef - 1.0).abs() < 1e-12 { |
| 1976 | + Some(sin_2a) |
| 1977 | + } else if remaining_coef.fract() == 0.0 { |
| 1978 | + Some(Expr::mul(vec![Expr::Integer(remaining_coef as i64), sin_2a])) |
| 1979 | + } else { |
| 1980 | + Some(Expr::mul(vec![Expr::Float(remaining_coef), sin_2a])) |
| 1981 | + } |
| 1982 | + } |
| 1983 | + |
| 1984 | + /// Cancel common base^exp factors in a Mul node's factor list. |
| 1985 | + /// E.g. x^3 * x^(-2) → x, a * b * a^(-1) → b |
| 1986 | + fn cancel_common_factors(factors: &mut Vec<Expr>) { |
| 1987 | + // Extract (base, exponent) for each factor |
| 1988 | + fn base_exp(e: &Expr) -> (Expr, f64) { |
| 1989 | + match e { |
| 1990 | + Expr::Pow(base, exp) => { |
| 1991 | + if let Expr::Integer(n) = &**exp { |
| 1992 | + return ((**base).clone(), *n as f64); |
| 1993 | + } |
| 1994 | + if let Expr::Neg(inner) = &**exp { |
| 1995 | + if let Expr::Integer(n) = &**inner { |
| 1996 | + return ((**base).clone(), -(*n as f64)); |
| 1997 | + } |
| 1998 | + } |
| 1999 | + (e.clone(), 1.0) |
| 2000 | + } |
| 2001 | + _ => (e.clone(), 1.0), |
| 2002 | + } |
| 2003 | + } |
| 2004 | + |
| 2005 | + // Group by base string representation |
| 2006 | + use std::collections::HashMap; |
| 2007 | + let mut base_map: HashMap<String, (Expr, f64)> = HashMap::new(); |
| 2008 | + let mut order = Vec::new(); |
| 2009 | + |
| 2010 | + for f in factors.iter() { |
| 2011 | + let (base, exp) = base_exp(f); |
| 2012 | + let key = base.to_string(); |
| 2013 | + if let Some((_, existing_exp)) = base_map.get_mut(&key) { |
| 2014 | + *existing_exp += exp; |
| 2015 | + } else { |
| 2016 | + order.push(key.clone()); |
| 2017 | + base_map.insert(key, (base, exp)); |
| 2018 | + } |
| 2019 | + } |
| 2020 | + |
| 2021 | + // Check if anything actually cancelled |
| 2022 | + if order.len() == factors.len() { |
| 2023 | + return; // No duplicates found |
| 2024 | + } |
| 2025 | + |
| 2026 | + factors.clear(); |
| 2027 | + for key in order { |
| 2028 | + if let Some((base, exp)) = base_map.remove(&key) { |
| 2029 | + if exp == 0.0 { |
| 2030 | + // Cancelled completely |
| 2031 | + continue; |
| 2032 | + } else if exp == 1.0 { |
| 2033 | + factors.push(base); |
| 2034 | + } else if exp == -1.0 { |
| 2035 | + factors.push(Expr::pow(base, Expr::Integer(-1))); |
| 2036 | + } else if exp.fract() == 0.0 { |
| 2037 | + factors.push(Expr::pow(base, Expr::Integer(exp as i64))); |
| 2038 | + } else { |
| 2039 | + factors.push(Expr::pow(base, Expr::Float(exp))); |
| 2040 | + } |
| 2041 | + } |
| 2042 | + } |
| 2043 | + } |
| 2044 | + |
| 1789 | 2045 | fn factorial_i64_checked(n: i64) -> Option<i64> { |
| 1790 | 2046 | let n = u64::try_from(n).ok()?; |
| 1791 | 2047 | let mut acc: i64 = 1; |