gardesk/garcalc / 9540a99

Browse files

add trig identity simplification and common factor cancellation

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
9540a99ce219ee8ad73d8b2e8a1388f00ebe5f93
Parents
839c390
Tree
ba80dc1

1 changed file

StatusFile+-
M garcalc-cas/src/symbolic.rs 257 1
garcalc-cas/src/symbolic.rsmodified
@@ -1607,7 +1607,11 @@ impl Simplifier {
16071607
         }
16081608
 
16091609
         // 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);
16111615
 
16121616
         let mut final_terms = result_terms;
16131617
         if has_numeric && numeric_sum != 0.0 {
@@ -1752,6 +1756,9 @@ impl Simplifier {
17521756
             }
17531757
         }
17541758
 
1759
+        // Cancel common symbolic factors (x^3 / x^2 -> x)
1760
+        Self::cancel_common_factors(&mut non_numeric);
1761
+
17551762
         // Simplify the numeric part
17561763
         let mut final_factors = non_numeric;
17571764
 
@@ -1782,10 +1789,259 @@ impl Simplifier {
17821789
         } else if final_factors.len() == 1 {
17831790
             final_factors.into_iter().next().unwrap()
17841791
         } 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
+            }
17851796
             Expr::Mul(final_factors)
17861797
         }
17871798
     }
17881799
 
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
+
17892045
     fn factorial_i64_checked(n: i64) -> Option<i64> {
17902046
         let n = u64::try_from(n).ok()?;
17912047
         let mut acc: i64 = 1;