Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Software Engineering for Scientists

Full companion reading · COMP 521

This companion retains the extended COMP 536 examples and eleven commandments. For COMP 521 requirements and the transit-project examples, use Scientific Software: From Claim to Evidence. The current project brief determines required commands, filenames and deliverables.

Open the slides and handout for the lecture’s short examples and arithmetic-mean walkthrough.

Why This Matters

Most scientists learn to code by trial and error. This works for small scripts, but fails catastrophically for:

This guide covers what CS students learn over 4 years, compressed into what you actually need.


TL;DR — Read This Every Week

Read this list before every coding session until it becomes instinct.


Part 0: The Reproducibility Contract (COMP 536)

In COMP 536, reproducibility is not an “extra” — it is a core requirement. Your code must be runnable by someone who has never met you, on a clean clone, without clicking around or guessing which cell to run.

The following was the COMP 536 Project 1 contract. It is retained as a worked example, not a COMP 521 grading requirement:

Concretely, that means you should be able to run:

python run.py validate
python run.py test
python run.py make-figures

If you build your workflow around these commands, everything else in this guide becomes easier: debugging is faster, your repo stays reviewable, and CI can run your project automatically. See Project 1 for the exact submission contract.

Part 1: Think Before You Code

The #1 Mistake

“Let me just start coding and figure it out as I go.”

This is how you end up debugging for 6 hours instead of thinking for 20 minutes.

Professional developers spend more time reading and thinking than typing. The keyboard is the last step, not the first.

The Pre-Coding Checklist

Before you write a single line of code, answer these questions in writing (paper, notes app, comments — anywhere):

QuestionWhy it matters
What are the inputs?Types, units, valid ranges, edge cases
What are the outputs?Types, units, format, precision
What could go wrong?Invalid inputs, edge cases, numerical issues
How will I know it’s correct?Test cases, validation values, sanity checks
What are the units?CGS? SI? Solar units? Mixed?

Example: Before Implementing L(M)L(M)

Bad approach: “I’ll just translate the equation to Python.”

Good approach:

INPUTS:
- mass: float or array, in solar units (M/M_sun)
- Z: float, metallicity, range [0.0001, 0.03]

OUTPUTS:
- luminosity: same shape as mass, in solar units (L/L_sun)

WHAT COULD GO WRONG:
- Mass outside valid range [0.1, 100] — formula becomes unphysical
- Z outside range — coefficients not calibrated
- Negative mass — physically meaningless
- Array vs scalar confusion

HOW I'LL KNOW IT'S CORRECT:
- L(1.0 M_sun) ~ 0.698 L_sun (from validation table)
- L increases with M (physical requirement)
- L at low Z > L at high Z for same mass

Now you can code. You know what to check, what to handle, what to test.


Part 2: Contracts and Invariants

What is a Contract?

A contract is a formal agreement between your function and its callers:

Think of it like a legal contract: “If you give me valid inputs (preconditions), I guarantee valid outputs (postconditions).”

Example Contract

def luminosity(mass, Z=0.02):
    """ZAMS luminosity from Tout et al. (1996) Eq. 1.

    CONTRACT:
    ---------
    Preconditions:
        - 0.1 <= mass <= 100 (solar units)
        - 0.0001 <= Z <= 0.03
        - mass is float or numpy array

    Postconditions:
        - Returns luminosity in solar units (L/L_sun)
        - Output shape matches input shape
        - All values are positive
        - L increases monotonically with M

    Invariants:
        - Uses Tout et al. (1996) coefficients (never modified)
        - Z_sun = 0.02 (fixed reference)
    """

Why Contracts Matter

  1. You know what to validate: Check preconditions at function entry

  2. You know what to test: Test that postconditions hold

  3. You know what NOT to change: Invariants are sacred

Enforcing Contracts in Code

def luminosity(mass, Z=0.02):
    # Enforce preconditions (fail fast)
    mass = np.asarray(mass)
    if not np.all((0.1 <= mass) & (mass <= 100)):
        raise ValueError("Mass must be in [0.1, 100] M_sun")
    if not 0.0001 <= Z <= 0.03:
        raise ValueError("Z must be in [0.0001, 0.03]")

    # ... compute result ...

    # Postcondition check (optional, for debugging)
    assert np.all(result > 0), "Luminosity must be positive"
    assert result.shape == mass.shape, "Shape mismatch"

    return result

Part 3: Reasoning from Specs

What “Specs” Means

A specification is a precise description of what code must do. It can be:

The Process

  1. Read the spec carefully — Every word matters

  2. Identify the requirements — What MUST the code do?

  3. Identify the constraints — What are the limits?

  4. Derive test cases — How would you verify each requirement?

  5. Then implement — Translate requirements to code

Example: Reading a Paper as a Spec

From Tout et al. (1996):

“The fits are valid for 0.1≤M/M⊙≤1000.1 \leq M/M_\odot \leq 100 and 0.0001≤Z≤0.030.0001 \leq Z \leq 0.03”

What this tells you:

“The coefficients are given in Table 1”

What this tells you:

Don’t Guess — Derive

Bad: “I think it should return an array...”

Good: “The spec says input can be array-like, so output must match input shape.”

Bad: “This probably uses natural log...”

Good: “Equation 3 explicitly shows log⁡10\log_{10}, so I’ll use np.log10().”


Part 4: Validation-First Development

The Mindset Shift

Support correctness claims with tests and validation evidence.

Not “assume it works if it runs.” Not “assume it’s correct if the output looks reasonable.”

Assume it’s wrong. Then test that assumption with independent evidence.

The Validation Workflow

  1. Before coding: Identify validation checkpoints

  2. After each function: Run validation immediately

  3. Before making plots: Validate the underlying data

  4. Before submission: Run full validation suite

What Makes a Good Validation Check?

Good validationBad validation
Compare to known values from paper“The plot looks right”
Check physical trends (L increases with M)“It runs without errors”
Verify edge cases (M = 0.1, M = 100)“The numbers seem reasonable”
Cross-check with independent calculation“It matches my intuition”

Example Validation Strategy

def validate_luminosity():
    """Validate luminosity function against known values."""

    # Anchor check: known value from paper
    L_solar = luminosity(1.0)
    assert abs(L_solar - 0.698) < 0.01, f"L(1 M_sun) = {L_solar}, expected 0.698"

    # Trend check: L increases with M
    masses = np.logspace(-1, 2, 100)
    L = luminosity(masses)
    assert np.all(np.diff(L) > 0), "L must increase with M"

    # Physics check: low Z -> higher L
    L_solar_Z = luminosity(1.0, Z=0.02)
    L_low_Z = luminosity(1.0, Z=0.0001)
    assert L_low_Z > L_solar_Z, "Low-Z stars should be more luminous"

    print("All validation checks passed!")

Part 5: Debugging Systematically

The Wrong Way to Debug

  1. Stare at code hoping to spot the bug

  2. Add random print statements everywhere

  3. Change things and see if it helps

  4. Google the error message and copy-paste solutions

The Right Way to Debug

Step 1: Reproduce the bug

Step 2: Isolate the bug

Step 3: Form a hypothesis

Step 4: Test the hypothesis

How to Read a Stack Trace (Traceback)

When Python crashes, the traceback is telling you a story: which function called which function, and where things went wrong.

Use this rule of thumb:

  1. Read bottom-up. The last line is the exception type and message.

  2. Find the first frame in your code. Skip frames inside libraries unless the bug is truly in the library.

  3. Capture the inputs that triggered it. Your next move is to reproduce the crash with the smallest possible input.

Use a Debugger When Print Isn’t Enough

Print statements are fine for quick checks, but if you need to inspect state step-by-step, use a debugger.

The simplest built-in option is breakpoint():

def luminosity(mass, Z=0.02):
    mass = np.asarray(mass)
    breakpoint()  # inspect variables, then step through
    # ...

If you run your code and it drops into the debugger, you can inspect variables, step line-by-line, and test hypotheses quickly. If you prefer a GUI, VS Code’s debugger does the same thing with a visual interface.

The Binary Search Method

If you don’t know where the bug is:

  1. Check the output at the end — is it wrong? (Yes)

  2. Check the output at the middle — is it wrong?

    • Yes -> Bug is in first half

    • No -> Bug is in second half

  3. Repeat until you find the exact line

With ordered checkpoints and a failure that can be localized to one half, checkpoint decisions grow as O(log⁡n)O(\log n). Arbitrary bugs do not automatically satisfy that assumption.

Common Bug Categories

SymptomLikely cause
Off by factor of ~2.3log vs log10 confusion
Off by powers of 10Unit conversion error
Wrong signSubtraction order, angle convention
NaN or InfDivision by zero, log of negative
Shape mismatchScalar vs array confusion
“Looks reasonable but wrong”Coefficient transcription error

Part 6: Testing That Actually Tests

What Tests Are For

Tests encode requirements as code. A passing test suite means the implemented checks passed for the tested cases; omitted requirements and untested cases remain open.

What Tests Are NOT For

Good Tests vs. Bad Tests

Bad test: Tests that the code does what the code does

def test_luminosity():
    # This tests nothing — just verifies code runs
    L = luminosity(1.0)
    assert L == luminosity(1.0)  # Tautology!

Good test: Tests that the code meets requirements

def test_luminosity_solar_mass():
    # Tests against INDEPENDENT known value from paper
    L = luminosity(1.0)
    assert L == pytest.approx(0.698, rel=0.02)

Good test: Tests physical constraints

def test_luminosity_increases_with_mass():
    # Tests that physics makes sense
    masses = np.logspace(-1, 2, 100)
    L = luminosity(masses)
    assert np.all(np.diff(L) > 0)

Good test: Tests error handling

def test_invalid_mass_raises():
    # Tests that preconditions are enforced
    with pytest.raises(ValueError):
        luminosity(-1.0)

Test Independence

Each test should:


Part 7: Code Organization

Single Source of Truth

Every piece of information should live in exactly ONE place.

Bad:

# In file1.py
SOLAR_MASS = 1.989e33

# In file2.py
SOLAR_MASS_G = 1.989e33  # Same value, different name!

# In file3.py
mass_solar = 1.989e33  # Copied again!

Good:

# In constants.py (the ONLY place)
MSUN = 1.989e33

# In every other file
from constants import MSUN

Separation of Concerns

Each module should do ONE thing:

ModuleResponsibility
constants.pyDefine constants (no computation)
zams.pyCompute L(M,Z) and R(M,Z)
star.pyRepresent a single star
astro_plot.pyCreate plots (no physics)
run.pyCLI interface (no logic)

Why This Matters


Part 8: Delete, Rewrite, Iterate

The Sunk Cost Trap

You’ve spent 3 hours on a function. It’s ugly, buggy, and you hate it. But you keep patching it because:

“I’ve already put so much time into this...”

This is the sunk cost fallacy. Those 3 hours are gone whether you keep the code or not. The only question is: what’s the fastest path to working code from here?

Often the answer is: delete it and start over.

Why Rewriting is Faster

Your first attempt taught you:

Armed with this knowledge, your second attempt will be:

What Students Get Wrong

FearReality
“I’ll lose my work”Git preserves committed content. Uncommitted and ignored files need separate protection.
“Starting over means I failed”Starting over means you learned.
“I’m so close to fixing it”You’ve been “so close” for an hour.
“The new version might have bugs too”Yes, but different bugs you’ll understand.

Permission to Delete

You have permission to:

Code is cheap. Your time and sanity are expensive.

Before deleting, preserve the failing input and its test. Git protects content saved in a commit; untracked and ignored files need separate protection.

The Iteration Mindset

Professional software development is iterative:

  1. Write something that works (ugly is fine)

  2. Validate that it’s correct

  3. Refactor to make it clean

  4. Repeat as requirements evolve

Your first version is a draft, not a final product. Drafts get rewritten. That’s the process, not a failure.


Part 9: Getting Unstuck

The Walk Away Rule

If you’ve been stuck for 30+ minutes:

  1. Stop typing

  2. Go do something else — walk, shower, eat, sleep

  3. Come back with fresh eyes

This isn’t procrastination. Your brain continues working on problems in the background. The breakthrough often comes away from the keyboard.

Rubber Duck Debugging

Before asking for help, explain the problem out loud — to a rubber duck, a stuffed animal, an empty chair, or a wall.

The act of articulating the problem often reveals the solution. You’ll say “and then this happens because...” and suddenly realize that’s exactly where the bug is.

This works because:

How to Ask for Help (The Right Way)

When you do need help, a good question gets answered faster. A bad question gets “what have you tried?”

The Minimal Reproducible Example (MRE):

  1. Minimal — Smallest code that shows the problem

  2. Reproducible — Someone else can run it and see the issue

  3. Example — Actual code, not “my function doesn’t work”

Bad question:

“My luminosity function gives wrong values. Help?”

Good question:

“My luminosity(1.0) returns 0.45, but the paper says it should be ~0.698. Here’s my code: [10 lines that reproduce the problem]. I’ve checked: coefficients match Table 1, using log10 not ln, mass is in solar units. What am I missing?”

What to Include When Asking for Help

IncludeWhy
What you expected“I expected L ~ 0.698”
What you got“I got L = 0.45”
What you tried“I checked X, Y, Z”
Minimal code10-20 lines that reproduce it
Error message (if any)Exact text, not paraphrased

The better your question, the faster the answer. Putting effort into the question often solves it yourself.


Part 10: Plot First, Not Last

Plots Are Currency

In software engineering, working software is the measure of progress.

In scientific computing, plots are currency — they’re how you:

A function that runs without errors means nothing. A plot showing correct behavior means everything.

For example, suppose the expected relation is y=x2y=x^2. The implementation y = 2*x**2 produces a smooth, symmetric parabola, but at x = 2 it gives 8 instead of 4. Compare the plot with the predicted values as well as its shape.

The Anti-Pattern

Most students do this:

  1. Write all the code

  2. Debug until it runs

  3. Make plots at the end

  4. Discover something is fundamentally wrong

  5. Start over

The Right Way

Plot as soon as you have plottable output:

Implement luminosity() -> Plot L vs M -> Does it look right?
    -> YES
Implement radius() -> Plot R vs M -> Does it look right?
    -> YES
Implement T_eff -> Plot HR diagram -> Does it look right?
    -> YES
Continue...

Each plot is a checkpoint. If it looks wrong, stop. Debug now, while you know exactly which function caused the problem.

What “Looks Right” Means

You’re not just looking for “a curve.” You’re checking:

CheckWhat to look for
TrendDoes it go up/down as physics predicts?
MagnitudeIs the y-axis in the right ballpark?
SmoothnessAre there weird kinks or discontinuities?
EndpointsDo the edges behave sensibly?
ComparisonDoes it match the paper’s figure?

Appendix: Agile Principles for Scientists

The Agile Manifesto (Paraphrased for Science)

Agile principleFor scientists
Working software over comprehensive documentationA running script beats a perfect plan
Responding to change over following a planYour understanding evolves — let your code evolve too
Individuals and interactions over processes and toolsTalk to your advisor/collaborators early and often
Customer collaboration over contract negotiationGet feedback on results before polishing details

The Key Insight: Iterate

Agile’s core insight is that you can’t know everything upfront. Instead of planning everything, then building everything, then testing everything:

Plan a little -> Build a little -> Test a little -> Repeat

This is why we emphasize:

You’re not building a cathedral. You’re iterating toward correctness.


Summary: The Professional Workflow

  1. Understand — Read specs, identify requirements

  2. Plan — Write contracts, identify validation checks

  3. Implement — One function at a time, validate immediately

  4. Test — Encode requirements as tests

  5. Debug — Systematically, with hypotheses

Think → Plan → Code → Check. Plan the checks before coding; execute them after implementing.

In software engineering terms: Spec → Contract and planned checks → Implement → Run checks and diagnostics → Interpret → Iterate.