PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
cli.py
Go to the documentation of this file.
1"""Argument parser construction and command dispatch for PICurv."""
2
3from .core import *
4from .storage import add_storage_parser, storage_workflow
5
6
7# ==============================================================================
8# MAIN COMMAND-LINE INTERFACE PARSER
9# ==============================================================================
10
11#: Output encodings every command that prints a structured report accepts.
12CLI_OUTPUT_FORMATS = ("text", "json")
13
14#: Which stages `--only` may select.
15RUN_STAGE_CHOICES = ("all", "solve", "post-process")
16
17#: State carried from a restart checkpoint into a branched run.
18RESTART_STATISTICS_MODES = ("reset", "carry")
19
20#: Canonical workspace homes accepted by `picurv inputs import`.
21WORKSPACE_INPUT_KINDS = ("grid", "initial-condition", "inlet-profile", "reference-field")
22
23#: Materialization modes accepted by `picurv inputs import`.
24WORKSPACE_INPUT_MODES = ("copy", "reflink", "hardlink", "reference")
25
26
28 """!
29 @brief Attach the executable-selection switches to a staging parser: pinning and index width.
30 @details Unset pinning means the workspace's `reproducibility.pin_executables` decides; without
31 it the run launches the installation's executables. Unset index width means the
32 conductor chooses from the run's particle and grid sizes, or its restart source.
33 @param[in] parser Parser or argument group receiving the switches.
34 @return None.
35 """
36 pinning = parser.add_mutually_exclusive_group()
37 pinning.add_argument(
38 "--pin-executables", dest="pin_executables", action="store_const", const=True, default=None,
39 help="Copy simulator and postprocessor into the run's config/bin/ at staging, so a later\n"
40 "rebuild of the installation cannot change what the job runs. On a pinned run,\n"
41 "re-pins it to the current build.",
42 )
43 pinning.add_argument(
44 "--no-pin-executables", dest="pin_executables", action="store_const", const=False,
45 help="Launch the installation's executables even when the workspace sets\n"
46 "reproducibility.pin_executables. Slurm jobs still check at start that they\n"
47 "report the build identity read at staging.",
48 )
49 parser.add_argument(
50 "--petsc-index-bits", dest="petsc_index_bits", choices=PETSC_INDEX_BITS_CHOICES, default=None,
51 help="Run on the 32- or 64-bit-index PETSc build (simulator64/postprocessor64, from\n"
52 "`make INDEX_BITS=64`). Default: 64 only when a particle or grid field outgrows\n"
53 "32-bit indices; a restart keeps its source's width. Also PICURV_PETSC_INDEX_BITS.",
54 )
55
56
57def _add_run_parser(subparsers):
58 """!
59 @brief Attach `run` parser with staged execution and dry-run support.
60 @param[in] subparsers Argument passed to `_add_run_parser()`.
61 @return Value returned by `_add_run_parser()`.
62 """
63 p_run = subparsers.add_parser(
64 "run",
65 help="Execute a simulation workflow (solve and/or post-process).",
66 formatter_class=argparse.RawTextHelpFormatter,
67 description=(
68 "Execute solver and/or post-processing stages.\n\n"
69 "Notes:\n"
70 " - --num-procs applies to solver and post-processing stage launches.\n"
71 " - With --solve, --continue resumes the existing run directory in-place.\n"
72 " - --post-process processes only the steps whose output is missing or stale: no output,\n"
73 " a changed recipe, or a changed checkpoint. Steps without a committed checkpoint\n"
74 " wait for the solver. --recompute regenerates every step.\n\n"
75 "Diagnostics:\n"
76 " - PETSc and runtime memory diagnostics live under monitor.yml -> diagnostics.\n"
77 " - Use --dry-run to inspect resolved PETSc flags and expected log destinations.\n\n"
78 "Examples:\n"
79 " picurv run --solve -n 8 --case case.yml --solver solver.yml --monitor monitor.yml\n"
80 " picurv run --solve --restart-from runs/old_run --case case.yml --solver solver.yml --monitor monitor.yml\n"
81 " picurv run --solve --continue --run-dir runs/my_run --case case.yml --solver solver.yml --monitor monitor.yml\n"
82 " picurv run --post-process --run-dir runs/my_run --post post.yml\n"
83 " picurv run --post-process --recompute --run-dir runs/my_run --post post.yml\n"
84 " picurv run --solve --case case.yml --solver solver.yml --monitor monitor.yml --dry-run"
85 ),
86 epilog="Next: run `picurv validate ...` first for config-only checks.",
87 )
88 run_group = p_run.add_argument_group("stages")
89 run_group.add_argument("--solve", action="store_true", help="Execute the solver stage (creates a new run directory).")
90 run_group.add_argument("--post-process", action="store_true", help="Execute the post-processing stage on a run directory.")
91
92 solver_group = p_run.add_argument_group("solver inputs (required for --solve)")
93 solver_group.add_argument("--case", help="Path to the case definition file (e.g., case.yml).")
94 solver_group.add_argument("--solver", help="Path to the solver settings profile (e.g., solver.yml).")
95 solver_group.add_argument("--monitor", help="Path to the monitoring, diagnostics, and I/O profile (e.g., monitor.yml).")
96 solver_group.add_argument(
97 "--restart-from", "--from",
98 dest="restart_from",
99 help="Path to an existing run directory to restart from.\n"
100 "Use 'latest' to select the newest compatible local workspace run.",
101 )
102 solver_group.add_argument(
103 "--statistics-state",
104 choices=RESTART_STATISTICS_MODES,
105 default=None,
106 help="For a branched restart with field statistics enabled, this is required:\n"
107 "'reset' discards the parent's accumulated windows, 'carry' resumes compatible\n"
108 "saved window state. Ignored when field statistics are disabled.",
109 )
110 solver_group.add_argument(
111 "--require-precomputed",
112 action="store_true",
113 help="Refuse to build missing or stale deterministic assets while staging the run.",
114 )
115 solver_group.add_argument(
116 "--fetch-missing",
117 action="store_true",
118 help="Try the configured storage profile before rebuilding a missing workspace asset.",
119 )
120 run_group.add_argument(
121 "--continue",
122 action="store_true",
123 dest="continue_run",
124 help="Extend an existing run's solve in place. Requires --solve and --run-dir,\n"
125 "and start_step > 0; appends to existing solver output/logs.\n"
126 "Post-processing needs no flag: it always skips up-to-date steps.",
127 )
128
129 post_group = p_run.add_argument_group("post-processor inputs (required for --post-process)")
130 post_group.add_argument("--run-dir", help="Path to an existing run directory.\n(Used with --post-process or --solve --continue).")
131 post_group.add_argument("--post", help="Path to the post-processing recipe file (e.g., post.yml).")
132 post_group.add_argument(
133 "--only",
134 help="Comma-separated post stages to run: 'fields' (the field post-processor),\n"
135 "'spectra', and 'reducers' (the post.yml reducers tasks, on profiles the\n"
136 "field stage wrote). Defaults to every stage.\n"
137 "Use --only spectra to measure spectra without running the field stage.",
138 )
139 post_group.add_argument(
140 "--recompute",
141 action="store_true",
142 dest="recompute_post",
143 help="Regenerate every requested step that has a committed checkpoint, including\n"
144 "steps whose output is up to date (for example, after rebuilding the\n"
145 "postprocessor). Applies to every selected stage.",
146 )
147
148 p_run.add_argument(
149 "-n",
150 "--num-procs",
151 type=int,
152 default=None,
153 help="Number of MPI processes for solver and post-processing stages (default 1, or\n"
154 "the product of --ni --nj --nk). The decomposition is chosen from the grid and\n"
155 "its boundary conditions unless given.",
156 )
157 for axis in ("i", "j", "k"):
158 p_run.add_argument(
159 f"--n{axis}", type=int, default=None,
160 help=f"MPI ranks along the grid's {axis} axis. Give --ni, --nj and --nk together.",
161 )
162 p_run.add_argument("--cluster", help="Path to cluster.yml for Slurm execution mode.")
163 p_run.add_argument("--scheduler", help="Explicit scheduler selector (currently 'slurm').")
164 p_run.add_argument("--no-submit", action="store_true", help="Stage run artifacts without starting local execution or Slurm submission.")
166 p_run.add_argument(
167 "--dry-run",
168 action="store_true",
169 help="Resolve and print planned commands/artifacts, including diagnostic flags and log paths, without writing files.",
170 )
171 p_run.add_argument(
172 "--format",
173 dest="output_format",
174 choices=list(CLI_OUTPUT_FORMATS),
175 default="text",
176 help="Output format for --dry-run (default: text).",
177 )
178 return p_run
179
180
181def _add_sweep_parser(subparsers):
182 """!
183 @brief Perform add sweep parser.
184 @param[in] subparsers Argument passed to `_add_sweep_parser()`.
185 @return Value returned by `_add_sweep_parser()`.
186 """
187 p_sweep = subparsers.add_parser(
188 "sweep",
189 help="Launch or continue a Slurm-based parameter sweep/study.",
190 formatter_class=argparse.RawTextHelpFormatter,
191 description=(
192 "Launch studies from study.yml + cluster.yml, whether the study uses\n"
193 "cross-product parameter combinations or explicit coupled parameter_sets, continue\n"
194 "partially-completed studies, or re-aggregate metrics.\n\n"
195 "Examples:\n"
196 " picurv sweep --study study.yml --cluster cluster.yml\n"
197 " picurv sweep --study study.yml --cluster cluster.yml --no-submit\n"
198 " picurv sweep --continue --study-dir studies/<id>\n"
199 " picurv sweep --continue --study-dir studies/<id> --cluster cluster_more_time.yml\n"
200 " picurv sweep --reaggregate --study-dir studies/<id>"
201 ),
202 epilog=(
203 "Study files support either `parameters` cross-product expansion or explicit `parameter_sets`.\n"
204 "Next: inspect studies/<study_id>/output/analysis/metrics_table.csv for aggregated metrics."
205 ),
206 )
207 p_sweep.add_argument("--study", help="Path to study.yml defining either a `parameters` cross-product expansion or explicit parameter_sets, plus metrics.")
208 p_sweep.add_argument("--cluster", help="Path to cluster.yml defining Slurm resources.")
209 p_sweep.add_argument("--no-submit", action="store_true", help="Generate all study artifacts without submitting jobs.")
211 p_sweep.add_argument("--study-dir", help="Path to an existing study directory (for --continue/--reaggregate).")
212 p_sweep.add_argument("--continue", action="store_true", dest="continue_study",
213 help="Continue a partially-completed study. Requires --study-dir.")
214 p_sweep.add_argument("--reaggregate", action="store_true",
215 help="Re-run metrics aggregation on a completed study. Requires --study-dir.")
216 p_sweep.add_argument("--auto-fetch", action="store_true",
217 help="Restore cold-storage study members automatically instead of\n"
218 "refusing. Requires a configured storage profile.")
219 return p_sweep
220
221
222class _LazyVersionAction(argparse.Action):
223 """!
224 @brief `--version`, resolving the build identity only when the flag is given.
225 @details argparse's own version action formats its string while the parser is built,
226 which would run git for every command; this one defers it to the flag.
227 """
228
229 def __init__(self, option_strings, dest=argparse.SUPPRESS, default=argparse.SUPPRESS, help=None):
230 """!
231 @brief Register a flag that takes no value.
232 @param[in] option_strings Flag spellings.
233 @param[in] dest Unused destination.
234 @param[in] default Unused default.
235 @param[in] help Help text.
236 """
237 super().__init__(option_strings=option_strings, dest=dest, default=default, nargs=0,
238 help=help or "show program's version number and exit")
239
240 def __call__(self, parser, namespace, values, option_string=None):
241 """!
242 @brief Print `picurv <version>` and exit.
243 @param[in] parser Parser being run.
244 @param[in] namespace Unused namespace.
245 @param[in] values Unused values.
246 @param[in] option_string Flag used.
247 """
248 # stdout, as argparse's own version action prints it.
249 print(f"picurv {picurv_build()['version']}")
250 parser.exit()
251
252
253def _add_validate_parser(subparsers):
254 """!
255 @brief Perform add validate parser.
256 @param[in] subparsers Argument passed to `_add_validate_parser()`.
257 @return Value returned by `_add_validate_parser()`.
258 """
259 p_validate = subparsers.add_parser(
260 "validate",
261 help="Validate config files without launching solver/post.",
262 formatter_class=argparse.RawTextHelpFormatter,
263 description=(
264 "Validate one or more config roles. No solver/post execution and no run/study artifact writes.\n\n"
265 "Examples:\n"
266 " picurv validate --case case.yml --solver solver.yml --monitor monitor.yml\n"
267 " picurv validate --post post.yml --cluster cluster.yml\n"
268 " picurv validate --study study.yml --cluster cluster.yml --strict"
269 ),
270 epilog="Next: run `picurv run --dry-run ...` to inspect resolved commands/artifacts.",
271 )
272 p_validate.add_argument("--case", help="Path to case.yml")
273 p_validate.add_argument("--solver", help="Path to solver.yml")
274 p_validate.add_argument("--monitor", help="Path to monitor.yml (logging, diagnostics, profiling, and I/O)")
275 p_validate.add_argument("--restart-from", "--from", dest="restart_from",
276 help="Path to a run directory to validate restart from.")
277 p_validate.add_argument("--continue", action="store_true", dest="continue_run", help="Validate continue-in-place mode. Requires --run-dir.")
278 p_validate.add_argument("--run-dir", help="Path to an existing run directory (for --continue validation).")
279 p_validate.add_argument("--post", help="Path to post.yml")
280 p_validate.add_argument("--cluster", help="Path to cluster.yml")
281 p_validate.add_argument("--study", help="Path to study.yml")
282 p_validate.add_argument("--strict", action="store_true", help="Enable additional strict checks for selected roles.")
283 return p_validate
284
286 """!
287 @brief Attach `precompute` parser for deterministic artifact generation.
288 @param[in] subparsers Argument passed to `_add_precompute_parser()`.
289 @return Configured parser.
290 """
291 p_precompute = subparsers.add_parser(
292 "precompute",
293 help="Generate deterministic case artifacts without launching the solver.",
294 formatter_class=argparse.RawTextHelpFormatter,
295 description=(
296 "Generate configured deterministic artifacts, such as grid_gen grids,\n"
297 "generated prescribed-flow inlet profiles, and ic_gen initial conditions,\n"
298 "as immutable objects in the initialized workspace asset store.\n\n"
299 "Examples:\n"
300 " picurv precompute --case config/case.yml\n"
301 " picurv precompute --case config/case.yml --only grid,initial-condition"
302 ),
303 epilog="Next: inspect the reported asset set, then stage a run that reuses it.",
304 )
305 p_precompute.add_argument("--case", required=True, help="Path to case.yml containing grid/profile/IC generator settings.")
306 p_precompute.add_argument(
307 "--only",
308 default="all",
309 help="Comma-separated asset kinds (grid, initial-condition, inlet-profiles), or all.",
310 )
311 return p_precompute
312
313
314def _add_inputs_parser(subparsers):
315 """!
316 @brief Attach explicit workspace input-ingress commands.
317 @param[in] subparsers Top-level argparse subparser collection.
318 @return Configured inputs command parser.
319 """
320 parser = subparsers.add_parser(
321 "inputs",
322 help="Import or register files used by workspace configurations.",
323 formatter_class=argparse.RawTextHelpFormatter,
324 )
325 actions = parser.add_subparsers(dest="inputs_action", required=True)
326 p_import = actions.add_parser("import", help="Import one external file into its canonical workspace home.")
327 p_import.add_argument(
328 "kind",
329 choices=WORKSPACE_INPUT_KINDS,
330 )
331 p_import.add_argument("source", help="Existing source file.")
332 p_import.add_argument("--name", help="Destination name; defaults to the source basename.")
333 p_import.add_argument(
334 "--mode", choices=WORKSPACE_INPUT_MODES, default="copy",
335 help="How to retain the input. Reference mode records but does not copy an external path.",
336 )
337 p_import.add_argument("--workspace", help="Workspace root; defaults to discovery from the current directory.")
338 return parser
339
340
341def _add_version_parsers(subparsers):
342 """!
343 @brief Attach unified source/build/workspace-version commands.
344 @param[in] subparsers Top-level argparse subparser collection.
345 @return Version, versions, and source command parsers.
346 """
347 p_version = subparsers.add_parser(
348 "version",
349 help="Report the build identity of the conductor and every native executable.",
350 formatter_class=argparse.RawTextHelpFormatter,
351 description=(
352 "Report the build identity shared by the Python conductor and the native\n"
353 "executables, plus any workspace version requirement.\n\n"
354 "With the 'status' action this also validates that they agree, and exits 1\n"
355 "when they do not, so a job script can refuse an incoherent build.\n\n"
356 "Examples:\n"
357 " picurv version\n"
358 " picurv version status\n"
359 " picurv version status --format json"
360 ),
361 )
362 p_version.add_argument(
363 "version_action", nargs="?", choices=["status"], default=None,
364 help="'status' validates conductor/executable/workspace coherence and exits 1 on disagreement.",
365 )
366 p_version.add_argument("--format", dest="output_format", choices=list(CLI_OUTPUT_FORMATS), default="text",
367 help="Output format (default: text).")
368
369 p_versions = subparsers.add_parser("versions", help="List, install, or activate PICurv source versions.")
370 actions = p_versions.add_subparsers(dest="versions_action", required=True)
371 actions.add_parser("list", help="List local Git tags and the active build.")
372 p_install = actions.add_parser("install", help="Fetch a named version and build it in the source checkout.")
373 p_install.add_argument("version", help="Tag, bare release (resolved to v<release>), or commit.")
374 p_activate = actions.add_parser("activate", help="Checkout and build the version required by this workspace.")
375 p_activate.add_argument("version", nargs="?", help="Version/tag; defaults to the workspace requirement.")
376 p_activate.add_argument("--workspace", help="Workspace root; defaults to discovery from the current directory.")
377 for parser in (p_install, p_activate):
378 parser.add_argument(
379 "make_args", nargs=argparse.REMAINDER,
380 help="Make variables passed to the build, as with 'picurv build' (for example "
381 "SYSTEM=cluster); make options go after '--'. Place them after every picurv option.",
382 )
383
384 p_source = subparsers.add_parser("source", help="Manage the source checkout used by PICurv.")
385 source_actions = p_source.add_subparsers(dest="source_action", required=True)
386 p_update = source_actions.add_parser("update", help="Fetch source updates without changing the active version.")
387 p_update.add_argument("--remote", default="origin", help="Git remote to fetch from (default: origin).")
388 return p_version, p_versions, p_source
389
390
391def _add_summarize_parser(subparsers):
392 """!
393 @brief Attach `summarize` parser for read-only run-health views.
394 @param[in] subparsers Argument passed to `_add_summarize_parser()`.
395 @return Value returned by `_add_summarize_parser()`.
396 """
397 p_summarize = subparsers.add_parser(
398 "summarize",
399 help="Summarize run configs/health and plot scalar log histories.",
400 formatter_class=argparse.RawTextHelpFormatter,
401 description=(
402 "Build read-only configuration overviews, run-health summaries, and scalar time-history plots.\n"
403 "It does not modify solver output and works for active or completed runs.\n\n"
404 "Examples:\n"
405 " picurv summarize --run-dir runs/my_run --overview\n"
406 " picurv summarize --run-dir runs/my_run --case --solver\n"
407 " picurv summarize --run-dir runs/my_run --latest\n"
408 " picurv summarize --run-dir runs/my_run --monitor --step 500\n"
409 " picurv summarize --run-dir runs/my_run --list-plot-series\n"
410 " picurv summarize --run-dir runs/my_run --plot momentum.residual_norm --last 100\n"
411 " picurv summarize --run-dir runs/my_run --table poisson --last 10\n"
412 " picurv summarize --run-dir runs/my_run --latest --format json"
413 ),
414 epilog="Next: use `picurv run ...` to create runs or `picurv sweep ...` for multi-case studies.",
415 )
416 p_summarize.add_argument("--run-dir", required=True, help="Path to the run directory to inspect.")
417 p_summarize.add_argument(
418 "--overview",
419 action="store_true",
420 help="Summarize run metadata plus copied case, solver, and monitor configs without implicitly requesting health.",
421 )
422 p_summarize.add_argument("--case", action="store_true", help="Summarize the copied run-local case.yml.")
423 p_summarize.add_argument("--solver", action="store_true", help="Summarize the copied run-local solver.yml.")
424 p_summarize.add_argument("--monitor", action="store_true", help="Summarize the copied run-local monitor.yml.")
425 plot_group = p_summarize.add_mutually_exclusive_group()
426 plot_group.add_argument(
427 "--list-plot-series",
428 dest="list_plot_series",
429 action="store_true",
430 help="List scalar histories available to --plot.",
431 )
432 plot_group.add_argument(
433 "--list-series",
434 dest="list_plot_series",
435 action="store_true",
436 help=argparse.SUPPRESS,
437 )
438 plot_group.add_argument(
439 "--plot",
440 dest="plot_series",
441 help="Plot one qualified scalar history, such as momentum.residual_norm (requires matplotlib).",
442 )
443 plot_group.add_argument(
444 "--plot-spectrum",
445 dest="plot_spectrum",
446 nargs="?",
447 const="",
448 help="Plot up to six representative measured energy spectra, including the first\n"
449 "and last states, overlaid on the initial-condition spectrum. Takes an optional\n"
450 "task-name substring when the recipe measured more than one spectrum\n"
451 "(requires matplotlib).",
452 )
453 plot_group.add_argument(
454 "--table",
455 dest="table",
456 help="Print the last rows of one of the run's CSV records as aligned columns, such as\n"
457 "continuity, poisson, search or memory; a per-block record takes ':<block>'.",
458 )
459 plot_group.add_argument(
460 "--list-tables",
461 dest="list_tables",
462 action="store_true",
463 help="List the CSV records this run has, for --table.",
464 )
465 p_summarize.add_argument("--columns", dest="table_columns",
466 help="With --table: comma-separated columns to show (the first column is always kept).")
467 p_summarize.add_argument("--last", dest="last_n", type=int,
468 help="With --plot: only the last N chronological records per line.\n"
469 "With --table: the last N rows (default 20).")
470 p_summarize.add_argument("--plot-output", help="Save the plot to this path instead of opening an interactive window.")
471 p_summarize.add_argument("--linear-y", action="store_true", help="Force linear y-axis scaling instead of automatic residual/norm log scaling.")
472 step_group = p_summarize.add_mutually_exclusive_group()
473 step_group.add_argument("--step", type=int, help="Specific completed timestep to summarize.")
474 step_group.add_argument(
475 "--latest",
476 action="store_true",
477 help="Summarize the most recently appended completed step found in available artifacts (default behavior).",
478 )
479 step_group.add_argument(
480 "--max-step",
481 action="store_true",
482 help="Summarize the numerically largest timestep found in available artifacts.",
483 )
484 p_summarize.add_argument(
485 "--snapshot-rows",
486 type=int,
487 default=5,
488 help="Number of sampled particle snapshot rows to preview when solver stream output contains them.",
489 )
490 p_summarize.add_argument(
491 "--format",
492 dest="output_format",
493 choices=list(CLI_OUTPUT_FORMATS),
494 default="text",
495 help="Output format (default: text).",
496 )
497 return p_summarize
498
499
500def _add_cancel_parser(subparsers):
501 """!
502 @brief Perform add cancel parser.
503 @param[in] subparsers Argument passed to `_add_cancel_parser()`.
504 @return Value returned by `_add_cancel_parser()`.
505 """
506 p_cancel = subparsers.add_parser(
507 "cancel",
508 help="Cancel Slurm-submitted jobs for an existing run directory.",
509 formatter_class=argparse.RawTextHelpFormatter,
510 description=(
511 "Look up scheduler/submission.json inside an existing run directory and cancel\n"
512 "the recorded Slurm job(s) without requiring manual job-id lookup.\n\n"
513 "Examples:\n"
514 " picurv cancel --run-dir runs/my_run\n"
515 " picurv cancel --run-dir runs/my_run --stage solve\n"
516 " picurv cancel --run-dir runs/my_run --stage solve --graceful\n"
517 " picurv cancel --run-dir runs/my_run --dry-run\n\n"
518 "Default cancellation is a hard Slurm cancel (`scancel <job_id>`). With\n"
519 "`--graceful`, Slurm sends SIGUSR1 to the solver process tree so PICurv can write the latest\n"
520 "safe off-cadence step at the next runtime checkpoint before exiting.\n"
521 "Post-process jobs still use ordinary hard cancellation."
522 ),
523 epilog="Next: use `picurv summarize --run-dir ...` to inspect whatever output the run already produced.",
524 )
525 p_cancel.add_argument("--run-dir", required=True, help="Path to the run directory whose Slurm job(s) should be canceled.")
526 p_cancel.add_argument(
527 "--stage",
528 choices=list(RUN_STAGE_CHOICES),
529 default="all",
530 help="Which recorded stage job(s) to cancel (default: all).",
531 )
532 p_cancel.add_argument(
533 "--dry-run",
534 action="store_true",
535 help="Show which `scancel` command(s) would run without actually canceling anything.",
536 )
537 p_cancel.add_argument(
538 "--graceful",
539 action="store_true",
540 help=(
541 "For solver jobs, request a clean runtime shutdown by sending SIGUSR1 to the process tree instead of "
542 "hard-canceling immediately. The solver writes the latest safe off-cadence step at "
543 "the next checkpoint. Non-solver stages still use ordinary scancel."
544 ),
545 )
546 return p_cancel
547
548
549def _add_submit_parser(subparsers):
550 """!
551 @brief Perform add submit parser.
552 @param[in] subparsers Argument passed to `_add_submit_parser()`.
553 @return Value returned by `_add_submit_parser()`.
554 """
555 p_submit = subparsers.add_parser(
556 "submit",
557 help="Execute or submit previously staged artifacts from an existing run or study directory.",
558 formatter_class=argparse.RawTextHelpFormatter,
559 description=(
560 "Consume an existing artifact set created by 'picurv run ... --no-submit' or\n"
561 "'picurv sweep ... --no-submit' and execute/submit it later without\n"
562 "regenerating configs or scripts.\n"
563 "A post-process job decides when it starts which steps need work, so\n"
564 "submitting it again skips output that is already up to date.\n\n"
565 "Examples:\n"
566 " picurv submit --run-dir runs/my_run\n"
567 " picurv submit --run-dir runs/my_run --stage solve\n"
568 " picurv submit --study-dir studies/my_study --dry-run"
569 ),
570 epilog="Next: use `picurv summarize --run-dir ...` or `picurv cancel --run-dir ...` after submission as needed.",
571 )
572 target_group = p_submit.add_mutually_exclusive_group(required=True)
573 target_group.add_argument("--run-dir", help="Path to a staged run directory created by `picurv run ... --no-submit`.")
574 target_group.add_argument("--study-dir", help="Path to a staged study directory created by `picurv sweep --cluster ... --no-submit`.")
575 p_submit.add_argument(
576 "--stage",
577 choices=list(RUN_STAGE_CHOICES),
578 default="all",
579 help="Which staged job(s) to submit (default: all, every stage the target staged).",
580 )
581 p_submit.add_argument(
582 "--force",
583 action="store_true",
584 help="Allow re-submitting a stage already marked as submitted in scheduler/submission.json.",
585 )
586 p_submit.add_argument(
587 "--dry-run",
588 action="store_true",
589 help="Show which local command(s) or `sbatch` command(s) would run without starting anything.",
590 )
591 return p_submit
592
593
594def _add_init_parser(subparsers):
595 """!
596 @brief Perform add init parser.
597 @param[in] subparsers Argument passed to `_add_init_parser()`.
598 @return Value returned by `_add_init_parser()`.
599 """
600 p_init = subparsers.add_parser(
601 "init",
602 help="Initialize a new case workspace from a template.",
603 formatter_class=argparse.RawTextHelpFormatter,
604 description=(
605 "Create a case workspace from examples/<template_name>.\n\n"
606 "Examples:\n"
607 " picurv init flat_channel --dest my_case\n"
608 " picurv init bent_channel --dest my_bent_case\n"
609 " picurv init decaying_isotropic_turbulence --dest dit_case"
610 ),
611 epilog="Next: run `picurv validate --case ... --solver ... --monitor ...` before execution.",
612 )
613 p_init.add_argument("template_name", help="Name of the case template directory to copy (e.g., 'flat_channel').")
614 p_init.add_argument(
615 "--dest",
616 dest="dest_name",
617 help="Optional name for the new directory. Defaults to the template name.\nPath is relative to your current working directory.",
618 )
619 p_init.add_argument(
620 "--source-root",
621 help="Optional override for the PICurv source repository root.\nUseful when running from a copied case without metadata.",
622 )
623 p_init.add_argument(
624 "--pin-binaries",
625 action="store_true",
626 default=False,
627 help="(Legacy) Copy simulator and postprocessor into the case directory, used only when\n"
628 "that directory's own picurv is invoked. To freeze the executables a run uses,\n"
629 "use 'picurv run --pin-executables'.",
630 )
631 return p_init
632
633
634def _add_build_parser(subparsers):
635 """!
636 @brief Perform add build parser.
637 @param[in] subparsers Argument passed to `_add_build_parser()`.
638 @return Value returned by `_add_build_parser()`.
639 """
640 p_build = subparsers.add_parser(
641 "build",
642 help="Build project executables using the Makefile.",
643 formatter_class=argparse.RawTextHelpFormatter,
644 description=(
645 "Calls the project's Makefile directly through `make`.\n"
646 "If you do not pass an explicit make target, `picurv build` runs `make all`.\n"
647 "Targets and variables after 'build' are passed directly to make; make options\n"
648 "such as -j8 go after '--'.\n\n"
649 "Examples:\n"
650 " picurv build\n"
651 " picurv build clean-project\n"
652 " picurv build SYSTEM=cluster\n"
653 " picurv build all SYSTEM=cluster\n"
654 " picurv build postprocessor\n"
655 " picurv build -- -j8 all\n"
656 " picurv build INDEX_BITS=64 PETSC_DIR=$HOME/sw/petsc-int64 # simulator64/postprocessor64\n"
657 " ./picurv build clean-project # from an initialized case directory"
658 ),
659 epilog="Next: run `picurv --help` or `picurv run --help` for execution commands.",
660 )
661 p_build.add_argument(
662 "--source-root",
663 help="Optional override for the PICurv source repository root.",
664 )
665 p_build.add_argument(
666 "--case-dir",
667 help="Optional case directory used to resolve .picurv-origin.json when not running from that case.",
668 )
669 p_build.add_argument(
670 "make_args",
671 nargs=argparse.REMAINDER,
672 help="Targets and variables passed to make (e.g., 'clean-project'); make options go after '--'.",
673 )
674 return p_build
675
676
678 """!
679 @brief Perform add sync config parser.
680 @param[in] subparsers Argument passed to `_add_sync_config_parser()`.
681 @return Value returned by `_add_sync_config_parser()`.
682 """
683 p_sync_config = subparsers.add_parser(
684 "sync-config",
685 help="Refresh template-managed files in a case directory from examples/<template>.",
686 formatter_class=argparse.RawTextHelpFormatter,
687 description=(
688 "Copy updated example template files into an existing case directory.\n"
689 "Modified files are preserved unless --overwrite is used.\n"
690 "--prune removes only files that were previously tracked as template-managed and\n"
691 "have since been removed from the source template.\n\n"
692 "Examples:\n"
693 " ./picurv sync-config\n"
694 " ./picurv sync-config --overwrite\n"
695 " ./picurv sync-config --prune\n"
696 " ./bin/picurv sync-config --case-dir my_case --template-name flat_channel"
697 ),
698 epilog="Next: run `picurv validate ...` after syncing configs.",
699 )
700 p_sync_config.add_argument("--case-dir", help="Optional case directory to refresh. Defaults to the current case.")
701 p_sync_config.add_argument("--source-root", help="Optional override for the PICurv source repository root.")
702 p_sync_config.add_argument(
703 "--template-name",
704 help="Optional template name override (e.g., flat_channel). Required when metadata is absent.",
705 )
706 p_sync_config.add_argument("--overwrite", action="store_true", help="Overwrite case files even when they differ from the template.")
707 p_sync_config.add_argument(
708 "--prune",
709 action="store_true",
710 help="Remove stale files that were previously tracked as template-managed but no longer exist in the source template.",
711 )
712 return p_sync_config
713
714
716 """!
717 @brief Perform add pull source parser.
718 @param[in] subparsers Argument passed to `_add_pull_source_parser()`.
719 @return Value returned by `_add_pull_source_parser()`.
720 """
721 p_pull = subparsers.add_parser(
722 "pull-source",
723 help="(Legacy; use 'source update') Refresh source branches from a case directory.",
724 formatter_class=argparse.RawTextHelpFormatter,
725 description=(
726 "Legacy maintenance command; new workspaces use `picurv source update` and\n"
727 "`picurv versions`. Updates the source repository without leaving an\n"
728 "initialized case directory.\n\n"
729 "By default this refreshes every local branch that tracks an upstream,\n"
730 "then restores the branch you started on.\n\n"
731 "Examples:\n"
732 " ./picurv pull-source\n"
733 " ./picurv pull-source --current-branch-only\n"
734 " ./picurv pull-source --no-rebase\n"
735 " ./picurv pull-source --remote origin --branch main"
736 ),
737 epilog="Next: run `./picurv build` if source changes require rebuilt executables.",
738 )
739 p_pull.add_argument("--case-dir", help="Optional case directory used to resolve .picurv-origin.json.")
740 p_pull.add_argument("--source-root", help="Optional override for the PICurv source repository root.")
741 p_pull.add_argument("--remote", help="Optional git remote name (e.g., origin).")
742 p_pull.add_argument("--branch", help="Optional branch name. If provided without --remote, origin is assumed.")
743 p_pull.add_argument(
744 "--current-branch-only",
745 action="store_true",
746 help="Only pull the currently checked out branch instead of iterating across all local tracking branches.",
747 )
748 p_pull.add_argument("--no-rebase", action="store_true", help="Use plain git pull instead of git pull --rebase.")
749 return p_pull
750
751
753 """!
754 @brief Perform add status source parser.
755 @param[in] subparsers Argument passed to `_add_status_source_parser()`.
756 @return Value returned by `_add_status_source_parser()`.
757 """
758 p_status = subparsers.add_parser(
759 "status-source",
760 help="(Legacy; use 'version status') Report source/case drift for a case directory.",
761 formatter_class=argparse.RawTextHelpFormatter,
762 description=(
763 "Legacy maintenance command; `picurv version status` reports build coherence for\n"
764 "new workspaces. Inspects whether the source repo, copied binaries, and template-managed files have drifted\n"
765 "from the current case directory.\n\n"
766 "Examples:\n"
767 " ./picurv status-source\n"
768 " ./picurv status-source --format json\n"
769 " ./bin/picurv status-source --case-dir my_case"
770 ),
771 epilog="Next: use `source update`, `build`, or `sync-config` based on the reported drift.",
772 )
773 p_status.add_argument("--case-dir", help="Optional case directory used to resolve .picurv-origin.json.")
774 p_status.add_argument("--source-root", help="Optional override for the PICurv source repository root.")
775 p_status.add_argument("--template-name", help="Optional template name override when metadata is absent.")
776 p_status.add_argument(
777 "--format",
778 dest="output_format",
779 choices=list(CLI_OUTPUT_FORMATS),
780 default="text",
781 help="Output format (default: text).",
782 )
783 return p_status
784
785
787 """!
788 @brief Build and return the top-level CLI parser.
789 @return Value returned by `build_main_parser()`.
790 """
791 parser = argparse.ArgumentParser(
792 description="picurv: A comprehensive conductor for the PICurv simulation platform.",
793 formatter_class=argparse.RawTextHelpFormatter,
794 epilog=(
795 "Examples:\n"
796 " picurv validate --case case.yml --solver solver.yml --monitor monitor.yml --post post.yml\n"
797 " picurv run --solve --post-process --case case.yml --solver solver.yml --monitor monitor.yml --post post.yml --dry-run\n"
798 " picurv run --solve --post-process --case case.yml --solver solver.yml --monitor monitor.yml --post post.yml --no-submit\n"
799 " picurv precompute --case config/case.yml --only grid,initial-condition\n"
800 " picurv run --post-process --run-dir runs/my_run --post post.yml\n"
801 " picurv summarize --run-dir runs/my_run --latest\n"
802 " picurv summarize --run-dir runs/my_run --list-plot-series\n"
803 " picurv summarize --run-dir runs/my_run --plot momentum.residual_norm --last 100\n"
804 " picurv submit --run-dir runs/my_run\n"
805 " picurv cancel --run-dir runs/my_run\n"
806 " picurv cancel --run-dir runs/my_run --stage solve --graceful\n"
807 " picurv sweep --study study.yml --cluster cluster.yml\n\n"
808 "Next commands:\n"
809 " - First run: picurv init ... -> picurv validate ... -> picurv run ...\n"
810 " - Config debugging: picurv validate ...\n"
811 " - Artifact generation: picurv precompute ...\n"
812 " - Launch planning: picurv run ... --dry-run\n"
813 " - Post-only catch-up: picurv run --post-process --run-dir ... --post ...\n"
814 " - Deferred submission: picurv submit --run-dir ...\n"
815 " - Run inspection/plots: picurv summarize ...\n"
816 " - Run cancellation: picurv cancel --run-dir ...; use --graceful for solver final-output shutdown"
817 ),
818 )
819 parser.add_argument(
820 "-v", "--version", action=_LazyVersionAction,
821 help="Print the conductor's build identity and exit. 'picurv version' also reports\n"
822 "each native executable and the PETSc it uses.",
823 )
824 subparsers = parser.add_subparsers(dest="command", required=True, help="Available commands")
825 _add_run_parser(subparsers)
826 _add_sweep_parser(subparsers)
827 _add_validate_parser(subparsers)
828 _add_precompute_parser(subparsers)
829 _add_inputs_parser(subparsers)
830 _add_summarize_parser(subparsers)
831 _add_submit_parser(subparsers)
832 _add_cancel_parser(subparsers)
833 _add_init_parser(subparsers)
834 _add_build_parser(subparsers)
835 _add_sync_config_parser(subparsers)
836 _add_pull_source_parser(subparsers)
837 _add_status_source_parser(subparsers)
838 _add_version_parsers(subparsers)
839 add_storage_parser(subparsers)
840 return parser
841
842
843def dispatch_command(args):
844 """!
845 @brief Validate argument combinations and dispatch to command handlers.
846 @param[in] args Command-line style argument list supplied to the function.
847 """
848 if args.command == "run":
849 if not args.solve and not args.post_process:
850 fail_cli_usage("At least one stage (--solve or --post-process) must be selected.")
851 if args.solve and (not args.case or not args.solver or not args.monitor):
852 fail_cli_usage("--solve requires --case, --solver, and --monitor.")
853 if args.post_process and not args.post:
854 fail_cli_usage("--post-process requires --post.")
855 if args.post_process and not args.solve and not args.run_dir:
856 fail_cli_usage("--post-process requires --run-dir when not used with --solve.")
857 if args.continue_run and not args.run_dir:
858 fail_cli_usage(RESTART_RUN_DIR_REQUIRED_MESSAGE)
859 if getattr(args, "only", None) and not args.post_process:
860 fail_cli_usage("--only selects post-processing stages and requires --post-process.")
861 try:
862 resolve_post_stage_selection(getattr(args, "only", None))
863 except ValueError as exc:
864 fail_cli_usage(str(exc))
865 if getattr(args, "recompute_post", False) and not args.post_process:
866 fail_cli_usage("--recompute regenerates post-processing output and requires --post-process.")
867 if args.scheduler and not args.cluster:
868 fail_cli_usage("--scheduler requires --cluster in this version.")
870 run_workflow(args)
871 return
872 if args.command == "sweep":
873 if args.continue_study:
874 if not args.study_dir:
875 fail_cli_usage("--continue requires --study-dir.")
877 return
878 if args.reaggregate:
879 if not args.study_dir:
880 fail_cli_usage("--reaggregate requires --study-dir.")
882 return
883 # Default: launch new study
884 if not args.study:
885 fail_cli_usage("--study is required when launching a new sweep.")
886 if not args.cluster:
887 fail_cli_usage("--cluster is required when launching a new sweep.")
888 sweep_workflow(args)
889 return
890 if args.command == "validate":
892 return
893 if args.command == "precompute":
894 try:
896 except ValueError as e:
898 ERROR_CODE_CFG_INVALID_VALUE,
899 key="precompute",
900 file_path=getattr(args, "case", "-"),
901 message=str(e),
902 hint="'picurv precompute --help' lists the asset kinds --only accepts.",
903 )
904 sys.exit(1)
905 return
906 if args.command == "inputs":
907 try:
908 inputs_workflow(args)
909 except ValueError as exc:
910 print(f"[FATAL] {exc}", file=sys.stderr)
911 sys.exit(1)
912 return
913 if args.command == "version":
914 version_workflow(args)
915 return
916 if args.command == "versions":
917 try:
919 except ValueError as exc:
920 print(f"[FATAL] {exc}", file=sys.stderr)
921 sys.exit(1)
922 return
923 if args.command == "source":
924 try:
925 source_workflow(args)
926 except ValueError as exc:
927 print(f"[FATAL] {exc}", file=sys.stderr)
928 sys.exit(1)
929 return
930 if args.command == "summarize":
932 return
933 if args.command == "submit":
935 return
936 if args.command == "cancel":
937 cancel_run_jobs(args)
938 return
939 if args.command == "init":
940 init_case(args)
941 return
942 if args.command == "build":
943 build_project(args)
944 return
945 if args.command == "sync-config":
947 return
948 if args.command == "pull-source":
949 pull_source_repo(args)
950 return
951 if args.command == "status-source":
953 return
954 if args.command == "storage":
955 storage_workflow(args)
956 return
957 fail_cli_usage(f"Unsupported command '{args.command}'.")
--version, resolving the build identity only when the flag is given.
Definition cli.py:222
__call__(self, parser, namespace, values, option_string=None)
Print picurv <version> and exit.
Definition cli.py:240
__init__(self, option_strings, dest=argparse.SUPPRESS, default=argparse.SUPPRESS, help=None)
Register a flag that takes no value.
Definition cli.py:229
_add_sweep_parser(subparsers)
Perform add sweep parser.
Definition cli.py:181
_add_version_parsers(subparsers)
Attach unified source/build/workspace-version commands.
Definition cli.py:341
_add_run_parser(subparsers)
Attach run parser with staged execution and dry-run support.
Definition cli.py:57
_add_build_parser(subparsers)
Perform add build parser.
Definition cli.py:634
_add_init_parser(subparsers)
Perform add init parser.
Definition cli.py:594
build_main_parser()
Build and return the top-level CLI parser.
Definition cli.py:786
_add_executable_pinning_arguments(parser)
Attach the executable-selection switches to a staging parser: pinning and index width.
Definition cli.py:27
_add_sync_config_parser(subparsers)
Perform add sync config parser.
Definition cli.py:677
_add_pull_source_parser(subparsers)
Perform add pull source parser.
Definition cli.py:715
_add_precompute_parser(subparsers)
Attach precompute parser for deterministic artifact generation.
Definition cli.py:285
_add_cancel_parser(subparsers)
Perform add cancel parser.
Definition cli.py:500
_add_status_source_parser(subparsers)
Perform add status source parser.
Definition cli.py:752
_add_submit_parser(subparsers)
Perform add submit parser.
Definition cli.py:549
_add_inputs_parser(subparsers)
Attach explicit workspace input-ingress commands.
Definition cli.py:314
_add_summarize_parser(subparsers)
Attach summarize parser for read-only run-health views.
Definition cli.py:391
_add_validate_parser(subparsers)
Perform add validate parser.
Definition cli.py:253
summarize_workflow(args)
Build and render a read-only health summary for a run step.
Definition core.py:25250
set resolve_post_stage_selection(only)
Resolve the --only selector into the set of post stages to execute.
Definition core.py:4034
status_source_command(args)
Report source/case drift for an initialized case directory.
Definition core.py:3249
validate_workflow(args)
Implements picurv validate without launching solver/post workflows.
Definition core.py:19325
version_workflow(args)
Report, and for the status action validate, the shared build identity.
Definition core.py:26581
sweep_reaggregate_workflow(args)
Re-run metrics aggregation and plot generation for an existing study.
Definition core.py:21983
emit_structured_error(str code, str key="-", str file_path="-", str message="", str hint=None, stream=None)
Emit one standardized error line for tooling and users.
Definition core.py:2233
init_case(args)
Implements the 'init' command.
Definition core.py:26828
sweep_workflow(args)
Study/sweep orchestration using Slurm job arrays.
Definition core.py:21372
inputs_workflow(args)
Handle explicit workspace input management.
Definition core.py:26562
submit_staged_jobs(args)
Submit previously staged Slurm artifacts from an existing run/study directory.
Definition core.py:25768
source_workflow(args)
Fetch source history without silently changing the active code.
Definition core.py:26695
None normalize_decomposition_args(args)
Resolve -n and the optional --ni/--nj/--nk of picurv run into one rank count.
Definition core.py:13807
sync_case_config_command(args)
Refresh template-managed config/docs files in a case directory.
Definition core.py:26919
build_project(args)
Implements the 'build' command.
Definition core.py:27018
fail_cli_usage(str message, str hint=None)
Emit a structured CLI usage error and exit with code 2.
Definition core.py:2256
sweep_continue_workflow(args)
Continue a partially-completed Slurm parameter sweep study.
Definition core.py:21713
cancel_run_jobs(args)
Cancel Slurm-submitted jobs for an existing run directory.
Definition core.py:26083
versions_workflow(args)
List or activate a release using the existing source/build owners.
Definition core.py:26728
precompute_workflow(args)
Resolve, preflight, and atomically publish reusable workspace assets.
Definition core.py:20487
run_workflow(args)
Main orchestrator for the 'run' command (local and Slurm modes).
Definition core.py:20520
pull_source_repo(args)
Refresh source branches in the repository resolved from a case directory.
Definition core.py:26972
Head of a generic C-style linked list.
Definition variables.h:518