PyRPOD has a formal operational logging system built entirely on the Python
standard library (logging, hashlib, tracemalloc, subprocess, ...). It
records what a run did — which case and assets were used, their checksums,
workflow phases, per-firing progress, performance, and the final status — so a
plume-strike run is fully traceable and reproducible.
Operational logs are not scientific result files. Full numerical fields (per-face strike/pressure/shear/heat-flux arrays, JFH records, meshes) stay in the existing VTK/STL/CSV/JFH outputs. The runtime log contains only summaries, paths, dimensions, input checksums, and performance data.
Importing pyrpod (or any submodule) has no logging side effects: no log
files, no directories, no handlers, no root-logger changes, no output. Modules
obtain a logger the standard way and never configure handlers:
import logging
logger = logging.getLogger(__name__)An application turns logging on explicitly at its boundary.
from pyrpod.logging_utils import configure_logging
case_dir = "case/rpod/base_case/"
session = configure_logging(case_dir)
try:
# ... build the study and run the PyRPOD workflow ...
# e.g. study.jfh_plume_strikes()
session.finalize("successful")
finally:
session.close()configure_logging attaches PyRPOD-owned handlers to the pyrpod package
logger (never the root logger), opens a fresh runtime log, writes the startup
metadata block, and snapshots the case configuration. session.close() detaches
and closes only the handlers PyRPOD installed; handlers belonging to an
embedding application are never inspected or modified. Calling
configure_logging again replaces PyRPOD's own handlers, so messages and
handlers are never duplicated.
Several case/rpod/ cases ship a ready-to-run driver and a logging.ini that
each highlight a different capability. They mirror the corresponding
tests/rpod/ cases minus the assertions:
python case/rpod/base_case/run.py # default INFO, console + file
python case/rpod/1d_approach/run.py # progress_every_n_firings = 2
python case/rpod/koz/run.py # level = DEBUG (paths, array summaries)
python case/rpod/hollow_cube/run.py # console = false (file only)
python case/rpod/multi_thrusters_square/run.py # kinetics on: per-firing pressure/shear/heat-flux
python case/rpod/stl_to_vtk/run.py # asset + directory logging around STL -> VTKEach writes its runtime log to <case>/results/logs/<case>_<timestamp>.log.
Each case may optionally contain <case_dir>/logging.ini. When absent, the
built-in defaults are used and PyRPOD logs that it fell back to defaults. See
docs/logging.ini.example for a fully commented file.
[logging]
enabled = true
console = true
file = true
level = INFO
progress_every_n_firings = 1
log_array_stats = true
log_performance = true
snapshot_config = true
checksum_inputs = true
# format = %(asctime)s [%(levelname)s] %(name)s: %(message)sconsoleandfileare toggled independently.- Console and file handlers share one level.
- No log rotation; retention is user-managed.
- Plain-text logging only (no JSON/JSONL).
Highest wins:
- Explicit
configure_logging(...)keyword arguments - Environment variables —
PYRPOD_LOG_LEVEL,PYRPOD_LOG_FORMAT <case_dir>/logging.ini- Built-in defaults
# Raise verbosity for one run without editing any file:
PYRPOD_LOG_LEVEL=DEBUG python your_driver.py
# Override the record format:
PYRPOD_LOG_FORMAT="%(levelname)s %(name)s %(message)s" python your_driver.py# Explicit arguments beat everything, e.g. silence the console for a batch job:
session = configure_logging(case_dir, console=False, level="INFO")Runtime logs are written under:
<case_dir>/results/logs/<case-name>_<timestamp>.log
<case-name> is the normalized final path component of case_dir; the
timestamp is YYYYMMDD_HHMMSS. A new file is created for every configured
run, e.g. base_case_20260725_143102.log. There is one primary runtime log
per run — not one per firing or artifact. (.log is reserved for these runtime
logs; expected-result fixtures use .txt/.dat.)
timestamp [LEVEL] module.name: message
2026-07-25 14:31:02 [INFO] pyrpod.rpod.PlumeStrikeEstimationStudy: Firing 17/95 completed: struck_faces=246 ...
| Level | Use |
|---|---|
DEBUG |
Candidate/resolved paths, array shapes and summary stats, per-thruster transforms, existing-directory messages, per-artifact paths, DataFrame summaries. |
INFO |
Run started, config loaded, assets loaded, JFH loaded, geometry/thruster config loaded, phase start/complete, progress updates, per-firing maxima, summary artifacts, run completed, final status. |
WARNING |
Optional config absent, optional visualization failed, parallel→serial fallback, questionable-but-usable input, nonessential write failed (with traceback). |
ERROR |
Failed requested operations, invalid required inputs (fail fast after logging), unrecoverable failures. |
CRITICAL is not used.
configure_logging writes a compact startup block once: absolute case path,
start time, Python version, CPU count, git dirty-working-tree status (via a safe
git status --porcelain; unknown if git/repo is unavailable), effective log
level, console/file enabled flags, the active runtime-log path, and whether
logging.ini or built-in defaults were used. The plume-strike run start (next
section) adds execution mode, worker count, and model parameters.
When snapshot_config is enabled, the case config.ini (and logging.ini if
present) are copied verbatim into results/logs/ as
<case-name>_<timestamp>_config.ini / ..._logging.ini, and their SHA-256
checksums are logged. Snapshots are for reproducibility and never replace the
original case files.
Each loaded input asset (target/visiting-vehicle STL, thruster/plume STL, JFH,
TCF, TDF, CCF, ...) is logged at INFO with its category, filename, absolute
resolved path, source (case-local vs shared-data), size in bytes, and
streaming SHA-256 (checksum_inputs toggles the hash). Checksums are cached per
run so an unchanged asset is not rehashed. Candidate paths and case-local vs
shared fallback decisions are logged at DEBUG (see
pyrpod.util.io.fs.resolve_asset_path).
PlumeStrikeEstimationStudy.jfh_plume_strikes():
- Fail-fast validation of every required input (config keys, readable
non-empty target mesh / JFH / thruster config, valid thruster references; and,
when kinetics is enabled, TDF / surface_temp / sigma / thruster metrics). A
missing required input is logged at ERROR and raises — never a silent
None. - Run started (INFO): firings, target faces, kinetics on/off, plume model, radius, wedge angle, serial/parallel mode, worker count, progress interval.
- Progress (INFO): every
progress_every_n_firingsfirings and the final firing — index, JFH id, sim time, active thrusters, worker PID, struck-face count, and (when kinetics is on) max pressure/shear/heat-flux/heat-flux-load, plus per-firing wall time and tracked-array memory. Kinetics-specific maxima are omitted (not faked) when kinetics is disabled. - DEBUG: array summaries and per-artifact paths.
- Completion (INFO): firings completed, total struck-face events, unique struck faces, overall maxima, wall/CPU time, average wall time per firing, peak Python-tracked memory, tracked array memory, artifact count, final execution mode, whether fallback occurred, and final run status.
tqdm progress bars have been replaced by this logging-based progress.
Standard library only: time.perf_counter() (wall), time.process_time()
(CPU), tracemalloc (Python-managed current/peak allocations), and NumPy
.nbytes for tracked arrays. tracemalloc does not capture all native
memory allocated by NumPy/compiled libraries, so peak memory is reported as
python_peak_memory_mb (not total process memory) and known major arrays are
summed separately as tracked_array_memory_mb. To avoid slowing unit tests,
tracemalloc is engaged only for an explicitly configured run
(configure_logging called) with log_performance = true.
Unit-bearing field names are used for physical quantities, e.g. pressure_pa,
shear_pa, heat_flux_w_m2, heat_flux_load_j_m2, wall_time_s,
cpu_time_s, python_peak_memory_mb, tracked_array_memory_mb.
Parallel execution uses parent-process logging: workers return only non-numerical metadata (PID, per-firing wall/CPU time); the parent emits the records. Numerical accumulation order is unchanged — cumulative arrays and VTK output are produced serially in JFH order by the parent. Serial and parallel modes emit the same major event categories.
If process-based execution fails, the full traceback is logged at WARNING
(exc_info=True), serial fallback runs, and the final status becomes
successful_with_warning when the serial calculation succeeds (or failed if
it does not).
One of successful, successful_with_warning, partial, or failed. Call
session.finalize(status) to record it along with warning/error counts,
runtime, and the runtime-log path. session.close() releases the handlers and
should run from a finally block so finalization happens even on exceptions
(without suppressing the original exception).
From pyrpod.logging_utils:
configure_logging(case_dir, **overrides) -> LoggingSessionLoggingSession.finalize(status, summary=None),LoggingSession.close()get_active_session(),get_settings(),performance_logging_enabled()log_asset(category, filename, resolved_path, case_dir, logger=None)summarize_array(name, array),log_array_summary(logger, name, array, level=DEBUG)sha256_file(path),git_dirty_status(path)
run_summary.json, output-artifact checksums, JSONL structured logs, log
rotation, true total-process memory via an optional external dependency, a
multiprocessing logging queue, and generated run IDs are documented as possible
future work and are intentionally out of scope for this version.