Source code for homodyne.cli.args_parser

"""Argument Parser for Homodyne CLI
===================================

Simplified argument parsing system for the minimal CLI interface.
Focuses on essential commands while maintaining compatibility.

Note: GPU support removed in v2.3.0 - CPU-only execution.
"""

import argparse
from pathlib import Path

# Import version from package (with fallback for development)
try:
    from homodyne._version import __version__
except ImportError:
    __version__ = "unknown"


[docs] def create_parser() -> argparse.ArgumentParser: """Create the main argument parser for Homodyne CLI. Returns: Configured ArgumentParser with essential CLI options (CPU-only) """ # Create epilog with version interpolation epilog_text = f""" Examples: %(prog)s # Run with default NLSQ method %(prog)s --method nlsq # NLSQ trust-region least squares (default) %(prog)s --method cmc # Consensus Monte Carlo (per-shard NUTS) %(prog)s --method both # Sequential NLSQ then CMC (recommended) %(prog)s --config my_config.yaml # Use custom config file %(prog)s --output-dir ./results # Custom output directory %(prog)s --verbose # Enable verbose logging %(prog)s --plot-experimental-data # Generate data validation plots %(prog)s --plot-simulated-data # Plot theoretical heatmaps %(prog)s --plot-simulated-data --contrast 0.5 --offset 1.05 # Custom contrast/offset %(prog)s --phi-angles "0,45,90,135" # Custom phi angles for simulated data %(prog)s --static-mode # Force static mode (3 parameters) %(prog)s --laminar-flow --method cmc # Force laminar flow (7 parameters) with CMC Recommended NLSQ -> CMC Workflow: Step 1: Run NLSQ to get point estimates %(prog)s --method nlsq --config config.yaml --output-dir results/ Step 2: Run CMC with pre-computed NLSQ warm-start (RECOMMENDED) %(prog)s --method cmc --config config.yaml --nlsq-result results/ The --nlsq-result flag loads parameters from results/nlsq/parameters.json, avoiding redundant NLSQ computation and ensuring consistent warm-start values. Optimization Methods: nlsq: NLSQ trust-region nonlinear least squares (PRIMARY) Use for: Fast, reliable parameter estimation cmc: Consensus Monte Carlo with stratified sharding (SECONDARY) Use for: Uncertainty quantification, publication-quality analysis Per-shard NUTS sampling with NumPyro/BlackJAX backend. Physical Model: c2(phi,t1,t2) = 1 + contrast * [c1(phi,t1,t2)]^2 Static Mode: 3 parameters [D0, alpha, D_offset] Laminar Flow: 7 parameters [D0, alpha, D_offset, shear0, beta, shear_offset, phi0] Homodyne v{__version__} - CPU-Optimized JAX Architecture """ parser = argparse.ArgumentParser( prog="homodyne", description="CPU-optimized homodyne scattering analysis for XPCS", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=epilog_text, ) # Version information parser.add_argument( "--version", action="version", version=f"Homodyne v{__version__}", ) # Method selection - JAX-first methods only parser.add_argument( "--method", choices=["nlsq", "cmc", "both"], default="nlsq", help=( "Optimization method: nlsq (NLSQ trust-region), cmc (Consensus Monte Carlo), " "both (sequential NLSQ then CMC with automatic warm-start). " "CMC uses stratified sharding with per-shard NumPyro/BlackJAX NUTS sampling; " "configure sharding and initial values via config." ), ) # Configuration and I/O parser.add_argument( "--config", type=Path, default=Path("./homodyne_config.yaml"), help="Path to configuration file (YAML) (default: %(default)s)", ) parser.add_argument( "--output-dir", type=Path, default=Path("./homodyne_results"), help="Output directory for results (default: %(default)s)", ) # Data file parser.add_argument( "--data-file", type=Path, help="Path to experimental data file (overrides config)", ) # Analysis mode parser.add_argument( "--static-mode", action="store_true", help="Force static analysis mode (3 parameters)", ) parser.add_argument( "--laminar-flow", action="store_true", help="Force laminar flow analysis mode (7 parameters)", ) # NLSQ-specific options nlsq_group = parser.add_argument_group("NLSQ Options") nlsq_group.add_argument( "--max-iterations", type=int, default=10000, help="Maximum NLSQ iterations (default: %(default)s)", ) nlsq_group.add_argument( "--tolerance", type=float, default=1e-8, help="NLSQ convergence tolerance (default: %(default)s)", ) # CMC-specific options cmc_group = parser.add_argument_group( "Consensus Monte Carlo (CMC) Options", description="Options for --method cmc. " "CMC uses stratified sharding with per-shard NUTS sampling.", ) cmc_group.add_argument( "--n-samples", type=int, default=None, help="Number of CMC samples per chain (default: from config or 1000)", ) cmc_group.add_argument( "--n-warmup", type=int, default=None, help="Number of CMC warmup samples (default: from config or 500)", ) cmc_group.add_argument( "--n-chains", type=int, default=None, help="Number of CMC chains (default: from config or 4)", ) cmc_group.add_argument( "--cmc-num-shards", type=int, default=None, help="Number of data shards for CMC (overrides config, default: auto-detect based on dataset size)", ) cmc_group.add_argument( "--cmc-backend", choices=["auto", "pjit", "multiprocessing", "pbs"], default=None, help="CMC backend for parallel execution (overrides config, default: auto-detect based on hardware)", ) cmc_group.add_argument( "--cmc-plot-diagnostics", action="store_true", help="[DEPRECATED] ArviZ diagnostic plots are now always generated for --method cmc. " "This flag is kept for backward compatibility but has no effect.", ) cmc_group.add_argument( "--no-nlsq-warmstart", action="store_true", help="Disable automatic NLSQ warm-start for CMC. NOT RECOMMENDED - " "without warm-start, CMC may have ~28%% divergence rate vs <5%% with warm-start.", ) cmc_group.add_argument( "--nlsq-result", type=Path, default=None, help="Path to pre-computed NLSQ results directory (from hm-nlsq pipeline). " "If provided, CMC will use parameters from <path>/nlsq/parameters.json " "for warm-start instead of running NLSQ inline. RECOMMENDED for production workflows.", ) # Parameter override options override_group = parser.add_argument_group( "Parameter Override Options", description="Override initial parameter values and MCMC thresholds from config file. " "Priority: CLI args > config file > package defaults. " "Useful for exploratory analysis without modifying config files.", ) # Initial parameter overrides (static mode: 3 parameters) override_group.add_argument( "--initial-d0", type=float, default=None, help="Override initial D0 (diffusion coefficient at t=1s, nm^2/s) from config", ) override_group.add_argument( "--initial-alpha", type=float, default=None, help="Override initial alpha (time-dependent diffusion exponent) from config", ) override_group.add_argument( "--initial-d-offset", type=float, default=None, help="Override initial D_offset (constant offset diffusion, nm^2/s) from config", ) # Initial parameter overrides (laminar flow mode: 4 additional parameters) override_group.add_argument( "--initial-gamma-dot-t0", type=float, default=None, help="Override initial gamma_dot_t0 (shear rate at t=1s, 1/s) from config (laminar flow only)", ) override_group.add_argument( "--initial-beta", type=float, default=None, help="Override initial beta (time-dependent shear exponent) from config (laminar flow only)", ) override_group.add_argument( "--initial-gamma-dot-offset", type=float, default=None, help="Override initial gamma_dot_t_offset (constant offset shear rate, 1/s) from config (laminar flow only)", ) override_group.add_argument( "--initial-phi0", type=float, default=None, help="Override initial phi0 (flow direction angle, radians) from config (laminar flow only)", ) # MCMC/CMC mass matrix option override_group.add_argument( "--dense-mass-matrix", action="store_true", help="Use dense mass matrix for NUTS/CMC (default: diagonal). May improve sampling for correlated parameters.", ) # Output options parser.add_argument( "--save-plots", action="store_true", help="Save result plots to output directory", ) parser.add_argument( "--plot-experimental-data", action="store_true", help="Generate validation plots of experimental data for quality checking", ) parser.add_argument( "--plot-simulated-data", action="store_true", help="Plot theoretical C2 heatmaps using parameters from config (no experimental data required)", ) parser.add_argument( "--plotting-backend", type=str, choices=["auto", "matplotlib", "datashader"], default="auto", help="Plotting backend: auto (use Datashader if available), matplotlib (slower), datashader (5-10x faster, requires datashader package) (default: %(default)s)", ) parser.add_argument( "--parallel-plots", action="store_true", help="Generate plots in parallel using multiprocessing (faster for multiple angles, requires Datashader backend)", ) # Simulated data parameters (only valid with --plot-simulated-data) parser.add_argument( "--contrast", type=float, default=0.3, help="Contrast parameter for simulated data: c2 = 1 + contrast * c1^2 (default: %(default)s, requires --plot-simulated-data)", ) parser.add_argument( "--offset", type=float, default=1.0, help="Offset parameter for simulated data: c2 = offset + contrast * c1^2 (default: %(default)s, requires --plot-simulated-data)", ) parser.add_argument( "--phi-angles", type=str, help="Comma-separated list of phi angles in degrees (e.g., '0,45,90,135'). Default uses config file values or evenly spaced angles", ) parser.add_argument( "--output-format", choices=["yaml", "json", "npz"], default="yaml", help="Output format for results (default: %(default)s)", ) # Logging parser.add_argument( "--verbose", "-v", action="count", default=0, help="Increase logging verbosity (-v: INFO, -vv: DEBUG, -vvv: TRACE)", ) parser.add_argument( "--quiet", action="store_true", help="Suppress all output except errors", ) # Runtime tuning runtime_group = parser.add_argument_group("Runtime Options") runtime_group.add_argument( "--threads", type=int, default=None, help="Number of CPU threads for XLA intra-op parallelism (default: all cores)", ) runtime_group.add_argument( "--no-jit", action="store_true", help="Disable JAX JIT compilation (useful for debugging)", ) return parser
[docs] def validate_args(args: argparse.Namespace) -> bool: """Validate parsed command-line arguments. Parameters ---------- args : argparse.Namespace Parsed command-line arguments Returns ------- bool True if arguments are valid, False otherwise """ # Check for conflicting analysis modes if args.static_mode and args.laminar_flow: print("Error: Cannot specify both --static-mode and --laminar-flow") return False # Check for conflicting logging options if args.verbose > 0 and args.quiet: print("Error: Cannot specify both --verbose and --quiet") return False # Validate numeric ranges if args.max_iterations <= 0: print("Error: Maximum iterations must be positive") return False if args.tolerance <= 0: print("Error: Tolerance must be positive") return False if hasattr(args, "threads") and args.threads is not None and args.threads <= 0: print("Error: --threads must be positive") return False # Validate MCMC parameters if provided via CLI if args.n_samples is not None and args.n_samples <= 0: print("Error: MCMC samples must be positive") return False if args.n_warmup is not None and args.n_warmup <= 0: print("Error: MCMC warmup must be positive") return False if args.n_chains is not None and args.n_chains <= 0: print("Error: MCMC chains must be positive") return False # Validate CMC parameters if args.cmc_num_shards is not None and args.cmc_num_shards <= 0: print("Error: CMC num_shards must be positive") return False # Warn if CMC arguments provided with non-CMC method if args.method not in ("cmc", "both"): if args.cmc_num_shards is not None: print( f"Warning: --cmc-num-shards ignored (not applicable for method={args.method})" ) if args.cmc_backend is not None: print( f"Warning: --cmc-backend ignored (not applicable for method={args.method})" ) if ( args.n_samples is not None or args.n_warmup is not None or args.n_chains is not None ): print( f"Warning: --n-samples/--n-warmup/--n-chains ignored (not applicable for method={args.method})" ) # Note: --cmc-plot-diagnostics is deprecated and ignored for all methods # ArviZ diagnostic plots are now always generated for CMC # T3-8: Validate --phi-angles format (comma-separated floats) if args.phi_angles is not None: try: [float(x.strip()) for x in args.phi_angles.split(",")] except ValueError: print( f"Error: --phi-angles must be comma-separated numbers " f"(e.g., '0,45,90,135'), got: '{args.phi_angles}'" ) return False # Validate parameter override values if args.initial_d0 is not None and args.initial_d0 <= 0: print("Error: --initial-d0 must be positive") return False # Check config file exists if provided and not default DEFAULT_CONFIG = Path("./homodyne_config.yaml").resolve() if args.config.resolve() != DEFAULT_CONFIG and not args.config.exists(): print(f"Error: Configuration file not found: {args.config}") return False # Check data file exists if provided if args.data_file and not args.data_file.exists(): print(f"Error: Data file not found: {args.data_file}") return False return True