From a young age, I've had a profound appreciation for exemplary documentation. My fascination was significantly influenced during my undergraduate studies, particularly in a Statistical Thermodynamics course led by Professor Philip Camp. His methodical approach and the exceptional quality of his lecture notes simplified complex theories, setting a benchmark in educational excellence.
While Microsoft Excel stands out as a versatile tool, Microsoft Word often falls short in facilitating high-quality documentation. My extensive experience with Word has been marked by challenges in achieving the desired precision and clarity. Conversely, LaTeX, especially platforms like Overleaf, emerges as the superior choice for crafting documents that meet high academic and professional standards. Professor Camp's use of LaTeX to produce unparalleled lecture notes exemplifies its efficacy.
The current features of my full-stack application, AI to LaTeX.
LaTeX, despite its steep learning curve and the occasional frustration when errors prevent a document from compiling, offers unparalleled control over formatting. Recognising the evolving landscape of technology and the increasing relevance of accessible, high-quality documentation, I am developing a project aimed at democratising this power. Still in its infancy, it aims to bridge the gap between professional formatting and user-friendly interfaces, making LaTeX's capabilities accessible to all.
The project is not just a tool for seasoned LaTeX users but a gateway for novices to embrace the art of professional document creation without the intricacies of traditional coding. It's a glimpse of a future where formatting is no longer a barrier to quality. See the projects section for more.
Looking back, October 2026. When I built this, LLM APIs could only answer from their training data, so the app ran a web search first and had the model write from the pages it found. Built-in web search arrived in the major LLM APIs in 2025; grounding a model in live sources is now the standard pattern, and it is the same idea behind Q&AI.
An interesting coding / maths challenge
4 min readProbability · Python
Throughout my journey solving coding and maths problems, I've stumbled upon various questions that sparked my curiosity and tested my analytical skills. One challenge in particular has stayed with me. It involves dice and probabilities, and it's captivating enough that I want to share it, along with what I learnt from it.
The problem
You have \(n\) six-sided dice. You throw all of them, again and again, until any die shows a value greater than \(r\). Write a function that returns the expected number of throws.
Before reading on, you're more than welcome to try it yourself.
Unravelling it probabilistically
The probability that a single die rolls a number greater than a threshold \(r\) is:
\[P(\text{die} > r) = \frac{6 - r}{6}\]
With \(n\) dice, the probability that all of them show a number less than or equal to \(r\) is:
def expected_throws(n, r):
"""
Expected number of throws of n six-sided dice until at least one die shows more than r.
Args:
n (int): Number of dice.
r (int): Threshold value (0 <= r < 6).
Returns:
float: Expected number of throws.
"""
probability = 1 - (r / 6) ** n
return 1 / probability
Validating with a Monte Carlo simulation
The beauty of Monte Carlo simulation is that it can emulate a probabilistic scenario directly, giving a practical counterpart to the mathematical result. By playing the game many times and averaging how many throws each game takes, we can approximate the expected number of throws and check the formula.
import numpy as np
def simulate_throws(n, r, trials=10_000, seed=None):
"""
Estimate the expected number of throws with a Monte Carlo simulation.
Args:
n (int): Number of dice.
r (int): Threshold value (0 <= r < 6).
trials (int): Number of games to simulate.
seed (int, optional): Seed for reproducible results.
Returns:
float: Average number of throws needed across all games.
"""
rng = np.random.default_rng(seed)
total = 0
for _ in range(trials):
throws = 1
while rng.integers(1, 7, size=n).max() <= r: # no die above r: throw again
throws += 1
total += throws
return total / trials