"""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