DEV Community

Om Kapale 🐙
Om Kapale 🐙

Posted on Edited on

The summer I deleted 700 lines and called it my best work

About PEcAn

The Predictive Ecosystem Analyzer (PEcAn) is a scientific workflow system to manage the immense amounts of publicly available environmental data, paired with a Bayesian data assimilation system to synthesize this information within state-of-the-art ecosystem models. Put simply: it turns messy trait measurements into ecosystem-model predictions that come with an honest, quantified error bar.

Project Summary
Organization: PEcAn Project
Mentor: Mike Dietze (mdietze)
Co-mentors: Chris Black (infotroph), Henry Priest, David LeBauer (dlebauer)
Contributor: Om Kapale (omkarrr2533)
Project Duration: 350 hours
Project Title: Optimizing PEcAn's uncertainty pipeline for standalone, in-memory use

A huge thank you to my mentor Mike Dietze for carefully listening to my approaches and sharpening them into something much better, and to my co-mentors Chris Black, Henry Priest, and David LeBauer for the review time and for being blunt in exactly the right places.

About the Project

PEcAn's strength comes from a chain of tightly connected steps: pull trait data from the database, run a Bayesian meta-analysis, sample parameters, build a simulation design, write a configuration file per run, and read the results back. But those steps historically spoke to each other through the filesystem each function wrote its output to a file on disk and the next one read it back which made the individual steps hard to reuse, test, or call on their own.

The objective of this project was to re-loosen those couplings. Each function was split into a pure core that takes R objects and returns R objects, and a thin wrapper that preserves the old file-based behaviour so no existing script breaks. Saving became opt-in: pass an output directory to persist everything for provenance, or leave it out and nothing touches disk. The result is a pipeline whose modules are independently operable, easy to test, and composable in memory without breaking a decade of existing workflows.

In March I opened my first pull request on a codebase I did not understand, in a language I could not really read, for a science I knew nothing about.

Five months later I deleted a function that had lived in that codebase for over a decade the most satisfying thing I have ever done in code. The maintainer who asked me to delete it was the same person whose blunt review had stung me two weeks before, and that review turned out to be the best thing that happened to my work all summer.

The whole distance between those two sentences was covered by showing up and reading carefully. I started knowing none of it. If you have ever looked at a serious open-source project and thought not for someone like me, this is the story of why it is exactly for you.

Finding PEcAn

PEcAn is software ecologists use to run ecosystem models. You point it at a forest, tell it which model to use, and it pulls trait measurements out of a database, runs a Bayesian meta-analysis to work out what each plant parameter probably is, samples from those distributions, decides which combinations to simulate, writes a configuration file per run, launches the model, and reads everything back.

The output is not "this forest takes up 3 kg of carbon per square metre". It is that number with an honest error bar, plus a ranked list of which parameter you would need to measure better to shrink it.

I had never seen software where being uncertain correctly was the entire
product. Most code answers a question; this code tells you how much to trust the answer and what to measure next. I found that genuinely thrilling the week I discovered it, and four months of living inside it later, I still do. That is what made me stay.

Before the application

I did not want to write a proposal for a codebase I had only skimmed, so I spent March and April contributing to it instead and that turned out to be the best decision of the whole journey.

The first thing I did was pull one function out of the meta-analysis
pipeline
and give it a version that took arguments and returned a value, instead of reading and writing files. That one small pull request became the pattern the entire summer was built on. Then regression tests for the trait
extraction
, tests for the meta-analysis pipeline, and a couple of bug fixes in the REST API — plus a stray one in a diagnostic plot that I found just by poking around and reading.

Five merged pull requests before the proposal deadline. More importantly, five conversations with the maintainers, so by the time I wrote the proposal I was proposing something I had already started, to people who already knew how I worked.

If you are reading this while deciding whether to apply somewhere or just whether to open a first pull request anywhere this is the part that mattered. Not the proposal document. Five small contributions taught me more, and opened more doors, than any amount of planning could have. And you could start that this week.

The problem, counted

The pipeline worked beautifully. But the steps talked to each other through the filesystem.

One function computed parameter samples, saved them to samples.Rdata, threw them away, and the next function opened that file and read them back. The trait step wrote a file for the meta-analysis to find. The meta-analysis wrote a file for the sampling step to find. Every handoff was a filename that two functions had privately agreed on.

During community bonding, instead of rushing to code, I did something that felt almost too simple to count as work: I went through the pipeline and counted. Forty filesystem interactions across nine files.

That audit was the smartest thing I did all summer, and it is exactly the kind of thing anyone can do on day one. It turned a vague feeling of "this is messy" into a concrete list my mentor and I could actually argue about, and it told me which of the forty were load-bearing and which were incidental. Half the good design decisions I made later came from having that one list in front of me.

Making it boring on purpose

The plan we settled on was unglamorous, which is usually a great sign. Split each function in two: a pure one that takes R objects and returns R objects, and a thin wrapper that keeps the old file behaviour so nobody's existing script breaks.

The part I loved was making saving opt-in. Pass an output directory and it saves everything for provenance; leave it out and nothing touches disk. That distinction matters more in science than in most software. A researcher has to be able to reconstruct exactly which parameter values produced the figure in their paper. The goal was never "stop writing files", it was "stop making files the only way two functions can speak".

Before-and-after diagram of PEcAn pipeline architecture showing file-based vs in-memory data passing

The whole change in one picture. Before, every handoff is a file two functions privately agreed on forty filesystem interactions across nine files. After, the steps pass R objects directly, and writing to disk is opt-in: pass an output directory for provenance, or nothing touches disk.

Through May and June the trait extraction moved, then the
meta-analysis
, then parameter sampling, then integration tests, then [the design generators (https://github.com/PecanProject/pecan/pull/4042). One merged pull
request at a time, each small enough to actually review. Watching the pipeline get cleaner PR by PR is a very specific kind of fun.

I passed my midterm evaluation feeling like I had this figured out.

Then I broke the development branch for someone else and it turned into one of my favourite days of the summer.

The afternoon I broke the build, and found the real skill

A maintainer pinged me: my merged change had broken his run. No drama, no
verdict just "hey, this stopped working." That is the first wonderful thing to know about a good project: the safety nets are there, people are kind about it, and a broken build is a normal Tuesday, not a judgment on you.

The immediate cause was almost funny. I had put the samples.Rdata write inside the block that generates the design. It reads fine until you notice the file has nothing to do with where the design came from. It exists for the analysis steps at the end, which need it either way. So if you built your own design and handed it in, the generation block was skipped, the file was never written, and the next step went looking for something that did not exist.

Annoying, my fault, an afternoon to fix. And then I kept digging, and found the much more interesting thing underneath.

The design's param column is just an index. Row one means "use parameter draw one". The actual values live in a separate samples object. So if you handed in a design without the samples it was built from, the code would quietly pair it with a completely fresh set of draws instead.

No error. No crash. A different set of simulations than the one you asked for, producing output that looks perfectly reasonable the kind you would put in a paper.

I think about that one with something close to delight now, because chasing it taught me the instinct the whole rest of the summer ran on. A crash is a gift: it tells you loudly and immediately to stop. The subtle ones hand you a plot instead. Learning to feel the difference is one of the most valuable things you can pick up as a programmer, and PEcAn is a wonderful place to pick it up in a tool whose entire purpose is quantifying uncertainty, catching a silent wrong answer is not a chore, it is the mission.

The fix was to resolve the samples once, at the top, before any branching, and write the file from that regardless of where the design came from. A design that turns up without its samples is now rejected outright rather than silently repaired.

The maintainer who found it had one small condition: settle the shape once, so external callers do not have to migrate twice. That single sentence sharpened how I made every decision afterwards.

The tiny naming decision that taught me the most

Someone asked whether the design object could have a friendlier name than X, which comes from the Sobol code and means nothing to anyone who did not write it. A five-minute job, I thought.

Obvious answer: input_design. That is what every function in the codebase calls the argument. I had it typed out.

Then I saw the trap, and it was the good kind — the kind that makes you feel like a detective. After the earlier change, the thing you pass as input_design is the whole object, design plus samples. If the matrix inside were also called input_design, the names would line up beautifully and invite you to write input_design = result$input_design, which is exactly the bare design that now gets rejected. The friendly name would have quietly pointed everyone straight at the one call that fails.

So I [went with design_matrix (https://github.com/PecanProject/pecan/pull/4060),
which both generators already used internally and which the docs already used in prose, and kept X alongside it so nothing broke. I checked that a half-updated install still worked in both directions, and that the data assimilation code, which reads the result positionally rather than by name, still got what it expected.

A tiny decision and the moment I stopped choosing names by feel and started going to look at what the code already said. That is a small superpower, and it transfers to every codebase you will ever touch.

The notebooks earned their keep in four minutes

My mentor wanted the tutorial notebooks updated to the new
pattern
, partly so new users see the current shape and partly because rendering them in CI acts as an end-to-end test.

The very first render failed and I was thrilled, because it caught something no unit test had. Not my change, but something that had been sitting on the development branch: the ensemble generator sampled with the sensitivity quantiles hardcoded to null, while the workflow helper sampled using the quantiles from the settings. Same settings, two different sample sets, depending on which one happened to do the sampling.

Every unit test passed, because each function was individually correct. Running the actual notebook end to end did not. I have happily quoted that at people several times since: correct in isolation is not the same as correct together, and the notebooks are what proved it.

The review that unlocked my best work

Near the end of the summer I put up a change I was genuinely proud of: it routed a sensitivity design to the right slot in the config writer, verified across every combination I could think of.

My mentor's reply was that it was a very superficial fix which preserved the duplication he had asked me to remove, and that what he actually wanted was for the sensitivity config writer to disappear entirely, because once you have a design it should not matter which function generated it.

My first instinct was to explain why my version was reasonable. I am so glad I did not because the moment I went to find out why his was hard instead, the whole thing cracked open. This is the plot twist of the entire summer: the review I least wanted to read is the one that led to the work I am proudest of.

For writing config files he was completely right: it makes no difference where the design came from. Where it made a difference was reading results back. The sensitivity writer was building a lookup table as it wrote, mapping each parameter and quantile to a run id, and the analysis steps read that table to know which run is which. The spline fits and the variance decomposition are built on it.

So the writer was not doing anything special when writing. It was producing that mapping as a side effect. And it had to be a side effect, because the design carried no information about what its own rows were.

Once I could see that, the whole thing unlocked. Let the design describe itself: which row is the median run, and which parameter each other row moves to which quantile. From those labels you can build the parameter sets, the run names, the database entries and the lookup table before writing anything at all which leaves the sensitivity writer with nothing the ensemble writer cannot already do.

So it went. Roughly seven hundred lines, replaced by about seventy — the
change
is up for review as I write this, not yet merged.

Deleting that function is the most satisfying thing I have done in a codebase. Not because deletion is clever, but because it only became possible after understanding what the code was actually for — and that understanding came entirely from a review that stung. The best part is that this is available to anyone. The unlock did not come from being brilliant. It came from reading the code until it told me what it was really for.

The thing I am quietly proudest of is that it does so without breaking the
analysis steps. The file they read keeps exactly the same contents; only the thing producing it changed. Moving those steps onto the design directly is worth doing next, but it did not have to happen in the same commit, and pretending it did would have meant shipping nothing.

Three things CI knew that I did not

I could not run the full stack on my laptop Windows on ARM, a scientific R stack with geospatial and MCMC dependencies, and a database. I lost a couple of days to that early on before doing the smart thing instead: mock the heavy packages, run the unit tests locally, and let CI be the real environment. That setup is worth building on day one, and it meant CI became less of a gatekeeper and more of a tireless extra reviewer.

I verified each piece by hand against the original behaviour: same parameter values per run, same run ids, same lookup table, checked across single and multiple plant types. That was worth doing. It was also not quite enough and CI cheerfully caught the rest.

It found three more. The run manifest needed each run's parameter and quantile, and mine was leaving them blank, so the analysis could not locate a single run. The median run needs a row per parameter rather than one untagged row, because one run covers every parameter's median and the analysis looks runs up by parameter.

And the good one. With no ensemble block in the settings, the ensemble writer replaces the samples with copies of the first row — exactly right when the samples are one parameter set meant to be repeated, and exactly wrong when the caller has already built one row per run. Left alone, every sensitivity run would have come out identical. Silently, obviously.

Same class of bug as the one I met in July, found the same way: by asking what happens to the values, not whether the code runs. Once you have that question, you never lose it.

Where it stands

Twenty-five pull requests over the summer, fifteen on this project and the rest bug fixes and cleanup elsewhere in the codebase twenty-four of them merged, and the last, the config-writer collapse this whole post builds toward, up for review rather than merged as I write this. Trait extraction, the meta-analysis, parameter sampling, design generation and config writing all pass R objects now. Both tutorials run the current pattern and double as end-to-end tests, one showing an ensemble and one showing a sensitivity analysis, which is the split the architecture now expects.

Carbon Pools Over Time chart showing ensemble output with ten parameter draws

Demo 1 ensemble run output showing uncertainty spread across ten simulation lines

Demo 1, running as a ten-member ensemble on the current pattern. Each of the ten lines is one parameter draw; the spread between them is the uncertainty the whole pipeline exists to quantify — now produced by a notebook that passes R objects end to end and doubles as a CI test.

And here is the part I am most excited to hand off: the analysis steps still read their run lookup from a file rather than from the design, which is the next real piece and the one that would let the last sensitivity-specific branches go. The workflow scripts also still let the pipeline build their design internally, which works but now warns. I have written up the whole sequence so whoever picks it up is not starting from scratch. It is a genuinely satisfying thread to pull — and it could absolutely be you.

What I would tell myself in March

Contribute before you apply. Everything good that happened this summer traces back to five pull requests I opened before I had any reason to.

Go and read the code. Almost every good decision I made came from looking at what the codebase already did, and almost every wobbly one came from guessing at what it probably did.

When someone says your work is not what they asked for, do not rush to explain why yours is reasonable. Go find out why theirs is hard. The best work I did all summer came directly out of the review I least wanted to open.

And do not wait until you know a language to use it. I learned enough R to be dangerous in a week and enough to be useful over four months, entirely by reading functions that better programmers had already written. The skill that actually mattered was never syntax. It was reading an unfamiliar codebase carefully enough to find the contract nobody had written down — and that skill is yours the moment you decide to use it.

Thank you to Mike Dietze, Chris Black, Henry Priest and David LeBauer for the review time and for being blunt in exactly the right places, and to PEcAn for taking a chance on someone who started the year unable to read the language the project is written in.

I would do it again tomorrow.

Your turn

If any of this made you want to try it: do. Here is the honest, low-drama way in — the same one that worked for me with zero background in R or ecology.

  • Start absurdly small. Find a rough edge and smooth it: a missing test, a confusing error message, a doc that lies a little. Browse the open issues and look for the good first issue label. My very first merged PR was one extracted function. That was enough to be on the map.
  • Read one function all the way to the end. Not the whole codebase — one function, until you actually understand what it does. That single habit carried me the entire summer.
  • Open the pull request before you feel ready. You will learn more from one real review than from a month of reading in private. The reviews here are blunt in the best way, and they are how you level up fast.
  • Say hi. The maintainers genuinely want you there. Get the tools running with the documentation, join the project Slack (the invite is linked from the repo), and if you are a student, the GSoC ideas list is a great map of where help is wanted.

You do not need to know R. You do not need to know ecology. You need to be willing to read carefully and open one small thing. That is the whole barrier — and it is far lower than it looks from the outside. I know, because in March I was standing exactly where you are.


The pull requests

The work product, as merged (or open) code on
PecanProject/pecan, authored by
omkarrr2533.

Before the application

During the summer

Top comments (1)

Collapse
 
chaturthig52702 profile image
Chaturthi Gavande •

okay this was actually so motivating ,one thing i always think is that if we want to enter a large organisation or work on a serious project, we have to be an expert first. this post really explained why that's not necessarily true, and i loved that part. thanks for sharing this btw! some of the terms were definitely above my current level 😂 but it was still really good and motivating to read....