friendly / matlib

Matrix Functions for Teaching and Learning Linear Algebra and Multivariate Statistics
http://friendly.github.io/matlib/
65 stars 16 forks source link

Eqn parser #59

Open philchalmers opened 3 weeks ago

philchalmers commented 3 weeks ago

I've placed a barebones parser in dev/eqn_parser.R which implements some of the ideas I was thinking w.r.t. reducing the amount of LaTeX code for the novice user, but still allow power users freedom. The idea is to try and reduce equations such as

Eqn("\\mathcal{H}_0 : \\mathcal{C} \\mathcal{B} & = ")
Eqn("\\mathbf{H}_0 : \\mathbf{C} \\mathbf{B} & = ")

to something analogous to markdown but for LaTeX equations, such as

Eqn("*H*_0 : *C B* & = ")
Eqn("**H**_0 : **C B** & = ")

Of course, the main limitation is that LaTeX has many more environments to choose from, so I've left it possible for users to define what * and ** should mean in this context, among others. This parser also allows for macro definitions, where for example '*' will give \\times and %n will give the same string that's currently provided by Eqn_newline(). Other macros starting with % are easy to add at this point.

There are number of other avenues to go, such as detecting Greek letter inputs to, say, replace beta -> \\beta to reduce the \\ requirements, but I worry about going too deep here.

philchalmers commented 2 weeks ago

Related to #57, the equation Michael was presenting can now be constructed using the following dev/printEqn.R, which follows a similar presentation format as sprintf() but for latexMatrix, matrix, and character vector substitutions flagged with an % symbol. So the original

C <- latexMatrix(matrix(c(0,1,1,0), nrow=1), matrix = "bmatrix")
B <- latexMatrix('\\beta', ncol = 3, nrow=4,   comma=TRUE, prefix.col = 'y_')
B0 <- latexMatrix('\\beta', ncol = 3, nrow=2, comma=TRUE, prefix.col = 'y_')

with the presentation construction

Eqn("\\mathcal{H}_0 : \\mathbf{C} \\mathbf{B} & = ",
    C, B,
    Eqn_newline(), Eqn_newline(), 
    '&\n',
    B0,
    "= \\mathbf{0}_{(2 \\times 3)}", 
    align=TRUE)

can now be presented with the structure first + substitutions

printEqn("*H*_0 : **C B** & = %C %B \\\\
                          & = %B0 = **0**_{(2 `*` 3)}",
             list(C=C, B=B, B0=B0), `**`='mathbf', align=TRUE)

Or, even more simply if boldsymbol is sufficient (default for ** wrapper) and the objects C, B, and B0 are available in the parent frame, then

printEqn("*H*_0 : **C B** & = %C %B \\\\
                          & = %B0 = **0**_{(2 `*` 3)}", align=TRUE)

I personally find these much more readable and easier to modify.

friendly commented 2 weeks ago

That looks very interesting indeed, but I'll have to study it some more to get a better sense of what can be done with this.

philchalmers commented 2 weeks ago

Just to complete Phil's example (perhaps for the vignette):

  A <- latexMatrix("a", 2, 2)
  B <- latexMatrix("b", 2, 2)
  kronecker(A, B)

  # Generate the 'definition' of Kronecker product,
  Bmat <- latexMatrix('\\mathbf{B}', ncol=1, nrow=1)
  kronecker(A, Bmat)

  Eqn("\\mathbf{A} \\otimes \\mathbf{B} = &",
      kronecker(A, Bmat),
      "\\\\[1.5ex]\n= & ",
      kronecker(A, B),
      align = TRUE)

image

For future posterity, here's how printEqn() would look for the Kronecker example:

A <- latexMatrix("a", 2, 2)
B <- latexMatrix("b", 2, 2)
kronecker(A, B) |> Eqn()

# Generate the 'definition' of Kronecker product,
Bmat <- latexMatrix('\\mathbf{B}', ncol=1, nrow=1)
KABmat <- kronecker(A, Bmat)
KAB <- kronecker(A, B)

Eqn("\\mathbf{A} \\otimes \\mathbf{B} = &",
    KABmat,
    "\\\\[1.5ex]\n= & ",
    KAB,
    align = TRUE)

source(here::here('dev', 'printEqn.R'))
printEqn("**A** \\otimes **B** = & %KABmat \\\\[1.5ex]
                               = & %KAB", align=TRUE)
philchalmers commented 2 weeks ago

With the new partition.R, which is always fun with Kronecker products.

A <- latexMatrix("a", 2, 2)
B <- latexMatrix("b", 2, 2)

# Generate the 'definition' of Kronecker product,
Bmat <- latexMatrix('\\mathbf{B}', ncol=1, nrow=1)

source(here::here('dev', 'partition.R'))
source(here::here('dev', 'printEqn.R'))
KABmat <- kronecker(A, Bmat) |> partition(rows=1, columns=1)
KAB <- kronecker(A, B) |> partition(rows=2, columns=2)

printEqn("**A** \\otimes **B** = & %KABmat \\\\[1.5ex]
                               = & %KAB", align=TRUE)

The last of which gives

equation (1)

EDIT: uploaded wrong jpg.

friendly commented 2 weeks ago

In the examples above, we use Eqn_newline(), but also "\\\\[1.5ex] where it's desired to add some extra space. Could Eqn_newline() gain a vspace= argument for this purpose?

philchalmers commented 2 weeks ago

Added. Eqn_newline(1.5) now gives the desired output, though the metric can be changed as well.

philchalmers commented 1 week ago

Quarto support added for equations by adding Eqn(, quarto=TRUE), but AFAICS there's no way to include ref() as the quarto compiler seems to treat @ instances at different points in the complication. Equations can still be referenced via @eq-myeqn in the usual Quarto way, but not in the inline {r} chunks. Slightly more awkward, but I suppose if one if going the Quarto route then this is a minimal inconvenience as the HTML and PDF outputs use the same format.

friendly commented 1 week ago

Great! Hope you don't mind if I tweak the documentation a bit.

philchalmers commented 1 week ago

Took some digging, but I finally found a way to make quarto behave well with ref(), which now gives the same behavior as the other methods. I'm looking into a more automated way to detect the CLI type similar to knitr::is_html_output(), but the best I've found so far is quarto::is_using_quarto() and I don't particularly like it in this context as the function is very greedy (returns TRUE if either "_quarto.yml⁠ at its root" or "at least one .qmd file in the directory").

I'm thinking about using something like knitr::current_input() to see if the file extension is .qmd, but this would require some testing and may not behave well with secondary applications such as pkgdown. I'll keep you posted.

friendly commented 1 week ago

That looks good. Is it the case that options("quartoEqn") is NULL by default, and presently must be set TRUE to use in a quarto doc?

philchalmers commented 1 week ago

Currently, yes, one would need to use options("quartoEqn" = TRUE) at the beginning of the document to switch to Quarto mode.

I have a working solution to this problem in https://github.com/friendly/matlib/blob/master/dev/Eqn_test_quarto.qmd but it involves a file name constraint in that, for example, mydoc.qmd and mydoc.Rmd or mydoc.Rnw cannot live in the same directory, otherwise an error will be raised. If we're fine with this file naming constraint I'd be happy to port this automation as it should work with 'pkgdown' too.

friendly commented 1 week ago

Is setQuartoEqn() to be called by the user, or will this be called by Eqn() and friends?

philchalmers commented 1 week ago

The latter; this shouldn't be the user's problem to automate.

philchalmers commented 5 days ago

Updates to this thread: quarto support now fully functional up to the unique .qmd file limitation, and preview.pdf and preview.packages options have been added to generate equations using LaTeX compliers rather than MathJax whenever this is useful.

image

and with a complied document

image

(not that it's helpful with the border matrix feature, just showing this is possible to preview when PDFs are built).

Also, printEqn() now detects Greek letters so that ** will apply a \boldsymbol{} to detected Greek letters rather than the less attractive \mathbf{}.

john-d-fox commented 5 days ago

Does it make sense to support \bordermatrix optionally in print.latexMatrix(), say via an argument bordermatrix, which gets its default value from an option and defaults to FALSE if the option isn't set?

philchalmers commented 5 days ago

Yes, I think that's reasonable. It's unfortunate that a side-step is required to deal with MathJax limitations, though having the option to use the more kosher version for PDF outputs is attractive and IMO preferable.

john-d-fox commented 5 days ago

OK, I'll do that when I have a chance.

john-d-fox commented 5 days ago

I added support for \bordermatrix{} in print.latexMatrix(). The implementation is straightforward and seems to work fine (but, as usual, should be tested more). A limitation of \bordermatrix{} is that it apparently doesn't support matrix delimiters other than parentheses.