r"""CLI entry point for NLSQ workflow commands.
This module provides the main command-line interface for NLSQ, supporting:
- `nlsq gui` - Launch the interactive Qt desktop GUI
- `nlsq fit workflow.yaml` - Execute single curve fit from YAML config
- `nlsq batch w1.yaml w2.yaml ...` - Execute parallel batch fitting
- `nlsq info` - Display system and environment information
- `nlsq config` - Copy configuration templates to current directory
Example Usage
-------------
From command line::
$ nlsq gui
$ nlsq fit workflow.yaml
$ nlsq fit workflow.yaml --output results.json
$ nlsq fit workflow.yaml --stdout
$ nlsq batch configs/\*.yaml --workers 4
$ nlsq info
$ nlsq config
$ nlsq config --workflow
$ nlsq config --model -o my_model.py
$ nlsq --version
$ nlsq --verbose fit workflow.yaml
"""
import argparse
import sys
from typing import Any
import nlsq
from nlsq.cli.errors import CLIError, ConfigError, DataLoadError, FitError, ModelError
[docs]
def create_parser() -> argparse.ArgumentParser:
"""Create the argument parser with subcommands.
Returns
-------
argparse.ArgumentParser
Configured argument parser with fit, batch, gui, and info subcommands.
"""
parser = argparse.ArgumentParser(
prog="nlsq",
description="NLSQ: GPU/TPU-accelerated nonlinear least squares curve fitting",
epilog="For more information, visit https://github.com/imewei/NLSQ",
)
# Top-level arguments
parser.add_argument(
"--version",
action="version",
version=f"nlsq {nlsq.__version__}",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Enable verbose output",
)
# Create subcommands
subparsers = parser.add_subparsers(
dest="command",
title="commands",
description="Available commands",
help="Use 'nlsq <command> --help' for more information",
)
# --- nlsq gui ---
subparsers.add_parser(
"gui",
help="Launch the interactive Qt desktop GUI",
description="Start the NLSQ graphical user interface for interactive curve fitting",
)
# --- nlsq fit ---
fit_parser = subparsers.add_parser(
"fit",
help="Execute single curve fit from YAML workflow configuration",
description="Load workflow configuration from YAML and execute curve fitting",
)
fit_parser.add_argument(
"workflow",
type=str,
help="Path to workflow YAML configuration file",
)
fit_parser.add_argument(
"-o",
"--output",
type=str,
default=None,
help="Override export.results_file path",
)
fit_parser.add_argument(
"--style",
type=str,
default=None,
choices=["publication", "presentation", "nature", "science", "minimal"],
help="Override visualization style preset",
)
fit_parser.add_argument(
"--stdout",
action="store_true",
help="Output results as JSON to stdout (for piping)",
)
# --- nlsq batch ---
batch_parser = subparsers.add_parser(
"batch",
help="Execute parallel batch fitting from multiple YAML files",
description="Process multiple workflow configurations in parallel",
)
batch_parser.add_argument(
"workflows",
type=str,
nargs="+",
help="Paths to workflow YAML configuration files",
)
batch_parser.add_argument(
"-s",
"--summary",
type=str,
default=None,
help="Path for aggregate summary file",
)
batch_parser.add_argument(
"-w",
"--workers",
type=int,
default=None,
help="Maximum number of parallel workers (default: auto-detect)",
)
batch_parser.add_argument(
"--continue-on-error",
action=argparse.BooleanOptionalAction,
default=True,
help="Continue processing on individual workflow failures (default: true)",
)
# --- nlsq info ---
subparsers.add_parser(
"info",
help="Display system and environment information",
description="Show NLSQ version, Python, JAX backend, GPU info, and builtin models",
)
# --- nlsq config ---
config_parser = subparsers.add_parser(
"config",
help="Copy configuration templates to current directory",
description="Copy workflow and/or custom model templates to start a new project",
)
config_parser.add_argument(
"--workflow",
action="store_true",
help="Copy only the workflow configuration template (workflow_config.yaml)",
)
config_parser.add_argument(
"--model",
action="store_true",
help="Copy only the custom model template (custom_model.py)",
)
config_parser.add_argument(
"-o",
"--output",
type=str,
default=None,
help="Custom output filename (only valid with --workflow or --model)",
)
config_parser.add_argument(
"-f",
"--force",
action="store_true",
help="Overwrite existing files without prompting",
)
return parser
[docs]
def handle_gui(args: argparse.Namespace) -> int:
"""Handle the 'gui' subcommand.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
Returns
-------
int
Exit code (0 for success, non-zero for failure).
"""
try:
from nlsq.gui_qt import run_desktop
print("Launching NLSQ Qt GUI...")
return run_desktop()
except ImportError as e:
print(
"\nError: Qt GUI dependencies not installed.\n"
'Install with: pip install "nlsq[gui_qt]"\n'
f"Details: {e}",
file=sys.stderr,
)
return 1
except KeyboardInterrupt:
print("\nGUI closed.")
return 0
[docs]
def handle_fit(args: argparse.Namespace) -> int:
"""Handle the 'fit' subcommand.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
Returns
-------
int
Exit code (0 for success, non-zero for failure).
"""
from nlsq.cli.commands import fit
try:
result = fit.run_fit(
workflow_path=args.workflow,
output_override=args.output,
style_override=args.style,
stdout=args.stdout,
verbose=args.verbose if hasattr(args, "verbose") else False,
)
if result is None:
return 1
return 0
except ConfigError as e:
_print_error("Configuration Error", e)
return 1
except DataLoadError as e:
_print_error("Data Loading Error", e)
return 1
except ModelError as e:
_print_error("Model Error", e)
return 1
except FitError as e:
_print_error("Fitting Error", e)
return 1
except CLIError as e:
_print_error("CLI Error", e)
return 1
[docs]
def handle_batch(args: argparse.Namespace) -> int:
"""Handle the 'batch' subcommand.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
Returns
-------
int
Exit code (0 for success, non-zero for failure).
"""
from nlsq.cli.commands import batch
try:
results = batch.run_batch(
workflow_paths=args.workflows,
summary_file=args.summary,
max_workers=args.workers,
continue_on_error=args.continue_on_error,
verbose=args.verbose if hasattr(args, "verbose") else False,
)
# Return non-zero if any workflows failed
failures = [r for r in results if r.get("status") == "failed"]
if failures:
print(f"\nBatch completed with {len(failures)} failure(s)")
return 1
return 0
except CLIError as e:
_print_error("CLI Error", e)
return 1
[docs]
def handle_info(args: argparse.Namespace) -> int:
"""Handle the 'info' subcommand.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
Returns
-------
int
Exit code (always 0 for info command).
"""
from nlsq.cli.commands import info
info.run_info(verbose=args.verbose if hasattr(args, "verbose") else False)
return 0
[docs]
def handle_config(args: argparse.Namespace) -> int:
"""Handle the 'config' subcommand.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
Returns
-------
int
Exit code (0 for success, non-zero for failure).
"""
from nlsq.cli.commands import config
try:
config.run_config(
workflow=args.workflow,
model=args.model,
output=args.output,
force=args.force,
verbose=args.verbose if hasattr(args, "verbose") else False,
)
return 0
except FileExistsError as e:
print(f"\nError: {e}", file=sys.stderr)
return 1
except ValueError as e:
print(f"\nError: {e}", file=sys.stderr)
return 1
def _print_error(error_type: str, error: CLIError) -> None:
"""Print a formatted error message to stderr.
Parameters
----------
error_type : str
Type of error for the header.
error : CLIError
The error object with message and context.
"""
print(f"\nError: {error_type}", file=sys.stderr)
print(f" {error.message}", file=sys.stderr)
if error.context:
for key, value in error.context.items():
print(f" {key}: {value}", file=sys.stderr)
if error.suggestion:
print(f"\nSuggestion: {error.suggestion}", file=sys.stderr)
[docs]
def main(argv: list[str] | None = None) -> int:
"""Main entry point for the NLSQ CLI.
Parameters
----------
argv : list[str], optional
Command-line arguments. If None, uses sys.argv.
Returns
-------
int
Exit code (0 for success, non-zero for failure).
Examples
--------
>>> main(["gui"])
0
>>> main(["fit", "workflow.yaml"])
0
>>> main(["info"])
0
"""
parser = create_parser()
args = parser.parse_args(argv)
# No command specified - print help
if args.command is None:
parser.print_help()
return 0
# Dispatch to appropriate handler
handlers: dict[str, Any] = {
"gui": handle_gui,
"fit": handle_fit,
"batch": handle_batch,
"info": handle_info,
"config": handle_config,
}
handler = handlers.get(args.command)
if handler is None:
parser.print_help()
return 1
return handler(args)
if __name__ == "__main__":
sys.exit(main())