May 2023: Problem Generator

I recently got asked to tutor somebody in math. They're still in secondary school, so their relationship with math will very likely be slightly complicated, if they need tutoring. Math as it's taught in school kinda sucks either way, since if you're good at it you'll be bored very quickly and if you're not, you'll feel stressed, and in the end everybody's just waiting for it the lesson to end. However, if the issue is just the speed at which math problems are solved, or even the speed at which calculations are done, nothing helps more than just straight up practice. This puts me in front of a problem: I don't feel like writing down 20+ simple calculation instructions every week. Ideally, I can give them more of them to take home as well. Now, I'm not a person with a lot of patience - I can be easily entertained, but not usually for very long. I understand that makes me sound like a dog, but we all know our shortcomings, don't we? Either way, my solution to problems like these are: automate it. So I wrote a code-generator.

As uni students in stem, we like LaTeX, and we already know how to write that, so there's little reason not to generate LaTeX code and compile it from system. The latter step I'm already pretty familiar with anyways, so it's not going to take learning a lot. There's some thinking in generating the numbers and brackets involved. In general, for integer numbers to be readable, they shouldn't start with zeroes, and we're currently only interested in integers, as that's all the student knows at the time. I had two different lists of integers, one including the zero, one not, the latter only ever goes first. Because numbers have a different number of digits, I need to have a list for that too (I could have gone through range, but I felt better doing it this way). The random library did the picking for me.

def make_num():
    digits = [0,1,2,3,4,5,6,7,8,9]
    fdigits = [1,2,3,4,5,6,7,8,9]
    idx = [1, 2, 3, 4]
        
    num1 = 0 
    len1 = random.choice(idx)

    for i in range(0, len1):
        if i == 0:
            num1 += random.choice(fdigits) * pow(10, (len1 - i - 1)) 
        else:
            num1 += random.choice(digits) * pow(10, (len1 - i - 1)) 

    return num1

The full function returns a number with anywhere between 1 and 4 digits. Everything beyond 4 felt excessive to me. Operators I sourced from a list, because there's really only 4 interesting at the minute. Roots and exponents come later. I wanted one function to construct one problem, which I would then feed through the code generator.

def naive_generate():
    ops = ["+", "-", "*", ":"]
    idx = [2, 3, 4]

    in_brackets = False

    returner = ""

    nums = random.choice(idx)
    nums_arr = []

    for k in range(0, nums):
        nums_arr.append(make_num())

    returner += str(nums_arr[0])
    for k in range(1, len(nums_arr)):
        if random.choice(range(0, 10)) == 5 and k != len(nums_arr) - 1:
            if in_brackets == False:
                returner += random.choice(ops) + "(" + str(nums_arr[k]) 
                in_brackets = True
            else:
                returner += random.choice(ops) + str(nums_arr[k]) +")"
                in_brackets = False
        else:
            returner += random.choice(ops) + str(nums_arr[k]) 

    if in_brackets == True:
        returner += ")" 

    return returner + " ="

We should mostly take note of how the brackets are integrated. First is of course the bracket switch in_brackets, which tells us, whether we still have to close one, when we're done. At the chance of 1 in ten, the brackets open and flip it to True, at another chance of 1 in ten, it closes and flips it back to false.

In general, a randomizer picks the number of contributors to the problem, and then the loop immediately builds the string, that then gets directly handed back into the code generator.

Now, it's been a while since I last wrote a code generator, but it's not difficult per se, especially if you get to output and compile it without having to do real cleanup. Still, I had to go back to all of my old .tex files to plagiarize the start and end of the document. I'll post the generator function here, so you know what settings and packages I used, but it's not otherwise worth noting.

def pdf_gen(text):
    start = ''' 
\\documentclass{article}
\\usepackage[T1]{fontenc}
\\usepackage[utf8]{inputenc}
\\usepackage{mathtools}
\\usepackage[a4paper, total={7.5in, 10.5in}]{geometry}

\\begin{document}
\\begin{large}
\\begin{align*}
    '''

    end = ''' 
\\end{align*}
\\end{large}
\\end{document}
    '''
    return start + text + end 

The margins in the a4paper line is probably most important to use enough space on the paper. I wanted to have 3 problems every line of equations, and as many of them down the page as I could fit. Turns out, that's about 40, meaning each page will net you 120 equations. I think I spent less time writing the generator than it would have taken for me write 120 individual equations on paper. The resulting .tex file is compiled using pdflatex.

if __name__ == "__main__":
    text = ""
    for i in range(0, 40):
        text += naive_generate() + "\qquad\qquad\qquad &"
        text += naive_generate() + "\qquad\qquad &"
        text += naive_generate() + "\\\\"
    tex = pdf_gen(text)

    problem_sheet = open("problem_sheet.tex", "w")
    problem_sheet.write(tex)
    problem_sheet.close()

    comp = subprocess.run(["pdflatex", "problem_sheet.tex"], 
        stdout=subprocess.DEVNULL)
    rm = subprocess.run(["rm", "problem_sheet.aux", 
            "problem_sheet.log"], stdout=subprocess.DEVNULL)

And this is how far I got this month, because I was away for 2 weeks, and had an exam to prep for. I could have made the problems more complex, including trigonometry functions and such, but that would go beyond what my student can do at the moment, and I wouldn't get much utility out of it, nor would I learn anything worth mentioning. Instead I would like to try my hands at a word problem generator. That requires me doing more reading than I had time for, but maybe there'll be a part 2 in the works, when I've done that.

Previous
Previous

July 2023: Lean - What's a Proof Assistant?

Next
Next

April 2023: Tax Returns & Systems