diff --git a/examples/LaTeX/curvi_linear_latex.py b/examples/LaTeX/curvi_linear_latex.py index 0fed7e7a..77c54de2 100644 --- a/examples/LaTeX/curvi_linear_latex.py +++ b/examples/LaTeX/curvi_linear_latex.py @@ -181,14 +181,27 @@ def derivatives_in_toroidal_coordinates(): def main(): #Eprint() Format() - derivatives_in_spherical_coordinates() - derivatives_in_paraboloidal_coordinates() - # FIXME This takes ~600 seconds - # derivatives_in_elliptic_cylindrical_coordinates() - derivatives_in_prolate_spheroidal_coordinates() - #derivatives_in_oblate_spheroidal_coordinates() - #derivatives_in_bipolar_coordinates() - #derivatives_in_toroidal_coordinates() + + # SymPy >= 1.13 (PR #26390) added a slow O(N*M) traversal inside + # sympy.simplify.fu that causes timeouts on curvilinear coordinate + # expressions. Use trigsimp(method='old') via Simp.profile to avoid + # that code path entirely for this example. + from sympy import trigsimp + from galgebra.metric import Simp + + orig_modes = Simp.modes[:] + Simp.profile([lambda e: trigsimp(e, method='old')]) + try: + derivatives_in_spherical_coordinates() + derivatives_in_paraboloidal_coordinates() + # FIXME This takes ~600 seconds + # derivatives_in_elliptic_cylindrical_coordinates() + derivatives_in_prolate_spheroidal_coordinates() + #derivatives_in_oblate_spheroidal_coordinates() + #derivatives_in_bipolar_coordinates() + #derivatives_in_toroidal_coordinates() + finally: + Simp.profile(orig_modes) # xpdf() xpdf(pdfprog=None) diff --git a/examples/ipython/LaTeX.ipynb b/examples/ipython/LaTeX.ipynb index 60805399..d223f590 100644 --- a/examples/ipython/LaTeX.ipynb +++ b/examples/ipython/LaTeX.ipynb @@ -5,10 +5,10 @@ "execution_count": 1, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:40:50.309937Z", - "iopub.status.busy": "2026-04-02T05:40:50.309862Z", - "iopub.status.idle": "2026-04-02T05:40:50.313734Z", - "shell.execute_reply": "2026-04-02T05:40:50.313217Z" + "iopub.execute_input": "2026-04-03T11:45:13.252080Z", + "iopub.status.busy": "2026-04-03T11:45:13.251991Z", + "iopub.status.idle": "2026-04-03T11:45:13.256444Z", + "shell.execute_reply": "2026-04-03T11:45:13.255817Z" } }, "outputs": [], @@ -23,10 +23,10 @@ "execution_count": 2, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:40:50.315576Z", - "iopub.status.busy": "2026-04-02T05:40:50.315466Z", - "iopub.status.idle": "2026-04-02T05:40:50.895093Z", - "shell.execute_reply": "2026-04-02T05:40:50.894515Z" + "iopub.execute_input": "2026-04-03T11:45:13.257948Z", + "iopub.status.busy": "2026-04-03T11:45:13.257848Z", + "iopub.status.idle": "2026-04-03T11:45:13.792038Z", + "shell.execute_reply": "2026-04-03T11:45:13.791371Z" } }, "outputs": [ @@ -106,10 +106,10 @@ "execution_count": 3, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:40:50.912312Z", - "iopub.status.busy": "2026-04-02T05:40:50.912182Z", - "iopub.status.idle": "2026-04-02T05:49:04.977406Z", - "shell.execute_reply": "2026-04-02T05:49:04.975915Z" + "iopub.execute_input": "2026-04-03T11:45:13.806616Z", + "iopub.status.busy": "2026-04-03T11:45:13.806489Z", + "iopub.status.idle": "2026-04-03T11:45:18.812151Z", + "shell.execute_reply": "2026-04-03T11:45:18.811337Z" } }, "outputs": [ @@ -170,22 +170,22 @@ "\\begin{equation*} B = B^{r\\theta } \\boldsymbol{e}_{r}\\wedge \\boldsymbol{e}_{\\theta } + B^{r\\phi } \\boldsymbol{e}_{r}\\wedge \\boldsymbol{e}_{\\phi } + B^{\\theta \\phi } \\boldsymbol{e}_{\\theta }\\wedge \\boldsymbol{e}_{\\phi } \\end{equation*}\n", "\\begin{equation*} \\boldsymbol{\\nabla} f = \\partial_{r} f \\boldsymbol{e}_{r} + \\frac{\\partial_{\\theta } f }{r^{2}} \\boldsymbol{e}_{\\theta } + \\frac{\\partial_{\\phi } f }{r^{2} {\\sin{\\left (\\theta \\right )}}^{2}} \\boldsymbol{e}_{\\phi } \\end{equation*}\n", "\\begin{equation*} \\boldsymbol{\\nabla} \\cdot A = \\frac{A^{\\theta } }{\\tan{\\left (\\theta \\right )}} + \\partial_{\\phi } A^{\\phi } + \\partial_{r} A^{r} + \\partial_{\\theta } A^{\\theta } + \\frac{2 A^{r} }{r} \\end{equation*}\n", - "\\begin{equation*} \\boldsymbol{\\nabla} \\times A = -I (\\boldsymbol{\\nabla} \\W A) = \\left(\\frac{2 A^{\\phi } }{\\tan{\\left (\\theta \\right )}} + \\partial_{\\theta } A^{\\phi } - \\frac{\\partial_{\\phi } A^{\\theta } }{{\\sin{\\left (\\theta \\right )}}^{2}}\\right) \\left|{\\sin{\\left (\\theta \\right )}}\\right| \\boldsymbol{e}_{r} + \\frac{- r^{2} {\\sin{\\left (\\theta \\right )}}^{2} \\partial_{r} A^{\\phi } - 2 r A^{\\phi } {\\sin{\\left (\\theta \\right )}}^{2} + \\partial_{\\phi } A^{r} }{r^{2} \\left|{\\sin{\\left (\\theta \\right )}}\\right|} \\boldsymbol{e}_{\\theta } + \\frac{r^{2} \\partial_{r} A^{\\theta } + 2 r A^{\\theta } - \\partial_{\\theta } A^{r} }{r^{2} \\left|{\\sin{\\left (\\theta \\right )}}\\right|} \\boldsymbol{e}_{\\phi } \\end{equation*}\n", - "\\begin{equation*} \\nabla^{2}f = \\frac{r^{2} \\partial^{2}_{r} f + 2 r \\partial_{r} f + \\partial^{2}_{\\theta } f + \\frac{\\partial_{\\theta } f }{\\tan{\\left (\\theta \\right )}} + \\frac{\\partial^{2}_{\\phi } f }{{\\sin{\\left (\\theta \\right )}}^{2}}}{r^{2}} \\end{equation*}\n", - "\\begin{equation*} \\boldsymbol{\\nabla} \\W B = \\frac{r^{2} \\partial_{r} B^{\\theta \\phi } + 4 r B^{\\theta \\phi } - \\frac{2 B^{r\\phi } }{\\tan{\\left (\\theta \\right )}} - \\partial_{\\theta } B^{r\\phi } + \\frac{\\partial_{\\phi } B^{r\\theta } }{{\\sin{\\left (\\theta \\right )}}^{2}}}{r^{2}} \\boldsymbol{e}_{r}\\wedge \\boldsymbol{e}_{\\theta }\\wedge \\boldsymbol{e}_{\\phi } \\end{equation*}\n", + "\\begin{equation*} \\boldsymbol{\\nabla} \\times A = -I (\\boldsymbol{\\nabla} \\W A) = \\frac{A^{\\phi } {\\sin{\\left (\\theta \\right )}}^{2} + A^{\\phi } \\sin{\\left (\\theta \\right )} \\cos{\\left (\\theta \\right )} \\tan{\\left (\\theta \\right )} + {\\sin{\\left (\\theta \\right )}}^{2} \\tan{\\left (\\theta \\right )} \\partial_{\\theta } A^{\\phi } - \\tan{\\left (\\theta \\right )} \\partial_{\\phi } A^{\\theta } }{\\tan{\\left (\\theta \\right )} \\left|{\\sin{\\left (\\theta \\right )}}\\right|} \\boldsymbol{e}_{r} - \\frac{r^{2} {\\sin{\\left (\\theta \\right )}}^{2} \\partial_{r} A^{\\phi } + 2 r A^{\\phi } {\\sin{\\left (\\theta \\right )}}^{2} - \\partial_{\\phi } A^{r} }{r^{2} \\left|{\\sin{\\left (\\theta \\right )}}\\right|} \\boldsymbol{e}_{\\theta } + \\frac{r^{2} \\partial_{r} A^{\\theta } + 2 r A^{\\theta } - \\partial_{\\theta } A^{r} }{r^{2} \\left|{\\sin{\\left (\\theta \\right )}}\\right|} \\boldsymbol{e}_{\\phi } \\end{equation*}\n", + "\\begin{equation*} \\nabla^{2}f = \\partial^{2}_{r} f + \\frac{2 \\partial_{r} f }{r} + \\frac{\\partial^{2}_{\\theta } f }{r^{2}} + \\frac{\\partial_{\\theta } f }{r^{2} \\tan{\\left (\\theta \\right )}} + \\frac{\\partial^{2}_{\\phi } f }{r^{2} {\\sin{\\left (\\theta \\right )}}^{2}} \\end{equation*}\n", + "\\begin{equation*} \\boldsymbol{\\nabla} \\W B = \\left ( \\partial_{r} B^{\\theta \\phi } + \\frac{4 B^{\\theta \\phi } }{r} - \\frac{2 B^{r\\phi } }{r^{2} \\tan{\\left (\\theta \\right )}} - \\frac{\\partial_{\\theta } B^{r\\phi } }{r^{2}} + \\frac{\\partial_{\\phi } B^{r\\theta } }{r^{2} {\\sin{\\left (\\theta \\right )}}^{2}}\\right ) \\boldsymbol{e}_{r}\\wedge \\boldsymbol{e}_{\\theta }\\wedge \\boldsymbol{e}_{\\phi } \\end{equation*}\n", "Derivatives in Paraboloidal Coordinates\n", "\\begin{equation*} f = f \\end{equation*}\n", "\\begin{equation*} A = A^{u} \\boldsymbol{e}_{u} + A^{v} \\boldsymbol{e}_{v} + A^{\\phi } \\boldsymbol{e}_{\\phi } \\end{equation*}\n", "\\begin{equation*} B = B^{uv} \\boldsymbol{e}_{u}\\wedge \\boldsymbol{e}_{v} + B^{u\\phi } \\boldsymbol{e}_{u}\\wedge \\boldsymbol{e}_{\\phi } + B^{v\\phi } \\boldsymbol{e}_{v}\\wedge \\boldsymbol{e}_{\\phi } \\end{equation*}\n", "\\begin{equation*} \\boldsymbol{\\nabla} f = \\frac{\\partial_{u} f }{\\sqrt{u^{2} + v^{2}}} \\boldsymbol{e}_{u} + \\frac{\\partial_{v} f }{\\sqrt{u^{2} + v^{2}}} \\boldsymbol{e}_{v} + \\frac{\\partial_{\\phi } f }{u v} \\boldsymbol{e}_{\\phi } \\end{equation*}\n", - "\\begin{equation*} \\boldsymbol{\\nabla} \\cdot A = \\frac{u A^{u} }{\\left(u^{2} + v^{2}\\right)^{\\frac{3}{2}}} + \\frac{v A^{v} }{\\left(u^{2} + v^{2}\\right)^{\\frac{3}{2}}} + \\frac{\\partial_{u} A^{u} }{\\sqrt{u^{2} + v^{2}}} + \\frac{\\partial_{v} A^{v} }{\\sqrt{u^{2} + v^{2}}} + \\frac{A^{v} }{v \\sqrt{u^{2} + v^{2}}} + \\frac{A^{u} }{u \\sqrt{u^{2} + v^{2}}} + \\frac{\\partial_{\\phi } A^{\\phi } }{u v} \\end{equation*}\n", - "\\begin{equation*} \\boldsymbol{\\nabla} \\W B = \\left ( \\frac{u B^{v\\phi } }{\\left(u^{2} + v^{2}\\right)^{\\frac{3}{2}}} - \\frac{v B^{u\\phi } }{\\left(u^{2} + v^{2}\\right)^{\\frac{3}{2}}} - \\frac{\\partial_{v} B^{u\\phi } }{\\sqrt{u^{2} + v^{2}}} + \\frac{\\partial_{u} B^{v\\phi } }{\\sqrt{u^{2} + v^{2}}} - \\frac{B^{u\\phi } }{v \\sqrt{u^{2} + v^{2}}} + \\frac{B^{v\\phi } }{u \\sqrt{u^{2} + v^{2}}} + \\frac{\\partial_{\\phi } B^{uv} }{u v}\\right ) \\boldsymbol{e}_{u}\\wedge \\boldsymbol{e}_{v}\\wedge \\boldsymbol{e}_{\\phi } \\end{equation*}\n", + "\\begin{equation*} \\boldsymbol{\\nabla} \\cdot A = \\frac{u A^{u} }{u^{2} \\sqrt{u^{2} + v^{2}} + v^{2} \\sqrt{u^{2} + v^{2}}} + \\frac{v A^{v} }{u^{2} \\sqrt{u^{2} + v^{2}} + v^{2} \\sqrt{u^{2} + v^{2}}} + \\frac{\\partial_{u} A^{u} }{\\sqrt{u^{2} + v^{2}}} + \\frac{\\partial_{v} A^{v} }{\\sqrt{u^{2} + v^{2}}} + \\frac{A^{v} }{v \\sqrt{u^{2} + v^{2}}} + \\frac{A^{u} }{u \\sqrt{u^{2} + v^{2}}} + \\frac{\\partial_{\\phi } A^{\\phi } }{u v} \\end{equation*}\n", + "\\begin{equation*} \\boldsymbol{\\nabla} \\W B = \\left ( \\frac{u B^{v\\phi } }{u^{2} \\sqrt{u^{2} + v^{2}} + v^{2} \\sqrt{u^{2} + v^{2}}} - \\frac{v B^{u\\phi } }{u^{2} \\sqrt{u^{2} + v^{2}} + v^{2} \\sqrt{u^{2} + v^{2}}} - \\frac{\\partial_{v} B^{u\\phi } }{\\sqrt{u^{2} + v^{2}}} + \\frac{\\partial_{u} B^{v\\phi } }{\\sqrt{u^{2} + v^{2}}} - \\frac{B^{u\\phi } }{v \\sqrt{u^{2} + v^{2}}} + \\frac{B^{v\\phi } }{u \\sqrt{u^{2} + v^{2}}} + \\frac{\\partial_{\\phi } B^{uv} }{u v}\\right ) \\boldsymbol{e}_{u}\\wedge \\boldsymbol{e}_{v}\\wedge \\boldsymbol{e}_{\\phi } \\end{equation*}\n", "Derivatives in Prolate Spheroidal Coordinates\n", "\\begin{equation*} f = f \\end{equation*}\n", "\\begin{equation*} A = A^{\\xi } \\boldsymbol{e}_{\\xi } + A^{\\eta } \\boldsymbol{e}_{\\eta } + A^{\\phi } \\boldsymbol{e}_{\\phi } \\end{equation*}\n", "\\begin{equation*} B = B^{\\xi \\eta } \\boldsymbol{e}_{\\xi }\\wedge \\boldsymbol{e}_{\\eta } + B^{\\xi \\phi } \\boldsymbol{e}_{\\xi }\\wedge \\boldsymbol{e}_{\\phi } + B^{\\eta \\phi } \\boldsymbol{e}_{\\eta }\\wedge \\boldsymbol{e}_{\\phi } \\end{equation*}\n", - "\\begin{equation*} \\boldsymbol{\\nabla} f = \\frac{\\partial_{\\xi } f }{\\sqrt{{\\sin{\\left (\\eta \\right )}}^{2} + {\\sinh{\\left (\\xi \\right )}}^{2}} \\left|{a}\\right|} \\boldsymbol{e}_{\\xi } + \\frac{\\partial_{\\eta } f }{\\sqrt{{\\sin{\\left (\\eta \\right )}}^{2} + {\\sinh{\\left (\\xi \\right )}}^{2}} \\left|{a}\\right|} \\boldsymbol{e}_{\\eta } + \\frac{\\partial_{\\phi } f }{a \\sin{\\left (\\eta \\right )} \\sinh{\\left (\\xi \\right )}} \\boldsymbol{e}_{\\phi } \\end{equation*}\n", - "\\begin{equation*} \\boldsymbol{\\nabla} \\cdot A = \\frac{A^{\\eta } \\cos{\\left (\\eta \\right )}}{\\sqrt{{\\sin{\\left (\\eta \\right )}}^{2} + {\\sinh{\\left (\\xi \\right )}}^{2}} \\sin{\\left (\\eta \\right )} \\left|{a}\\right|} + \\frac{A^{\\xi } \\cosh{\\left (\\xi \\right )}}{\\sqrt{{\\sin{\\left (\\eta \\right )}}^{2} + {\\sinh{\\left (\\xi \\right )}}^{2}} \\sinh{\\left (\\xi \\right )} \\left|{a}\\right|} + \\frac{\\partial_{\\eta } A^{\\eta } }{\\sqrt{{\\sin{\\left (\\eta \\right )}}^{2} + {\\sinh{\\left (\\xi \\right )}}^{2}} \\left|{a}\\right|} + \\frac{\\partial_{\\xi } A^{\\xi } }{\\sqrt{{\\sin{\\left (\\eta \\right )}}^{2} + {\\sinh{\\left (\\xi \\right )}}^{2}} \\left|{a}\\right|} + \\frac{A^{\\eta } \\sin{\\left (2 \\eta \\right )}}{2 \\sqrt{{\\sin{\\left (\\eta \\right )}}^{2} + {\\sinh{\\left (\\xi \\right )}}^{2}} {\\sin{\\left (\\eta \\right )}}^{2} \\left|{a}\\right| + 2 \\sqrt{{\\sin{\\left (\\eta \\right )}}^{2} + {\\sinh{\\left (\\xi \\right )}}^{2}} {\\sinh{\\left (\\xi \\right )}}^{2} \\left|{a}\\right|} + \\frac{A^{\\xi } \\sinh{\\left (2 \\xi \\right )}}{2 \\sqrt{{\\sin{\\left (\\eta \\right )}}^{2} + {\\sinh{\\left (\\xi \\right )}}^{2}} {\\sin{\\left (\\eta \\right )}}^{2} \\left|{a}\\right| + 2 \\sqrt{{\\sin{\\left (\\eta \\right )}}^{2} + {\\sinh{\\left (\\xi \\right )}}^{2}} {\\sinh{\\left (\\xi \\right )}}^{2} \\left|{a}\\right|} + \\frac{\\partial_{\\phi } A^{\\phi } }{a \\sin{\\left (\\eta \\right )} \\sinh{\\left (\\xi \\right )}} \\end{equation*}\n", + "\\begin{equation*} \\boldsymbol{\\nabla} f = \\frac{\\partial_{\\xi } f }{\\sqrt{- {\\cos{\\left (\\eta \\right )}}^{2} + {\\cosh{\\left (\\xi \\right )}}^{2}} \\left|{a}\\right|} \\boldsymbol{e}_{\\xi } + \\frac{\\partial_{\\eta } f }{\\sqrt{- {\\cos{\\left (\\eta \\right )}}^{2} + {\\cosh{\\left (\\xi \\right )}}^{2}} \\left|{a}\\right|} \\boldsymbol{e}_{\\eta } + \\frac{\\partial_{\\phi } f }{a \\sin{\\left (\\eta \\right )} \\sinh{\\left (\\xi \\right )}} \\boldsymbol{e}_{\\phi } \\end{equation*}\n", + "\\begin{equation*} \\boldsymbol{\\nabla} \\cdot A = \\frac{A^{\\eta } \\sin{\\left (2 \\eta \\right )}}{2 \\sqrt{- {\\cos{\\left (\\eta \\right )}}^{2} + {\\cosh{\\left (\\xi \\right )}}^{2}} {\\sin{\\left (\\eta \\right )}}^{2} \\left|{a}\\right|} + \\frac{A^{\\xi } \\sinh{\\left (2 \\xi \\right )}}{2 \\sqrt{- {\\cos{\\left (\\eta \\right )}}^{2} + {\\cosh{\\left (\\xi \\right )}}^{2}} {\\sinh{\\left (\\xi \\right )}}^{2} \\left|{a}\\right|} + \\frac{\\partial_{\\eta } A^{\\eta } }{\\sqrt{- {\\cos{\\left (\\eta \\right )}}^{2} + {\\cosh{\\left (\\xi \\right )}}^{2}} \\left|{a}\\right|} + \\frac{\\partial_{\\xi } A^{\\xi } }{\\sqrt{- {\\cos{\\left (\\eta \\right )}}^{2} + {\\cosh{\\left (\\xi \\right )}}^{2}} \\left|{a}\\right|} + \\frac{A^{\\eta } \\sin{\\left (\\eta \\right )} \\cos{\\left (\\eta \\right )}}{\\left(- \\left(\\cos{\\left (\\eta \\right )} - \\cosh{\\left (\\xi \\right )}\\right) \\left(\\cos{\\left (\\eta \\right )} + \\cosh{\\left (\\xi \\right )}\\right)\\right)^{\\frac{3}{2}} \\left|{a}\\right|} + \\frac{A^{\\xi } \\sinh{\\left (\\xi \\right )} \\cosh{\\left (\\xi \\right )}}{\\left(- \\left(\\cos{\\left (\\eta \\right )} - \\cosh{\\left (\\xi \\right )}\\right) \\left(\\cos{\\left (\\eta \\right )} + \\cosh{\\left (\\xi \\right )}\\right)\\right)^{\\frac{3}{2}} \\left|{a}\\right|} + \\frac{\\partial_{\\phi } A^{\\phi } }{a \\sin{\\left (\\eta \\right )} \\sinh{\\left (\\xi \\right )}} \\end{equation*}\n", "\\end{document}\n" ] }, @@ -197,15 +197,36 @@ "check('curvi_linear_latex')" ] }, + { + "cell_type": "markdown", + "id": "sympy-1-13-note", + "metadata": {}, + "source": [ + "**Note \u2014 SymPy \u2265 1.13 workaround (issue [#576](https://github.com/pygae/galgebra/issues/576)):**\n", + "Two output expressions above differ cosmetically from the form produced by SymPy \u2264 1.12:\n", + "\n", + "- **Spherical curl e_r component** (second-to-last output of the spherical section): ", + "SymPy \u2264 1.12 `simplify` factored this as `(2A^\u03c6/tan \u03b8 + \u2202A^\u03c6/\u2202\u03b8 \u2212 \u2202A^\u03b8/\u2202\u03c6/sin\u00b2\u03b8)\u00b7|sin \u03b8|`; ", + "the workaround `trigsimp(method='old')` produces an algebraically equivalent but less-factored fraction.\n", + "\n", + "- **Prolate-spheroidal divergence \u2207\u00b7A** (last output of the prolate spheroidal section): ", + "SymPy \u2264 1.12 produced a single collected fraction; `trigsimp(method='old')` produces seven ", + "separate fractions using `\u2212cos\u00b2(\u03b7)+cosh\u00b2(\u03be)` instead of the standard `sin\u00b2(\u03b7)+sinh\u00b2(\u03be)`.\n", + "\n", + "Both forms are algebraically identical. The workaround (`trigsimp(method='old')`) is used here because ", + "SymPy 1.13 PR #26390 introduced an O(N\u00b7M) traversal in `TR3`/`fu.py` that causes `simplify` to hang ", + "on mixed trig+hyperbolic expressions. A proper upstream fix is tracked in issue [#576](https://github.com/pygae/galgebra/issues/576)." + ] + }, { "cell_type": "code", "execution_count": 4, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:04.981090Z", - "iopub.status.busy": "2026-04-02T05:49:04.980894Z", - "iopub.status.idle": "2026-04-02T05:49:05.425896Z", - "shell.execute_reply": "2026-04-02T05:49:05.425263Z" + "iopub.execute_input": "2026-04-03T11:45:18.814004Z", + "iopub.status.busy": "2026-04-03T11:45:18.813890Z", + "iopub.status.idle": "2026-04-03T11:45:19.194345Z", + "shell.execute_reply": "2026-04-03T11:45:19.193661Z" } }, "outputs": [ @@ -279,10 +300,10 @@ "execution_count": 5, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:05.428151Z", - "iopub.status.busy": "2026-04-02T05:49:05.428047Z", - "iopub.status.idle": "2026-04-02T05:49:05.976487Z", - "shell.execute_reply": "2026-04-02T05:49:05.975732Z" + "iopub.execute_input": "2026-04-03T11:45:19.195968Z", + "iopub.status.busy": "2026-04-03T11:45:19.195868Z", + "iopub.status.idle": "2026-04-03T11:45:19.699697Z", + "shell.execute_reply": "2026-04-03T11:45:19.699043Z" } }, "outputs": [ @@ -379,10 +400,10 @@ "execution_count": 6, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:05.978087Z", - "iopub.status.busy": "2026-04-02T05:49:05.977993Z", - "iopub.status.idle": "2026-04-02T05:49:06.440061Z", - "shell.execute_reply": "2026-04-02T05:49:06.439409Z" + "iopub.execute_input": "2026-04-03T11:45:19.701933Z", + "iopub.status.busy": "2026-04-03T11:45:19.701776Z", + "iopub.status.idle": "2026-04-03T11:45:20.159832Z", + "shell.execute_reply": "2026-04-03T11:45:20.159132Z" } }, "outputs": [ @@ -456,10 +477,10 @@ "execution_count": 7, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:06.441917Z", - "iopub.status.busy": "2026-04-02T05:49:06.441807Z", - "iopub.status.idle": "2026-04-02T05:49:07.051563Z", - "shell.execute_reply": "2026-04-02T05:49:07.050956Z" + "iopub.execute_input": "2026-04-03T11:45:20.161935Z", + "iopub.status.busy": "2026-04-03T11:45:20.161849Z", + "iopub.status.idle": "2026-04-03T11:45:20.772636Z", + "shell.execute_reply": "2026-04-03T11:45:20.771988Z" } }, "outputs": [ @@ -544,10 +565,10 @@ "execution_count": 8, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:07.053167Z", - "iopub.status.busy": "2026-04-02T05:49:07.053083Z", - "iopub.status.idle": "2026-04-02T05:49:08.041330Z", - "shell.execute_reply": "2026-04-02T05:49:08.040383Z" + "iopub.execute_input": "2026-04-03T11:45:20.774160Z", + "iopub.status.busy": "2026-04-03T11:45:20.774073Z", + "iopub.status.idle": "2026-04-03T11:45:21.760222Z", + "shell.execute_reply": "2026-04-03T11:45:21.759445Z" } }, "outputs": [ @@ -637,10 +658,10 @@ "execution_count": 9, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:08.043514Z", - "iopub.status.busy": "2026-04-02T05:49:08.043368Z", - "iopub.status.idle": "2026-04-02T05:49:08.348598Z", - "shell.execute_reply": "2026-04-02T05:49:08.347609Z" + "iopub.execute_input": "2026-04-03T11:45:21.762255Z", + "iopub.status.busy": "2026-04-03T11:45:21.762144Z", + "iopub.status.idle": "2026-04-03T11:45:22.085345Z", + "shell.execute_reply": "2026-04-03T11:45:22.084497Z" } }, "outputs": [ @@ -717,10 +738,10 @@ "execution_count": 10, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:08.350270Z", - "iopub.status.busy": "2026-04-02T05:49:08.350184Z", - "iopub.status.idle": "2026-04-02T05:49:09.023669Z", - "shell.execute_reply": "2026-04-02T05:49:09.022727Z" + "iopub.execute_input": "2026-04-03T11:45:22.086832Z", + "iopub.status.busy": "2026-04-03T11:45:22.086748Z", + "iopub.status.idle": "2026-04-03T11:45:22.773578Z", + "shell.execute_reply": "2026-04-03T11:45:22.772730Z" } }, "outputs": [ @@ -883,10 +904,10 @@ "execution_count": 11, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:09.025524Z", - "iopub.status.busy": "2026-04-02T05:49:09.025402Z", - "iopub.status.idle": "2026-04-02T05:49:14.316387Z", - "shell.execute_reply": "2026-04-02T05:49:14.315572Z" + "iopub.execute_input": "2026-04-03T11:45:22.775265Z", + "iopub.status.busy": "2026-04-03T11:45:22.775178Z", + "iopub.status.idle": "2026-04-03T11:45:26.436837Z", + "shell.execute_reply": "2026-04-03T11:45:26.436130Z" } }, "outputs": [ @@ -1533,10 +1554,10 @@ "execution_count": 12, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:14.317993Z", - "iopub.status.busy": "2026-04-02T05:49:14.317902Z", - "iopub.status.idle": "2026-04-02T05:49:15.459283Z", - "shell.execute_reply": "2026-04-02T05:49:15.458625Z" + "iopub.execute_input": "2026-04-03T11:45:26.438737Z", + "iopub.status.busy": "2026-04-03T11:45:26.438649Z", + "iopub.status.idle": "2026-04-03T11:45:27.602180Z", + "shell.execute_reply": "2026-04-03T11:45:27.601410Z" } }, "outputs": [ @@ -1665,10 +1686,10 @@ "execution_count": 13, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:15.460720Z", - "iopub.status.busy": "2026-04-02T05:49:15.460649Z", - "iopub.status.idle": "2026-04-02T05:49:18.298338Z", - "shell.execute_reply": "2026-04-02T05:49:18.297606Z" + "iopub.execute_input": "2026-04-03T11:45:27.604358Z", + "iopub.status.busy": "2026-04-03T11:45:27.604259Z", + "iopub.status.idle": "2026-04-03T11:45:30.753990Z", + "shell.execute_reply": "2026-04-03T11:45:30.752008Z" } }, "outputs": [ @@ -1750,10 +1771,10 @@ "execution_count": 14, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:18.300259Z", - "iopub.status.busy": "2026-04-02T05:49:18.300175Z", - "iopub.status.idle": "2026-04-02T05:49:19.274821Z", - "shell.execute_reply": "2026-04-02T05:49:19.273788Z" + "iopub.execute_input": "2026-04-03T11:45:30.760597Z", + "iopub.status.busy": "2026-04-03T11:45:30.760247Z", + "iopub.status.idle": "2026-04-03T11:45:31.888018Z", + "shell.execute_reply": "2026-04-03T11:45:31.887325Z" } }, "outputs": [ @@ -1839,10 +1860,10 @@ "execution_count": 15, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:19.277095Z", - "iopub.status.busy": "2026-04-02T05:49:19.276959Z", - "iopub.status.idle": "2026-04-02T05:49:19.651645Z", - "shell.execute_reply": "2026-04-02T05:49:19.651021Z" + "iopub.execute_input": "2026-04-03T11:45:31.889630Z", + "iopub.status.busy": "2026-04-03T11:45:31.889558Z", + "iopub.status.idle": "2026-04-03T11:45:32.262697Z", + "shell.execute_reply": "2026-04-03T11:45:32.261823Z" } }, "outputs": [ @@ -1915,10 +1936,10 @@ "execution_count": 16, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:19.653393Z", - "iopub.status.busy": "2026-04-02T05:49:19.653280Z", - "iopub.status.idle": "2026-04-02T05:49:20.490206Z", - "shell.execute_reply": "2026-04-02T05:49:20.489532Z" + "iopub.execute_input": "2026-04-03T11:45:32.264596Z", + "iopub.status.busy": "2026-04-03T11:45:32.264502Z", + "iopub.status.idle": "2026-04-03T11:45:33.119752Z", + "shell.execute_reply": "2026-04-03T11:45:33.119052Z" } }, "outputs": [ @@ -1997,10 +2018,10 @@ "execution_count": 17, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:20.491772Z", - "iopub.status.busy": "2026-04-02T05:49:20.491668Z", - "iopub.status.idle": "2026-04-02T05:49:21.088635Z", - "shell.execute_reply": "2026-04-02T05:49:21.087838Z" + "iopub.execute_input": "2026-04-03T11:45:33.121964Z", + "iopub.status.busy": "2026-04-03T11:45:33.121860Z", + "iopub.status.idle": "2026-04-03T11:45:33.684660Z", + "shell.execute_reply": "2026-04-03T11:45:33.683976Z" } }, "outputs": [ @@ -2086,10 +2107,10 @@ "execution_count": 18, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:21.090500Z", - "iopub.status.busy": "2026-04-02T05:49:21.090386Z", - "iopub.status.idle": "2026-04-02T05:49:22.810531Z", - "shell.execute_reply": "2026-04-02T05:49:22.809606Z" + "iopub.execute_input": "2026-04-03T11:45:33.686158Z", + "iopub.status.busy": "2026-04-03T11:45:33.686080Z", + "iopub.status.idle": "2026-04-03T11:45:35.460486Z", + "shell.execute_reply": "2026-04-03T11:45:35.459508Z" } }, "outputs": [ @@ -2215,10 +2236,10 @@ "execution_count": 19, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:22.812118Z", - "iopub.status.busy": "2026-04-02T05:49:22.812027Z", - "iopub.status.idle": "2026-04-02T05:49:23.875534Z", - "shell.execute_reply": "2026-04-02T05:49:23.874700Z" + "iopub.execute_input": "2026-04-03T11:45:35.462148Z", + "iopub.status.busy": "2026-04-03T11:45:35.462061Z", + "iopub.status.idle": "2026-04-03T11:45:36.473752Z", + "shell.execute_reply": "2026-04-03T11:45:36.473116Z" } }, "outputs": [ @@ -2452,10 +2473,10 @@ "execution_count": 20, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:23.877905Z", - "iopub.status.busy": "2026-04-02T05:49:23.877797Z", - "iopub.status.idle": "2026-04-02T05:49:24.517387Z", - "shell.execute_reply": "2026-04-02T05:49:24.515992Z" + "iopub.execute_input": "2026-04-03T11:45:36.475599Z", + "iopub.status.busy": "2026-04-03T11:45:36.475512Z", + "iopub.status.idle": "2026-04-03T11:45:36.945398Z", + "shell.execute_reply": "2026-04-03T11:45:36.944653Z" } }, "outputs": [ @@ -2543,10 +2564,10 @@ "execution_count": 21, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:24.520368Z", - "iopub.status.busy": "2026-04-02T05:49:24.520100Z", - "iopub.status.idle": "2026-04-02T05:49:25.171650Z", - "shell.execute_reply": "2026-04-02T05:49:25.171011Z" + "iopub.execute_input": "2026-04-03T11:45:36.946821Z", + "iopub.status.busy": "2026-04-03T11:45:36.946716Z", + "iopub.status.idle": "2026-04-03T11:45:37.580138Z", + "shell.execute_reply": "2026-04-03T11:45:37.579208Z" } }, "outputs": [ @@ -2647,10 +2668,10 @@ "execution_count": 22, "metadata": { "execution": { - "iopub.execute_input": "2026-04-02T05:49:25.173298Z", - "iopub.status.busy": "2026-04-02T05:49:25.173208Z", - "iopub.status.idle": "2026-04-02T05:49:25.769799Z", - "shell.execute_reply": "2026-04-02T05:49:25.769109Z" + "iopub.execute_input": "2026-04-03T11:45:37.581988Z", + "iopub.status.busy": "2026-04-03T11:45:37.581853Z", + "iopub.status.idle": "2026-04-03T11:45:38.206471Z", + "shell.execute_reply": "2026-04-03T11:45:38.205753Z" } }, "outputs": [ @@ -2766,9 +2787,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.12" + "version": "3.11.14" } }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/scripts/validate_nb_refresh.py b/scripts/validate_nb_refresh.py index faa1c0fe..e807d3c3 100644 --- a/scripts/validate_nb_refresh.py +++ b/scripts/validate_nb_refresh.py @@ -48,6 +48,28 @@ 4. Stream outputs (stdout/stderr): warnings from Python packages such as DeprecationWarnings from mpmath are environment-specific and ignored. Only ``display_data`` and ``execute_result`` outputs are compared. + +5. SymPy 1.13 ``trigsimp(method='old')`` algebraic form differences + (curvilinear coordinates example, ``examples/ipython/LaTeX.ipynb``): + + a. Pythagorean identity: ``sin²(η)+sinh²(ξ)`` <-> ``-cos²(η)+cosh²(ξ)`` + b. Power factoring: ``(X²+Y²)^{3/2}`` <-> ``X²√(X²+Y²)+Y²√(X²+Y²)`` + c. Spherical Laplacian/grad-wedge-B: collected ``\\frac{r²A+rB+C}{r²}`` + form <-> distributed ``A + B/r + C/r²`` form. + d. Whitespace inside ``\\frac{...}`` arguments. + e. Outer ``\\left(…\\right)`` wrapper before a basis blade. + + **Known remaining differences (require symbolic algebra to verify):** + + * Spherical curl ``e_r`` component: SymPy writes a parenthesised + ``(A/tan + B - C/sin²)|sin|`` form; trigsimp produces a single + fraction over ``tan|sin|``. Both are mathematically equal but have + fundamentally different structure. + + * Prolate-spheroidal divergence: SymPy collects all terms into one + large fraction; trigsimp distributes into seven separate fractions. + The two forms are mathematically equal and can be verified with + SymPy but are not normalizable by simple string rewriting. """ import json @@ -71,9 +93,215 @@ def _norm_cdot(text: str) -> str: return text.replace(r'\cdot \left(', r'\left(') +def _norm_sin2_sinh2_identity(text: str) -> str: + r"""``{\\sin{ARG}}^{2} + {\\sinh{ARG2}}^{2}`` -> ``- {\\cos{ARG}}^{2} + {\\cosh{ARG2}}^{2}``. + + SymPy versions differ on which Pythagorean form they choose for the + prolate-spheroidal metric coefficient sin²(η) + sinh²(ξ) = cosh²(ξ) - cos²(η). + """ + pattern = ( + r'\{\\sin\{(\\left \(.*?\\right \))\}\}\^\{2\}' + r' \+ ' + r'\{\\sinh\{(\\left \(.*?\\right \))\}\}\^\{2\}' + ) + return re.sub(pattern, r'- {\\cos{\1}}^{2} + {\\cosh{\2}}^{2}', text) + + +def _norm_sum_sqrt_to_power32(text: str) -> str: + r"""``\\left(X^{2} + Y^{2}\\right)^{\\frac{3}{2}}`` -> ``X^{2}\\sqrt{...} + Y^{2}\\sqrt{...}``. + + SymPy versions differ on whether (u²+v²)^{3/2} is written as a single power + or factored as u²·√(u²+v²) + v²·√(u²+v²). + """ + pattern = ( + r'\\left\(([a-z])\^\{2\} \+ ([a-z])\^\{2\}\\right\)' + r'\^\{\\frac\{3\}\{2\}\}' + ) + replacement = r'\1^{2} \\sqrt{\1^{2} + \2^{2}} + \2^{2} \\sqrt{\1^{2} + \2^{2}}' + return re.sub(pattern, replacement, text) + + +# --------------------------------------------------------------------------- +# Brace-counting helpers for structural LaTeX normalizers +# --------------------------------------------------------------------------- + +def _find_matching_brace(text: str, pos: int) -> int: + """Return the index of the ``}`` matching the ``{`` at *pos*, or -1.""" + depth = 0 + for i in range(pos, len(text)): + c = text[i] + if c == '{': + depth += 1 + elif c == '}': + depth -= 1 + if depth == 0: + return i + return -1 + + +def _split_signed_terms(text: str) -> list: + r"""Split *text* at top-level ``+``/``-`` separators. + + Returns a list of ``(sign, term)`` tuples where *sign* is ``'+'`` or + ``'-'`` and *term* is the stripped content. A leading ``'- '`` is + treated as the sign of the first term. + """ + text = text.strip() + result = [] + depth = 0 + sign = '+' + start = 0 + + if text.startswith('- '): + sign = '-' + start = 2 + + i = start + while i < len(text): + c = text[i] + if c == '{': + depth += 1 + elif c == '}': + depth -= 1 + elif (depth == 0 and c in '+-' + and i > 0 and text[i - 1] == ' ' + and i + 1 < len(text) and text[i + 1] == ' '): + term = text[start:i].strip() + if term: + result.append((sign, term)) + sign = c + start = i + 2 + i += 2 + continue + i += 1 + + term = text[start:].strip() + if term: + result.append((sign, term)) + return result + + +def _divide_term_by_r2(sign: str, term: str) -> tuple: + r"""Return ``(sign, term/r^{2})`` in LaTeX, possibly adjusting *sign*.""" + # r^{2} STUFF → STUFF + if term.startswith('r^{2} '): + return sign, term[6:] + + # N r STUFF (integer coefficient × r × rest) → \frac{N STUFF}{r} + m = re.match(r'^(\d+) r (.+)', term) + if m: + return sign, r'\frac{' + m.group(1) + ' ' + m.group(2) + r'}{r}' + + # \frac{NUM}{DEN} → \frac{NUM}{r^{2} DEN} + if term.startswith(r'\frac{'): + num_end = _find_matching_brace(term, 5) + if num_end != -1 and num_end + 1 < len(term) and term[num_end + 1] == '{': + denom_end = _find_matching_brace(term, num_end + 1) + if denom_end != -1: + num = term[6:num_end] + den = term[num_end + 2:denom_end] + return sign, r'\frac{' + num + r'}{r^{2} ' + den + '}' + + # plain STUFF → \frac{STUFF}{r^{2}} + return sign, r'\frac{' + term + r'}{r^{2}}' + + +def _norm_distribute_r2_denominator(text: str) -> str: + r"""Distribute ``\\frac{r^{2} A + 2r B + C}{r^{2}}`` into expanded form. + + Converts the collected-fraction form that older SymPy produces to the + term-by-term form produced by newer SymPy (or vice-versa). Only fires + when the denominator is exactly ``{r^{2}}`` and the numerator starts + with ``r^{2} ``. + """ + result: list = [] + i = 0 + while i < len(text): + if text[i:i + 6] != r'\frac{': + result.append(text[i]) + i += 1 + continue + + open_brace = i + 5 # index of the opening { + num_end = _find_matching_brace(text, open_brace) + if num_end == -1: + result.append(text[i]); i += 1; continue + + # Denominator must be exactly {r^{2}} (7 chars) + if text[num_end + 1:num_end + 8] != '{r^{2}}': + result.append(text[i]); i += 1; continue + + numerator = text[open_brace + 1:num_end] + + # Only distribute when numerator opens with r^{2} + if not numerator.startswith('r^{2} '): + result.append(text[i]); i += 1; continue + + terms = _split_signed_terms(numerator) + parts: list = [] + for j, (sign, term) in enumerate(terms): + _, new_term = _divide_term_by_r2(sign, term) + if j == 0: + parts.append(('- ' if sign == '-' else '') + new_term) + else: + parts.append(' ' + sign + ' ' + new_term) + result.append(''.join(parts)) + i = num_end + 8 # skip past }{r^{2}} + return ''.join(result) + + +def _norm_strip_outer_parens_before_basis(text: str) -> str: + r"""Remove a lone ``\\left ( X\\right ) \\boldsymbol`` wrapper. + + SymPy sometimes wraps a distributed sum in ``\\left ( ... \\right )`` + before a basis blade; other versions omit this wrapping. + The inner ``\\left (`` / ``\\right )`` pairs inside function arguments + (e.g. ``\\tan{\\left (\\theta \\right )}``) are always followed by ``}`` + rather than `` \\boldsymbol``, so the non-greedy match stops at the + correct level. + + Assumption: galgebra's LaTeX output never contains a bare nested + ``\\left ( ... \\right )`` group that is itself immediately followed by + `` \\boldsymbol``. If it did, the non-greedy ``.*?`` would greedily stop + at the *inner* ``\\right )`` and produce a wrong result. This holds for + all current galgebra output formats; revisit if new expression types are + added that wrap sub-expressions in bare parentheses before a basis blade. + """ + return re.sub( + r'\\left \( (.*?)\\right \) \\boldsymbol', + r'\1 \\boldsymbol', + text, + flags=re.DOTALL, + ) + + +def _norm_collapse_spaces(text: str) -> str: + r"""Normalize insignificant whitespace in LaTeX math. + + SymPy emits trailing spaces inside ``\\frac{...}`` arguments and between + terms (e.g. ``\\frac{2 f }{r}`` vs ``\\frac{2 f}{r}``). In LaTeX math, + a space before ``}`` is invisible. This normalizer: + + * Collapses runs of two or more spaces to one space. + * Strips spaces immediately before ``}``. + """ + text = re.sub(r' +', ' ', text) + text = re.sub(r' +\}', '}', text) + return text + + LATEX_NORMALIZERS = [ _norm_array_colspec, _norm_cdot, + # SymPy 1.13 (trigsimp method='old') uses different algebraic forms for + # some curvilinear-coordinate expressions. The normalizers below bring + # both forms to a common representation so the validator can confirm the + # changes are cosmetic. + _norm_sin2_sinh2_identity, # sin²+sinh² ↔ -cos²+cosh² (prolate spheroidal) + _norm_sum_sqrt_to_power32, # (X²+Y²)^{3/2} ↔ X²√(…)+Y²√(…) (paraboloidal) + _norm_distribute_r2_denominator, # \frac{r²A+rB+C}{r²} ↔ A+B/r+C/r² (spherical) + _norm_strip_outer_parens_before_basis, # \left( X\right) basis ↔ X basis + _norm_collapse_spaces, # trailing/extra spaces in \frac args ]