Skip to content

Commit 580c167

Browse files
committed
feat: add support for additional derivative notations including Newton's dot and Lagrange prime notations
1 parent 36a00fa commit 580c167

7 files changed

Lines changed: 378 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
## [Unreleased]
22

3+
### Features
4+
5+
- **([#163](https://github.com/cortex-js/compute-engine/issues/163)) Additional
6+
Derivative Notations**: Added support for parsing multiple derivative notations
7+
beyond Leibniz notation:
8+
9+
- **Newton's dot notation** for time derivatives: `\dot{x}``["D", "x", "t"]`,
10+
`\ddot{x}` for second derivative, `\dddot{x}` and `\ddddot{x}` for higher orders.
11+
The time variable is configurable via the new `timeDerivativeVariable` parser
12+
option (default: `"t"`).
13+
14+
- **Lagrange prime notation with arguments**: `f'(x)` now parses to
15+
`["D", ["f", "x"], "x"]`, inferring the differentiation variable from the
16+
function argument. Works for `f''(x)`, `f'''(x)`, etc. for higher derivatives.
17+
18+
- **Euler's subscript notation**: `D_x f``["D", "f", "x"]` and
19+
`D^2_x f` or `D_x^2 f` for second derivatives.
20+
321
### Bug Fixes
422

523
- **Matrix Operations Type Validation**: Fixed matrix operations (`Shape`, `Rank`,

src/compute-engine/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1947,6 +1947,7 @@ export class ComputeEngine implements IComputeEngine {
19471947
parseUnexpectedToken: (_lhs, _parser) => null,
19481948
preserveLatex: false,
19491949
quantifierScope: 'tight',
1950+
timeDerivativeVariable: 't',
19501951
};
19511952

19521953
const result = parse(

src/compute-engine/latex-syntax/dictionary/definitions-core.ts

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -909,7 +909,7 @@ export const DEFINITIONS_CORE: LatexDictionary = [
909909
// @todo: Leibniz notation: {% latex " \\frac{d^n}{dx^n} f(x)" %}
910910
// @todo: Euler modified notation: This notation is used by Mathematica. The Euler notation uses `D` instead of
911911
// `\partial`: `\partial_{x} f`, `\partial_{x,y} f`
912-
// @todo: Newton notation: `\dot{v}` -> first derivative relative to time t `\ddot{v}` -> second derivative relative to time t
912+
// Newton notation (\dot{v}, \ddot{v}) is implemented below
913913

914914
serialize: (serializer: Serializer, expr: Expression): string => {
915915
const degree = machineValue(operand(expr, 2)) ?? 1;
@@ -921,6 +921,100 @@ export const DEFINITIONS_CORE: LatexDictionary = [
921921
return base + '^{(' + serializer.serialize(operand(expr, 2)) + ')}';
922922
},
923923
},
924+
925+
// Newton notation for time derivatives: \dot{x}, \ddot{x}, etc.
926+
{
927+
name: 'NewtonDerivative1',
928+
latexTrigger: ['\\dot'],
929+
kind: 'prefix',
930+
precedence: 740,
931+
parse: (parser: Parser): Expression | null => {
932+
const body = parser.parseGroup();
933+
if (body === null) return null;
934+
const t = parser.options.timeDerivativeVariable;
935+
return ['D', body, t] as Expression;
936+
},
937+
},
938+
{
939+
name: 'NewtonDerivative2',
940+
latexTrigger: ['\\ddot'],
941+
kind: 'prefix',
942+
precedence: 740,
943+
parse: (parser: Parser): Expression | null => {
944+
const body = parser.parseGroup();
945+
if (body === null) return null;
946+
const t = parser.options.timeDerivativeVariable;
947+
return ['D', ['D', body, t], t] as Expression;
948+
},
949+
},
950+
{
951+
name: 'NewtonDerivative3',
952+
latexTrigger: ['\\dddot'],
953+
kind: 'prefix',
954+
precedence: 740,
955+
parse: (parser: Parser): Expression | null => {
956+
const body = parser.parseGroup();
957+
if (body === null) return null;
958+
const t = parser.options.timeDerivativeVariable;
959+
return ['D', ['D', ['D', body, t], t], t] as Expression;
960+
},
961+
},
962+
{
963+
name: 'NewtonDerivative4',
964+
latexTrigger: ['\\ddddot'],
965+
kind: 'prefix',
966+
precedence: 740,
967+
parse: (parser: Parser): Expression | null => {
968+
const body = parser.parseGroup();
969+
if (body === null) return null;
970+
const t = parser.options.timeDerivativeVariable;
971+
return ['D', ['D', ['D', ['D', body, t], t], t], t] as Expression;
972+
},
973+
},
974+
975+
// Euler notation for derivatives: D_x f, D^2_x f, D_x^2 f
976+
// Uses latexTrigger to intercept before symbol parsing combines D with subscript
977+
{
978+
name: 'EulerDerivative',
979+
latexTrigger: ['D'],
980+
kind: 'expression',
981+
parse: (parser: Parser): Expression | null => {
982+
let degree = 1;
983+
let variable: Expression | null = null;
984+
985+
// Parse subscript and superscript in either order (D_x^2 or D^2_x)
986+
let done = false;
987+
while (!done) {
988+
if (parser.match('_')) {
989+
// Parse the subscript (variable)
990+
variable = parser.parseGroup() ?? parser.parseToken();
991+
if (!variable) return null;
992+
} else if (parser.match('^')) {
993+
// Parse the superscript (degree)
994+
const degExpr = parser.parseGroup() ?? parser.parseToken();
995+
degree = machineValue(degExpr) ?? 1;
996+
} else {
997+
done = true;
998+
}
999+
}
1000+
1001+
// Only trigger if we have a subscript (to distinguish from D as a variable)
1002+
if (!variable) return null;
1003+
1004+
// Parse the function/expression to differentiate
1005+
parser.skipSpace();
1006+
const fn = parser.parseExpression({ minPrec: 740 });
1007+
if (!fn) return null;
1008+
1009+
// Build nested D for the degree
1010+
let result: Expression = fn;
1011+
for (let i = 0; i < degree; i++) {
1012+
result = ['D', result, variable] as Expression;
1013+
}
1014+
return result;
1015+
},
1016+
},
1017+
9241018
{
9251019
kind: 'environment',
9261020
name: 'Which',
@@ -1199,6 +1293,14 @@ function parsePrime(
11991293
lhs: Expression,
12001294
order: number
12011295
): Expression | null {
1296+
// Accumulate additional prime marks (e.g., f''' -> order 3)
1297+
while (!parser.atEnd) {
1298+
if (parser.match("'") || parser.match('\\prime')) order++;
1299+
else if (parser.match('\\doubleprime')) order += 2;
1300+
else if (parser.match('\\tripleprime')) order += 3;
1301+
else break;
1302+
}
1303+
12021304
// If the lhs is a Prime/Derivative, increase the derivation order
12031305
const lhsh = operator(lhs);
12041306
if (lhsh === 'Derivative' || lhsh === 'Prime') {
@@ -1211,6 +1313,31 @@ function parsePrime(
12111313

12121314
const sym = symbol(lhs);
12131315
if ((sym && parser.getSymbolType(sym).matches('function')) || operator(lhs)) {
1316+
// Check if followed by arguments - if so, use D notation
1317+
// e.g., f'(x) -> ["D", ["f", "x"], "x"]
1318+
parser.skipSpace();
1319+
const args = parser.parseArguments('enclosure');
1320+
1321+
if (args && args.length > 0) {
1322+
// Infer differentiation variable from first argument (if it's a symbol)
1323+
const firstArg = args[0];
1324+
const variable = symbol(firstArg) ?? 'x';
1325+
1326+
// Build function call: f(x, y, ...) -> ['f', x, y, ...]
1327+
const fnCall =
1328+
typeof lhs === 'string'
1329+
? ([lhs, ...args] as Expression)
1330+
: (['Apply', lhs, ...args] as Expression);
1331+
1332+
// Wrap with nested D for the order
1333+
let result: Expression = fnCall;
1334+
for (let i = 0; i < order; i++) {
1335+
result = ['D', result, variable] as Expression;
1336+
}
1337+
return result;
1338+
}
1339+
1340+
// No arguments - return Derivative as before
12141341
if (order === 1) return ['Derivative', lhs];
12151342
return ['Derivative', lhs, order];
12161343
}

src/compute-engine/latex-syntax/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,16 @@ export type ParseLatexOptions = NumberFormat & {
796796
* // parses as: ∀x. (P(x) → Q(x))
797797
*/
798798
quantifierScope: 'tight' | 'loose';
799+
800+
/**
801+
* The variable used for time derivatives in Newton notation
802+
* (`\dot{x}`, `\ddot{x}`, etc.).
803+
*
804+
* When parsing `\dot{x}`, it will be interpreted as `["D", "x", timeDerivativeVariable]`.
805+
*
806+
* **Default:** `"t"`
807+
*/
808+
timeDerivativeVariable: string;
799809
};
800810

801811
/**

test/compute-engine/latex-syntax/__snapshots__/calculus.test.ts.snap

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,36 @@ int^(1)(sin(x) dx)
6666
]
6767
`;
6868

69+
exports[`EULER DERIVATIVE NOTATION D without subscript - should parse as symbol 1`] = `
70+
D
71+
D
72+
D
73+
`;
74+
75+
exports[`EULER DERIVATIVE NOTATION D^2_x f - second derivative 1`] = `
76+
D(x\\mapsto D(x\\mapsto f, x), x)
77+
D((x) |-> D((x) |-> f, x), x)
78+
["D", ["Function", ["D", ["Function", "f", "x"], "x"], "x"], "x"]
79+
`;
80+
81+
exports[`EULER DERIVATIVE NOTATION D_t x - different variable 1`] = `
82+
D(t\\mapsto x, t)
83+
D((t) |-> x, t)
84+
["D", ["Function", "x", "t"], "t"]
85+
`;
86+
87+
exports[`EULER DERIVATIVE NOTATION D_x (x^2 + 1) - derivative of expression 1`] = `
88+
D(x\\mapsto x^2+1, x)
89+
D((x) |-> x^2 + 1, x)
90+
["D", ["Function", ["Add", ["Square", "x"], 1], "x"], "x"]
91+
`;
92+
93+
exports[`EULER DERIVATIVE NOTATION D_x f - first derivative 1`] = `
94+
D(x\\mapsto f, x)
95+
D((x) |-> f, x)
96+
["D", ["Function", "f", "x"], "x"]
97+
`;
98+
6999
exports[`EXOTIC INTEGRALS \\iiint 1`] = `
70100
\\int\\int\\int_{V}\\!f(x, y, z)\\, \\mathrm{d}x \\mathrm{d}y \\mathrm{d}z
71101
int_(V)(f(x, y, z) dx dy dz)
@@ -386,6 +416,64 @@ int(sin(x) dx) + 1 === 2
386416
]
387417
`;
388418

419+
exports[`LAGRANGE PRIME NOTATION WITH ARGUMENTS \\sin'(x) - known function with prime 1`] = `
420+
D(x\\mapsto\\sin(x), x)
421+
D((x) |-> sin(x), x)
422+
["D", ["Function", ["Sin", "x"], "x"], "x"]
423+
`;
424+
425+
exports[`LAGRANGE PRIME NOTATION WITH ARGUMENTS f' without arguments - returns Derivative 1`] = `
426+
f^{\\prime}
427+
Derivative(f)
428+
["Derivative", "f"]
429+
`;
430+
431+
exports[`LAGRANGE PRIME NOTATION WITH ARGUMENTS f'''(x) - triple prime with argument 1`] = `
432+
D(x\\mapsto D(x\\mapsto D(x\\mapsto f(x), x), x), x)
433+
D((x) |-> D((x) |-> D((x) |-> f(x), x), x), x)
434+
[
435+
"D",
436+
[
437+
"Function",
438+
[
439+
"D",
440+
["Function", ["D", ["Function", ["f", "x"], "x"], "x"], "x"],
441+
"x"
442+
],
443+
"x"
444+
],
445+
"x"
446+
]
447+
`;
448+
449+
exports[`LAGRANGE PRIME NOTATION WITH ARGUMENTS f''(x) - double prime with argument 1`] = `
450+
D(x\\mapsto D(x\\mapsto f(x), x), x)
451+
D((x) |-> D((x) |-> f(x), x), x)
452+
[
453+
"D",
454+
["Function", ["D", ["Function", ["f", "x"], "x"], "x"], "x"],
455+
"x"
456+
]
457+
`;
458+
459+
exports[`LAGRANGE PRIME NOTATION WITH ARGUMENTS f'(x) - single prime with argument 1`] = `
460+
D(x\\mapsto f(x), x)
461+
D((x) |-> f(x), x)
462+
["D", ["Function", ["f", "x"], "x"], "x"]
463+
`;
464+
465+
exports[`LAGRANGE PRIME NOTATION WITH ARGUMENTS f'(x, y) - multiple arguments uses first as variable 1`] = `
466+
D(x\\mapsto f(x, y), x)
467+
D((x) |-> f(x, y), x)
468+
["D", ["Function", ["f", "x", "y"], "x"], "x"]
469+
`;
470+
471+
exports[`LAGRANGE PRIME NOTATION WITH ARGUMENTS g'(t) - different variable 1`] = `
472+
(g^\\prime,t)
473+
(Prime(g), t)
474+
["Pair", ["Prime", "g"], "t"]
475+
`;
476+
389477
exports[`MULTIPLE INTEGRALS Double integral 1`] = `
390478
\\int_{1}^{2}\\!\\int\\int_{0}^{1}\\!x^2+y^2\\, \\mathrm{d}x \\mathrm{d}y
391479
int_(1)^(2)(int_(0)^(1)(x^2 + y^2 dx dy) dx dy)
@@ -501,6 +589,74 @@ int_(1)^(2)(int_(0)^(1)(int_(3)^(4)(x^2 + y^2 + z^2 dx dy dz) dx dy dz) dx
501589
]
502590
`;
503591

592+
exports[`NEWTON DOT NOTATION Dot notation with expression 1`] = `
593+
\\mathtip{\\error{\\blacksquare}}{\\in \\text{expression}\\notin \\text{number}}+\\mathtip{\\error{\\blacksquare}}{\\in \\text{expression}\\notin \\text{number}}
594+
Error(ErrorCode("incompatible-type", "number", "expression")) + Error(ErrorCode("incompatible-type", "number", "expression"))
595+
[
596+
"Add",
597+
[
598+
"Error",
599+
["ErrorCode", "incompatible-type", "'number'", "'expression'"]
600+
],
601+
[
602+
"Error",
603+
["ErrorCode", "incompatible-type", "'number'", "'expression'"]
604+
]
605+
]
606+
`;
607+
608+
exports[`NEWTON DOT NOTATION First derivative \\dot{x} 1`] = `
609+
D(t\\mapsto x, t)
610+
D((t) |-> x, t)
611+
["D", ["Function", "x", "t"], "t"]
612+
`;
613+
614+
exports[`NEWTON DOT NOTATION Fourth derivative \\ddddot{z} 1`] = `
615+
D(t\\mapsto D(t\\mapsto D(t\\mapsto D(t\\mapsto z, t), t), t), t)
616+
D((t) |-> D((t) |-> D((t) |-> D((t) |-> z, t), t), t), t)
617+
[
618+
"D",
619+
[
620+
"Function",
621+
[
622+
"D",
623+
[
624+
"Function",
625+
[
626+
"D",
627+
["Function", ["D", ["Function", "z", "t"], "t"], "t"],
628+
"t"
629+
],
630+
"t"
631+
],
632+
"t"
633+
],
634+
"t"
635+
],
636+
"t"
637+
]
638+
`;
639+
640+
exports[`NEWTON DOT NOTATION Second derivative \\ddot{x} 1`] = `
641+
D(t\\mapsto D(t\\mapsto x, t), t)
642+
D((t) |-> D((t) |-> x, t), t)
643+
["D", ["Function", ["D", ["Function", "x", "t"], "t"], "t"], "t"]
644+
`;
645+
646+
exports[`NEWTON DOT NOTATION Third derivative \\dddot{y} 1`] = `
647+
D(t\\mapsto D(t\\mapsto D(t\\mapsto y, t), t), t)
648+
D((t) |-> D((t) |-> D((t) |-> y, t), t), t)
649+
[
650+
"D",
651+
[
652+
"Function",
653+
["D", ["Function", ["D", ["Function", "y", "t"], "t"], "t"], "t"],
654+
"t"
655+
],
656+
"t"
657+
]
658+
`;
659+
504660
exports[`REAL WORLD INTEGRALS Integral with non standard typesetting 1`] = `
505661
\\mathrm{S_{t}}=\\mathrm{S_0}+\\int_{\\mathrm{t_{i}}}^{\\mathrm{t_{e}}}\\!G-F\\, \\mathrm{d}t
506662
"S_t" === "S_0" + int_("t_i")^("t_e")(-F + "CatalanConstant" dF)

0 commit comments

Comments
 (0)