Skip to content

Commit 7d6f52a

Browse files
authored
Merge pull request #69 from pie-framework/feat/PD-4890
feat: fallback to numeric comparison for symbolic exponents in equations comparison, added test cases PD-4890
2 parents cd4d4be + 209391c commit 7d6f52a

4 files changed

Lines changed: 251 additions & 2 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
export default {
2+
mode: "symbolic",
3+
tests: [
4+
// Cases where symbolic validation failed before
5+
6+
{
7+
target: "B=500(\\frac{3}{4})^n",
8+
ne: ["B=500(\\frac{1}{4})^{n-1}"],
9+
},
10+
{
11+
target: "y=500(\\frac{3}{4})^n",
12+
ne: ["y=500(\\frac{1}{4})^{n-1}"],
13+
},
14+
{
15+
target: "a=500(\\frac{3}{4})^d",
16+
ne: ["a=(\\frac{1}{4})^{d-1}"],
17+
},
18+
{
19+
target: "b=500(\\frac{3}{4})^d",
20+
ne: ["b=(\\frac{1}{4})^{d-1}"],
21+
},
22+
{
23+
target: "f=4^g",
24+
ne: ["f=4^{g-1}"],
25+
},
26+
{
27+
target: "h=4^g",
28+
ne: ["h=4^{g-1}"],
29+
},
30+
31+
// Control test — should NOT be equal when using multiplication instead of exponentiation
32+
{
33+
target: "a=4*g",
34+
ne: ["a=4*(g-1)"],
35+
},
36+
37+
// Valid symbolic equivalence (should return true)
38+
{
39+
target: "x=3^n",
40+
eq: ["x=3^n"],
41+
},
42+
{
43+
target: "z=2^y",
44+
eq: ["z=2^y"],
45+
},
46+
{
47+
target: "v=(\\frac{5}{2})^x",
48+
eq: ["v=(\\frac{5}{2})^x"],
49+
},
50+
{
51+
target: "m=(\\frac{3}{4})^a",
52+
eq: ["m=(\\frac{3}{4})^a"],
53+
},
54+
{
55+
target: "k=(1.5)^p",
56+
eq: ["k=(1.5)^p"],
57+
},
58+
],
59+
};
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
export default {
2+
mode: "symbolic",
3+
tests: [
4+
{
5+
target: "\\left(\\frac{1}{2}\\right)^n",
6+
eq: ["\\left(\\frac{10}{20}\\right)^n"],
7+
},
8+
{
9+
target: "\\left(\\frac{1}{2}\\right)^n",
10+
eq: ["\\left(0.5\\right)^n"],
11+
},
12+
{
13+
target: "\\left(\\frac{1}{2}\\right)^n",
14+
eq: ["\\left(\\frac{2}{4}\\right)^n"],
15+
},
16+
{
17+
target: "\\left(\\frac{10}{20}\\right)^n",
18+
eq: ["\\left(\\frac{1}{2}\\right)^n"],
19+
},
20+
{
21+
target: "\\left(0.5\\right)^n",
22+
eq: ["\\left(\\frac{1}{2}\\right)^n"],
23+
},
24+
{
25+
target: "\\left(\\frac{3}{2}\\right)^a",
26+
eq: ["\\left(\\frac{6}{4}\\right)^a"],
27+
},
28+
{
29+
target: "\\left(\\frac{3}{2}\\right)^a",
30+
eq: ["\\left(1.5\\right)^a"],
31+
},
32+
{
33+
target: "\\left(\\frac{1}{2}\\right)^n",
34+
eq: ["\\left(1\\div2\\right)^n"],
35+
},
36+
37+
// Division written with \left and \right — also should be equivalent
38+
39+
{
40+
target: "\\left(1\\div2\\right)^n",
41+
eq: ["\\left(10\\div20\\right)^n"],
42+
},
43+
{
44+
target: "\\left(1\\div2\\right)^n",
45+
eq: ["\\left(0.5\\right)^n"],
46+
},
47+
{
48+
target: "\\left(1\\div2\\right)^n",
49+
eq: ["\\left(2\\div4\\right)^n"],
50+
},
51+
{
52+
target: "\\left(10\\div20\\right)^n",
53+
eq: ["\\left(1\\div2\\right)^n"],
54+
},
55+
{
56+
target: "\\left(0.5\\right)^n",
57+
eq: ["\\left(1\\div2\\right)^n"],
58+
},
59+
{
60+
target: "\\left(3\\div2\\right)^n",
61+
eq: ["\\left(6\\div4\\right)^n"],
62+
},
63+
{
64+
target: "\\left(3\\div2\\right)^n",
65+
eq: ["\\left(1.5\\right)^n"],
66+
},
67+
// All of the following SHOULD be considered equivalent (eq)
68+
69+
{
70+
target: "(\\frac{1}{2})^n",
71+
eq: ["(\\frac{10}{20})^n"],
72+
},
73+
{
74+
target: "(\\frac{1}{2})^n",
75+
eq: ["(0.5)^n"],
76+
},
77+
{
78+
target: "(\\frac{1}{2})^n",
79+
eq: ["(\\frac{2}{4})^n"],
80+
},
81+
{
82+
target: "(\\frac{10}{20})^n",
83+
eq: ["(\\frac{1}{2})^n"],
84+
},
85+
{
86+
target: "(0.5)^n",
87+
eq: ["(\\frac{1}{2})^n"],
88+
},
89+
{
90+
target: "(\\frac{3}{2})^a",
91+
eq: ["(\\frac{6}{4})^a"],
92+
},
93+
{
94+
target: "(\\frac{3}{2})^a",
95+
eq: ["(1.5)^a"],
96+
},
97+
{
98+
target: "(\\frac{1}{2})^n",
99+
eq: ["(1\\div2)^n"],
100+
},
101+
102+
// Control set – same expressions using division
103+
{
104+
target: "(1\\div2)^n",
105+
eq: ["(10\\div20)^n"],
106+
},
107+
{
108+
target: "(1\\div2)^n",
109+
eq: ["(0.5)^n"],
110+
},
111+
{
112+
target: "(1\\div2)^n",
113+
eq: ["(2\\div4)^n"],
114+
},
115+
{
116+
target: "(10\\div20)^n",
117+
eq: ["(1\\div2)^n"],
118+
},
119+
{
120+
target: "(0.5)^n",
121+
eq: ["(1\\div2)^n"],
122+
},
123+
{
124+
target: "(3\\div2)^n",
125+
eq: ["(6\\div4)^n"],
126+
},
127+
{
128+
target: "(3\\div2)^n",
129+
eq: ["(1.5)^n"],
130+
},
131+
],
132+
};

src/symbolic/compare-equations.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
transformEqualityInExpression,
1010
expressionsCanBeCompared,
1111
solveQuadraticEquation,
12+
hasSymbolicExponent,
13+
areNumericallyEquivalent,
1214
} from "./utils";
1315

1416
const m: any = mathjs;
@@ -49,6 +51,19 @@ export const compareEquations = (
4951
let firstEquationCoefficients: number[];
5052
let secondEquationCoefficients: number[];
5153

54+
if (
55+
hasSymbolicExponent(firstExpression) ||
56+
hasSymbolicExponent(secondExpression)
57+
) {
58+
const isEquivalent = areNumericallyEquivalent(
59+
firstExpression,
60+
secondExpression,
61+
firstEquationVariablesName
62+
);
63+
64+
return isEquivalent;
65+
}
66+
5267
if (firstEquationVariablesName.length === 1) {
5368
firstEquationCoefficients = getCoefficients(
5469
firstExpression,

src/symbolic/utils.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ export const expressionsCanBeCompared = (
2828

2929
firstEquation.traverse(function (node, path, parent) {
3030
if (node.isSymbolNode) {
31-
firstSymbolNode = true
32-
seriesNode = seriesNode || node.name.includes("[")
31+
firstSymbolNode = true;
32+
seriesNode = seriesNode || node.name.includes("[");
3333
}
3434
noFunctionOrArray =
3535
noFunctionOrArray || node.isFunctionNode || node.isArrayNode;
@@ -46,6 +46,49 @@ export const expressionsCanBeCompared = (
4646
return noFunctionOrArray && symbolNode && !seriesNode;
4747
};
4848

49+
export const hasSymbolicExponent = (node: MathNode): boolean => {
50+
let found = false;
51+
52+
node.traverse((n) => {
53+
if (
54+
n.isOperatorNode &&
55+
n.op === "^" &&
56+
!n.args[1]?.isConstantNode // Exponent is symbolic or compound
57+
) {
58+
found = true;
59+
}
60+
});
61+
62+
return found;
63+
};
64+
65+
export const areNumericallyEquivalent = (
66+
exprA: MathNode,
67+
exprB: MathNode,
68+
variables: string[],
69+
tolerance: number = 1e-10
70+
): boolean => {
71+
const compiledA = m.compile(exprA.toString());
72+
const compiledB = m.compile(exprB.toString());
73+
74+
const testValues = [1, 2, 3, 4, 5];
75+
76+
return testValues.every((val) => {
77+
const scope = variables.reduce((acc, v) => {
78+
acc[v] = val;
79+
return acc;
80+
}, {} as Record<string, number>);
81+
82+
try {
83+
const resultA = compiledA.evaluate(scope);
84+
const resultB = compiledB.evaluate(scope);
85+
return Math.abs(resultA - resultB) < tolerance;
86+
} catch {
87+
return false;
88+
}
89+
});
90+
};
91+
4992
// move the terms of the equations to the left hand side
5093
export const transformEqualityInExpression = (equality: MathNode) =>
5194
// remove added/subtracted numbers/variables from both sides of the equation

0 commit comments

Comments
 (0)