mathhook_core/calculus/summation/
educational.rs

1//! Educational features for summation operations
2
3use crate::calculus::summation::{ConvergenceResult, Summation, SummationMethods};
4use crate::core::{Expression, Number, Symbol};
5use crate::educational::message_registry::core::{
6    MessageCategory, MessageKey, MessageType, MESSAGE_REGISTRY,
7};
8use crate::educational::step_by_step::{Step, StepByStepExplanation};
9use crate::expr;
10use crate::simplify::Simplify;
11
12/// Educational extension trait for summation
13pub trait SummationEducational {
14    /// Explain finite sum computation with step-by-step guidance
15    ///
16    /// # Examples
17    ///
18    /// ```rust
19    /// use mathhook_core::{expr, symbol};
20    /// use mathhook_core::calculus::summation::educational::SummationEducational;
21    ///
22    /// let i = symbol!(i);
23    /// let sum_expr = expr!(i);
24    /// let explanation = sum_expr.explain_finite_sum(&i, &expr!(1), &expr!(10));
25    ///
26    /// for step in &explanation.steps {
27    ///     println!("{}: {}", step.title, step.description);
28    /// }
29    /// ```
30    fn explain_finite_sum(
31        &self,
32        variable: &Symbol,
33        start: &Expression,
34        end: &Expression,
35    ) -> StepByStepExplanation;
36
37    /// Explain infinite sum computation with convergence analysis
38    ///
39    /// # Examples
40    ///
41    /// ```rust
42    /// use mathhook_core::{expr, symbol};
43    /// use mathhook_core::calculus::summation::educational::SummationEducational;
44    ///
45    /// let n = symbol!(n);
46    /// let sum_expr = expr!(n ^ (-2));
47    /// let explanation = sum_expr.explain_infinite_sum(&n, &expr!(1));
48    ///
49    /// for step in &explanation.steps {
50    ///     println!("{}: {}", step.title, step.description);
51    /// }
52    /// ```
53    fn explain_infinite_sum(&self, variable: &Symbol, start: &Expression) -> StepByStepExplanation;
54}
55
56impl SummationEducational for Expression {
57    fn explain_finite_sum(
58        &self,
59        variable: &Symbol,
60        start: &Expression,
61        end: &Expression,
62    ) -> StepByStepExplanation {
63        let mut steps = Vec::new();
64        let initial_expr = self.clone();
65
66        let num_terms = Expression::add(vec![
67            end.clone(),
68            Expression::mul(vec![expr!(-1), start.clone()]),
69            expr!(1),
70        ])
71        .simplify();
72
73        if let Some(template) = MESSAGE_REGISTRY.get(&MessageKey::new(
74            MessageCategory::Calculus,
75            MessageType::SummationIntroduction,
76            0,
77        )) {
78            let mut desc = template.content.to_owned();
79            desc = desc.replace("{variable}", variable.name());
80            desc = desc.replace("{start}", &format!("{}", start));
81            desc = desc.replace("{end}", &format!("{}", end));
82            desc = desc.replace("{term_count}", &format!("{}", num_terms));
83
84            steps.push(Step {
85                title: template.title.to_owned(),
86                description: desc,
87                expression: self.clone(),
88                rule_applied: "Introduction".to_owned(),
89                latex: None,
90            });
91        }
92
93        let series_type = detect_series_type(self, variable);
94        match series_type {
95            SeriesType::Arithmetic(first, diff) => {
96                if let Some(template) = MESSAGE_REGISTRY.get(&MessageKey::new(
97                    MessageCategory::Calculus,
98                    MessageType::SummationArithmeticSeries,
99                    0,
100                )) {
101                    let mut desc = template.content.to_owned();
102                    desc = desc.replace("{first_term}", &format!("{}", first));
103                    desc = desc.replace("{common_difference}", &format!("{}", diff));
104                    desc = desc.replace("{term_count}", &format!("{}", num_terms));
105
106                    steps.push(Step {
107                        title: template.title.to_owned(),
108                        description: desc,
109                        expression: self.clone(),
110                        rule_applied: "Series Detection".to_owned(),
111                        latex: None,
112                    });
113                }
114
115                if let Some(template) = MESSAGE_REGISTRY.get(&MessageKey::new(
116                    MessageCategory::Calculus,
117                    MessageType::SummationArithmeticSeries,
118                    1,
119                )) {
120                    steps.push(Step {
121                        title: template.title.to_owned(),
122                        description: template.content.to_owned(),
123                        expression: self.clone(),
124                        rule_applied: "Formula".to_owned(),
125                        latex: None,
126                    });
127                }
128            }
129            SeriesType::PowerSum(k) => {
130                if let Some(template) = MESSAGE_REGISTRY.get(&MessageKey::new(
131                    MessageCategory::Calculus,
132                    MessageType::SummationPowerSum,
133                    k.min(4) as u8,
134                )) {
135                    let mut desc = template.content.to_owned();
136                    if k > 3 {
137                        desc = desc.replace("{power}", &format!("{}", k));
138                    }
139
140                    steps.push(Step {
141                        title: template.title.to_owned(),
142                        description: desc,
143                        expression: self.clone(),
144                        rule_applied: "Power Sum Formula".to_owned(),
145                        latex: None,
146                    });
147                }
148            }
149            SeriesType::General => {}
150        }
151
152        let result = self.finite_sum(variable, start, end);
153
154        if let Some(template) = MESSAGE_REGISTRY.get(&MessageKey::new(
155            MessageCategory::Calculus,
156            MessageType::SummationResult,
157            0,
158        )) {
159            let mut desc = template.content.to_owned();
160            desc = desc.replace("{result}", &format!("{}", result));
161
162            steps.push(Step {
163                title: template.title.to_owned(),
164                description: desc,
165                expression: result.clone(),
166                rule_applied: "Final Result".to_owned(),
167                latex: None,
168            });
169        }
170
171        let total_steps = steps.len().saturating_sub(2);
172
173        StepByStepExplanation {
174            initial_expression: initial_expr,
175            final_expression: result,
176            steps,
177            total_steps,
178            rules_used: vec!["Summation".to_owned()],
179        }
180    }
181
182    fn explain_infinite_sum(&self, variable: &Symbol, start: &Expression) -> StepByStepExplanation {
183        let mut steps = Vec::new();
184        let initial_expr = self.clone();
185
186        if let Some(template) = MESSAGE_REGISTRY.get(&MessageKey::new(
187            MessageCategory::Calculus,
188            MessageType::SummationIntroduction,
189            1,
190        )) {
191            let mut desc = template.content.to_owned();
192            desc = desc.replace("{expression}", &format!("{}", self));
193            desc = desc.replace("{start}", &format!("{}", start));
194
195            steps.push(Step {
196                title: template.title.to_owned(),
197                description: desc,
198                expression: self.clone(),
199                rule_applied: "Introduction".to_owned(),
200                latex: None,
201            });
202        }
203
204        let convergence = SummationMethods::convergence_test(self, variable);
205        let (convergent, reason) = match convergence {
206            ConvergenceResult::Convergent => (
207                true,
208                "the terms decay fast enough (p-series test with p > 1)",
209            ),
210            ConvergenceResult::Divergent => (
211                false,
212                "the terms do not decay to zero fast enough (p-series test with p ≤ 1)",
213            ),
214            _ => (false, "convergence cannot be determined automatically"),
215        };
216
217        let variant = if convergent { 0 } else { 1 };
218        if let Some(template) = MESSAGE_REGISTRY.get(&MessageKey::new(
219            MessageCategory::Calculus,
220            MessageType::SummationConvergence,
221            variant,
222        )) {
223            let mut desc = template.content.to_owned();
224            desc = desc.replace("{reason}", reason);
225
226            steps.push(Step {
227                title: template.title.to_owned(),
228                description: desc,
229                expression: self.clone(),
230                rule_applied: "Convergence Test".to_owned(),
231                latex: None,
232            });
233        }
234
235        let result = self.infinite_sum(variable, start);
236
237        if let Some(template) = MESSAGE_REGISTRY.get(&MessageKey::new(
238            MessageCategory::Calculus,
239            MessageType::SummationResult,
240            0,
241        )) {
242            let mut desc = template.content.to_owned();
243            desc = desc.replace("{result}", &format!("{}", result));
244
245            steps.push(Step {
246                title: template.title.to_owned(),
247                description: desc,
248                expression: result.clone(),
249                rule_applied: "Final Result".to_owned(),
250                latex: None,
251            });
252        }
253
254        let total_steps = steps.len().saturating_sub(2);
255
256        StepByStepExplanation {
257            initial_expression: initial_expr,
258            final_expression: result,
259            steps,
260            total_steps,
261            rules_used: vec!["Infinite Sum".to_owned()],
262        }
263    }
264}
265
266fn detect_series_type(expr: &Expression, variable: &Symbol) -> SeriesType {
267    if let Expression::Symbol(sym) = expr {
268        if sym == variable {
269            return SeriesType::Arithmetic(expr!(1), expr!(1));
270        }
271    }
272
273    if let Expression::Pow(base, exp) = expr {
274        if let (Expression::Symbol(sym), Expression::Number(Number::Integer(k))) =
275            (base.as_ref(), exp.as_ref())
276        {
277            if sym == variable && k.is_positive() && *k <= 100 {
278                return SeriesType::PowerSum(*k as u32);
279            }
280        }
281    }
282
283    SeriesType::General
284}
285
286#[derive(Debug, Clone)]
287#[allow(dead_code)]
288enum SeriesType {
289    Arithmetic(Expression, Expression),
290    PowerSum(u32),
291    General,
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::symbol;
298
299    #[test]
300    fn test_explain_finite_sum_arithmetic() {
301        let i = symbol!(i);
302        let expr_i: Expression = i.clone().into();
303        let explanation = expr_i.explain_finite_sum(&i, &expr!(1), &expr!(10));
304
305        assert!(explanation.steps.len() >= 3);
306        assert_eq!(explanation.final_expression, expr!(55));
307    }
308
309    #[test]
310    fn test_explain_finite_sum_power() {
311        let i = symbol!(i);
312        let expr = expr!(i ^ 2);
313        let explanation = expr.explain_finite_sum(&i, &expr!(1), &expr!(3));
314
315        assert!(
316            !explanation.steps.is_empty(),
317            "Should have at least some steps"
318        );
319
320        assert!(
321            explanation.steps.len() >= 2,
322            "Should have at least intro and result"
323        );
324    }
325
326    #[test]
327    fn test_explain_infinite_sum_convergent() {
328        let n = symbol!(n);
329        let expr = expr!(n ^ (-2));
330        let explanation = expr.explain_infinite_sum(&n, &expr!(1));
331
332        assert!(explanation.steps.len() >= 2);
333    }
334}