1pub use num_bigint::BigInt;
4use num_rational::BigRational;
5#[cfg(test)]
6use num_traits::FromPrimitive;
7use num_traits::{One, Signed, ToPrimitive, Zero};
8pub use problemreductions_expr::{
9 Expr, ExprNode, ExprNodeId, ParseError, SubstitutionError, Symbol,
10};
11use std::cmp::Ordering;
12use std::collections::{BTreeMap, HashMap};
13use std::fmt;
14
15use crate::types::ProblemParameters;
16
17#[derive(Clone, Debug)]
20pub(crate) struct AlgebraicAnalysis {
21 facts: HashMap<ExprNodeId, AlgebraicFacts>,
22}
23
24#[derive(Clone, Debug)]
25pub(crate) struct AlgebraicFacts {
26 pub(crate) is_constant: bool,
27 pub(crate) exact_rational: Option<BigRational>,
28 pub(crate) linear: Option<BTreeMap<Symbol, BigRational>>,
29 pub(crate) constant_domain: Option<bool>,
30 pub(crate) sign: Option<Ordering>,
31 pub(crate) cmp_one: Option<Ordering>,
32}
33
34impl AlgebraicAnalysis {
35 pub(crate) fn new(expressions: &[&Expr]) -> Self {
36 let mut facts = HashMap::new();
37 for expression in expressions {
38 analyze_algebraic(expression, &mut facts);
39 }
40 Self { facts }
41 }
42
43 pub(crate) fn facts(&self, expression: &Expr) -> &AlgebraicFacts {
44 &self.facts[&expression.node_identity()]
45 }
46}
47
48fn analyze_algebraic(
49 expression: &Expr,
50 memo: &mut HashMap<ExprNodeId, AlgebraicFacts>,
51) -> AlgebraicFacts {
52 if let Some(facts) = memo.get(&expression.node_identity()) {
53 return facts.clone();
54 }
55 let facts = match expression.node() {
56 ExprNode::Const(value) => AlgebraicFacts {
57 is_constant: true,
58 exact_rational: Some(value.clone()),
59 linear: Some(BTreeMap::new()),
60 constant_domain: Some(true),
61 sign: Some(value.cmp(&BigRational::from_integer(0.into()))),
62 cmp_one: Some(value.cmp(&BigRational::from_integer(1.into()))),
63 },
64 ExprNode::Var(symbol) => AlgebraicFacts {
65 is_constant: false,
66 exact_rational: None,
67 linear: Some(BTreeMap::from([(
68 symbol.clone(),
69 BigRational::from_integer(1.into()),
70 )])),
71 constant_domain: None,
72 sign: None,
73 cmp_one: None,
74 },
75 ExprNode::Add(values) => {
76 let children = values
77 .iter()
78 .map(|value| analyze_algebraic(value, memo))
79 .collect::<Vec<_>>();
80 let is_constant = children.iter().all(|facts| facts.is_constant);
81 AlgebraicFacts {
82 is_constant,
83 exact_rational: sum_exact(&children),
84 linear: sum_linear(&children),
85 constant_domain: is_constant.then(|| all_domains(&children)).flatten(),
86 sign: is_constant.then(|| sum_sign(&children)).flatten(),
87 cmp_one: None,
88 }
89 }
90 ExprNode::Mul(values) => {
91 let children = values
92 .iter()
93 .map(|value| analyze_algebraic(value, memo))
94 .collect::<Vec<_>>();
95 let is_constant = children.iter().all(|facts| facts.is_constant);
96 let exact_rational = product_exact(&children);
97 AlgebraicFacts {
98 is_constant,
99 linear: product_linear(&children),
100 constant_domain: is_constant.then(|| all_domains(&children)).flatten(),
101 sign: is_constant.then(|| product_sign(&children)).flatten(),
102 cmp_one: exact_rational
103 .as_ref()
104 .map(|value| value.cmp(&BigRational::from_integer(1.into()))),
105 exact_rational,
106 }
107 }
108 ExprNode::Pow(base, exponent) => {
109 let base = analyze_algebraic(base, memo);
110 let exponent = analyze_algebraic(exponent, memo);
111 let is_constant = base.is_constant && exponent.is_constant;
112 let exact_rational = match (
113 base.exact_rational.as_ref(),
114 exponent.exact_rational.as_ref(),
115 ) {
116 (Some(base), Some(exponent)) if exponent == &-BigRational::one() => {
117 (!base.is_zero()).then(|| base.recip())
118 }
119 _ => None,
120 };
121 let domain = if is_constant {
122 power_domain(&base, &exponent)
123 } else {
124 None
125 };
126 let sign = domain
127 .is_some_and(|defined| defined)
128 .then(|| power_sign(&base, &exponent))
129 .flatten();
130 let cmp_one = domain
131 .is_some_and(|defined| defined)
132 .then(|| power_cmp_one(&base, &exponent))
133 .flatten();
134 AlgebraicFacts {
135 is_constant,
136 exact_rational,
137 linear: is_constant.then(BTreeMap::new),
138 constant_domain: domain,
139 sign,
140 cmp_one,
141 }
142 }
143 ExprNode::Exp(value) => {
144 let value = analyze_algebraic(value, memo);
145 let domain = value.is_constant.then_some(value.constant_domain).flatten();
146 AlgebraicFacts {
147 is_constant: value.is_constant,
148 exact_rational: value
149 .exact_rational
150 .as_ref()
151 .filter(|value| value.is_zero())
152 .map(|_| BigRational::from_integer(1.into())),
153 linear: value.is_constant.then(BTreeMap::new),
154 constant_domain: domain,
155 sign: domain
156 .is_some_and(|defined| defined)
157 .then_some(Ordering::Greater),
158 cmp_one: value.sign,
159 }
160 }
161 ExprNode::Log(value) => {
162 let value = analyze_algebraic(value, memo);
163 let domain = if value.is_constant {
164 value
165 .constant_domain
166 .map(|defined| defined && value.sign == Some(Ordering::Greater))
167 } else {
168 None
169 };
170 AlgebraicFacts {
171 is_constant: value.is_constant,
172 exact_rational: value
173 .exact_rational
174 .as_ref()
175 .filter(|value| value.is_one())
176 .map(|_| BigRational::from_integer(0.into())),
177 linear: value.is_constant.then(BTreeMap::new),
178 constant_domain: domain,
179 sign: domain
180 .is_some_and(|defined| defined)
181 .then_some(value.cmp_one)
182 .flatten(),
183 cmp_one: None,
184 }
185 }
186 ExprNode::Factorial(value) => {
187 let value = analyze_algebraic(value, memo);
188 let valid = value
189 .exact_rational
190 .as_ref()
191 .map(|value| value.is_integer() && !value.is_negative());
192 AlgebraicFacts {
193 is_constant: value.is_constant,
194 exact_rational: None,
195 linear: value.is_constant.then(BTreeMap::new),
196 constant_domain: value.is_constant.then_some(valid).flatten(),
197 sign: valid
198 .is_some_and(|valid| valid)
199 .then_some(Ordering::Greater),
200 cmp_one: valid.and_then(|valid| {
201 valid.then(|| {
202 if value
203 .exact_rational
204 .as_ref()
205 .is_some_and(|value| value <= &BigRational::from_integer(1.into()))
206 {
207 Ordering::Equal
208 } else {
209 Ordering::Greater
210 }
211 })
212 }),
213 }
214 }
215 };
216 memo.insert(expression.node_identity(), facts.clone());
217 facts
218}
219
220fn sum_exact(children: &[AlgebraicFacts]) -> Option<BigRational> {
221 children.iter().try_fold(BigRational::zero(), |sum, child| {
222 Some(sum + child.exact_rational.as_ref()?)
223 })
224}
225
226fn product_exact(children: &[AlgebraicFacts]) -> Option<BigRational> {
227 children
228 .iter()
229 .try_fold(BigRational::one(), |product, child| {
230 Some(product * child.exact_rational.as_ref()?)
231 })
232}
233
234fn sum_linear(children: &[AlgebraicFacts]) -> Option<BTreeMap<Symbol, BigRational>> {
235 let mut result = BTreeMap::new();
236 for child in children {
237 for (symbol, coefficient) in child.linear.as_ref()? {
238 *result
239 .entry(symbol.clone())
240 .or_insert_with(BigRational::zero) += coefficient;
241 }
242 }
243 result.retain(|_, coefficient| !coefficient.is_zero());
244 Some(result)
245}
246
247fn product_linear(children: &[AlgebraicFacts]) -> Option<BTreeMap<Symbol, BigRational>> {
248 if children.iter().all(|child| child.is_constant) {
249 return Some(BTreeMap::new());
250 }
251 let mut coefficient = BigRational::one();
252 let mut linear = None;
253 for child in children {
254 if child.is_constant {
255 coefficient *= child.exact_rational.as_ref()?;
256 } else if linear.is_some() {
257 return None;
258 } else {
259 linear = Some(child.linear.clone()?);
260 }
261 }
262 let mut linear = linear?;
263 for value in linear.values_mut() {
264 *value *= &coefficient;
265 }
266 linear.retain(|_, value| !value.is_zero());
267 Some(linear)
268}
269
270fn all_domains(children: &[AlgebraicFacts]) -> Option<bool> {
271 let mut defined = true;
272 for child in children {
273 defined &= child.constant_domain?;
274 }
275 Some(defined)
276}
277
278fn sum_sign(children: &[AlgebraicFacts]) -> Option<Ordering> {
279 if let Some(value) = sum_exact(children) {
280 return Some(value.cmp(&BigRational::zero()));
281 }
282 let signs = children
283 .iter()
284 .map(|child| child.sign)
285 .collect::<Option<Vec<_>>>()?;
286 if signs.iter().all(|sign| *sign != Ordering::Less) {
287 Some(if signs.contains(&Ordering::Greater) {
288 Ordering::Greater
289 } else {
290 Ordering::Equal
291 })
292 } else if signs.iter().all(|sign| *sign != Ordering::Greater) {
293 Some(Ordering::Less)
294 } else {
295 None
296 }
297}
298
299fn product_sign(children: &[AlgebraicFacts]) -> Option<Ordering> {
300 let mut sign = Ordering::Greater;
301 for child in children {
302 match child.sign? {
303 Ordering::Equal => return Some(Ordering::Equal),
304 Ordering::Less => sign = sign.reverse(),
305 Ordering::Greater => {}
306 }
307 }
308 Some(sign)
309}
310
311fn power_domain(base: &AlgebraicFacts, exponent: &AlgebraicFacts) -> Option<bool> {
312 if !base.constant_domain? || !exponent.constant_domain? {
313 return Some(false);
314 }
315 match base.sign? {
316 Ordering::Greater => Some(true),
317 Ordering::Equal => Some(exponent.sign? == Ordering::Greater),
318 Ordering::Less => exponent
319 .exact_rational
320 .as_ref()
321 .map(|value| value.is_integer()),
322 }
323}
324
325fn power_sign(base: &AlgebraicFacts, exponent: &AlgebraicFacts) -> Option<Ordering> {
326 let exponent_value = exponent.exact_rational.as_ref()?;
327 if exponent_value.is_zero() {
328 return Some(Ordering::Greater);
329 }
330 match base.sign? {
331 Ordering::Greater => Some(Ordering::Greater),
332 Ordering::Equal => Some(Ordering::Equal),
333 Ordering::Less => {
334 let exponent = exponent_value.to_integer();
335 if (&exponent % 2u8).is_zero() {
336 Some(Ordering::Greater)
337 } else {
338 Some(Ordering::Less)
339 }
340 }
341 }
342}
343
344fn power_cmp_one(base: &AlgebraicFacts, exponent: &AlgebraicFacts) -> Option<Ordering> {
345 let exponent = exponent.exact_rational.as_ref()?;
346 if exponent.is_zero() || base.cmp_one == Some(Ordering::Equal) {
347 Some(Ordering::Equal)
348 } else if exponent.is_positive() {
349 base.cmp_one
350 } else {
351 base.cmp_one.map(Ordering::reverse)
352 }
353}
354
355pub fn evaluate_approximate(
357 expression: &Expr,
358 variables: &ProblemParameters,
359) -> Result<f64, ApproximationError> {
360 evaluate_approximate_inner(expression, variables, &mut HashMap::new())
361}
362
363fn evaluate_approximate_inner(
364 expression: &Expr,
365 variables: &ProblemParameters,
366 memo: &mut HashMap<ExprNodeId, f64>,
367) -> Result<f64, ApproximationError> {
368 if let Some(value) = memo.get(&expression.node_identity()) {
369 return Ok(*value);
370 }
371 let value = match expression.node() {
372 ExprNode::Const(value) => rational_to_f64(value),
373 ExprNode::Var(name) => variables
374 .get(name.as_str())
375 .map(|value| value as f64)
376 .ok_or_else(|| ApproximationError::MissingVariable(name.to_string())),
377 ExprNode::Add(values) => values.iter().try_fold(0.0, |sum, value| {
378 Ok(sum + evaluate_approximate_inner(value, variables, memo)?)
379 }),
380 ExprNode::Mul(values) => values.iter().try_fold(1.0, |product, value| {
381 Ok(product * evaluate_approximate_inner(value, variables, memo)?)
382 }),
383 ExprNode::Pow(base, exponent) => Ok(evaluate_approximate_inner(base, variables, memo)?
384 .powf(evaluate_approximate_inner(exponent, variables, memo)?)),
385 ExprNode::Exp(value) => Ok(evaluate_approximate_inner(value, variables, memo)?.exp()),
386 ExprNode::Log(value) => Ok(evaluate_approximate_inner(value, variables, memo)?.ln()),
387 ExprNode::Factorial(value) => {
388 approximate_factorial(evaluate_approximate_inner(value, variables, memo)?)
389 }
390 }?;
391 if !value.is_finite() {
392 return Err(ApproximationError::NonFiniteResult(expression.to_string()));
393 }
394 memo.insert(expression.node_identity(), value);
395 Ok(value)
396}
397
398#[cfg(test)]
400pub(crate) fn expression_from_approximation(value: f64) -> Expr {
401 Expr::constant(
402 BigRational::from_f64(value)
403 .expect("growth-domain expression constants must be finite numbers"),
404 )
405}
406
407pub(crate) fn rational_to_f64(value: &BigRational) -> Result<f64, ApproximationError> {
408 value
409 .to_f64()
410 .filter(|value| value.is_finite())
411 .ok_or_else(|| ApproximationError::OutOfRange(value.to_string()))
412}
413
414pub(crate) fn approximate_factorial(value: f64) -> Result<f64, ApproximationError> {
415 if !value.is_finite() || value < 0.0 || value.fract() != 0.0 {
416 return Err(ApproximationError::InvalidFactorialArgument(
417 value.to_string(),
418 ));
419 }
420 if value > 170.0 {
421 Err(ApproximationError::NonFiniteResult(format!(
422 "factorial({value})"
423 )))
424 } else {
425 Ok((2..=value as u64).fold(1.0, |product, factor| product * factor as f64))
426 }
427}
428
429#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
430pub enum ApproximationError {
431 #[error("missing expression variable {0}")]
432 MissingVariable(String),
433 #[error("exact constant {0} is outside the f64 approximation domain")]
434 OutOfRange(String),
435 #[error("factorial argument must be a non-negative integer, found {0}")]
436 InvalidFactorialArgument(String),
437 #[error("expression {0} has no finite real approximation")]
438 NonFiniteResult(String),
439}
440
441#[derive(Clone, Debug, PartialEq, Eq)]
443pub enum AsymptoticAnalysisError {
444 Unsupported(String),
445}
446
447impl fmt::Display for AsymptoticAnalysisError {
448 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
449 match self {
450 Self::Unsupported(expression) => {
451 write!(formatter, "unsupported asymptotic expression: {expression}")
452 }
453 }
454 }
455}
456
457impl std::error::Error for AsymptoticAnalysisError {}
458
459#[cfg(test)]
460#[path = "unit_tests/expr.rs"]
461mod tests;