1use crate::dae::{
20 differentiate_equation, extend_dae_for_derivative_symbols, pantelides, DaeError,
21 PantelidesResult, DAE,
22};
23use crate::errors::AlkahestError;
24use crate::kernel::{ExprData, ExprId, ExprPool};
25use crate::poly::groebner::{
26 GbPoly, GroebnerBasis, MonomialOrder, ParamGbPoly, ParamGroebnerBasis, ParamGroebnerError,
27};
28use crate::solver::SolverError;
29use crate::solver::{expr_to_gbpoly, expr_to_param_gbpoly};
30use std::collections::{BTreeMap, HashSet};
31use std::fmt;
32
33const DEFAULT_MAX_PROLONG_ROUNDS: usize = 8;
35
36#[derive(Clone, Debug)]
39pub struct DifferentialRanking {
40 pub vars: Vec<ExprId>,
41}
42
43#[derive(Clone, Debug)]
46pub struct DifferentialIdeal {
47 pub generators: Vec<GbPoly>,
48}
49
50#[derive(Clone, Debug)]
52pub struct DifferentialRing {
53 pub time: ExprId,
54 pub ranked_indeterminates: Vec<ExprId>,
55}
56
57#[derive(Clone, Debug)]
60pub struct RegularDifferentialChain {
61 pub basis: GroebnerBasis,
62}
63
64#[derive(Clone, Debug)]
66pub struct RosenfeldGroebnerResult {
67 pub consistent: bool,
69 pub chains: Vec<RegularDifferentialChain>,
71 pub working_dae: DAE,
73 pub final_basis: Option<GroebnerBasis>,
75 pub prolongation_rounds: usize,
77 pub truncated: bool,
80}
81
82#[derive(Clone, Debug)]
85pub enum DaeIndexReduction {
86 Pantelides(PantelidesResult),
87 Rosenfeld(RosenfeldGroebnerResult),
88}
89
90#[derive(Debug, Clone)]
92pub enum DiffAlgError {
93 DiffError(String),
94 NotPolynomial(String),
95 EmptySystem,
96}
97
98impl fmt::Display for DiffAlgError {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 match self {
101 DiffAlgError::DiffError(s) => write!(f, "differentiation error: {s}"),
102 DiffAlgError::NotPolynomial(s) => write!(f, "not a polynomial: {s}"),
103 DiffAlgError::EmptySystem => write!(f, "empty equation system"),
104 }
105 }
106}
107
108impl std::error::Error for DiffAlgError {}
109
110impl AlkahestError for DiffAlgError {
111 fn code(&self) -> &'static str {
112 match self {
113 DiffAlgError::DiffError(_) => "E-DIFFALG-001",
114 DiffAlgError::NotPolynomial(_) => "E-DIFFALG-002",
115 DiffAlgError::EmptySystem => "E-DIFFALG-003",
116 }
117 }
118
119 fn remediation(&self) -> Option<&'static str> {
120 match self {
121 DiffAlgError::DiffError(_) => {
122 Some("ensure the DAE is polynomial in its state and derivative symbols")
123 }
124 DiffAlgError::NotPolynomial(_) => {
125 Some("declare all jet variables and parameters; remove transcendental functions")
126 }
127 DiffAlgError::EmptySystem => Some("pass at least one implicit equation"),
128 }
129 }
130}
131
132fn solver_err_to_diffalg(e: SolverError) -> DiffAlgError {
133 DiffAlgError::NotPolynomial(e.to_string())
134}
135
136fn is_unit_ideal_gb(gb: &GroebnerBasis) -> bool {
137 gb.generators().iter().any(|g| {
138 g.terms.len() == 1
139 && g.terms
140 .keys()
141 .next()
142 .is_some_and(|e| e.iter().all(|&x| x == 0))
143 && g.terms.values().next().is_some_and(|c| *c != 0)
144 })
145}
146
147fn pad_gbpoly(p: &GbPoly, new_n: usize) -> GbPoly {
148 if new_n == p.n_vars {
149 return p.clone();
150 }
151 assert!(new_n > p.n_vars);
152 let pad = new_n - p.n_vars;
153 let mut terms = BTreeMap::new();
154 for (e, c) in &p.terms {
155 let mut ne = e.clone();
156 ne.extend(std::iter::repeat(0u32).take(pad));
157 terms.insert(ne, c.clone());
158 }
159 GbPoly {
160 terms,
161 n_vars: new_n,
162 }
163}
164
165fn children(expr: ExprId, pool: &ExprPool) -> Vec<ExprId> {
166 pool.with(expr, |data| match data {
167 ExprData::Add(args) | ExprData::Mul(args) | ExprData::Func { args, .. } => args.clone(),
168 ExprData::Pow { base, exp } => vec![*base, *exp],
169 ExprData::BigO(inner) => vec![*inner],
170 _ => vec![],
171 })
172}
173
174fn collect_symbols(
175 expr: ExprId,
176 pool: &ExprPool,
177 seen: &mut HashSet<ExprId>,
178 out: &mut Vec<ExprId>,
179) {
180 let is_sym = pool.with(expr, |d| matches!(d, ExprData::Symbol { .. }));
181 if is_sym && seen.insert(expr) {
182 out.push(expr);
183 }
184 for c in children(expr, pool) {
185 collect_symbols(c, pool, seen, out);
186 }
187}
188
189fn vars_for_dae(dae: &DAE, scratch: &[ExprId], pool: &ExprPool) -> Vec<ExprId> {
190 let mut seen = HashSet::new();
191 let mut out = Vec::new();
192 let mut push = |id: ExprId| {
193 if seen.insert(id) {
194 out.push(id);
195 }
196 };
197 push(dae.time_var);
198 for i in 0..dae.variables.len() {
199 push(dae.variables[i]);
200 push(dae.derivatives[i]);
201 }
202 for &root in scratch {
203 collect_symbols(root, pool, &mut seen, &mut out);
204 }
205 out
206}
207
208fn merge_vars(vars: &mut Vec<ExprId>, fresh: Vec<ExprId>) {
219 for v in fresh {
220 if !vars.contains(&v) {
221 vars.push(v);
222 }
223 }
224}
225
226fn polys_from_equations(
227 eqs: &[ExprId],
228 vars: &[ExprId],
229 pool: &ExprPool,
230) -> Result<Vec<GbPoly>, DiffAlgError> {
231 eqs.iter()
232 .map(|&eq| expr_to_gbpoly(eq, vars, pool).map_err(solver_err_to_diffalg))
233 .collect()
234}
235
236pub fn rosenfeld_groebner_algebraic(
239 gens: Vec<GbPoly>,
240 order: MonomialOrder,
241) -> Result<Vec<RegularDifferentialChain>, DiffAlgError> {
242 if gens.is_empty() {
243 return Err(DiffAlgError::EmptySystem);
244 }
245 let gb = GroebnerBasis::compute(gens, order);
246 if is_unit_ideal_gb(&gb) {
247 return Ok(vec![]);
248 }
249 Ok(vec![RegularDifferentialChain { basis: gb }])
250}
251
252pub fn rosenfeld_groebner_with_options(
258 dae: &DAE,
259 pool: &ExprPool,
260 order: MonomialOrder,
261 max_prolong_rounds: usize,
262) -> Result<RosenfeldGroebnerResult, DiffAlgError> {
263 rosenfeld_groebner_ranked(dae, pool, order, max_prolong_rounds).map(|(result, _)| result)
264}
265
266pub fn rosenfeld_groebner_ranked(
274 dae: &DAE,
275 pool: &ExprPool,
276 order: MonomialOrder,
277 max_prolong_rounds: usize,
278) -> Result<(RosenfeldGroebnerResult, DifferentialRanking), DiffAlgError> {
279 if dae.equations.is_empty() {
280 return Err(DiffAlgError::EmptySystem);
281 }
282
283 let source_eqs = dae.equations.clone();
284 let mut work = dae.clone();
285 let mut scratch: Vec<ExprId> = source_eqs.clone();
286 let mut vars = vars_for_dae(&work, &scratch, pool);
287 let mut active = polys_from_equations(&work.equations, &vars, pool)?;
288
289 let mut prolong_exprs = source_eqs.clone();
290 let mut prolongation_rounds: usize = 0;
291
292 for round in 0..max_prolong_rounds {
293 let gb = GroebnerBasis::compute(active.clone(), order);
294 if is_unit_ideal_gb(&gb) {
295 return Ok((
296 RosenfeldGroebnerResult {
297 consistent: false,
298 chains: vec![],
299 working_dae: work,
300 final_basis: None,
301 prolongation_rounds,
302 truncated: false,
303 },
304 DifferentialRanking { vars },
305 ));
306 }
307
308 let mut next_prolong = Vec::with_capacity(prolong_exprs.len());
309 for &eq in &prolong_exprs {
310 let d_eq =
311 differentiate_equation(eq, &work.variables, &work.derivatives, work.time_var, pool)
312 .map_err(|e| DiffAlgError::DiffError(e.to_string()))?;
313 extend_dae_for_derivative_symbols(&mut work, d_eq, pool);
314 next_prolong.push(d_eq);
315 }
316 prolong_exprs = next_prolong;
317 scratch = source_eqs
318 .iter()
319 .copied()
320 .chain(prolong_exprs.iter().copied())
321 .collect();
322 merge_vars(&mut vars, vars_for_dae(&work, &scratch, pool));
323 let n = vars.len();
324 for p in &mut active {
325 *p = pad_gbpoly(p, n);
326 }
327
328 let gb_check = GroebnerBasis::compute(active.clone(), order);
329 let mut to_add: Vec<GbPoly> = Vec::new();
330 for &d_eq in &prolong_exprs {
331 let p = expr_to_gbpoly(d_eq, &vars, pool).map_err(solver_err_to_diffalg)?;
332 if !gb_check.contains(&p) {
333 to_add.push(p);
334 }
335 }
336
337 if to_add.is_empty() {
338 let final_basis = GroebnerBasis::compute(active, order);
339 let consistent = !is_unit_ideal_gb(&final_basis);
340 let chains = if consistent {
341 vec![RegularDifferentialChain {
342 basis: final_basis.clone(),
343 }]
344 } else {
345 vec![]
346 };
347 return Ok((
348 RosenfeldGroebnerResult {
349 consistent,
350 chains,
351 working_dae: work,
352 final_basis: if consistent { Some(final_basis) } else { None },
353 prolongation_rounds,
354 truncated: false,
355 },
356 DifferentialRanking { vars },
357 ));
358 }
359
360 active.extend(to_add);
361 prolongation_rounds += 1;
362
363 if round + 1 == max_prolong_rounds {
364 let final_basis = GroebnerBasis::compute(active, order);
365 let consistent = !is_unit_ideal_gb(&final_basis);
366 let chains = if consistent {
367 vec![RegularDifferentialChain {
368 basis: final_basis.clone(),
369 }]
370 } else {
371 vec![]
372 };
373 return Ok((
374 RosenfeldGroebnerResult {
375 consistent,
376 chains,
377 working_dae: work,
378 final_basis: if consistent { Some(final_basis) } else { None },
379 prolongation_rounds,
380 truncated: true,
381 },
382 DifferentialRanking { vars },
383 ));
384 }
385 }
386
387 let final_basis = GroebnerBasis::compute(active, order);
388 let consistent = !is_unit_ideal_gb(&final_basis);
389 Ok((
390 RosenfeldGroebnerResult {
391 consistent,
392 chains: if consistent {
393 vec![RegularDifferentialChain {
394 basis: final_basis.clone(),
395 }]
396 } else {
397 vec![]
398 },
399 working_dae: work,
400 final_basis: if consistent { Some(final_basis) } else { None },
401 prolongation_rounds,
402 truncated: true,
403 },
404 DifferentialRanking { vars },
405 ))
406}
407
408#[derive(Clone, Copy, Debug)]
414pub struct ParametricProlongOpts<'a> {
415 pub order: MonomialOrder,
418 pub max_prolong_rounds: usize,
420 pub eliminate: &'a [ExprId],
426 pub minimal: bool,
432}
433
434impl Default for ParametricProlongOpts<'_> {
435 fn default() -> Self {
436 ParametricProlongOpts {
437 order: MonomialOrder::Lex,
438 max_prolong_rounds: DEFAULT_MAX_PROLONG_ROUNDS,
439 eliminate: &[],
440 minimal: false,
441 }
442 }
443}
444
445#[derive(Clone, Debug)]
447pub struct ParametricRosenfeldResult {
448 pub consistent: bool,
450 pub truncated: bool,
454 pub prolongation_rounds: usize,
456 pub working_dae: DAE,
458 pub final_basis: Option<ParamGroebnerBasis>,
460 pub minimal_prolongation_rounds: Option<usize>,
478}
479
480fn param_err_to_diffalg(e: ParamGroebnerError) -> DiffAlgError {
481 DiffAlgError::NotPolynomial(e.to_string())
482}
483
484fn is_unit_ideal_param(gb: &ParamGroebnerBasis) -> bool {
485 gb.generators().iter().any(|g| {
486 g.terms.len() == 1
487 && g.terms
488 .keys()
489 .next()
490 .is_some_and(|e| e.iter().all(|&x| x == 0))
491 })
492}
493
494fn jet_closure_slots(dae: &DAE, vars: &[ExprId], roots: &[ExprId]) -> Vec<usize> {
500 let mut closed: HashSet<ExprId> = roots.iter().copied().collect();
501 loop {
502 let mut grew = false;
503 for (i, v) in dae.variables.iter().enumerate() {
504 if closed.contains(v) {
505 if let Some(&d) = dae.derivatives.get(i) {
506 grew |= closed.insert(d);
507 }
508 }
509 }
510 if !grew {
511 break;
512 }
513 }
514 vars.iter()
515 .enumerate()
516 .filter(|(_, v)| closed.contains(v))
517 .map(|(i, _)| i)
518 .collect()
519}
520
521fn param_elimination_is_informative(gb: &ParamGroebnerBasis, slots: &[usize]) -> bool {
524 gb.generators().iter().any(|g| {
525 !g.terms
526 .keys()
527 .any(|e| slots.iter().any(|&i| e.get(i).copied().unwrap_or(0) > 0))
528 })
529}
530
531fn jet_chain(v: ExprId, dv: ExprId, depth: usize, pool: &ExprPool) -> Vec<ExprId> {
534 let mut out = vec![v, dv];
535 let mut cur = dv;
536 for _ in 0..depth {
537 let name = pool.with(cur, |d| match d {
538 ExprData::Symbol { name, .. } => format!("d{name}/dt"),
539 _ => "d?/dt".to_string(),
540 });
541 cur = pool.symbol(&name, crate::kernel::Domain::Real);
542 out.push(cur);
543 }
544 out
545}
546
547fn ranked_jet_vars(
559 dae: &DAE,
560 eliminate: &[ExprId],
561 params: &[ExprId],
562 depth: usize,
563 pool: &ExprPool,
564) -> Vec<ExprId> {
565 let mut elim_first: Vec<ExprId> = Vec::new();
566 let mut rest: Vec<ExprId> = Vec::new();
567 for (i, &v) in dae.variables.iter().enumerate() {
568 let Some(&dv) = dae.derivatives.get(i) else {
569 continue;
570 };
571 let chain = jet_chain(v, dv, depth, pool);
572 if eliminate.contains(&v) {
573 elim_first.extend(chain);
574 } else {
575 rest.extend(chain);
576 }
577 }
578 let mut out: Vec<ExprId> = vec![dae.time_var];
579 for v in elim_first.into_iter().chain(rest) {
580 if !params.contains(&v) && !out.contains(&v) {
581 out.push(v);
582 }
583 }
584 out
585}
586
587fn param_vars_for_dae(
588 dae: &DAE,
589 scratch: &[ExprId],
590 params: &[ExprId],
591 pool: &ExprPool,
592) -> Vec<ExprId> {
593 vars_for_dae(dae, scratch, pool)
594 .into_iter()
595 .filter(|v| !params.contains(v))
596 .collect()
597}
598
599fn pad_param_gbpoly(p: &ParamGbPoly, new_n: usize) -> ParamGbPoly {
600 if new_n == p.n_vars {
601 return p.clone();
602 }
603 assert!(new_n > p.n_vars);
604 let pad = new_n - p.n_vars;
605 ParamGbPoly {
606 terms: p
607 .terms
608 .iter()
609 .map(|(e, c)| {
610 let mut e = e.clone();
611 e.extend(std::iter::repeat(0u32).take(pad));
612 (e, c.clone())
613 })
614 .collect(),
615 n_vars: new_n,
616 n_params: p.n_params,
617 }
618}
619
620pub fn rosenfeld_groebner_parametric(
638 dae: &DAE,
639 pool: &ExprPool,
640 params: &[ExprId],
641 opts: ParametricProlongOpts<'_>,
642) -> Result<(ParametricRosenfeldResult, DifferentialRanking), DiffAlgError> {
643 if dae.equations.is_empty() {
644 return Err(DiffAlgError::EmptySystem);
645 }
646
647 let source_eqs = dae.equations.clone();
648 let mut work = dae.clone();
649 let mut scratch: Vec<ExprId> = source_eqs.clone();
650 let mut vars = if opts.eliminate.is_empty() {
651 param_vars_for_dae(&work, &scratch, params, pool)
652 } else {
653 let mut v = ranked_jet_vars(
654 &work,
655 opts.eliminate,
656 params,
657 opts.max_prolong_rounds + 1,
658 pool,
659 );
660 merge_vars(&mut v, param_vars_for_dae(&work, &scratch, params, pool));
661 v
662 };
663 let mut active: Vec<ParamGbPoly> = work
664 .equations
665 .iter()
666 .map(|&eq| expr_to_param_gbpoly(eq, &vars, params, pool).map_err(solver_err_to_diffalg))
667 .collect::<Result<_, _>>()?;
668
669 let mut prolong_exprs = source_eqs.clone();
670 let mut minimal_prolongation_rounds: Option<usize> = None;
671
672 for round in 0..=opts.max_prolong_rounds {
675 let gb = ParamGroebnerBasis::compute(active.clone(), opts.order)
676 .map_err(param_err_to_diffalg)?;
677
678 if is_unit_ideal_param(&gb) {
679 return Ok((
680 ParametricRosenfeldResult {
681 consistent: false,
682 truncated: false,
683 prolongation_rounds: round,
684 working_dae: work,
685 final_basis: None,
686 minimal_prolongation_rounds,
687 },
688 DifferentialRanking { vars },
689 ));
690 }
691
692 if !opts.eliminate.is_empty() {
693 let slots = jet_closure_slots(&work, &vars, opts.eliminate);
694 if minimal_prolongation_rounds.is_none()
695 && param_elimination_is_informative(&gb, &slots)
696 {
697 minimal_prolongation_rounds = Some(round);
698 if opts.minimal {
699 return Ok((
700 ParametricRosenfeldResult {
701 consistent: true,
702 truncated: true,
703 prolongation_rounds: round,
704 working_dae: work,
705 final_basis: Some(gb),
706 minimal_prolongation_rounds,
707 },
708 DifferentialRanking { vars },
709 ));
710 }
711 }
712 }
713
714 if round == opts.max_prolong_rounds {
715 return Ok((
716 ParametricRosenfeldResult {
717 consistent: true,
718 truncated: true,
719 prolongation_rounds: round,
720 working_dae: work,
721 final_basis: Some(gb),
722 minimal_prolongation_rounds,
723 },
724 DifferentialRanking { vars },
725 ));
726 }
727
728 let mut next_prolong = Vec::with_capacity(prolong_exprs.len());
729 for &eq in &prolong_exprs {
730 let d_eq =
731 differentiate_equation(eq, &work.variables, &work.derivatives, work.time_var, pool)
732 .map_err(|e| DiffAlgError::DiffError(e.to_string()))?;
733 extend_dae_for_derivative_symbols(&mut work, d_eq, pool);
734 next_prolong.push(d_eq);
735 }
736 prolong_exprs = next_prolong;
737 scratch = source_eqs
738 .iter()
739 .copied()
740 .chain(prolong_exprs.iter().copied())
741 .collect();
742 let old_n = vars.len();
743 merge_vars(&mut vars, param_vars_for_dae(&work, &scratch, params, pool));
744 let n = vars.len();
745 for p in &mut active {
746 *p = pad_param_gbpoly(p, n);
747 }
748 let gb_check = gb.extend_vars(n - old_n);
751
752 let mut to_add: Vec<ParamGbPoly> = Vec::new();
753 for &d_eq in &prolong_exprs {
754 let p =
755 expr_to_param_gbpoly(d_eq, &vars, params, pool).map_err(solver_err_to_diffalg)?;
756 if !gb_check.contains(&p) {
757 to_add.push(p);
758 }
759 }
760
761 if to_add.is_empty() {
762 return Ok((
764 ParametricRosenfeldResult {
765 consistent: true,
766 truncated: false,
767 prolongation_rounds: round,
768 working_dae: work,
769 final_basis: Some(gb_check),
770 minimal_prolongation_rounds,
771 },
772 DifferentialRanking { vars },
773 ));
774 }
775
776 active.extend(to_add);
777 }
778
779 unreachable!("the `round == max_prolong_rounds` arm returns")
780}
781
782pub fn rosenfeld_groebner(
784 dae: &DAE,
785 pool: &ExprPool,
786 order: MonomialOrder,
787) -> Result<RosenfeldGroebnerResult, DiffAlgError> {
788 rosenfeld_groebner_with_options(dae, pool, order, DEFAULT_MAX_PROLONG_ROUNDS)
789}
790
791pub fn dae_index_reduce(
793 dae: &DAE,
794 pool: &ExprPool,
795 order: MonomialOrder,
796) -> Result<DaeIndexReduction, DaeError> {
797 dae_index_reduce_ranked(dae, pool, order).map(|(r, _)| r)
798}
799
800pub fn dae_index_reduce_ranked(
803 dae: &DAE,
804 pool: &ExprPool,
805 order: MonomialOrder,
806) -> Result<(DaeIndexReduction, Option<DifferentialRanking>), DaeError> {
807 match pantelides(dae, pool) {
808 Ok(p) => Ok((DaeIndexReduction::Pantelides(p), None)),
809 Err(DaeError::IndexTooHigh) => {
810 let (r, ranking) =
811 rosenfeld_groebner_ranked(dae, pool, order, DEFAULT_MAX_PROLONG_ROUNDS).map_err(
812 |e| match e {
813 DiffAlgError::DiffError(s) | DiffAlgError::NotPolynomial(s) => {
814 DaeError::DiffError(s)
815 }
816 DiffAlgError::EmptySystem => DaeError::StructurallyInconsistent,
817 },
818 )?;
819 Ok((DaeIndexReduction::Rosenfeld(r), Some(ranking)))
820 }
821 Err(e) => Err(e),
822 }
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828 use crate::kernel::Domain;
829
830 fn pool() -> ExprPool {
831 ExprPool::new()
832 }
833
834 #[test]
835 fn algebraic_inconsistent_unit_ideal() {
836 let p = pool();
837 let _x = p.symbol("x", Domain::Real);
838 let one_p = GbPoly::constant(rug::Rational::from(1), 1);
839 let gx = GbPoly::monomial(vec![1], rug::Rational::from(1));
840 let f = gx.add(&one_p); let g = gx.sub(&one_p); let chains = rosenfeld_groebner_algebraic(vec![f, g], MonomialOrder::Lex).unwrap();
843 assert!(chains.is_empty());
844 }
845
846 #[test]
847 fn lotka_volterra_first_order_consistent() {
848 let p = pool();
849 let t = p.symbol("t", Domain::Real);
850 let x = p.symbol("x", Domain::Real);
851 let y = p.symbol("y", Domain::Real);
852 let dx = p.symbol("dx/dt", Domain::Real);
853 let dy = p.symbol("dy/dt", Domain::Real);
854 let eq1 = p.add(vec![dx, p.mul(vec![p.integer(-1), x]), p.mul(vec![x, y])]);
856 let eq2 = p.add(vec![dy, p.mul(vec![p.integer(-1), x, y]), y]);
857 let dae = DAE::new(vec![eq1, eq2], vec![x, y], vec![dx, dy], t);
858 let r = rosenfeld_groebner_with_options(&dae, &p, MonomialOrder::GRevLex, 0).unwrap();
860 assert!(r.consistent && r.final_basis.is_some());
861 assert!(r.truncated);
862 }
863
864 #[test]
865 fn contradictory_linear_equations_inconsistent() {
866 let p = pool();
867 let t = p.symbol("t", Domain::Real);
868 let y = p.symbol("y", Domain::Real);
869 let dy = p.symbol("dy/dt", Domain::Real);
870 let eq1 = p.add(vec![dy, p.mul(vec![p.integer(-1), y])]);
872 let eq2 = p.add(vec![dy, p.mul(vec![p.integer(-1), y]), p.integer(-1)]);
873 let dae = DAE::new(vec![eq1, eq2], vec![y], vec![dy], t);
874 let r = rosenfeld_groebner(&dae, &p, MonomialOrder::Lex).unwrap();
875 assert!(!r.consistent);
876 }
877
878 #[test]
896 fn prolongation_does_not_invent_relations() {
897 let p = pool();
898 let t = p.symbol("t", Domain::Real);
899 let i = p.symbol("I", Domain::Real);
900 let r_ = p.symbol("R", Domain::Real);
901 let di = p.symbol("dI/dt", Domain::Real);
902 let dr = p.symbol("dR/dt", Domain::Real);
903 let eq1 = p.add(vec![dr, p.mul(vec![p.integer(-1), i])]);
905 let eq2 = p.add(vec![di, i]);
906 let dae = DAE::new(vec![eq1, eq2], vec![r_, i], vec![dr, di], t);
907
908 let (res, ranking) = rosenfeld_groebner_ranked(&dae, &p, MonomialOrder::Lex, 2).unwrap();
909 assert!(res.consistent);
910 let gb = res.final_basis.expect("consistent");
911
912 let d2i = p.symbol("ddI/dt/dt", Domain::Real);
914 let truth = p.add(vec![d2i, p.mul(vec![p.integer(-1), i])]);
915 let truth_poly = expr_to_gbpoly(truth, &ranking.vars, &p).unwrap();
916 assert!(gb.contains(&truth_poly), "I'' - I is a consequence");
917
918 for false_claim in [i, di, d2i] {
920 let q = expr_to_gbpoly(false_claim, &ranking.vars, &p).unwrap();
921 assert!(
922 !gb.contains(&q),
923 "prolongation asserted a jet of the state vanishes identically"
924 );
925 }
926 }
927
928 #[test]
939 fn the_jet_ranking_is_append_only() {
940 let p = pool();
941 let t = p.symbol("t", Domain::Real);
942 let x = p.symbol("x", Domain::Real);
943 let dx = p.symbol("dx/dt", Domain::Real);
944 let a = p.symbol("a", Domain::Real);
945 let eq = p.add(vec![dx, p.mul(vec![p.integer(-1), a, x])]);
947 let dae = DAE::new(vec![eq], vec![x], vec![dx], t);
948
949 let (res, ranking) =
950 rosenfeld_groebner_ranked(&dae, &p, MonomialOrder::GRevLex, 1).unwrap();
951 let gb = res.final_basis.expect("consistent");
952
953 let src = expr_to_gbpoly(eq, &ranking.vars, &p).unwrap();
955 assert!(gb.contains(&src), "the input equation left the ideal");
956
957 let d2x = p.symbol("ddx/dt/dt", Domain::Real);
959 let bogus = p.add(vec![p.mul(vec![x, d2x]), p.mul(vec![p.integer(-1), dx])]);
960 let q = expr_to_gbpoly(bogus, &ranking.vars, &p).unwrap();
961 assert!(
962 !gb.contains(&q),
963 "a shifted exponent slot invented a relation"
964 );
965 }
966
967 #[test]
971 fn parametric_prolongation_yields_the_io_relation() {
972 let p = pool();
973 let t = p.symbol("t", Domain::Real);
974 let x = p.symbol("x", Domain::Real);
975 let y = p.symbol("y", Domain::Real);
976 let dx = p.symbol("dx/dt", Domain::Real);
977 let dy = p.symbol("dy/dt", Domain::Real);
978 let a = p.symbol("a", Domain::Real);
979 let eq1 = p.add(vec![dx, p.mul(vec![a, x])]);
981 let eq2 = p.add(vec![y, p.mul(vec![p.integer(-1), x])]);
982 let dae = DAE::new(vec![eq1, eq2], vec![x, y], vec![dx, dy], t);
983
984 let (r, ranking) = rosenfeld_groebner_parametric(
985 &dae,
986 &p,
987 &[a],
988 ParametricProlongOpts {
989 order: MonomialOrder::Lex,
990 max_prolong_rounds: 3,
991 eliminate: &[x],
992 minimal: true,
993 },
994 )
995 .unwrap();
996
997 assert_eq!(r.minimal_prolongation_rounds, Some(1));
999 assert_eq!(r.prolongation_rounds, 1);
1000 assert!(
1001 !ranking.vars.contains(&a),
1002 "`a` must not be a ring variable"
1003 );
1004
1005 let gb = r.final_basis.expect("consistent");
1006 let state_slots: Vec<usize> = ranking
1007 .vars
1008 .iter()
1009 .enumerate()
1010 .filter(|(_, &v)| {
1011 let name = p.with(v, |d| match d {
1012 ExprData::Symbol { name, .. } => name.clone(),
1013 _ => String::new(),
1014 });
1015 name.trim_start_matches('d').split('/').next() == Some("x")
1016 })
1017 .map(|(i, _)| i)
1018 .collect();
1019 let io = gb.eliminate(&state_slots);
1020 assert_eq!(io.len(), 1);
1021
1022 let relation = p.add(vec![dy, p.mul(vec![a, y])]);
1023 let q = expr_to_param_gbpoly(relation, &ranking.vars, &[a], &p).unwrap();
1024 assert!(io.contains(&q), "y' + a·y = 0 is the input–output relation");
1025 }
1026
1027 #[test]
1028 fn textbook_library_runs() {
1029 let mut n_ok = 0;
1031 for _ in 0..10 {
1032 let p = pool();
1033 let t = p.symbol("t", Domain::Real);
1034 let x = p.symbol("x", Domain::Real);
1035 let y = p.symbol("y", Domain::Real);
1036 let dx = p.symbol("dx/dt", Domain::Real);
1037 let dy = p.symbol("dy/dt", Domain::Real);
1038 let (eqs, v, d, consistent) = match n_ok % 3 {
1040 0 => {
1041 let e1 = p.add(vec![dx, p.mul(vec![p.integer(-1), y])]);
1043 let e2 = p.add(vec![dy, x]);
1044 (vec![e1, e2], vec![x, y], vec![dx, dy], true)
1045 }
1046 1 => {
1047 let e1 = p.add(vec![dx, p.mul(vec![p.integer(-1), x])]);
1049 let e2 = p.add(vec![dy, y]);
1050 (vec![e1, e2], vec![x, y], vec![dx, dy], true)
1051 }
1052 _ => {
1053 let e1 = p.add(vec![dx, p.mul(vec![p.integer(-1), x])]);
1054 let e2 = p.add(vec![dx, p.mul(vec![p.integer(-1), x]), p.integer(-1)]);
1055 (vec![e1, e2], vec![x], vec![dx], false)
1056 }
1057 };
1058 let dae = DAE::new(eqs, v, d, t);
1059 let r = rosenfeld_groebner(&dae, &p, MonomialOrder::GRevLex).unwrap();
1060 assert_eq!(r.consistent, consistent);
1061 n_ok += 1;
1062 }
1063 assert_eq!(n_ok, 10);
1064 }
1065}