Skip to content

Recurring: modulus/norm bar 'bump' returns whenever barsToGrid is swapped for straightenTallBars (braket-only tallArgQ + fixed 1.3x scale); |\psi(x)|^2 seams #67

Description

@mbahram

Recurring issue

This is the third time this exact rendering defect has surfaced (see #46, #48, #64). It keeps coming back because the two available bar renderers are not equivalent, and every time the bar path is re-aligned to straightenTallBars, the general case regresses again. This report is meant to end the cycle: it states the invariant and the one implementation that satisfies it.

Symptom

A modulus / norm bar renders with a visible mid-height seam (a "bump") whenever its argument is taller than one text line but is not a bra/ket. The canonical case is the quantum probability |\psi(x)|^2:

|\psi(x)|^2

The parenthesised argument \psi(x) pushes the bar past one line, the front end builds its extensible bracketing-bar glyph from pieces, and it seams. Same for |\frac{a}{b}|, \|\frac{\psi}{\phi}\|, etc. A bare braket |\langle\phi|\psi\rangle| does not bump, which is exactly why the defect hides.

Why it recurs

There are two bar renderers in the history of this repo, and only one is complete:

  • straightenTallBars (the one currently in texBoxes): rewrites an Abs / Norm TemplateBox to a fixed-scale glyph StyleBox["\[VerticalLine]" / "\[DoubleVerticalBar]", FontSize -> 1.3 Inherited], guarded by

    tallArgQ[arg_] := ! FreeQ[arg, TemplateBox[_, "Ket" | "Bra" | "Braket" | "BraKet"]]

    Two independent limits make this incomplete:

    1. tallArgQ is braket-only. |\psi(x)|^2 is an Abs of a parenthesised row, not a braket, so tallArgQ is False, straightenTallBars never fires, and the extensible Abs template survives and seams.
    2. The scale is fixed at 1.3x. Even where it does fire, 1.3 Inherited is a guess: too tall for a scalar, far too short for a two-line fraction. A bar height that does not track the content is wrong by construction.
  • barsToGrid (the content-adaptive renderer): after templatizeBars has folded every modulus into an Abs / Norm TemplateBox, rewrite those to a GridBox column-rule

    $absGridDividers = {"Columns" -> {True, True}, "Rows" -> {}}
    absGrid[arg_]  := GridBox[{{arg}}, GridBoxDividers -> $absGridDividers, GridBoxSpacings -> {"Columns" -> {{0.15}}}]
    normGrid[arg_] := GridBox[{{absGrid[arg]}}, GridBoxDividers -> $absGridDividers, GridBoxSpacings -> {"Columns" -> {{0.22}}}]
    barsToGrid[boxes_] := boxes //. {
        TemplateBox[{arg_}, "Abs"]  :> absGrid[arg],
        TemplateBox[{arg_}, "Norm"] :> normGrid[arg]
    }

    A GridBox divider is a plain solid rule that spans exactly the cell (content) height and is not a glyph, so it cannot be built up and cannot seam: short around |a|, tall around |\psi(x)| or a braket or a fraction, correct at every height with no per-argument tuning.

The regression path is always the same: a reconcile/refactor swaps barsToGrid back to straightenTallBars (because upstream main only ever had straightenTallBars), and |\psi(x)|^2 silently starts bumping again while the braket tests stay green.

How to avoid it (the invariant)

Render moduli/norms with the content-adaptive GridBox rule (barsToGrid), not the fixed-scale braket-only straightenTallBars. Do not swap it back.

Concretely:

  1. texBoxes ends with barsToGrid @ applyMathFont @ templatizeBars @ …not straightenTallBars @ …. Keep templatizeBars (it does the promotion, incl. the templatizeBars: an unpairable \| / \lVert norm keeps the non-stretching \[DoubleVerticalBar], so bars around a ket render one line tall #64 bare-\| and the norm-squared scripted-closer rules) and keep tallArgQ (those rules still use it); only the final render step is barsToGrid.

  2. Test the general case, not just brakets. The test that would have caught every recurrence asserts that a parenthesised modulus leaves no extensible template and produces the divider rule:

    VerificationTest[
        Map[Function[tex, With[{b = MarkdownToNotebook["$" <> tex <> "$", "Evaluate" -> False]},
            {! FreeQ[b, GridBoxDividers -> {"Columns" -> {True, True}, ___}],
             FreeQ[b, TemplateBox[_, "Abs" | "Norm"]]}]],
            {"|\\psi(x)|^2", "|x|", "\\lVert v\\rVert", "|\\langle\\phi|\\psi\\rangle|^2"}],
        {{True, True}, {True, True}, {True, True}, {True, True}},
        TestID -> "modulus / norm drawn as a seamless GridBox column-rule (issue #46/#48)"
    ]

    A green suite that only checks |\langle\phi|\psi\rangle| will pass while |\psi(x)|^2 bumps. Include a parenthesised argument.

  3. N2M integration caveat (important since NotebookToMarkdown delegates box→LaTeX to WolframParserExportLaTeX). ExportLaTeX reads a bare one-cell GridBox as \begin{matrix}, and it is monolithic (it consumes the whole box tree; it does not call back per sub-box). So a walkerMath[g_GridBox /; absGridQ[g]] rule only catches a modulus standing alone at top level, not the common |\psi(x)|^2 nested inside a SuperscriptBox. Fold the GridBox back to a template before delegating:

    absGridQ[GridBox[{{_}}, opts___]] := MatchQ[GridBoxDividers /. {opts}, {"Columns" -> {True, True}, ___}]
    absGridQ[_] := False
    absGridToTemplate[boxes_] := boxes //. {
        GridBox[{{inner_GridBox}}, o___] /; absGridQ[GridBox[{{inner}}, o]] && absGridQ[inner] :>
            TemplateBox[{inner[[1, 1, 1]]}, "Norm"],
        g : GridBox[{{inner_}}, ___] /; absGridQ[g] :> TemplateBox[{inner}, "Abs"]
    }
    walkerMath[x_] := Wolfram`Parser`ExportLaTeX[absGridToTemplate[x]]

    ExportLaTeX turns the Abs / Norm templates back into \lvert / \lVert (nested-safe), and a genuine matrix grid has no dividers so it falls through to the matrix path unchanged.

Reproduce

Get["MarkdownToNotebook.wl"]; ensureParser[];
texBoxes["|\\psi(x)|^2"]

With straightenTallBars: SuperscriptBox[RowBox[{TemplateBox[{RowBox[{…"("…")"…}]}, "Abs"]}], "2"] — the extensible Abs template survives → bump.
With barsToGrid: SuperscriptBox[RowBox[{GridBox[{{…}}, GridBoxDividers -> {"Columns" -> {True, True}, …}]}], "2"] — the divider rule → no bump, round-trips to \lvert \psi(x)\rvert^{2}.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions