Categories

Formatting Matrices from Numpy to LaTeX

LaTeX is a great tool for producing high quality documents, but it can sometimes be rather cumbersome for producing things like matrices, which hold large amounts of information in an array with many rows and columns. This is made especially frustrating when the matrix you wish to format has been computed using Python and Numpy, and is right there on the PC. I thought that writing a small Python function that formats a Numpy array into LaTeX syntax would be a nice, easy exercise in the Python course (for first year mathematics students) that I teach. However, this turned out to be rather more complex than I had originally thought. I’m going to give a full description of how I might solve this problem using Python, and how to overcome some of the issues that arise.

Before we can do any coding, we need to understand the format that we are aiming to produce. A matrix is formatted in LaTeX within the pmatrix environment, each element of each row is separated by an alignment character &, and each row is separated by a newline character \\. A simple 2 by 2 matrix might be formatted as follows.

\begin{pmatrix}
1 & 2\\
3 & 4
\end{pmatrix}

This should be relatively easy to create using Python in theory, since it is just a string with the numbers inserted into a template of sorts. There are, however, some subtle details to work through here. Let’s start by producing a naive implementation of the function that will do the actual formatting. It’s going to take a single argument, the matrix to format as a Numpy array, and return a string containing the formatted LaTeX code for that matrix.

def format_matrix(matrix):
"""Format a matrix using LaTeX syntax"""
rows, cols = matrix.shape
lines = ["\\begin{pmatrix}"]

for row in range(rows):
line = ""
for col in range(cols):
sep = " & " if col > 0 else ""
line = line + sep + str(matrix[row, col])
lineend = "\\\\" if row < rows-1 else ""
line = line + lineend
lines.append(line)

lines.append("\\end{pmatrix}")
matrix_formatted = "\n".join(lines)
return matrix_formatted

This function will perform as we expect, but it is horribly inefficient and not particularly clean. I would describe this as a reasonable first pass following exactly the procedure of formatting by hand. Let’s walk through this function definition step by step.

The first two lines are the standard declaration of a Python function, using the def keyword followed by the name of the function and the argument specification (signature), and then the documentation string for the function. On the next line, we retrieve the shape of the matrix, which describes the number of rows and columns of the matrix as a tuple of integers. We unpack the two integers into the variables rows and cols that we will use for the iteration.

Next comes the real body of the function, the part where we actually construct each line of the output as a string. In this implementation, we use two nested for loops to achieve the output. Our strategy is to build up a list of strings that constitute the lines of the output that we will join together as lines right at the end of the function using the string join method (docs). Before we start the looping, we first create the list of lines that we will populate that contains the start of the pmatrix environment as a string:

lines = ["\\begin{pmatrix}"]

Now we can start the looping. The first loop is over the range of indices of each row in the matrix, generated using the Python range object range(rows). In each of the iteration of this row we will build up the string that will be added to the lines list. Here we build this string sequentially, starting with a blank string. Now we start the inner loop, which iterates over each column index (just as we did for rows). Inside this loop we need to add each element of the matrix by index and add this to the string that we are building. This involves adding the separator & if it is not the first element in the row and the number. Here we are using the ternary assignment in Python

sep = " & " if row > 0 else ""

We can’t simply join a number to a string, we need to convert it into a string first. There are two ways that we can build up the string. The first is to simply convert the number to a string, by calling the str function to explicitly convert to string. The alternative is to use a format method or an f-string. The latter method is probably better in many cases but later we will replace this with an alternative anyway.

Once we’ve built the string for the line inside the inner loop, we need to add the line separators \\ to all but the last line, and then append the line string to the lines list. Inside the outer loop, but not the inner loop, we again use the ternary assignment to conditionally add the line separator, and then append the completed string to the list.

At the very end, we use the join method, as mentioned above, to join the strings in the lines list, and then return the completed string.

Fixing the obvious problems

As it stands, the function we wrote above is pretty basic. First, and probably most important, is that it is not very Pythonic. Roughly speaking, a piece of code is Pythonic if it (correctly) leverages the features of the Python language and follows the Zen of Python (link).

The first thing that jumps out at me when I look at the function we have written is the nested for loops. Generally speaking, this is a sign that the code we have written isn’t going to perform well, and certainly could be refactored to make it easier to debug. (Of course, there are some circumstances where nested for loops are simply unavoidable, but these cases are certainly rare.) Let’s take a closer look at the main body of the outer loop, and see if we can make some improvements.

line = ""
for col in range(cols):
sep = " & " if col > 0 else ""
line = line + sep + str(matrix[row, col])
lineend = "\\\\" if row < rows-1 else ""
line = line + lineend

The purpose of this code is to build up each line of the matrix in LaTeX format. As we’ve discussed, we start with a blank string, and build this up in the for loop that follows. Building up a string with a separator is a common task and, perhaps unsurprisingly, there is a fast and efficient way to do this in Python: the str.join method. Now we can’t simply apply this method to the row of the matrix such as follows.

line = " & ".join(matrix[row, :])

The problem here is that the join method expects an iterable of strings, not numbers. Instead we have to change each of the numbers to a string by applying the str function to each number. There are other ways of doing this, but in this context perhaps the easiest is to use the map function (docs), which creates a new iterable by applying a function to all the elements of the old iterable. Now we can replace most of the body of the outer for loop with a single line. (We opted to use the str function before because it allows us to apply it using the map function here.)

line = " & ".join(map(str, matrix[row, :]))

This code is more dense, but is somehow much more descriptive as to what is actually happening (from the inside out): we apply the str function to each number in the matrix row and then join these strings together with the separator “ & “. What we can’t change is the way that we apply the line ending to each line. (We’ll come back to this later.) Now our code for the body of the outer loop will look something like this:

lineend = "\\\\" if row < rows-1 else ""
line = " & ".join(map(str, matrix[row, :])) + lineend

Now let’s look at the outer loop. Here we are iterating over a range of indices generated by range(rows). This is not very Pythonic, and doesn’t make use of the fact that Numpy arrays are themselves iterators. This means we can use a Numpy array directly in a for loop to iterate over the rows of the (two dimensional) array. (Iterating over a 1 dimensional array yields the elements.) This means we could replace the outer loop code by the following.

for row in matrix:
line = " & ".join(map(str, row)) + "\\\\"
lines.append(line)

Notice that we’ve replaced the row lookup in body of the loop matrix[row, :] by just the row variable coming from the loop. This row variable now contains a 1 dimensional Numpy array rather than an integer. Unfortunately, by doing this we’ve gained an extra LaTeX new line at the end of the matrix body. (Actually this won’t cause any problems in the LaTeX compilation, but it is good from a code style point of view.) Our full code now looks as follows.

def format_matrix(matrix):
"""Format a matrix using LaTeX syntax"""
rows, cols = matrix.shape
lines = ["\\begin{pmatrix}"]

for row in matrix:
line = " & ".join(map(str, row)) + "\\\\"
lines.append(line)

lines.append("\\end{pmatrix}")
matrix_formatted = "\n".join(lines)
return matrix_formatted

This is already a great improvement on the original function, but we still have some way to go to clean this function up properly. Since we’ve changed our method of iteration in the one remaining for loop, we no longer need to retrieve the number of rows and columns of the matrix in the first line. Second, we can still improve the way construct the final string to return and, by doing so, make the loop simpler yet.

At present, we construct each line of the whole formatted matrix string and then join all these lines together to form the final string. However, we could instead reserve the join for the body of the matrix only, allowing us to simplify the loop. For this, we will replace the final few lines of the function with a f-string such as the following.

body = "\\\\\n".join(body_lines)
return f"\\begin{{pmatrix}}\n{body}\n\\end{{pmatrix}}"

Our task now is to define the body_lines list using only the lines that come from the matrix. The advantage of this over the code we had above is that we have also recovered the original functionality where the final line of the main matrix body did not have an extra LaTeX line end that was lost in the first pass rewrite.

This method also allows us to remove the clunky for loop in favour of the more Pythonic, and easy to read, list comprehension. This means we can replace the loop and list initialisation with the following list comprehension.

body_lines = [" & ".join(map(str, row)) for row in matrix]

Now we have the start of a nice, well-written Python function that has a fraction of the number of lines that we started with. The following is the full code that we have so far.

def format_matrix(matrix):
"""Format a matrix using LaTeX syntax"""
body_lines = [" & ".join(map(str, row)) for row in matrix]

body = "\\\\\n".join(body_lines)
return f"\\begin{{pmatrix}}\n{body}\n\\end{{pmatrix}}"

Fixing some potential problems

There is still some considerable way to go to make this function “idiot proof”. The first thing we should really do is add a better documentation string, but we won’t be extending this to save some space. For those who wish to know, there are official guidelines for writing documentation strings in PEP257 (link). The other things we need to address, such as checking the type and shape of the input array.

What I mean by this is that, at the moment, we could pass any variable we like into this function, even though we really only want this to work with 2 dimensional Numpy arrays. Of course, we will get an error at various points if the object we pass doesn’t conform to certain conditions. For example, if we pass None into this function, we will get a TypeError since None is not iterable. Moreover, the error message that we get from the function, as it currently stands, will not be particularly helpful in diagnosing problems later down the line.

The best thing to do here is to insert a type checking statement at the top of the function, that will raise a meaningful exception if the type of the argument is not a Numpy array. We can do this using the follow lines of code.

if not isinstance(matrix, np.ndarray):
raise TypeError("Function expects a Numpy array")

We also need to make sure the array is 2 dimensional, otherwise we will get another TypeError from the map function if the members of the array are numbers. We don’t need to raise an exception if the array is 1 dimensional though, because we can perform a cheap reshape of the array to make a 1 dimensional array into a 2 dimensional array. This is a perfect opportunity to use the “walrus operator” (PEP527) that is new in Python 3.8. If the array has more than 2 dimensions, we will need to throw an exception.

if len(shape := matrix.shape) == 1:
matrix = matrix.reshape(1, shape[0])
elif len(shape) > 2:
raise ValueError("Array must be 2 dimensional")

Adding these checks in gives the following “finished” code.

import numpy as np

def format_matrix(matrix):
"""Format a matrix using LaTeX syntax"""

if not isinstance(matrix, np.ndarray):
raise TypeError("Function expects a Numpy array")

if len(shape := matrix.shape) == 1:
matrix = matrix.reshape(1, shape[0])
elif len(shape) > 2:
raise ValueError("Array must be 2 dimensional")

body_lines = [" & ".join(map(str, row)) for row in matrix]

body = "\\\\\n".join(body_lines)
return f"\\begin{{pmatrix}}\n{body}\n\\end{{pmatrix}}"

This function will now give us useful error messages if we provide an argument that isn’t a Numpy array. Unfortunately this comes at a cost. Before we integrated our type checking, we could have called the function with nested lists, such as those that you might provide to np.array function to create a new Numpy array. For example, the following call will no longer work.

format_matrix([[1, 2], [3, 4]])

This is an important point about Python programming, that embracing the lack of strong type checking often leads to errors that can be difficult to diagnose, but implementing some type checking can make your code less flexible. We can recover some of the flexibility here by attempting to convert the argument to a Numpy array first, raising an exception if this conversion fails.

if not isinstance(matrix, np.ndarray):
try:
matrix = np.array(matrix)
except Exception:
raise TypeError("Could not convert to Numpy array")

This will mean that we can call this function with nested lists, as above, and it will work. In this case the run-time cost of converting to a Numpy array is relatively small, especially for matrices that we are likely to print into a LaTeX document. Hence our full function is now complete.

import numpy as np

def format_matrix(matrix):
"""Format a matrix using LaTeX syntax"""

if not isinstance(matrix, np.ndarray):
try:
matrix = np.array(matrix)
except Exception:
raise TypeError("Could not convert to Numpy array")

if len(shape := matrix.shape) == 1:
matrix = matrix.reshape(1, shape[0])
elif len(shape) > 2:
raise ValueError("Array must be 2 dimensional")

body_lines = [" & ".join(map(str, row)) for row in matrix]

body = "\\\\\n".join(body_lines)
return f"\\begin{{pmatrix}}\n{body}\n\\end{{pmatrix}}"

Going the extra mile

The function we have written already is functional and should be relatively easy to use, debug, and maintain in the future, even when we have forgotten how it works. Really the only thing we should have done is written a more complete documentation string. (As I mentioned earlier, we haven’t done this for space.) However, there are some further improvements that we can make that will greatly improve the functionality.

A very simple improvement we can make is to allow for optionally changing the LaTeX matrix environment from pmatrix to another matrix environment such as bmatrix. We can do this by adding an optional argument to the signature of the function, and then incorporating this environment variable into the f-string at the end of the function.

def format_matrix(matrix, environment="pmatrix"):
"""Format a matrix using LaTeX syntax"""

# -/- snip -/-

return f"""\\begin{{{environment}}}
{body}
\\end{{{environment}}}"""

At the moment, if we call the function with a matrix containing fractions then we will get a rather bloated LaTeX formatted matrix. This is because the default behaviour for converting a floating point number to a string is to print all the decimal points. Since we have used the str function to perform this conversion, we can adapt the function rather easily to accept custom formatters for printing the matrix elements. We can again include an optional argument to allow for this customisation.

def format_matrix(matrix, environment="pmatrix", formatter=str):
"""Format a matrix using LaTeX syntax"""

# -/- snip -/-

body_lines = [" & ".join(map(formatter, row)) for row in matrix]

# -/- snip -/-


This means we can truncate the number of decimal places or perform any other operation we like by supplying a custom formatting function beyond the standard str function.

All these improvements together gives us the final, finished version of the code as follows.

import numpy as np

def format_matrix(matrix, environment="pmatrix", formatter=str):
"""Format a matrix using LaTeX syntax"""

if not isinstance(matrix, np.ndarray):
try:
matrix = np.array(matrix)
except Exception:
raise TypeError("Could not convert to Numpy array")

if len(shape := matrix.shape) == 1:
matrix = matrix.reshape(1, shape[0])
elif len(shape) > 2:
raise ValueError("Array must be 2 dimensional")

body_lines = [" & ".join(map(formatter, row)) for row in matrix]

body = "\\\\\n".join(body_lines)
return f"""\\begin{{{environment}}}
{body}
\\end{{{environment}}}"""

I still think this exercise might have been a bit tricky, but there are a lot of elements involved here. Hopefully you have learned something by reading the code I have written here, and understood my reasoning for making all of these changes.

Categories

Grant applications and the EPSRC

I recently attended a workshop on the grant application procedure ran by the Engineering and Sciences Research Council (EPSRC). The EPSRC is one of several UK research councils and is responsible for funding research across engineering, science, and mathematics through a number of grant and fellowship schemes. I went to the workshop with many questions, and misconceptions, about the grant review process and how funds are allocated. I’m happy to say that I learned a lot, and most of my questions were answered.

The grant writing process is a bit of a mystery and I suspect that this is also true for many other early career academics. I hope that a better understanding of the review process for grant proposals will make it easier when I do eventually apply for a grant. I thought it wise to share what I learned from the workshop. Obviously this is specific to mathematics, but the procedure in other subjects is presumably similar.

Disclaimer: I do not claim to be an expert on grant proposal writing, and I do not represent the EPSRC in any way. I hope that this article is useful to people and that the information contained within is accurate. Please let me know if there are any errors so that they may be corrected.

Once a grant proposal has been submitted, the proposal is first passed to one of a number of portfolio managers, each covering one or more subject areas within mathematics. They will find three reviewers, who will read and comment on the proposal. Two of these reviewers will be found by the portfolio manager from the EPSRC college of reviewers. The third reviewer is chosen will be one of three reviewers suggested by the applicant. (Assuming that these suggestions are appropriate and able to provide a review.)

The reviewers will score the proposal and make comments to be passed back to the applicant. If the reviews are supportive of the proposal, it will be passed to the next stage; an unsupported proposal will be rejected. If the proposal progresses to the next stage, the reviews of the proposal will be passed back to the applicant, and they will have the opportunity to respond to some of the points raised by the reviewers. It is important to note that this response will not bee seen by the reviewers!

The response by the applicant should address any criticisms and concerns, and should be reasonably self-contained. The reviews and the applicants response will be the main points considered in the next stage of the application procedure, which is a panel.

The grant review panel meets several times per year to consider proposals and decide which have sufficient merit to be funded. The panel consists of a number of academics, around 15 I believe, who sit at a table and discuss each proposal, its reviews, and the applicant’s response to the criticism. The proposals are ranked, and a number of proposals from the top of this list are chosen to be funded. The number will depend on the value of each proposal and the funds available.

At present, the EPSRC offer a number of grant and fellowship schemes. For most, especially those in established positions who have received grants in the past, the standard grant scheme will be most appropriate. This scheme covers any proposals that fall within the EPSRC remit with no restrictions placed on the length or value of the proposal.

For early career academics who have not yet submitted a grant proposal, there is also the “new investigator award“, which is only available for those who have not yet been the recipient of a significant grant (over the value of £100k). There are no restrictions on when an proposal can be submitted to this scheme – this is a recent change to allow more flexibility for those who have taken career breaks. Note that the applicant is expected to hold a permanent academic post in order to apply for this scheme.

In addition to these grant schemes are three fellowship schemes, which aim to pay for the applicants time to further their research and typically last three to five years. Unlike other awards a fellowship is a personal award, which means that the funding is tied to the investigator and not a specific institution. There are limitations placed on the subject of a fellowship: it must align with one of the EPSRC priority areas. At present, the areas “intradisciplinary research” and “new connections from mathematical sciences” cover a large number of possibilities.

There are three levels of fellowships available: postdoctoral; early career; established career. These levels represent the different career stages to which they are available, although it is left to the applicant to apply for the scheme that they believe is most appropriate. The EPSRC provides person specifications that describe typical applicants at each stage. Postdoctoral fellowships have a shorter duration but offer the greatest flexibility in subject area, whilst established career fellowships have the longest duration but are more restrictive on subject area.

It seems that there is a large amount of flexibility in the fellowship schemes. However, I feel that fellowships are still extremely competitive and will likely require a large investment of time, with the knowledge that the proposal might not be funded. The standard grant schemes may be less competitive but also seem to be intended for people who already hold (permanent) lectureship positions. (Even if this is not strictly the case, it would be troublesome to apply and receive a grant on a temporary contract.)

It seems that the EPSRC have made a number of positive steps recently to provide what support they can to academics on “non-standard” career paths; in particular, those who have taken career breaks or breaks from research. This is important for those who intend to start families early in their careers. It also seems that they value input from early career researchers through their early career forum, of which I was not aware before the workshop. They also seem to encourage the participation of early career researchers in the review and panel processes.

Categories

The napkin ring problem

I heard an interesting thought experiment on a podcast some time ago, but I am not sure of the origins of the problem. This problem is the napkin ring problem. Despite hearing this problem some time ago I have only just found time to convince myself that it is true, surprising as it is. I thought I would share the solution since it is a lovely application of multiple integrations. The problem is as follows:

Take a tennis ball and drill a cylindrical hole exactly through the centre of the ball so a to leave a ring around the circumference whose height is 2cm. (This ring would resemble a napkin ring.) Now take a much larger ball, say the Earth (if this were a perfect sphere), and perform the same task, leaving a ring around the equator of height 2cm. Then these two rings will have precisely the same volume.

Categories

Off on a tangent

I’ve recently become somewhat interested in smooth manifolds and surfaces as a result of preparation for various interviews, amongst other reasons. The concept of a surface is very intuitive, and is a concept that a student of mathematics is likely to encounter very early in their mathematical careers, though a reasonable definition of a surface takes more effort. The result of this formalisation is a remarkably elegant theory, which eventually leads to ideas of smooth manifolds – arguably one of the best sounding mathematical objects – and generalised calculus.

It took quite some time before I had a “big picture” of what a surface is, and how this fits into the grander theory. Undoubtedly I am missing some of the major pieces to this puzzle, but it does show some of the elegance of the theory (at least for me).

Categories

The ArXiv and me (Part 1)

The ArXiv is a popular pre-print article server for physics, mathematics, and computer science (and other subjects) hosted by Cornell University. It is a fairly common practice for academics to upload a preliminary version of their articles (or other works) to the ArXiv to make them publicly available before they are formally published in a journal. (The process of publication is often lengthy, and many consider it best to make the article available in advance, even though it probably has not yet been peer-reviewed.)

At present, there are around 1.4 million articles hosted on the ArXiv, and more are added every day. (Should you wish to see a visual representation of the articles on the ArXiv, which I assume you do, you should visit paperscape.) In the sub-topics that I watch, there are (approximately) between 4-10 new papers added per day, and these topics are not amongst the most active on the ArXiv. The problem then is to filter the daily uploads to find the papers that are likely to be of interest for me. Luckily, about the same time that I started to think of an automated solution to this problem, I discovered that the ArXiv has some tools that can help with this problem.

Categories

The Zombie Apocalypse

Zombies are a staple of modern popular culture, and appear in a variety of forms, including the traditional slow-moving, unintelligent zombie hordes and less common fast-moving – and perhaps intelligent – zombies. The common theme in media featuring zombies is the the zombie infection, which may affect either the living or the deceased, or both. It is generally agreed that this infection is passed from zombie to non-zombie by means of a scratch or bite, and infection always leads to a transformation into a zombie. (In some more modern interpretations, this transformation may be reversible.)

The spread of the zombie infection is an interesting problem to model mathematically. There are many factors to consider: the chances of a non-zombie becoming infected in an interaction with a zombie; the rate at which interactions between non-zombies and zombies occur; the spread the zombie horde from place to place; and the “critical mass” of the zombie horde at which point there is insufficient food for all zombies. We can model parts of this problem in isolation, with some simplifying assumptions.

Categories

Abstract Python

The use of computers in mathematics for long and complex calculations has allowed us to use mathematical tools to model the real world in detail that would have been unimaginable in past decades. In recent years, computational mathematical tools have been used for a huge variety of tasks, from detecting gravitational waves to contactless payment. All of these tasks are computational problems, usually involving very long or complex calculations, but the link between mathematics and computer science is much deeper than as a tool for solving computational problems.

Mathematics and computers share the same basic language, the language of logic, but they also share a philosophy. In object orientated programming, objects are created according to a template called a class. A class outlines the properties and operations that can be performed on the corresponding objects, and can inherit properties from parent classes. This allows the programmer to ensure objects that are similar in some way to share a common collection of properties and operations. This process of abstraction is very powerful and flexible, and is precisely the same as the process of abstraction that has been employed by mathematicians.