mathhook_core/calculus/ode/educational/
steps.rs1use crate::core::{Expression, Symbol};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub enum ODEPhase {
16 Detection,
18 Transformation,
20 Integration,
22 Solution,
24 Verification,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ODESolutionStep {
31 pub step_id: String,
33
34 pub phase: ODEPhase,
36
37 pub title: String,
39
40 pub description: String,
42
43 pub justification: String,
45
46 pub before: Expression,
48
49 pub after: Expression,
51
52 pub latex: String,
54
55 pub metadata: HashMap<String, String>,
57}
58
59impl ODESolutionStep {
60 pub fn new(
62 step_id: String,
63 phase: ODEPhase,
64 title: String,
65 description: String,
66 justification: String,
67 before: Expression,
68 after: Expression,
69 ) -> Self {
70 let latex = format!("{} \\rightarrow {}", before, after);
71 Self {
72 step_id,
73 phase,
74 title,
75 description,
76 justification,
77 before,
78 after,
79 latex,
80 metadata: HashMap::new(),
81 }
82 }
83
84 pub fn with_metadata(mut self, key: String, value: String) -> Self {
86 self.metadata.insert(key, value);
87 self
88 }
89
90 pub fn to_latex_detailed(&self) -> String {
92 format!(
93 "\\text{{{}}} &: \\quad {} \\\\\n\\text{{Justification}}: &\\quad \\text{{{}}}",
94 self.title, self.latex, self.justification
95 )
96 }
97}
98
99pub struct ODESolutionStepBuilder {
101 step_id: Option<String>,
102 phase: Option<ODEPhase>,
103 title: Option<String>,
104 description: Option<String>,
105 justification: Option<String>,
106 before: Option<Expression>,
107 after: Option<Expression>,
108 metadata: HashMap<String, String>,
109}
110
111impl ODESolutionStepBuilder {
112 pub fn new() -> Self {
114 Self {
115 step_id: None,
116 phase: None,
117 title: None,
118 description: None,
119 justification: None,
120 before: None,
121 after: None,
122 metadata: HashMap::new(),
123 }
124 }
125
126 pub fn step_id(mut self, id: String) -> Self {
128 self.step_id = Some(id);
129 self
130 }
131
132 pub fn phase(mut self, phase: ODEPhase) -> Self {
134 self.phase = Some(phase);
135 self
136 }
137
138 pub fn title(mut self, title: String) -> Self {
140 self.title = Some(title);
141 self
142 }
143
144 pub fn description(mut self, desc: String) -> Self {
146 self.description = Some(desc);
147 self
148 }
149
150 pub fn justification(mut self, just: String) -> Self {
152 self.justification = Some(just);
153 self
154 }
155
156 pub fn before(mut self, expr: Expression) -> Self {
158 self.before = Some(expr);
159 self
160 }
161
162 pub fn after(mut self, expr: Expression) -> Self {
164 self.after = Some(expr);
165 self
166 }
167
168 pub fn metadata(mut self, key: String, value: String) -> Self {
170 self.metadata.insert(key, value);
171 self
172 }
173
174 pub fn build(self) -> Result<ODESolutionStep, String> {
176 let mut step = ODESolutionStep::new(
177 self.step_id.ok_or("Missing step_id")?,
178 self.phase.ok_or("Missing phase")?,
179 self.title.ok_or("Missing title")?,
180 self.description.ok_or("Missing description")?,
181 self.justification.ok_or("Missing justification")?,
182 self.before.ok_or("Missing before expression")?,
183 self.after.ok_or("Missing after expression")?,
184 );
185
186 step.metadata = self.metadata;
187 Ok(step)
188 }
189}
190
191impl Default for ODESolutionStepBuilder {
192 fn default() -> Self {
193 Self::new()
194 }
195}
196
197pub struct ODEStepFactory;
199
200impl ODEStepFactory {
201 pub fn detection(ode_type: &str, equation: &Expression, reason: &str) -> ODESolutionStep {
203 ODESolutionStepBuilder::new()
204 .step_id(format!("detect_{}", ode_type))
205 .phase(ODEPhase::Detection)
206 .title(format!("Identify as {} ODE", ode_type))
207 .description(format!(
208 "Analyzing the equation, we identify it as a {} ordinary differential equation.",
209 ode_type
210 ))
211 .justification(reason.to_owned())
212 .before(equation.clone())
213 .after(equation.clone())
214 .metadata("ode_type".to_owned(), ode_type.to_owned())
215 .build()
216 .expect("Valid detection step")
217 }
218
219 pub fn separation(
221 original: &Expression,
222 separated: &Expression,
223 g_x: &str,
224 h_y: &str,
225 ) -> ODESolutionStep {
226 ODESolutionStepBuilder::new()
227 .step_id("separate_variables".to_owned())
228 .phase(ODEPhase::Transformation)
229 .title("Separate Variables".to_owned())
230 .description(format!(
231 "Rewrite the equation to separate x and y terms: dy/h(y) = g(x)dx where g(x) = {} and h(y) = {}",
232 g_x, h_y
233 ))
234 .justification("Variable separation allows us to integrate each side independently".to_owned())
235 .before(original.clone())
236 .after(separated.clone())
237 .metadata("g_x".to_owned(), g_x.to_owned())
238 .metadata("h_y".to_owned(), h_y.to_owned())
239 .build()
240 .expect("Valid separation step")
241 }
242
243 pub fn integration(
245 integrand: &Expression,
246 result: &Expression,
247 variable: &Symbol,
248 side: &str,
249 ) -> ODESolutionStep {
250 ODESolutionStepBuilder::new()
251 .step_id(format!("integrate_{}", side))
252 .phase(ODEPhase::Integration)
253 .title(format!("Integrate {} Side", side))
254 .description(format!(
255 "Compute the integral: ∫({}) d{}",
256 integrand,
257 variable.name()
258 ))
259 .justification(
260 "Integration yields the antiderivative plus constant of integration".to_owned(),
261 )
262 .before(integrand.clone())
263 .after(result.clone())
264 .metadata("variable".to_owned(), variable.name().to_owned())
265 .metadata("side".to_owned(), side.to_owned())
266 .build()
267 .expect("Valid integration step")
268 }
269
270 pub fn solution_construction(
272 implicit: &Expression,
273 explicit: &Expression,
274 method: &str,
275 ) -> ODESolutionStep {
276 ODESolutionStepBuilder::new()
277 .step_id("construct_solution".to_owned())
278 .phase(ODEPhase::Solution)
279 .title("Construct Final Solution".to_owned())
280 .description(format!("Solve for the dependent variable using {}", method))
281 .justification(
282 "Rearranging the integrated equation to express y explicitly in terms of x"
283 .to_owned(),
284 )
285 .before(implicit.clone())
286 .after(explicit.clone())
287 .metadata("method".to_owned(), method.to_owned())
288 .build()
289 .expect("Valid solution step")
290 }
291
292 pub fn verification(
294 solution: &Expression,
295 original_ode: &Expression,
296 verification_result: bool,
297 ) -> ODESolutionStep {
298 let status = if verification_result {
299 "verified"
300 } else {
301 "pending"
302 };
303 ODESolutionStepBuilder::new()
304 .step_id("verify_solution".to_owned())
305 .phase(ODEPhase::Verification)
306 .title("Verify Solution".to_owned())
307 .description(format!(
308 "Substitute the solution back into the original equation to verify correctness: {}",
309 status
310 ))
311 .justification(
312 "Verification confirms the solution satisfies the differential equation".to_owned(),
313 )
314 .before(solution.clone())
315 .after(original_ode.clone())
316 .metadata("verified".to_owned(), verification_result.to_string())
317 .build()
318 .expect("Valid verification step")
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::{expr, symbol};
326
327 #[test]
328 fn test_ode_solution_step_creation() {
329 let before = expr!(x);
330 let after = expr!(x ^ 2);
331
332 let step = ODESolutionStep::new(
333 "step1".to_string(),
334 ODEPhase::Integration,
335 "Integrate".to_string(),
336 "Integrate both sides".to_string(),
337 "Fundamental theorem of calculus".to_string(),
338 before.clone(),
339 after.clone(),
340 );
341
342 assert_eq!(step.step_id, "step1");
343 assert_eq!(step.phase, ODEPhase::Integration);
344 assert_eq!(step.title, "Integrate");
345 assert_eq!(step.before, before);
346 assert_eq!(step.after, after);
347 }
348
349 #[test]
350 fn test_step_builder() {
351 let before = expr!(1);
352 let after = expr!(2);
353
354 let step = ODESolutionStepBuilder::new()
355 .step_id("test".to_string())
356 .phase(ODEPhase::Detection)
357 .title("Test".to_string())
358 .description("Test step".to_string())
359 .justification("Testing".to_string())
360 .before(before.clone())
361 .after(after.clone())
362 .metadata("key".to_string(), "value".to_string())
363 .build()
364 .unwrap();
365
366 assert_eq!(step.step_id, "test");
367 assert_eq!(step.metadata.get("key"), Some(&"value".to_string()));
368 }
369
370 #[test]
371 fn test_detection_factory() {
372 let equation = expr!(x);
373 let step = ODEStepFactory::detection("separable", &equation, "Can factor into g(x)h(y)");
374
375 assert_eq!(step.phase, ODEPhase::Detection);
376 assert!(step.title.contains("separable"));
377 assert!(step.description.contains("separable"));
378 assert_eq!(
379 step.metadata.get("ode_type"),
380 Some(&"separable".to_string())
381 );
382 }
383
384 #[test]
385 fn test_separation_factory() {
386 let original = expr!(x);
387 let separated = expr!(x ^ 2);
388
389 let step = ODEStepFactory::separation(&original, &separated, "x", "1");
390
391 assert_eq!(step.phase, ODEPhase::Transformation);
392 assert!(step.title.contains("Separate"));
393 assert_eq!(step.metadata.get("g_x"), Some(&"x".to_string()));
394 assert_eq!(step.metadata.get("h_y"), Some(&"1".to_string()));
395 }
396
397 #[test]
398 fn test_integration_factory() {
399 let x = symbol!(x);
400 let integrand = expr!(x);
401 let result = expr!(x ^ 2);
402
403 let step = ODEStepFactory::integration(&integrand, &result, &x, "left");
404
405 assert_eq!(step.phase, ODEPhase::Integration);
406 assert!(step.title.contains("Integrate"));
407 assert_eq!(step.metadata.get("side"), Some(&"left".to_string()));
408 }
409}