Skip to content

Commit b1538aa

Browse files
committed
Add PossibleZeroQ function for zero-testing expressions
1 parent 55d3356 commit b1538aa

File tree

4 files changed

+261
-1
lines changed

4 files changed

+261
-1
lines changed

functions.csv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4307,7 +4307,7 @@ PositiveQ,Tests if a number is positive,✅,pure,,
43074307
PositiveRationals,,,,12,4479
43084308
PositiveReals,,,,12,1388
43094309
PositiveSemidefiniteMatrixQ,,,,10,2837
4310-
PossibleZeroQ,,,,6,1369
4310+
PossibleZeroQ,Tests if an expression is possibly zero,,pure,6,1369
43114311
Postfix,Symbol for postfix display formatting,,pure,1,3070
43124312
Power,Raises a number to a power,,pure,1,4
43134313
PowerDistribution,,,,8,3224

src/evaluator/dispatch/predicate_functions.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,9 @@ pub fn dispatch_predicate_functions(
333333
"SubsetQ" if args.len() == 2 => {
334334
return Some(crate::functions::predicate_ast::subset_q_ast(args));
335335
}
336+
"PossibleZeroQ" => {
337+
return Some(crate::functions::predicate_ast::possible_zero_q_ast(args));
338+
}
336339
"OptionQ" if args.len() == 1 => {
337340
return Some(crate::functions::predicate_ast::option_q_ast(args));
338341
}

src/functions/predicate_ast.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,6 +1316,99 @@ pub fn subset_q_ast(args: &[Expr]) -> Result<Expr, InterpreterError> {
13161316
}
13171317
}
13181318

1319+
/// PossibleZeroQ[expr] - Tests if expr is possibly zero
1320+
/// Uses symbolic simplification and numeric evaluation to determine
1321+
/// whether an expression could be zero.
1322+
pub fn possible_zero_q_ast(args: &[Expr]) -> Result<Expr, InterpreterError> {
1323+
if args.len() != 1 {
1324+
return Ok(Expr::FunctionCall {
1325+
name: "PossibleZeroQ".to_string(),
1326+
args: args.to_vec(),
1327+
});
1328+
}
1329+
let expr = &args[0];
1330+
1331+
// 1. Structural zero check
1332+
if is_structural_zero(expr) {
1333+
return Ok(bool_expr(true));
1334+
}
1335+
1336+
// 2. Non-numeric atoms that can never be zero
1337+
match expr {
1338+
Expr::String(_) => return Ok(bool_expr(false)),
1339+
Expr::Identifier(name) => match name.as_str() {
1340+
"True" | "False" | "Infinity" | "ComplexInfinity" | "Indeterminate" => {
1341+
return Ok(bool_expr(false));
1342+
}
1343+
_ => {}
1344+
},
1345+
_ => {}
1346+
}
1347+
1348+
// 3. Known nonzero constants
1349+
if let Expr::Constant(c) = expr {
1350+
match c.as_str() {
1351+
"Pi" | "E" | "Degree" | "EulerGamma" | "Catalan" | "GoldenRatio"
1352+
| "Glaisher" | "Khinchin" => {
1353+
return Ok(bool_expr(false));
1354+
}
1355+
_ => {}
1356+
}
1357+
}
1358+
1359+
// 4. Known positive/negative numbers are not zero
1360+
if let Some(true) = is_known_positive(expr) {
1361+
return Ok(bool_expr(false));
1362+
}
1363+
if let Some(true) = is_known_negative(expr) {
1364+
return Ok(bool_expr(false));
1365+
}
1366+
1367+
// 5. Simplify the expression and check
1368+
let simplified = crate::functions::polynomial_ast::simplify_expr(expr);
1369+
if is_structural_zero(&simplified) {
1370+
return Ok(bool_expr(true));
1371+
}
1372+
1373+
// 6. Try numeric evaluation on the simplified expression
1374+
if let Some(val) = crate::functions::math_ast::try_eval_to_f64(&simplified) {
1375+
return Ok(bool_expr(val.abs() < 1e-10));
1376+
}
1377+
1378+
// 7. Try numeric evaluation on the original expression
1379+
if let Some(val) = crate::functions::math_ast::try_eval_to_f64(expr) {
1380+
return Ok(bool_expr(val.abs() < 1e-10));
1381+
}
1382+
1383+
// 8. For expressions we can't evaluate numerically, return False
1384+
// (matches Wolfram behavior for symbolic unknowns like x)
1385+
Ok(bool_expr(false))
1386+
}
1387+
1388+
/// Check if an expression is structurally zero (literal 0, 0.0, 0/1, Complex[0,0], etc.)
1389+
fn is_structural_zero(expr: &Expr) -> bool {
1390+
match expr {
1391+
Expr::Integer(0) => true,
1392+
Expr::Real(f) => *f == 0.0,
1393+
Expr::BigInteger(n) => {
1394+
use num_traits::Zero;
1395+
n.is_zero()
1396+
}
1397+
Expr::BigFloat(digits, _) => digits.parse::<f64>().is_ok_and(|f| f == 0.0),
1398+
Expr::FunctionCall { name, args }
1399+
if name == "Rational" && args.len() == 2 =>
1400+
{
1401+
is_structural_zero(&args[0])
1402+
}
1403+
Expr::FunctionCall { name, args }
1404+
if name == "Complex" && args.len() == 2 =>
1405+
{
1406+
is_structural_zero(&args[0]) && is_structural_zero(&args[1])
1407+
}
1408+
_ => false,
1409+
}
1410+
}
1411+
13191412
/// OptionQ[expr] - Tests if expr is a Rule or RuleDelayed or a list thereof
13201413
pub fn option_q_ast(args: &[Expr]) -> Result<Expr, InterpreterError> {
13211414
if args.len() != 1 {

tests/interpreter_tests/math/predicates.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,3 +679,167 @@ mod abs_infinity {
679679
assert_eq!(interpret("Abs[ComplexInfinity]").unwrap(), "Infinity");
680680
}
681681
}
682+
683+
mod possible_zero_q {
684+
use super::*;
685+
686+
#[test]
687+
fn literal_zero() {
688+
assert_eq!(interpret("PossibleZeroQ[0]").unwrap(), "True");
689+
}
690+
691+
#[test]
692+
fn real_zero() {
693+
assert_eq!(interpret("PossibleZeroQ[0.0]").unwrap(), "True");
694+
}
695+
696+
#[test]
697+
fn nonzero_integer() {
698+
assert_eq!(interpret("PossibleZeroQ[1]").unwrap(), "False");
699+
}
700+
701+
#[test]
702+
fn nonzero_negative() {
703+
assert_eq!(interpret("PossibleZeroQ[-3]").unwrap(), "False");
704+
}
705+
706+
#[test]
707+
fn nonzero_real() {
708+
assert_eq!(interpret("PossibleZeroQ[1.5]").unwrap(), "False");
709+
}
710+
711+
#[test]
712+
fn nonzero_rational() {
713+
assert_eq!(interpret("PossibleZeroQ[1/2]").unwrap(), "False");
714+
}
715+
716+
#[test]
717+
fn rational_cancel_to_zero() {
718+
assert_eq!(interpret("PossibleZeroQ[3/4 - 3/4]").unwrap(), "True");
719+
}
720+
721+
#[test]
722+
fn symbolic_cancel() {
723+
assert_eq!(interpret("PossibleZeroQ[x - x]").unwrap(), "True");
724+
}
725+
726+
#[test]
727+
fn symbolic_cancel_with_coeff() {
728+
assert_eq!(interpret("PossibleZeroQ[2*a - 2*a]").unwrap(), "True");
729+
}
730+
731+
#[test]
732+
fn symbolic_cancel_power() {
733+
assert_eq!(interpret("PossibleZeroQ[a^2 - a^2]").unwrap(), "True");
734+
}
735+
736+
#[test]
737+
fn symbolic_unknown_false() {
738+
assert_eq!(interpret("PossibleZeroQ[x]").unwrap(), "False");
739+
}
740+
741+
#[test]
742+
fn sin_zero() {
743+
assert_eq!(interpret("PossibleZeroQ[Sin[0]]").unwrap(), "True");
744+
}
745+
746+
#[test]
747+
fn sin_pi() {
748+
assert_eq!(interpret("PossibleZeroQ[Sin[Pi]]").unwrap(), "True");
749+
}
750+
751+
#[test]
752+
fn cos_pi_half() {
753+
assert_eq!(interpret("PossibleZeroQ[Cos[Pi/2]]").unwrap(), "True");
754+
}
755+
756+
#[test]
757+
fn cos_zero_minus_one() {
758+
assert_eq!(interpret("PossibleZeroQ[Cos[0] - 1]").unwrap(), "True");
759+
}
760+
761+
#[test]
762+
fn log_one() {
763+
assert_eq!(interpret("PossibleZeroQ[Log[1]]").unwrap(), "True");
764+
}
765+
766+
#[test]
767+
fn sqrt_cancel() {
768+
assert_eq!(
769+
interpret("PossibleZeroQ[Sqrt[2] - Sqrt[2]]").unwrap(),
770+
"True"
771+
);
772+
}
773+
774+
#[test]
775+
fn constant_subtract() {
776+
assert_eq!(interpret("PossibleZeroQ[E - E]").unwrap(), "True");
777+
}
778+
779+
#[test]
780+
fn nonzero_constant_pi() {
781+
assert_eq!(interpret("PossibleZeroQ[Pi]").unwrap(), "False");
782+
}
783+
784+
#[test]
785+
fn nonzero_sum() {
786+
assert_eq!(interpret("PossibleZeroQ[2 + 3]").unwrap(), "False");
787+
}
788+
789+
#[test]
790+
fn complex_i_false() {
791+
assert_eq!(interpret("PossibleZeroQ[I]").unwrap(), "False");
792+
}
793+
794+
#[test]
795+
fn complex_zero() {
796+
assert_eq!(interpret("PossibleZeroQ[0 + 0*I]").unwrap(), "True");
797+
}
798+
799+
#[test]
800+
fn zero_times_i() {
801+
assert_eq!(interpret("PossibleZeroQ[0*I]").unwrap(), "True");
802+
}
803+
804+
#[test]
805+
fn infinity_false() {
806+
assert_eq!(interpret("PossibleZeroQ[Infinity]").unwrap(), "False");
807+
}
808+
809+
#[test]
810+
fn neg_infinity_false() {
811+
assert_eq!(interpret("PossibleZeroQ[-Infinity]").unwrap(), "False");
812+
}
813+
814+
#[test]
815+
fn complex_infinity_false() {
816+
assert_eq!(
817+
interpret("PossibleZeroQ[ComplexInfinity]").unwrap(),
818+
"False"
819+
);
820+
}
821+
822+
#[test]
823+
fn boolean_false() {
824+
assert_eq!(interpret("PossibleZeroQ[True]").unwrap(), "False");
825+
}
826+
827+
#[test]
828+
fn string_false() {
829+
assert_eq!(interpret("PossibleZeroQ[\"hello\"]").unwrap(), "False");
830+
}
831+
832+
#[test]
833+
fn x_squared_plus_one_false() {
834+
assert_eq!(interpret("PossibleZeroQ[x^2 + 1]").unwrap(), "False");
835+
}
836+
837+
#[test]
838+
fn wrong_arg_count_unevaluated() {
839+
assert_eq!(interpret("PossibleZeroQ[]").unwrap(), "PossibleZeroQ[]");
840+
assert_eq!(
841+
interpret("PossibleZeroQ[1, 2]").unwrap(),
842+
"PossibleZeroQ[1, 2]"
843+
);
844+
}
845+
}

0 commit comments

Comments
 (0)