Skip to content

09 · Turn analysis into a tool

A reusable analysis has a narrow command-line boundary, pure computational functions, project-safe paths, useful errors, and an observable output.

Decision: can another person run the analysis without editing source?

Output: a command-line program with checked inputs and outputs.

Frame

This is not a reusable interface:

path = "/Users/name/Desktop/new/final/really-final/log.csv"

The path is a caller choice disguised as source code. Expose it:

comphy-python101 summary run/log.csv

A thin main

def main(arguments=None):
    args = build_parser().parse_args(arguments)
    log = load_basilisk_log(args.input)
    summary = summarise_log(log)
    print(json.dumps(summary, indent=2))
    return 0

main coordinates. Parsing and reduction remain testable without launching a subprocess.

The course CLI follows this shape:

"""Command-line front door for the reference workflows."""

from __future__ import annotations

import argparse
import json
from collections.abc import Sequence
from pathlib import Path

from .analysis import summarise_log
from .io import load_basilisk_log, load_regime_map
from .plotting import plot_log, plot_regime_map


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="comphy-python101",
        description="Validated teaching workflows for scientific Python.",
    )
    commands = parser.add_subparsers(dest="command", required=True)

    summary = commands.add_parser("summary", help="summarise a Basilisk log")
    summary.add_argument("input", type=Path)

    log_plot = commands.add_parser("plot-log", help="plot a Basilisk log")
    log_plot.add_argument("input", type=Path)
    log_plot.add_argument("--output", type=Path, default=Path("log.png"))

    regime_plot = commands.add_parser("plot-regime", help="plot an Oh–Bo map")
    regime_plot.add_argument("input", type=Path)
    regime_plot.add_argument("--output", type=Path, default=Path("regime-map.png"))
    return parser


def main(arguments: Sequence[str] | None = None) -> int:
    args = build_parser().parse_args(arguments)
    if args.command == "summary":
        print(json.dumps(summarise_log(load_basilisk_log(args.input)), indent=2))
    elif args.command == "plot-log":
        output = plot_log(load_basilisk_log(args.input), args.output)
        print(output)
    elif args.command == "plot-regime":
        output = plot_regime_map(load_regime_map(args.input), args.output)
        print(output)
    return 0

Command-line arguments are a schema

Use argparse to state:

  • required inputs;
  • optional outputs;
  • types;
  • help;
  • mutually exclusive choices where needed.

The resulting --help is part of the interface:

uv run comphy-python101 --help

An argument should change a meaningful choice. Do not expose every local variable merely because you can.

Paths without folklore

Use pathlib.Path:

output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)

Do not build paths with manual "/" concatenation. Do not assume the user's home directory or current working directory.

External programs must fail visibly

Scientific workflows often call compiled extractors:

subprocess.run(
    [str(extractor), str(snapshot), str(output)],
    check=True,
)

Use a list of arguments and check=True. Avoid shell=True with interpolated input: it adds quoting ambiguity and command-injection risk.

After a successful command, check that the expected output exists and is non-empty. A zero exit status is not evidence that the right file was written.

The public figure rebuilder in Soft-Matter-Singularities-paper-figures does this well. A public legacy visualiser, BasiliskVisualization/vView2D_v1.py, is a useful refactoring contrast: manual sys.argv, global state, hard-coded assumptions, nested array loops, and shell=True.

Exit status is an output

Return zero for success and non-zero for failure. Let unexpected exceptions produce a traceback during development. For expected user mistakes, give a short actionable message.

Do not catch everything:

try:
    ...
except Exception:
    pass

That converts evidence into silence.

Make repeated work restartable

For a case or snapshot pipeline:

  • give outputs deterministic names;
  • skip only when an output passes a validity check;
  • write partial outputs atomically where possible;
  • record failed case IDs;
  • avoid deleting raw inputs;
  • support selecting a subset from the CLI.

Parallelise across independent cases. On HPC, request a compute node and keep concurrency within allocated resources. The Jumping-Drops/postProcess notes make the important operational point explicit: single-node batch processing, not login-node work.

Verify

Complete Exercise 09, then run:

uv run comphy-python101 summary data/basilisk_log.csv
uv run comphy-python101 plot-log data/basilisk_log.csv \
  --output build/log.png

Then run the first command from another directory with an absolute input path. The command should still behave predictably.

Refactor a small personal plotting script so that:

  1. source paths become arguments;
  2. computation moves into a function;
  3. output is explicit;
  4. invalid input returns a useful failure;
  5. one test calls the function directly.

Reflect

If another student must open your source and edit three path variables before running it, you have shared code, not yet a tool.