Physics libraries, climate models, geophysical solvers, transport codes, lab-ware analyzers — scientific software written between 1980 and 2010 still runs because it still produces the right numbers. This guide is about the practical shape of modernizing such software: what to touch, what never to touch, and where organizations routinely lose years.
Frame it as three inventories
Before choosing a strategy, count three things:
- What has to keep running. Batch runs a production process depends on. These freeze; your first classroom of work grows around them.
- What people actually call. The ten to fifty routines that encode today's workflow, buried inside a main program that does a hundred other things.
- What the answers should be. Known configurations with known outputs. This becomes your numerical baseline — and without it no modernization is verifiable.
The strategy that does not burn the asset
Interface-first: keep the numerics compiled and untouched, and expose a chosen surface of them through modern interfaces — Python bindings first, then an HTTP service if the audience needs one, then a container when deployment stops being tribal knowledge. Every rung delivers value independently; the original program never stops working.
The acceptance criterion at every step is the same numerical question: does it still return the same answers? A recorded baseline makes that a checkbox; the absence of one makes it a research program.
What this looks like with tooling
nativegate generates the whole interface-first path from one configuration: f2py/pybind11 bindings, the CMake build, an installable wheel, pytest, a FastAPI service, a Dockerfile, and the golden-record regression harness. It refuses, with a stated reason, anything the source makes ambiguous — and on genuinely messy legacy dialects (fixed-form F77, INCLUDE decks, COMMON-block state) that refusals-versus-guesses distinction is where the real effort goes.
Classic traps, named
- Equating conversion with modernization. Translating Fortran to readable Python loses the validation story and adds a new codebase to maintain. Interfacing keeps the validated bit validated.
- Binding everything by default. A 49-routine deck becomes a worse SDK than the ten routines a caller needs. Expose a chosen surface; leave the rest reachable through it.
- Ignoring global state until concurrency. COMMON blocks and static globals make any multi-caller design a correctness question first. Answer it in the design, not the incident.
- Fragmenting languages. Scientific shops carry both C++ and Fortran. A pipeline that handles both ethically keeps the team's story coherent.
A first week that produces value
Inventory, then one golden baseline on one configuration, then one routine bound, verified, imported in CI. If that closed loop works on your hardest routine, scaling it is mechanical — and if it doesn't, you've spent a week to learn that your modernization problem needs preparation before tooling, which is also worth knowing.