Argument parser helpers shared across TNFR CLI commands.
"""Argument parser helpers shared across TNFR CLI commands."""
from __future__ import annotations
import argparse
from pathlib import Path
from typing import Any, Iterable
from ..config.presets import PREFERRED_PRESET_NAMES
from ..gamma import GAMMA_REGISTRY
from ..telemetry.verbosity import TELEMETRY_VERBOSITY_LEVELS
from ..types import ArgSpec
from .utils import spec
_PRESET_HELP = "Available presets: {}.".format(
", ".join(PREFERRED_PRESET_NAMES),
)
TELEMETRY_VERBOSITY_CHOICES = TELEMETRY_VERBOSITY_LEVELS
GRAMMAR_ARG_SPECS: tuple[ArgSpec, ...] = (
spec("--grammar.enabled", action=argparse.BooleanOptionalAction),
spec("--grammar.zhir_requires_oz_window", type=int),
spec("--grammar.zhir_dnfr_min", type=float),
spec("--grammar.thol_min_len", type=int),
spec("--grammar.thol_max_len", type=int),
spec("--grammar.thol_close_dnfr", type=float),
spec("--grammar.si_high", type=float),
spec("--glyph.hysteresis_window", type=int),
)
# History export/save specifications
HISTORY_ARG_SPECS: tuple[ArgSpec, ...] = (
spec("--save-history", type=str),
spec("--export-history-base", type=str),
spec("--export-format", choices=["csv", "json"], default="json"),
)
# Arguments shared by CLI subcommands
COMMON_ARG_SPECS: tuple[ArgSpec, ...] = (
spec("--nodes", type=int, default=24),
spec("--topology", choices=["ring", "complete", "erdos"], default="ring"),
spec("--seed", type=int, default=1),
spec(
"--p",
type=float,
help="Edge probability when topology=erdos",
),
spec("--observer", action="store_true", help="Attach standard observer"),
spec(
"--trace-verbosity",
choices=TELEMETRY_VERBOSITY_CHOICES,
help="Select the trace capture preset",
),
spec(
"--metrics-verbosity",
choices=TELEMETRY_VERBOSITY_CHOICES,
help="Select the metrics capture preset",
),
spec("--config", type=str),
spec("--dt", type=float),
spec("--integrator", choices=["euler", "rk4"]),
spec("--remesh-mode", choices=["knn", "mst", "community"]),
spec("--gamma-type", choices=list(GAMMA_REGISTRY.keys()), default="none"),
spec("--gamma-beta", type=float, default=0.0),
spec("--gamma-R0", type=float, default=0.0),
spec("--um-candidate-count", type=int),
spec("--stop-early-window", type=int),
spec("--stop-early-fraction", type=float),
)
def add_arg_specs(parser: argparse._ActionsContainer, specs: Iterable[ArgSpec]) -> None:
"""Register arguments from ``specs`` on ``parser``."""
for opt, kwargs in specs:
parser.add_argument(opt, **kwargs)
def _args_to_dict(args: argparse.Namespace, prefix: str) -> dict[str, Any]:
"""Extract arguments matching a prefix."""
return {
k.removeprefix(prefix): v
for k, v in vars(args).items()
if k.startswith(prefix) and v is not None
}
def add_common_args(parser: argparse.ArgumentParser) -> None:
"""Add arguments shared across subcommands."""
add_arg_specs(parser, COMMON_ARG_SPECS)
def add_grammar_args(parser: argparse.ArgumentParser) -> None:
"""Add grammar and structural operator hysteresis options."""
group = parser.add_argument_group("Grammar")
add_arg_specs(group, GRAMMAR_ARG_SPECS)
def add_grammar_selector_args(parser: argparse.ArgumentParser) -> None:
"""Add grammar options and structural operator selector."""
add_grammar_args(parser)
parser.add_argument("--selector", choices=["basic", "param"], default="basic")
def add_history_export_args(parser: argparse.ArgumentParser) -> None:
"""Add arguments to save or export history."""
add_arg_specs(parser, HISTORY_ARG_SPECS)
def add_canon_toggle(parser: argparse.ArgumentParser) -> None:
"""Add option to disable canonical grammar."""
parser.add_argument(
"--no-canon",
dest="grammar_canon",
action="store_false",
default=True,
help="Disable canonical grammar",
)
def _add_run_parser(sub: argparse._SubParsersAction) -> None:
"""Configure the ``run`` subcommand."""
from .execution import DEFAULT_SUMMARY_SERIES_LIMIT, cmd_run
p_run = sub.add_parser(
"run",
help=("Run a free scenario or preset and optionally export history"),
)
add_common_args(p_run)
p_run.add_argument("--steps", type=int, default=100)
p_run.add_argument(
"--use-Si",
dest="use_Si",
action=argparse.BooleanOptionalAction,
default=None,
help="Recompute the Sense Index during the run (use --no-use-Si to disable)",
)
p_run.add_argument(
"--apply-glyphs",
dest="apply_glyphs",
action=argparse.BooleanOptionalAction,
default=None,
help="Apply structural operators at every step (use --no-apply-glyphs to disable)",
)
p_run.add_argument(
"--dnfr-n-jobs",
dest="dnfr_n_jobs",
type=int,
help="Override ΔNFR parallel jobs forwarded to the runtime",
)
add_canon_toggle(p_run)
add_grammar_selector_args(p_run)
add_history_export_args(p_run)
p_run.add_argument("--preset", type=str, default=None, help=_PRESET_HELP)
p_run.add_argument("--sequence-file", type=str, default=None)
p_run.add_argument("--summary", action="store_true")
p_run.add_argument(
"--summary-limit",
type=int,
default=DEFAULT_SUMMARY_SERIES_LIMIT,
help=(
"Maximum number of samples per series in the summary (<=0 to"
" disable trimming)"
),
)
math_group = p_run.add_argument_group("Mathematical dynamics")
math_group.add_argument(
"--math-engine",
action=argparse.BooleanOptionalAction,
default=False,
help=(
"Enable the spectral mathematical dynamics engine to project nodes"
" onto Hilbert space vectors and validate norm, coherence and"
" structural frequency invariants"
),
)
math_group.add_argument(
"--math-dimension",
type=int,
help="Hilbert space dimension to use when the math engine is enabled",
)
math_group.add_argument(
"--math-coherence-spectrum",
type=float,
nargs="+",
metavar="λ",
help=(
"Eigenvalues for the coherence operator (defaults to a flat"
" spectrum when omitted)"
),
)
math_group.add_argument(
"--math-coherence-c-min",
type=float,
help="Explicit coherence floor C_min for the operator",
)
math_group.add_argument(
"--math-coherence-threshold",
type=float,
help="Coherence expectation threshold enforced during validation",
)
math_group.add_argument(
"--math-frequency-diagonal",
type=float,
nargs="+",
metavar="ν",
help=(
"Diagonal entries for the structural frequency operator"
" (defaults to the identity spectrum)"
),
)
math_group.add_argument(
"--math-generator-diagonal",
type=float,
nargs="+",
metavar="ω",
help=(
"Diagonal ΔNFR generator used by the mathematical dynamics"
" engine (defaults to the null generator)"
),
)
p_run.set_defaults(func=cmd_run)
def _add_sequence_parser(sub: argparse._SubParsersAction) -> None:
"""Configure the ``sequence`` subcommand."""
from .execution import cmd_sequence
p_seq = sub.add_parser(
"sequence",
help="Execute a sequence (preset or YAML/JSON)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"JSON sequence example:\n"
"[\n"
' "A",\n'
' {"WAIT": 1},\n'
' {"THOL": {"body": ["A", {"WAIT": 2}], "repeat": 2}}\n'
"]"
),
)
add_common_args(p_seq)
p_seq.add_argument("--preset", type=str, default=None, help=_PRESET_HELP)
p_seq.add_argument("--sequence-file", type=str, default=None)
add_history_export_args(p_seq)
add_grammar_args(p_seq)
p_seq.set_defaults(func=cmd_sequence)
def _add_metrics_parser(sub: argparse._SubParsersAction) -> None:
"""Configure the ``metrics`` subcommand."""
from .execution import cmd_metrics
p_met = sub.add_parser("metrics", help="Run briefly and export key metrics")
add_common_args(p_met)
p_met.add_argument("--steps", type=int, default=None)
add_canon_toggle(p_met)
add_grammar_selector_args(p_met)
p_met.add_argument("--save", type=str, default=None)
p_met.add_argument(
"--summary-limit",
type=int,
default=None,
help=(
"Maximum number of samples per series in the summary (<=0 to"
" disable trimming)"
),
)
p_met.set_defaults(func=cmd_metrics)
def _add_profile_parser(sub: argparse._SubParsersAction) -> None:
"""Configure the ``profile-si`` subcommand."""
from .execution import cmd_profile_si
p_prof = sub.add_parser(
"profile-si",
help="Profile compute_Si with and without NumPy",
)
p_prof.add_argument("--nodes", type=int, default=240)
p_prof.add_argument("--chord-step", type=int, default=7)
p_prof.add_argument("--loops", type=int, default=5)
p_prof.add_argument("--output-dir", type=Path, default=Path("profiles"))
p_prof.add_argument("--format", choices=("pstats", "json"), default="pstats")
p_prof.add_argument("--sort", choices=("cumtime", "tottime"), default="cumtime")
p_prof.set_defaults(func=cmd_profile_si)
def _add_profile_pipeline_parser(sub: argparse._SubParsersAction) -> None:
"""Configure the ``profile-pipeline`` subcommand."""
from .execution import cmd_profile_pipeline
p_profile = sub.add_parser(
"profile-pipeline",
help="Profile the Sense Index + ΔNFR pipeline",
)
p_profile.add_argument("--nodes", type=int, default=240, help="Number of nodes")
p_profile.add_argument(
"--edge-probability",
type=float,
default=0.32,
help="Probability passed to the Erdos-Renyi generator",
)
p_profile.add_argument(
"--loops",
type=int,
default=5,
help="How many times to execute the pipeline inside the profiler",
)
p_profile.add_argument(
"--seed",
type=int,
default=42,
help="Random seed used when generating the graph",
)
p_profile.add_argument(
"--output-dir",
type=Path,
default=Path("profiles"),
help="Directory where profiling artefacts will be written",
)
p_profile.add_argument(
"--sort",
choices=("cumtime", "tottime"),
default="cumtime",
help="Sort order applied to profiling rows",
)
p_profile.add_argument(
"--si-chunk-sizes",
nargs="+",
metavar="SIZE",
help="Chunk sizes forwarded to G.graph['SI_CHUNK_SIZE']; use 'auto' for heuristics",
)
p_profile.add_argument(
"--dnfr-chunk-sizes",
nargs="+",
metavar="SIZE",
help="Chunk sizes forwarded to G.graph['DNFR_CHUNK_SIZE']; use 'auto' for heuristics",
)
p_profile.add_argument(
"--si-workers",
nargs="+",
metavar="COUNT",
help="Worker counts forwarded to G.graph['SI_N_JOBS']; use 'auto' for serial runs",
)
p_profile.add_argument(
"--dnfr-workers",
nargs="+",
metavar="COUNT",
help="Worker counts forwarded to G.graph['DNFR_N_JOBS']; use 'auto' for defaults",
)
p_profile.set_defaults(func=cmd_profile_pipeline)
def _add_math_run_parser(sub: argparse._SubParsersAction) -> None:
"""Configure the ``math.run`` subcommand."""
from .execution import cmd_math_run
p_math = sub.add_parser(
"math.run",
help="Run simulation with mathematical dynamics engine validation",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Examples:\n"
" # Run with default math engine settings\n"
" tnfr math.run --nodes 24 --steps 100\n\n"
" # Run with custom Hilbert dimension\n"
" tnfr math.run --math-dimension 32 --steps 50\n\n"
" # Run with custom coherence spectrum\n"
" tnfr math.run --math-coherence-spectrum 1.0 0.8 0.6 --steps 100\n"
),
)
add_common_args(p_math)
p_math.add_argument("--steps", type=int, default=100)
p_math.add_argument(
"--preset",
type=str,
default=None,
help=_PRESET_HELP,
)
p_math.add_argument("--sequence-file", type=str, default=None)
add_canon_toggle(p_math)
add_grammar_selector_args(p_math)
add_history_export_args(p_math)
# Math engine is always enabled for math.run
math_group = p_math.add_argument_group("Mathematical dynamics (always enabled)")
math_group.add_argument(
"--math-dimension",
type=int,
help="Hilbert space dimension",
)
math_group.add_argument(
"--math-coherence-spectrum",
type=float,
nargs="+",
metavar="λ",
help="Eigenvalues for the coherence operator",
)
math_group.add_argument(
"--math-coherence-c-min",
type=float,
help="Explicit coherence floor C_min",
)
math_group.add_argument(
"--math-coherence-threshold",
type=float,
help="Coherence threshold for validation",
)
math_group.add_argument(
"--math-frequency-diagonal",
type=float,
nargs="+",
metavar="ν",
help="Diagonal entries for the frequency operator",
)
math_group.add_argument(
"--math-generator-diagonal",
type=float,
nargs="+",
metavar="ω",
help="ΔNFR generator diagonal",
)
p_math.set_defaults(func=cmd_math_run)
def _add_epi_validate_parser(sub: argparse._SubParsersAction) -> None:
"""Configure the ``epi.validate`` subcommand."""
from .execution import cmd_epi_validate
p_epi = sub.add_parser(
"epi.validate",
help="Validate EPI structural integrity and coherence",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Examples:\n"
" # Validate a preset\n"
" tnfr epi.validate --preset resonant_bootstrap\n\n"
" # Validate with custom topology\n"
" tnfr epi.validate --nodes 48 --topology complete\n\n"
" # Validate from sequence file\n"
" tnfr epi.validate --sequence-file presets/resonant_bootstrap.yaml\n"
),
)
add_common_args(p_epi)
p_epi.add_argument("--steps", type=int, default=50)
p_epi.add_argument(
"--preset",
type=str,
default=None,
help=_PRESET_HELP,
)
p_epi.add_argument("--sequence-file", type=str, default=None)
add_canon_toggle(p_epi)
add_grammar_selector_args(p_epi)
validation_group = p_epi.add_argument_group("Validation options")
validation_group.add_argument(
"--check-coherence",
action="store_true",
default=True,
help="Validate coherence preservation (enabled by default)",
)
validation_group.add_argument(
"--check-frequency",
action="store_true",
default=True,
help="Validate structural frequency positivity (enabled by default)",
)
validation_group.add_argument(
"--check-phase",
action="store_true",
default=True,
help="Validate phase synchrony in couplings (enabled by default)",
)
validation_group.add_argument(
"--tolerance",
type=float,
default=1e-6,
help="Numerical tolerance for validation checks",
)
p_epi.set_defaults(func=cmd_epi_validate)