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 → Plan → Code → 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.
What should mean([2, 4]) return?
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⊙ |
| What are the outputs? | luminosity: float, L⊙ |
| What could go wrong? | Negative mass, wrong units |
| How will I validate? | L(1.0) ≈ 0.698 |
Which contract clause rejects luminosity(0)?
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)
project/
├── analysis_final_v3.ipynb
├── final_code_really.py
├── tmp2.py
├── plot_latest_new.png
└── test_script.pyGood (clear responsibilities)
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
For mass = float("nan"), what do mass < 0.1 and mass > 100 return?
Both return False. Require the valid range instead:
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 computeA 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
Before plotting y=x2: where is the minimum, and what should x=−2 and x=2 give?
The minimum is (0,0); both inputs give y=4. Check these predictions on the plot.
The Anti-Pattern
Most students:
-
Write all the code
-
Debug until it runs
-
Make plots at the end
-
Discover something is fundamentally wrong
-
Start over 😱
The Right Way
Plot as soon as you have plottable output:
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:
def test_luminosity():
L = luminosity(1.0)
assert L == luminosity(1.0) # Tautology!Good test:
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:
-
Check output at the end — wrong?
-
Check output at the middle — wrong?
-
Yes → Bug in first half
-
No → Bug in second half
-
-
Repeat until found
With ordered checkpoints and a failure you can localize to one half, the number of checkpoint decisions grows as O(logn). 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:
# 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:
# constants.py (the ONLY place)
MSUN = 1.989e33
# In each caller
from constants import MSUNCommandment 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 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.
git log --oneline -- filename.py
git diff <baseline-commit> -- filename.py
git restore --source=<baseline-commit> -- filename.pyCommitted 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:
-
Stop typing
-
Go do something else — walk, shower, eat
-
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
-
Understand — Read specs, identify requirements
-
Plan — Write contracts, identify validation
-
Implement — One function at a time, validate immediately
-
Test — Encode requirements as tests
-
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]gives2; 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
def mean_bug(values):
return sum(values) / (len(values) - 1)
print(mean_bug([1.0, 3.0])) # 4.0, expected 2.0Predict 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
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
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
-
Write the input, output, domain and failure contract.
-
Pick one reference case you can calculate independently.
-
Run the smallest implementation that can fail that check.
-
State a hypothesis, predict the outcome, and change one cause.
-
Check limits, convergence and diagnostics relevant to the method.
-
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 → Plan → Code → Check
Plan the check before coding; run it after implementing.
Before Next Class
-
Read the full guide: Software Engineering for Scientists
-
Keep the 11 principles available while you work; printing them is optional
-
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?”