Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add derivatives, incl. high-order and mixed partial, using macros #3703

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/support_table.md
Expand Up @@ -360,6 +360,7 @@ use `\ce` instead|
|\downharpoonleft|$\downharpoonleft$||
|\downharpoonright|$\downharpoonright$||
|{drcases}|$\begin{drcases}a&\text{if }b\\c&\text{if }d\end{drcases}$|`\begin{drcases}`<br>&nbsp;&nbsp;&nbsp;`a &\text{if } b \\`<br>&nbsp;&nbsp;&nbsp;`c &\text{if } d`<br>`\end{drcases}`|
|\dv|$\dv{f}{t} \dv[2]{y}{x}$|`\dv{f}{t} \dv[2]{y}{x}`|

## E

Expand Down Expand Up @@ -791,6 +792,7 @@ use `\ce` instead|
|\O|$\text{\O}$|`\text{\O}`|
|\o|$\text{\o}$|`\text{\o}`|
|\odot|$\odot$||
|\odv|$\odv{f}{t} \odv[2]{y}{x}$|`\odv{f}{t} \odv[2]{y}{x}`|
|\OE|$\text{\OE}$|`\text{\OE}`|
|\oe|$\text{\oe}$|`\text{\oe}`|
|\officialeuro|<span style="color:firebrick;">Not supported</span>||
Expand Down Expand Up @@ -837,6 +839,7 @@ use `\ce` instead|
|\parallel|$\parallel$||
|\part|<span style="color:firebrick;">Not supported</span>|[Deprecated](https://en.wikipedia.org/wiki/Help:Displaying_a_formula#Deprecated_syntax)|
|\partial|$\partial$||
|\pdv|$\pdv{f}{t} \pdv[2,2][4]{\psi}{x, y}$|`\pdv{f}{t} \pdv[2,2][4]{\psi}{x, y}`|
|\perp|$\perp$||
|\phantom|$\Gamma^{\phantom{i}j}_{i\phantom{j}k}$|`\Gamma^{\phantom{i}j}_{i\phantom{j}k}`|
|\phase|$\phase{-78^\circ}$|`\phase{-78^\circ}`|
Expand Down
34 changes: 34 additions & 0 deletions docs/supported.md
Expand Up @@ -559,6 +559,40 @@ Direct Input: $← ↑ → ↓ ↔ ↕ ↖ ↗ ↘ ↙ ↚ ↛ ↞ ↠ ↢ ↣

Extensible arrows all can take an optional argument in the same manner<br>as `\xrightarrow[under]{over}`.

## Derivatives

The syntax closely emulates popular $LaTeX$ packages that provide derivatives,
such as `physics`, `diffcoef`, and `derivative`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather see this follow one spec. Or perhaps it already does, and this sentence just needs rewording? Fine to link to other packages, but which one should be considered "correct" for this PR? We don't want to implement a brand new derivative behavior. Stated another way, if someone was to copy KaTeX code over to LaTeX, which \usepackage should they include? (This will also help review this PR; I'd rather not read the documentation of three different packages, though I've already spent some time skimming them all.)

Copy link
Author

@Leedehai Leedehai Oct 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this follows one spec. Let me reword..


|||
|:----------|:----------|
|$\odv{}{t}$ `\odv{}{t}` |$\odv[2]{}{x}$ `\odv[2]{}{x}` |
|$\odv{f}{t}$ `\odv{f}{t}` |$\odv[2]{y}{x}$ `\odv[2]{y}{x}`|
|$\pdv{}{t}$ `\pdv{}{t}` |$\pdv[2]{}{x}$ `\pdv[2]{}{x}` |
|$\pdv{f}{t}$ `\pdv{f}{t}` |$\pdv[2]{y}{x}$ `\pdv[2]{y}{x}`|
|$\pdv[1,1][2]{}{x,y}$ `\pdv[1,1][2]{}{x,y}` |$\pdv[1,n][1+n]{}{x,y}$ `\pdv[1,n][1+n]{}{x,y}` |
|$\pdv[1,1][2]{z}{x,y}$ `\pdv[1,1][2]{z}{x,y}`|$\pdv[1,n][1+n]{z}{x,y}$ `\pdv[1,n][1+n]{z}{x,y}`|
|$\pdv[1,N-1][N]{z}{x_{\mu,\sigma},y_\xi}$ `\pdv[1,N-1][N]{z}{x_{\mu,\sigma},y_\xi}`||

Ordinary derivative `\odv[order]{f}{x}`:
* The first required brace `{f}` holds the function to take derivative of. This
brace can be left empty.
* The second required brace `{x}` holds the variable.
* `[order]` holds the derivative order. If the order is 1, you may omit this
bracket.

Partial derivative `\pdv[var_orders][total_order]{f}{vars}`:
* The first required brace `{f}` holds the function to take derivative of. This
brace can be left empty.
* The second required brace `{vars}` holds the comma-separated variables, e.g.
`{x}`, `{x, y, z_k}`.
* The first optional braket `[var_orders]` holds the list of derivative orders
corresponding to the variables, e.g. `[2]`, `[1, n, m+1]`. If there is only
one variable and its order is 1, you may omit this bracket.
* The second optional braket `[total_order]` holds the total order of the
derivative. If there is only one variable order and that is the same as the
total order, you may omit this bracket.

## Special Notation

**Bra-ket Notation**
Expand Down
12 changes: 12 additions & 0 deletions src/macros.js
Expand Up @@ -16,6 +16,7 @@ import utils from "./utils";
import {makeEm} from "./units";
import ParseError from "./ParseError";

import {assembleDerivativeExpr} from "./macros/derivatives";

//////////////////////////////////////////////////////////////////////
// macro tools
Expand Down Expand Up @@ -895,6 +896,17 @@ defineMacro("\\argmin", "\\DOTSB\\operatorname*{arg\\,min}");
defineMacro("\\argmax", "\\DOTSB\\operatorname*{arg\\,max}");
defineMacro("\\plim", "\\DOTSB\\mathop{\\operatorname{plim}}\\limits");

//////////////////////////////////////////////////////////////////////
// derivative.sty
// https://ctan.math.illinois.edu/macros/latex/contrib/derivative/derivative.pdf

defineMacro(
"\\dv", (context) => assembleDerivativeExpr("\\textnormal{d}", context));
defineMacro(
"\\odv", (context) => assembleDerivativeExpr("\\textnormal{d}", context));
defineMacro(
"\\pdv", (context) => assembleDerivativeExpr("\\partial", context));

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make more sense to move these side-effects into src/macros/derivatives.js, which would then import defineMacro from "./defineMacro".

However, we haven't decided to make a src/macros directory. Perhaps you could provide your input on #2994? What do you think of putting your code in src/functions/derivatives.js, and possibly renaming functions to commands?

//////////////////////////////////////////////////////////////////////
// braket.sty
// http://ctan.math.washington.edu/tex-archive/macros/latex/contrib/braket/braket.pdf
Expand Down
102 changes: 102 additions & 0 deletions src/macros/derivatives.js
@@ -0,0 +1,102 @@
// @flow
/**
* Macro utilities for assembling derivatives.
*/

import type {MacroContextInterface} from "../defineMacro";

function splitByTopLevelComma(s: string): string[] {
if (!s || s.length === 0) {
return [];
}

// "1+a,n" => ["1+a", "n"]
// "1+a,{n,m}" => ["1+a", "{n,m}"]
// "1+a,{n,{p,q}}" => ["1+a", "{n,{p,q}}"]
// "1+a" => ["1+a"]
// "1+a,,n" => ["1+a", "", "n"]
// "1+a,," => ["1+a", ""]
// "1+a," => ["1+a"]
// "," => [""]
// "" => []
const res = [];
let currentStr = "";
let braceDepth = 0;
const isTopLevelDelimiter = (c) => c === "," && braceDepth <= 0;
for (let i = 0; i < s.length; ++i) {
const c = s[i];
if (c === "{") {
++braceDepth;
} else if (c === "}") {
--braceDepth;
}
if (!isTopLevelDelimiter(c)) {
currentStr += c;
}
if (isTopLevelDelimiter(c) || i === s.length - 1) {
res.push(currentStr);
currentStr = "";
}
}

return res;
}

function addTokenProtector(s: string): string {
return s.length > 1 ? ` ${s} ` : s;
}

function extractArgs(context: MacroContextInterface): {
func: string,
vars: string[],
totalOrders: string[],
varOrders: string[],
} {
const optionalArgGroups = [];
let arg = context.consumeArg().tokens;
while (arg.length === 1 && arg[0].text === "[") {
let bracketContent = '';
let token = context.expandNextToken();
while (token.text !== "]" && token.text !== "EOF") {
bracketContent += token.text;
token = context.expandNextToken();
}
optionalArgGroups.push(splitByTopLevelComma(bracketContent).reverse());
arg = context.consumeArg().tokens;
}

const func = arg.reverse().map(
e => addTokenProtector(e.text)).join("");
arg = context.consumeArg().tokens;
const vars = splitByTopLevelComma(arg.reverse().map(
e => addTokenProtector(e.text)).join(""));
const varOrders =
optionalArgGroups[0] ? optionalArgGroups[0].reverse() : ["1"];
const totalOrders =
optionalArgGroups[1] ? optionalArgGroups[1].reverse() : varOrders;

return {func, vars, totalOrders, varOrders};
}

const PLACEHOLDER = "\\square";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this placeholder behavior implemented in any of the existing LaTeX packages? I didn't see it anywhere, but I may have missed it in one of them.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's not implemented in any existing LaTeX package. That said, I think it's a useful improvement, so that users won't be puzzled by blankness.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The square is a meaningful operator in many contexts and that might confuse more than it helps. I suppose if someone leaves the [options] blank they should expect that location in fact to be blank.


export function assembleDerivativeExpr(
d: string, context: MacroContextInterface): string {
const {func, vars, totalOrders, varOrders} = extractArgs(context);
const numVars = vars.length;
const numVarOrders = varOrders.length;
if (numVars > numVarOrders) {
varOrders.push(...Array(numVars - numVarOrders).fill(PLACEHOLDER));
} else if (numVarOrders > numVars) {
vars.push(...Array(numVarOrders - numVars).fill(PLACEHOLDER));
}

const makeIndex = (s) => s === "1" ? "" : `^{${s}}`;
const numer = `{${d}}${makeIndex(totalOrders[0] ?? PLACEHOLDER)}{${func}}`;
const denom = vars.map((variable, i) => {
const order = varOrders[i];
return `{${d}}{${variable}}${makeIndex(order)}`;
}).join("");

return `\\frac{${numer}}{${denom}}`;
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/screenshotter/images/Derivatives-firefox.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions test/screenshotter/ss_data.yaml
Expand Up @@ -125,6 +125,17 @@ DelimiterSizing: |
a & b & c\\
a & b & c\\
\end{pmatrix}\; \Braket{ ϕ | \frac{∂^2}{∂ t^2} | ψ } \\
Derivatives: |
\dv{}{x},\odv{y}{x},\odv[2]{}{x},\odv[n+1]{y}{x} \\
\pdv{}{x},\pdv{y}{x},\pdv[2]{}{x},\pdv[n+1]{y}{x} \\
\pdv[1,1][2]{z}{x,y},\pdv[2,2][4]{z}{x,y}, \pdv[1,a][1+a]{z}{x,y} \\
\pdv[2][2]{\tiny\begin{pmatrix}\phi & 0 \\0 & 1\end{pmatrix}}{x},
\pdv[1+2^n,{(1,2)},k,\Lambda][N_k+1]{\langle\psi_{1,ûnïcoδé}|\phi_\alpha\rangle} {x,y,z_{\mu,\sigma},\tau} \\
DerivativesFallbacks: |
\square = \text{placeholder} \\
\odv[]{y}{x}, \pdv[]{y}{x} \\
\pdv[2][4]{z}{x,y}, \pdv[2,2][]{z}{x,y}, \pdv[2,2][4]{z}{x} \\
\pdv[2][6]{\psi}{x,y,z}, \pdv[2,2][]{\psi}{x,y,z}, \pdv[2,2,2][6]{\psi}{x} \\
DisplayMode:
tex: \sum_{i=0}^\infty \frac{1}{i}
pre: pre
Expand Down