Projects

Research software I built or contributed to, side projects I run on my own time, and the notes I write while learning a new field. Each entry says plainly what my role was.

Research software

antigen-prime

A forward-time epidemic simulator in which viruses evolve genetically and antigenically at once, under selection from the immune histories of the hosts they infect. It extends Trevor Bedford’s antigen with two things the original didn’t have.

The first is coupled evolution. Every virus now carries a nucleotide sequence alongside its coordinates in antigenic space, and mutations are drawn from a K80 model whose antigenic consequence depends on what kind of mutation it was: synonymous changes move the virus nowhere, nonsynonymous changes at epitope sites move it a long way, and nonsynonymous changes anywhere else move it essentially not at all. The second is host immunity sampling, which records what the host population has actually seen over the course of an epidemic — so the pressure a virus is escaping is something you can measure rather than assume.

The point of all of it is ground truth. To know whether a variant-assignment or growth-rate method can see a new antigenic variant coming, you need a world where you already know the answer.

My role: I wrote the coupled genetic–antigenic evolution and the host immunity sampling on top of Bedford’s simulator, and I’m first author on the manuscript (under review at Virus Evolution). Java 11 / Maven, GPLv3.

GitHub · Docs · Preprint


antigen-forecasting

The analysis half of the same project. A Python package (antigentools) and a pipeline that takes simulated epidemics and asks how well standard tools recover what actually happened: three ways of assigning variants — k-means in antigenic space, sequence embeddings via pathogen-embed, and Neher’s phylogenetic method — scored against the simulation’s true clades with normalized information distance, then growth rates inferred with evofr and compared against the truth.

It runs as an idempotent seven-step DAG with sentinel files, so a SLURM array that dies at step five resumes at step five instead of at step one.

My role: sole author — all 233 commits. Python 3.10, MIT licensed.

GitHub


polyclonal

A biophysical model of how polyclonal serum neutralizes a virus. It decomposes a serum into epitope-specific activities and infers, per mutation and per epitope, how much escape that mutation actually buys — which turns a single messy neutralization curve into a map you can read.

My role: co-first author on the paper describing the model, and a contributor to the package. Built in the Bloom lab.

GitHub · Paper


torchdms

PyTorch models for deep mutational scanning data, built around global epistasis: additive latent effects passed through a learned nonlinearity, with variants for modeling multiple phenotypes and for conditioning one phenotype on another.

My role: contributor. torchdms is also the subject of a chapter of my dissertation, and it’s the baseline I benchmark protein language models against in one of the projects below.

GitHub


Personal projects

bio-transformer-portfolio

Three experiments at the intersection of transformers and biology, sharing one infrastructure package (biotp: ESM-2 embeddings, zero-shot masked-marginal scoring, a LoRA and linear-probe fine-tuning harness, leakage-aware evaluation, and Hugging Face release helpers). The aim isn’t to beat a leaderboard. It’s to run the controls that tell you whether an improvement is real.

grounding-multimodalresults in hand. Does grounding a protein-sequence representation in functional-annotation text actually beat sequence alone, once controls rule out label leakage? On 2,773 held-out proteins from DeepLoc’s homology-partitioned test split, averaged over three seeds: sequence-only reaches 0.616 ± 0.004 macro-F1, sequence + free text reaches 0.740 ± 0.006. The control is what makes that mean something — shuffle the text so it no longer describes its protein and the score falls to 0.578 ± 0.006, below sequence-only. Short version: yes, mostly, and about a seventh of the gain is leakage.

dms-benchmarkin progress. When does a fine-tuned protein language model beat a simple structured baseline at predicting deep-mutational-scanning fitness, and how does the crossover move as labels accumulate? Every label is a wet-lab measurement, so the data-efficiency curve is the plot that matters. Zero-shot and fine-tuned arms are done; the structured baseline is pending.

epistasis-plm-torchdmsin progress. Masked-marginal scoring is additive over sites by construction: each term masks one position of the wild type, so nothing in the sum ever sees the other mutation, and a double mutant’s score is identically the sum of its singles. Measured deviation from additivity on real data: exactly 0.00e+00. That’s a property of the scoring rule rather than an empirical weakness of the model — which means a protein language model used zero-shot cannot represent epistasis at all. Can a supervised one, and how does it compare to torchdms? Trained on single mutants of the SARS-CoV-2 receptor binding domain, tested on the combinations.

My role: sole author. CI runs black, ruff, mypy and pytest on every push, plus a weekly network-marked test that re-checks ESM-2 embeddings against a fixed anchor, and the docs build under mkdocs --strict.

GitHub · Docs


Notes from learning in public

When I move into a new field I write the textbook I wish existed, and when I need to knock the rust off an old one I do the same thing. These four are study companions I built for myself and left public — two for fields I’m onboarding into, two for fields I already knew and wanted back in working memory. They’re drafts, they’re for me, and they’re written by a machine:

Each of these was drafted by a large language model with me directing, then checked against primary sources. They exist so their author can learn a subject by synthesizing its current literature. Treat them as well-cited study companions, not peer-reviewed authorities, and verify anything load-bearing against the sources they cite.

Onboarding into something new

Getting something old back