What happens
The parity identities — cos(-u) = cos(u), sin(-u) = -sin(u), tan(-u) = -tan(u), abs(-u) = abs(u) — are essentially absent, so expressions that cancel exactly do not.
Measured on a clean build of master at b4385a86, .NET 10, default settings:
"sin(-x) + sin(x)".Simplify() -> sin(-x) + sin(x) should be 0
"cos(-x) - cos(x)".Simplify() -> cos(-x) - cos(x) should be 0
"cos(-x)".Simplify() -> cos(-x)
"sin(-x)".Simplify() -> sin(-x)
"tan(-x)".Simplify() -> tan(-x)
"abs(-x)".Simplify() -> abs(-x)
"sin(-2 * x)".Simplify() -> sin((-2) * x)
"tan(-2 * x)".Simplify() -> tan((-2) * x)
"abs(-2 * x)".Simplify() -> abs((-2) * x)
"cos(-2 * x)".Simplify() -> cos(2 * x) the one case that does fold
The last line is the whole shape of it: cos folds a negative numeric coefficient and nothing else folds anything. sin, tan and abs do not fold even that, and no function folds a bare negation.
sin(-x) + sin(x) not reaching 0 is the part worth treating as more than a coverage gap. It is not a wrong answer — nothing false is asserted — but an expression that is identically zero is left standing, and anything downstream that tests a residual against zero (the equation solver's verification step does exactly this) sees a non-zero residual where there is none.
Why
Two different shapes are involved and only one is looked for.
"-x".ToEntity() prints -x
"-2 * x".ToEntity() prints (-2) * x
A bare negation and a negative numeric coefficient are not the same tree, and the rule that handles cos(-2 * x) keys on the second. Nothing keys on the first.
What would fix it
A parity rule per function, stated on the argument's sign rather than on a coefficient shape:
- even —
cos, sec, cosh, abs: drop a leading negation from the argument;
- odd —
sin, tan, cotan, cosec, sinh, tanh, sgn: lift it out to the front of the node.
The predicate has to be "the argument is a negation of something" in the tree sense, which covers both -x and (-2) * x and also -x - 1, none of which is reached today. Deciding it on the value would be wrong: cos(a - b) where a < b is not a case for this rule, because the rule is about the written form and must hold symbolically.
Worth checking before starting
- Which functions are genuinely even or odd over the complex plane, not just the reals. This is the class of thing that has shipped wrong here before — see
Contributing/SimplificationContract.md on branch cuts, and note that arccotan's range in this library is (-pi/2, pi/2] rather than the textbook one, so parity for the inverse functions wants measuring at +1, -1 and 0 before anything is written down.
- That lifting a negation out of an odd function does not fight the complexity criteria and lose the comparison it exists to win —
-sin(x) and sin(-x) rate closely, and a rewrite that produces a correct-but-not-shorter candidate is invisible. alternate:: in the analysis workspace's probe shows the candidate pool.
- Termination: an odd-function rule produces a negation above the node, and a rule that pushes negations down would cycle with it.
How it was found
canoncheck, a new harness for #746 tier 1's canonical-form work, which checks whether two writings of one expression reach the same form. abs(-x) vs abs(x), sin(-x) vs -sin(x) and cos(-x) vs cos(x) are three of its listed pairs and all three disagree under both InnerSimplified and Simplify. See Contributing/CanonicalForm.md.
What happens
The parity identities —
cos(-u) = cos(u),sin(-u) = -sin(u),tan(-u) = -tan(u),abs(-u) = abs(u)— are essentially absent, so expressions that cancel exactly do not.Measured on a clean build of
masteratb4385a86, .NET 10, default settings:The last line is the whole shape of it:
cosfolds a negative numeric coefficient and nothing else folds anything.sin,tanandabsdo not fold even that, and no function folds a bare negation.sin(-x) + sin(x)not reaching0is the part worth treating as more than a coverage gap. It is not a wrong answer — nothing false is asserted — but an expression that is identically zero is left standing, and anything downstream that tests a residual against zero (the equation solver's verification step does exactly this) sees a non-zero residual where there is none.Why
Two different shapes are involved and only one is looked for.
A bare negation and a negative numeric coefficient are not the same tree, and the rule that handles
cos(-2 * x)keys on the second. Nothing keys on the first.What would fix it
A parity rule per function, stated on the argument's sign rather than on a coefficient shape:
cos,sec,cosh,abs: drop a leading negation from the argument;sin,tan,cotan,cosec,sinh,tanh,sgn: lift it out to the front of the node.The predicate has to be "the argument is a negation of something" in the tree sense, which covers both
-xand(-2) * xand also-x - 1, none of which is reached today. Deciding it on the value would be wrong:cos(a - b)wherea < bis not a case for this rule, because the rule is about the written form and must hold symbolically.Worth checking before starting
Contributing/SimplificationContract.mdon branch cuts, and note thatarccotan's range in this library is(-pi/2, pi/2]rather than the textbook one, so parity for the inverse functions wants measuring at+1,-1and0before anything is written down.-sin(x)andsin(-x)rate closely, and a rewrite that produces a correct-but-not-shorter candidate is invisible.alternate::in the analysis workspace'sprobeshows the candidate pool.How it was found
canoncheck, a new harness for #746 tier 1's canonical-form work, which checks whether two writings of one expression reach the same form.abs(-x)vsabs(x),sin(-x)vs-sin(x)andcos(-x)vscos(x)are three of its listed pairs and all three disagree under bothInnerSimplifiedandSimplify. SeeContributing/CanonicalForm.md.