Software Engineering for Scientists

COMP 521

Anna Rosen · San Diego State University

Why This Matters

Most scientists learn to code by trial and error.

This works for small scripts, but fails for:

  • Code that must be correct (not just “seems to work”)

  • Code that others must read and trust

  • Code you must debug at 2am before a deadline

The Core Insight

Think →\to Plan →\to Code →\to Check

Plan the check before coding; run it after implementing.

Principles for scientific software

The 11 Commandments

Read these every week until they become instinct

Commandment 01 / 11

Think Before You Type

1. Think Before You Type

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

Write one expected result before implementing.

Predict

What should mean([2, 4]) return?

Explain

3: add the two values and divide by two.

Commandment 02 / 11

Write Down the Contract

2. Write Down the Contract

Before coding, answer in writing:

Question Example
What are the inputs? mass: float, [0.1, 100] M⊙M_\odot
What are the outputs? luminosity: float, L⊙L_\odot
What could go wrong? Negative mass, wrong units
How will I validate? L(1.0) ≈\approx 0.698
Predict

Which contract clause rejects luminosity(0)?

Explain

The input range: mass must be at least 0.1.

Commandment 03 / 11

Assume Your Code is Wrong

3. Assume Your Code is Wrong

Support correctness claims with tests and validation evidence.

Not “it runs without errors.”

Not “the output looks reasonable.”

Show me the evidence.

Three Kinds of Evidence

Name the claim a check probes. The command that runs it does not determine its evidence type.

Bad Repo vs Good Repo

Students usually copy structure before they copy principles.

Bad (hard to grade, hard to debug)

Repository layout
project/
├── analysis_final_v3.ipynb
├── final_code_really.py
├── tmp2.py
├── plot_latest_new.png
└── test_script.py

Good (clear responsibilities)

Repository layout
project/
├── run.py
├── src/
│   ├── model.py
│   └── physics.py
├── notebooks/
├── tests/
├── validation/
└── figures/

Reproducibility means another person can regenerate the reported results from the saved code, inputs, environment, and documented steps. Check that workflow from a clean clone.

Commandment 04 / 11

Fail Fast

4. Fail Fast

Predict

For mass = float("nan"), what do mass < 0.1 and mass > 100 return?

Both return False. Require the valid range instead:

Reject invalid mass
def luminosity(mass, Z=0.02):
    # Check inputs FIRST
    if not 0.1 <= mass <= 100:
        raise ValueError("Mass must be in [0.1, 100] M_sun")

    # ... then compute

A clear error at the input beats garbage output that “looks reasonable.”

Commandment 05 / 11

Plot First, Not Last

5. Plot First, Not Last

In software engineering: working software is progress.

In scientific computing: plots are currency.

  • Diagnose — Where does it go wrong?

  • Validate — Does the pattern match known trends?

  • Communicate — Papers, talks, reports

Predict

Before plotting y=x2y=x^2: where is the minimum, and what should x=−2x=-2 and x=2x=2 give?

Explain

The minimum is (0,0)(0,0); both inputs give y=4y=4. Check these predictions on the plot.

The Anti-Pattern

Most students:

  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:

Workflow
luminosity() -> Plot L vs M -> Look right?
    v YES
radius() -> Plot R vs M -> Look right?
    v YES
T_eff() -> Plot HR diagram -> Look right?
    v YES
Continue...

Each plot is a checkpoint.

Commandment 06 / 11

Test Requirements, Not Code

6. Test Requirements, Not Code

Bad test:

Python example
def test_luminosity():
    L = luminosity(1.0)
    assert L == luminosity(1.0)  # Tautology!

Good test:

Python example
def test_luminosity_solar_mass():
    L = luminosity(1.0)
    assert L == pytest.approx(0.698, rel=0.02)

Commandment 07 / 11

Debug with Hypotheses

7. Debug with Hypotheses

“I think X is wrong because Y”

Expected: f(x) = x**2. Observed: f(2) returns 2.

Hypothesis: the implementation is missing the square.

Changing that operation predicts f(2) == 4. Check this input before changing anything else.

The Binary Search Method

If you don’t know where the bug is:

  1. Check output at the end — wrong?

  2. Check output at the middle — wrong?

    • Yes →\to Bug in first half

    • No →\to Bug in second half

  3. Repeat until found

With ordered checkpoints and a failure you can localize to one half, the number of checkpoint decisions grows as O(log⁡n)O(\log n). Arbitrary bugs do not automatically satisfy that assumption.

Common Bug Symptoms

Symptom Likely cause
Off by factor of ~2.3 log vs log10
Off by powers of 10 Unit conversion
Wrong sign Subtraction order
NaN or Inf Division by zero, invalid domains, overflow, or earlier nonfinite input
Shape mismatch Scalar vs array

Commandment 08 / 11

Delete Bad Code

8. Delete Bad Code

You’ve spent 3 hours on a function. It’s ugly and buggy.

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

This is the sunk cost fallacy.

Those 3 hours are gone. What’s fastest from here?

Permission to Delete

You have permission to:

  • Delete functions that aren’t working

  • Rewrite modules that got tangled

  • Throw away your first approach entirely

  • Start fresh with what you learned

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

Commandment 09 / 11

One Source of Truth

9. One Source of Truth

Bad:

Python example
# file1.py
SOLAR_MASS = 1.989e33

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

If you change SOLAR_MASS in file1.py, will file2.py follow?

Good:

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

# In each caller
from constants import MSUN

Commandment 10 / 11

Read More Than You Write

10. Read More Than You Write

Professional developers spend more time reading than writing.

Before changing code:

  • Understand what it does now

  • Understand why it was written that way

  • Then modify

Commandment 11 / 11

Commit Before You Experiment

11. Commit Before You Experiment

Git commands
git status
git diff
git add path/to/changed_file.py
git commit -m "Save working baseline before experiment"

Committed: model.py. Untracked: notes.txt. Which file can you recover?

Only model.py. A commit protects the content staged in it.

If it goes badly: inspect before restoring
git log --oneline -- filename.py
git diff <baseline-commit> -- filename.py
git restore --source=<baseline-commit> -- filename.py

Committed content can be recovered. Uncommitted or ignored files are not protected by a commit; restoring a file replaces its current uncommitted contents.

Recover a useful next step

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

  3. Come back with fresh eyes

This isn’t procrastination. Your brain works on problems in the background.

Rubber Duck Debugging

Before asking for help, explain the problem out loud.

To a rubber duck. A stuffed animal. An empty chair.

The act of articulating often reveals the solution.

How to Ask for Help

Bad:

“My luminosity function gives wrong values. Help?”

Good:

“My luminosity(1.0) returns 0.45, but should be ~0.698. Here’s my code: [10 lines]. I checked: coefficients, log10 vs ln, units. What am I missing?”

Put the principles into practice

The Professional Workflow

The 5-Step Process

  1. Understand — Read specs, identify requirements

  2. Plan — Write contracts, identify validation

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

  4. Test — Encode requirements as tests

  5. Debug — Systematically, with hypotheses

The Professional Loop

Spec → Contract and planned checks → Implement → Run checks and diagnostics → Interpret → Iterate.

The Key Insight

Debug your understanding, not your code.

Understanding the problem gives you a contract and predictions to test. Implementation still requires careful work.

When repeated patches fail, revisit the assumptions and the smallest case you can explain by hand.

Worked example: specify the result first

We want the arithmetic mean of a nonempty sequence of finite real values.

  • Input: a sequence of finite numbers; the sequence is not modified.

  • Output: the arithmetic mean in the same units as the input.

  • Empty or nonfinite input: raise ValueError.

  • Independent checks: [1, 3] gives 2; one value returns itself.

This small example lets us practice the workflow before applying it to a numerical solver.

Worked example: a plausible implementation fails

Deliberately incorrect implementation
def mean_bug(values):
    return sum(values) / (len(values) - 1)

print(mean_bug([1.0, 3.0]))  # 4.0, expected 2.0

Predict the result for a one-element input before running it.

The result is finite and the program runs. Neither observation establishes correctness.

Worked example: test a causal hypothesis

Hypothesis: the denominator uses n - 1 instead of n.

For [1, 3], the sum is 4. The correct denominator is 2, so the mean is 2.

Changing only that denominator predicts that this test will pass. It does not yet address empty or nonfinite input.

Test the prediction first, then add the missing contract checks.

Worked example: implement the contract

A checked arithmetic mean
import math

def finite_mean(values):
    values = tuple(values)
    if not values:
        raise ValueError("At least one value is required")
    if not all(math.isfinite(x) for x in values):
        raise ValueError("All values must be finite")
    return math.fsum(values) / len(values)

The numerical range must also be representable in the chosen floating-point type. This is a teaching example, not an overflow-proof reduction.

Worked example: independent checks

Check requirements, not implementation
assert finite_mean([1.0, 3.0]) == 2.0
assert finite_mean([7.0]) == 7.0
assert finite_mean([4.0, 6.0]) == 5.0

import pytest

for values in ([], [float("nan")],
               [float("inf")], [-float("inf")]):
    with pytest.raises(ValueError):
        finite_mean(values)

The translated inputs [4, 6] shift the mean by 3. Each check has an expected result independent of calling the implementation twice.

Transfer the workflow to your project

  1. Write the input, output, domain and failure contract.

  2. Pick one reference case you can calculate independently.

  3. Run the smallest implementation that can fail that check.

  4. State a hypothesis, predict the outcome, and change one cause.

  5. Check limits, convergence and diagnostics relevant to the method.

  6. Save the configuration, commands and evidence needed to reproduce it.

Keep the original stellar examples as a second context: the same workflow applies, but the reference values and validity domain come from that model.

The Takeaway

Think →\to Plan →\to Code →\to Check

Plan the check before coding; run it after implementing.

Before Next Class

  1. Read the full guide: Software Engineering for Scientists

  2. Keep the 11 principles available while you work; printing them is optional

  3. Apply these principles to your current COMP 521 project

Questions?

Common questions:

  • “What if I’ve already started coding the wrong way?”

  • “How do I know when to rewrite vs. keep debugging?”

  • “What counts as a ‘good’ validation check?”