Skip to content

Commit 4818dc0

Browse files
committed
fix #230
1 parent 0740041 commit 4818dc0

4 files changed

Lines changed: 225 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
### Bug Fixes
44

5+
- **([#230](https://github.com/cortex-js/compute-engine/issues/230))
6+
Root Derivatives**: Fixed the `D` operator not differentiating expressions
7+
containing the `Root` operator (n-th roots). Previously, `D(Root(x, 3), x)`
8+
(derivative of ∛x) would return an unevaluated derivative expression instead
9+
of computing the result. Now correctly returns `1/(3x^(2/3))`, equivalent to
10+
the expected `(1/3)·x^(-2/3)`. The fix adds a special case in the `differentiate`
11+
function to handle `Root(base, n)` by applying the power rule with exponent `1/n`.
12+
513
- **Sign Simplification**: Fixed `Sign(x).simplify()` returning `1` instead of
614
`-1` when `x` is negative. The simplification rule incorrectly returned
715
`ce.One` for both positive and negative cases.

src/compute-engine/symbolic/derivative.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,24 @@ export function differentiate(
219219
return simplifyDerivative(add(...(terms as BoxedExpression[])));
220220
}
221221

222+
// Root rule: Root(base, n) = base^(1/n)
223+
// d/dx Root(base, n) = d/dx base^(1/n) = (1/n) * base^((1/n) - 1) * d/dx base
224+
if (expr.operator === 'Root') {
225+
const [base, n] = expr.ops!;
226+
if (!base.has(v)) return ce.Zero;
227+
228+
// Compute derivative using the power rule
229+
// d/dx base^(1/n) = (1/n) * base^((1/n) - 1) * base'
230+
const exponent = ce.One.div(n); // 1/n
231+
const basePrime = differentiate(base, v) ?? ce._fn('D', [base, ce.symbol(v)]);
232+
const newExponent = exponent.sub(ce.One); // (1/n) - 1 = (1-n)/n
233+
234+
// Create Power expression directly without canonicalization to avoid Root conversion
235+
const power = ce._fn('Power', [base, newExponent], { canonical: false });
236+
237+
return simplifyDerivative(exponent.mul(power).mul(basePrime));
238+
}
239+
222240
// Power rule
223241
if (expr.operator === 'Power') {
224242
const [base, exponent] = expr.ops!;

test/compute-engine/calculus.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,25 @@ describe('DERIVATION', () => {
2121

2222
test('no variable', () =>
2323
expect(evaluate('\\frac{d}{dx} 3t')).toMatchInlineSnapshot(`0`));
24+
25+
// Issue #230: Root operator should be differentiated correctly
26+
test('cube root derivative', () =>
27+
expect(evaluate('\\frac{d}{dx} \\sqrt[3]{x}')).toMatchInlineSnapshot(
28+
`(3x^(2/3))^-1`
29+
));
30+
31+
test('fifth root derivative', () =>
32+
expect(evaluate('\\frac{d}{dx} \\sqrt[5]{x}')).toMatchInlineSnapshot(
33+
`(5x^(4/5))^-1`
34+
));
35+
36+
test('root with chain rule', () =>
37+
expect(evaluate('\\frac{d}{dx} \\sqrt[3]{x^2 + 1}')).toMatchInlineSnapshot(
38+
`(2x) / (3(x^2 + 1)^(2/3))`
39+
));
40+
41+
test('root of constant', () =>
42+
expect(evaluate('\\frac{d}{dx} \\sqrt[3]{5}')).toMatchInlineSnapshot(`0`));
2443
});
2544

2645
describe('INDEFINITE INTEGRATION', () => {

test/compute-engine/latex-syntax/logic.test.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,3 +825,183 @@ describe('Iverson Bracket', () => {
825825
`);
826826
});
827827
});
828+
829+
describe('Predicate', () => {
830+
// Serialization tests
831+
it('should serialize Predicate with one argument', () => {
832+
expect(ce.box(['Predicate', 'P', 'x']).latex).toBe('P(x)');
833+
});
834+
835+
it('should serialize Predicate with multiple arguments', () => {
836+
expect(ce.box(['Predicate', 'Q', 'a', 'b']).latex).toBe('Q(a, b)');
837+
expect(ce.box(['Predicate', 'R', 'x', 'y', 'z']).latex).toBe('R(x, y, z)');
838+
});
839+
840+
// Round-trip tests: parse -> serialize -> parse should give same result
841+
it('should round-trip predicates inside ForAll', () => {
842+
const expr1 = ce.parse('\\forall x, P(x)');
843+
// Verify it contains Predicate
844+
expect(expr1.json).toMatchInlineSnapshot(`
845+
[
846+
ForAll,
847+
x,
848+
[
849+
Predicate,
850+
P,
851+
x,
852+
],
853+
]
854+
`);
855+
// Serialize and re-parse
856+
const latex = expr1.latex;
857+
const expr2 = ce.parse(latex);
858+
expect(expr2.json).toEqual(expr1.json);
859+
});
860+
861+
it('should round-trip predicates inside Exists', () => {
862+
const expr1 = ce.parse('\\exists x, Q(x, y)');
863+
expect(expr1.json).toMatchInlineSnapshot(`
864+
[
865+
Exists,
866+
x,
867+
[
868+
Predicate,
869+
Q,
870+
x,
871+
y,
872+
],
873+
]
874+
`);
875+
const expr2 = ce.parse(expr1.latex);
876+
expect(expr2.json).toEqual(expr1.json);
877+
});
878+
879+
it('should round-trip nested quantifiers with predicates', () => {
880+
const expr1 = ce.parse('\\forall x, \\exists y, R(x, y)');
881+
expect(expr1.json).toMatchInlineSnapshot(`
882+
[
883+
ForAll,
884+
x,
885+
[
886+
Exists,
887+
y,
888+
[
889+
Predicate,
890+
R,
891+
x,
892+
y,
893+
],
894+
],
895+
]
896+
`);
897+
const expr2 = ce.parse(expr1.latex);
898+
expect(expr2.json).toEqual(expr1.json);
899+
});
900+
901+
// Type inference tests
902+
it('should infer boolean type for Predicate', () => {
903+
const pred = ce.box(['Predicate', 'P', 'x']);
904+
expect(pred.type.toString()).toBe('boolean');
905+
});
906+
907+
it('should allow Predicate in boolean contexts', () => {
908+
// Predicate should work as argument to And, Or, Not, etc.
909+
const expr1 = ce.box(['And', ['Predicate', 'P', 'x'], ['Predicate', 'Q', 'x']]);
910+
expect(expr1.type.toString()).toBe('boolean');
911+
912+
const expr2 = ce.box(['Not', ['Predicate', 'P', 'x']]);
913+
expect(expr2.type.toString()).toBe('boolean');
914+
915+
const expr3 = ce.box(['Implies', ['Predicate', 'P', 'x'], ['Predicate', 'Q', 'x']]);
916+
expect(expr3.type.toString()).toBe('boolean');
917+
});
918+
919+
// D(f, x) should parse as Predicate, not derivative
920+
it('should parse D(f, x) as Predicate, not derivative', () => {
921+
// Outside quantifier scope - D is special-cased to always be Predicate
922+
expect(ce.parse('D(f, x)').json).toMatchInlineSnapshot(`
923+
[
924+
Predicate,
925+
D,
926+
f,
927+
x,
928+
]
929+
`);
930+
931+
// Inside quantifier scope
932+
expect(ce.parse('\\forall x, D(x)').json).toMatchInlineSnapshot(`
933+
[
934+
ForAll,
935+
x,
936+
[
937+
Predicate,
938+
D,
939+
x,
940+
],
941+
]
942+
`);
943+
});
944+
945+
// Predicates outside quantifier scope should be regular function applications
946+
it('should parse predicates outside quantifier scope as function applications', () => {
947+
// P(x) outside quantifier scope is a regular function application
948+
expect(ce.parse('P(x)').json).toMatchInlineSnapshot(`
949+
[
950+
P,
951+
x,
952+
]
953+
`);
954+
expect(ce.parse('Q(a, b)').json).toMatchInlineSnapshot(`
955+
[
956+
Q,
957+
a,
958+
b,
959+
]
960+
`);
961+
});
962+
});
963+
964+
describe('Single-letter library functions', () => {
965+
// N is a library function for numeric evaluation, but N(x) in LaTeX
966+
// is not standard math notation. Like D, N is excluded from automatic
967+
// function recognition so it can be used as a variable.
968+
it('should parse N(x) as Predicate, not as numeric function', () => {
969+
// N(x) should NOT be parsed as the numeric evaluation function
970+
// Instead, it's parsed as a Predicate (like D)
971+
const expr = ce.parse('N(\\pi)');
972+
expect(expr.json).toMatchInlineSnapshot(`
973+
[
974+
Predicate,
975+
N,
976+
Pi,
977+
]
978+
`);
979+
});
980+
981+
it('should allow N function via MathJSON', () => {
982+
// N function can be constructed directly in MathJSON
983+
const expr = ce.box(['N', 'Pi']);
984+
expect(expr.operator).toBe('N');
985+
expect(expr.op1?.symbol).toBe('Pi');
986+
987+
// Direct .N() on Pi gives numeric value (preferred way to get numeric values)
988+
const piNumeric = ce.box('Pi').N();
989+
expect(piNumeric.numericValue).not.toBeNull();
990+
});
991+
992+
// e and i are constants, not functions
993+
it('should parse e as Euler constant', () => {
994+
expect(ce.parse('e').json).toMatchInlineSnapshot(`ExponentialE`);
995+
});
996+
997+
it('should parse i as imaginary unit', () => {
998+
// i is canonicalized to Complex representation
999+
expect(ce.parse('i').json).toMatchInlineSnapshot(`
1000+
[
1001+
Complex,
1002+
0,
1003+
1,
1004+
]
1005+
`);
1006+
});
1007+
});

0 commit comments

Comments
 (0)