CLI Reference
=============

FHOPS exposes a Typer-based CLI called ``fhops``. Detailed command documentation will be
autogenerated with ``sphinx-click`` in upcoming iterations. For now, the primary commands are:

- ``fhops validate`` — validate scenario inputs.
- ``fhops solve-mip-operational`` — build and solve the shift-indexed operational MILP (the formulation documented in the FHOPS paper; bundle + Pyomo builder + HiGHS wrapper). ``--earliness`` (off by default) adds a lexicographic second solve that breaks ties in favour of early production without changing the reported objective (#141; ``fhops plan rolling`` enables it in MILP windows).
- ``fhops solve-mip`` — deprecated alias of ``solve-mip-operational`` (since 1.0.1; see below).
- ``fhops solve-heur`` — run the metaheuristic solver (simulated annealing v0.1).
- ``fhops solve-heur ... --iters 5000 --seed 123`` — adjust SA iteration budget/seed (additional tuning flags under review).
- ``fhops evaluate`` — replay a schedule and compute KPIs.
- ``fhops eval-playback`` — generate shift/day summaries from a schedule (deterministic playback).
- ``fhops benchmark`` — compare the operational MILP with simulated annealing on a single scenario (legacy helper).
- ``fhops bench suite`` — run the full benchmarking harness across sample or user-supplied scenarios.
- ``fhops geo distances`` — derive block-to-block distance matrices from GeoJSON geometry.
- ``fhops tree inventory validate`` / ``fhops tree register`` — validate and pin a producer-neutral tree inventory.
- ``fhops tree map`` / ``fhops tree stems`` / ``fhops tree simulate`` / ``fhops tree replay`` / ``fhops tree scene`` — run the tree-scale mapping, stem, simulation, replay, and glTF/GLB export path; see :doc:`../howto/tree_scale`.\n- ``fhops tree system`` — simulate a feller-buncher/skidder/loader ground-based system on one block; see :doc:`../reference/tree_system`.
- ``fhops report salvage-shelf-life`` — join a storage/age CSV to scenario blocks and report published volume retention, optional retained value, and advisory product windows; see :doc:`../howto/salvage_shelf_life`.
- ``fhops plan rolling`` — execute rolling-horizon solves (stub/SA/MIP hooks) with metadata-rich exports for per-iteration telemetry and locked assignments; see :doc:`../howto/rolling_horizon`. Supports solver options such as ``--mip-solver-option Threads=64`` (repeatable) or ``GRB_THREADS`` for Gurobi, plus ``--out-json``, ``--out-assignments``, and per-iteration JSONL/CSV exports (always written, also when a run fails part-way). Windows whose solver finds no solution are recorded and left idle; ``--fail-on-no-solution`` stops at the first one and ``--fail-on-empty-window`` at the first window without a solution or solved to an empty plan (exit code 1). MILP windows break ties in favour of early production by default (lexicographic second solve, up to ``--mip-earliness-time-limit`` seconds, default ``--mip-time-limit``; the window objective is unchanged; ``--no-mip-earliness`` disables it). Exit codes: ``0`` completed, ``1`` stopped by a ``--fail-on-*`` flag, failed, or no window returned a solution (e.g. every window hit a solver error), ``2`` usage error (invalid ``--master-days``/``--sub-days``/``--lock-days``, ``--max-iterations < 1``, unknown ``--solver``, unavailable ``--mip-solver``); see :ref:`rolling-no-plan`. Feed the locked assignments into ``fhops eval-playback`` or :func:`fhops.planning.compute_rolling_kpis` for KPI deltas against a baseline.

Run ``fhops --help`` to inspect the full command tree.

Baseline usage:

- ``fhops solve-mip-operational examples/med42/scenario.yaml --solver gurobi --time-limit 600 --out tmp/med42_gurobi.csv`` — run the MILP with the Gurobi backend (requires installing ``fhops[gurobi]`` and configuring a licence).
- ``fhops solve-mip-operational examples/tiny7/scenario.yaml --out tmp/tiny7_operational.csv --time-limit 60`` — run the day×shift operational MILP benchmark and emit assignments/KPIs for the tiny7 scenario.
  Use ``--dump-bundle foo.json`` to capture the serialized bundle for debugging, or ``--bundle-json foo.json`` to replay the solver without reloading the scenario. Telemetry/logging hooks mirror the heuristics (``--telemetry-log`` for JSONL records, ``--watch`` for a live snapshot even though the run is single-shot). ``--incumbent seed.csv`` accepts a heuristic schedule (``machine_id, block_id, day, shift_id[, assigned, production]``) as a warm start; the CLI expands those rows into the auxiliary Pyomo variables (transitions, activation binaries, landing inventories) before invoking the solver. With ``--solver highs`` (default) the start is passed through Pyomo's ``appsi_highs`` interface and the CLI prints whether HiGHS accepted it (``Warm start: method=appsi_highs … (accepted)``); ``gurobi``/``cbc``/``cplex`` receive Pyomo's ``warmstart=True``; other solvers run without the start and emit a ``MilpWarmStartWarning`` (see :doc:`../howto/mip_warm_starts`). Today the feature is operationally correct but only practically useful on the smallest scenarios—Gurobi still discards greedy/SA seeds on med42/large84 because they are far from its internal incumbents—so expect little to no runtime improvement unless you provide a near-feasible schedule.
- ``fhops plan rolling examples/med42/scenario.yaml --master-days 42 --sub-days 21 --lock-days 7 --solver mip --mip-solver gurobi --mip-solver-option Threads=64 --mip-time-limit 600 --out-json tmp/med42_rolling.json --out-assignments tmp/med42_rolling_assignments.csv`` — run the rolling MILP planner with a Gurobi backend and archive both telemetry JSON and playback-ready assignments. Use ``--out-iterations-jsonl``/``--out-iterations-csv`` to capture per-iteration runtimes/objectives and pass the assignments into ``fhops eval-playback`` to compute KPI deltas quickly.
- ``fhops solve-heur tests/fixtures/regression/regression.yaml --out tmp/regression_sa.csv``
- ``fhops evaluate tests/fixtures/regression/regression.yaml --assignments tmp/regression_sa.csv --kpi-mode extended``
- ``fhops eval-playback tests/fixtures/regression/regression.yaml --assignments tmp/regression_sa.csv --shift-out tmp/shift_summary.csv --day-out tmp/day_summary.csv``
- ``fhops eval-playback tests/fixtures/regression/regression.yaml --assignments tmp/regression_sa.csv --samples 10 --downtime-prob 0.1 --weather-prob 0.2`` — run stochastic playback, capturing downtime and weather variability.
- ``fhops eval-playback ... --downtime-prob 0.1 --downtime-mean 4 --downtime-std 1.5`` — each hit machine-shift loses a sampled number of hours (clipped to the shift length) and the matching fraction of its production; a full-shift loss cancels the assignment.
- ``fhops eval-playback ... --landing-prob 0.3 --landing-mult-min 0.3 --landing-mult-max 0.7 --landing-duration 2`` — simulate landing congestion shocks: each landing-day starts a shock with the given probability, and the shock scales every assignment on that landing for ``--landing-duration`` days (minimum multiplier when shocks overlap).
- ``fhops eval-playback ... --shift-parquet tmp/shift.parquet --day-parquet tmp/day.parquet --summary-md tmp/playback.md`` — export Parquet files and a Markdown summary alongside the CSV outputs.
  See :doc:`../howto/evaluation` for a full end-to-end example.
- ``fhops eval-playback ... --shift-out tmp/shift_summary.csv --day-out tmp/day_summary.csv`` — recommended when using shift calendars/blackouts so you can verify shift-level KPIs roll up to the day totals.
- ``fhops bench suite --scenario examples/tiny7/scenario.yaml --out-dir tmp/benchmarks``
- ``fhops synth generate tmp/custom_bundle --tier medium --seed 777 --blocks 10:12`` — create a synthetic scenario bundle using the medium preset with a custom block range; add ``--preview`` to inspect metadata without writing files.
- ``fhops synth batch docs/examples/synthetic_batch_plan.yaml --overwrite`` — process several bundles in one call using a YAML/TOML/JSON plan (each entry supports the same fields as `generate`), refreshing metadata automatically when writing to ``examples/synthetic``.

Both ``solve-mip-operational`` and ``solve-heur`` export schedules with the columns ``machine_id``,
``block_id``, ``day``, and ``shift_id``. The shift identifier matches the scenario's shift calendar (or defaults to
``S1`` when only day-level data is provided) so downstream tooling can analyse sub-daily assignments.

``solve-mip`` (deprecated alias) outcomes and exit codes
--------------------------------------------------------

Since FHOPS 1.0.1 (#127) ``fhops solve-mip`` is a deprecated alias: it prints one
``Deprecated: …`` line and solves the **operational MILP** (the same model as
``solve-mip-operational``, via :func:`fhops.optimization.mip.solve_mip`, which emits a
``LegacyMipDeprecationWarning``). Until 1.0.1 it solved a legacy day-level MIP whose loader-buffer
constraint cannot hold in the first shift, so every scenario whose harvest systems have a loader
role — every bundled example (``tiny7``, ``small21``, ``med42``, ``large84``) — was infeasible
(FHOPS 1.0.0 raised an exception for these models). Use ``fhops solve-mip-operational``.

Option mapping (``solve-mip`` → ``solve-mip-operational``):

- ``--out`` → ``--out``; ``--time-limit N`` (default 60) → ``--time-limit N``;
  ``--debug`` → ``--debug`` (solver log + tracebacks); ``--sequencing-debug`` → ``--sequencing-debug``.
- ``--driver auto`` (default) → ``--solver gurobi`` when Gurobi is installed (retried with
  ``--solver highs`` after a Gurobi solver error, noted as a ``Warning:``), otherwise
  ``--solver highs``; ``highs`` / ``highs-appsi`` / ``highs-exec`` → ``--solver highs``;
  ``gurobi`` → ``--solver gurobi``; ``gurobi-appsi`` → ``--solver appsi_gurobi``;
  ``gurobi-direct`` → ``--solver gurobi_direct``.
- ``--gap``, ``--solver-option``, ``--incumbent``, ``--telemetry-log``, ``--bundle-json``,
  ``--dump-bundle`` and ``--watch`` exist only on ``solve-mip-operational``.

The output keeps the 1.0.1 (#124) format::

   MIP outcome=<outcome> solver_status=<status> termination=<condition> objective=<value|n/a>

``outcome`` is ``optimal``, ``feasible`` (incumbent at the time limit), ``infeasible``,
``no_solution`` (time limit before any incumbent) or ``error`` (the solver failed, e.g. a HiGHS
option error; printed as ``Solver error: …``). Without a solution the CSV is an empty assignment
table with the usual columns (``machine_id, block_id, day, shift_id, assigned, production``) and
the KPI summary is skipped; an incumbent that assigns no machine is written as an empty table too.
The objective is the operational MILP objective (not comparable with legacy 1.0.0 values). Exit
codes:

- ``0`` — the solver ran, including infeasible models and time limits without an incumbent;
- ``1`` — solver error, or the requested solver is not available;
- ``2`` — invalid arguments (e.g. an unknown ``--driver``).

``fhops benchmark`` solves the operational MILP with the same ``--driver`` mapping (no deprecation
notice). It prints ``MIP obj=n/a (outcome=…)`` and skips the MIP metrics when the MILP has no
solution; SA still runs, and a MIP solver error exits ``1`` after the SA results. When SA applies
objective-weight overrides (FHOPS Tiny7/Small21 built-ins or ``--objective-weight``) it prints
``SA obj (override weights)`` and ``SA obj (scenario weights)`` (the SA plan scored under the
MILP's weights) plus a one-line notice; ``solve-heur``/``solve-ils``/``solve-tabu`` and
``bench suite`` print the same notice (#140, :ref:`heuristic-objective-weights`).

Heuristic configuration reference
----------------------------------

See :doc:`../howto/heuristic_presets` for an end-to-end walkthrough. Common CLI patterns:

- ``fhops solve-heur ... --operator swap --operator move`` — restrict the operator set (defaults to all registered operators).
- ``fhops solve-heur ... --operator-weight swap=2 --operator-weight move=0.5`` — adjust operator weights; zero disables an operator.
- ``fhops bench suite ... --operator swap --operator-weight swap=2`` — pass the same options when running aggregate benchmarks; the summary now records the operator configuration in ``operators_config``.
- ``fhops solve-heur ... --operator-preset swap-only`` — apply predefined operator weight profiles (available presets: ``balanced``, ``move-only``, ``swap-heavy``, ``swap-only``, ``diversify``, ``explore``, ``mobilisation``, ``stabilise``). Presets may be combined with explicit ``--operator`` and ``--operator-weight`` overrides.
- ``explore`` enables the advanced neighbourhood operators with moderate weights for general diversification, ``mobilisation`` prioritises mobilisation shake moves for distance-constrained scenarios, and ``stabilise`` tones down advanced operators to focus on consolidation.
- Tiny ladder defaults: ``fhops solve-heur examples/tiny7/scenario.yaml --out tmp/tiny7_sa.csv`` auto-enables small batched sampling (``batch_size=4``) and a mobilisation-shake boost after prolonged stalls; pass ``--batch-neighbours``/``--parallel-workers`` to override.
- ``fhops bench suite --operator-preset swap-heavy --operator-weight move=1`` — use presets within benchmarking; final configurations are captured in the summary output.
- ``fhops bench suite --list-operator-presets`` (or ``fhops solve-heur ... --list-operator-presets``, which still requires the scenario argument and ``--out``) — display all presets with their weights and descriptions.
- ``fhops bench suite --compare-preset explore --compare-preset mobilisation`` — sweep multiple presets in one run; the benchmark summary adds a ``preset_label`` column and exports per-preset assignment CSVs for side-by-side analysis.
- ``fhops bench suite --include-tabu`` — benchmark the Tabu prototype alongside SA (produces additional ``tabu`` rows in the summary output).
- ``fhops bench suite --include-ils --include-tabu`` — emit solver comparison columns (best heuristic, gap/ratio metrics) so you can rank heuristics against each other and against MIP when included.
- ``fhops solve-heur ... --batch-neighbours 4 --parallel-workers 4`` — sample multiple neighbour candidates per iteration and score them with a small worker pool (opt-in; defaults keep sequential evaluation).
- ``fhops solve-heur ... --parallel-multistart 8`` — launch several SA runs in parallel, using the best result while logging per-run telemetry (requires ``--parallel-workers`` for true parallelism).
- ``fhops solve-heur ... --telemetry-log fhops_runs.jsonl`` — append telemetry entries (objective, KPIs, operator stats, parallel configuration) to a JSONL file for later analysis.
- ``fhops solve-heur ... --kpi-mode basic`` — print the concise KPI bundle (production/mobilisation only). Use ``extended`` to show utilisation, downtime, and weather metrics.
- ``fhops solve-heur ... --show-operator-stats`` — print per-operator proposal/acceptance statistics at the end of a run (also available in the benchmark summaries).
- ``fhops solve-ils ... --perturbation-strength 3 --stall-limit 10 --hybrid-use-mip`` — run the Iterated Local Search solver. The optional hybrid flag solves the operational MILP (HiGHS, ``--hybrid-mip-time-limit`` seconds) with the best ILS schedule as a warm start (MIP start) each time ILS stalls, and keeps the MILP schedule only when it scores higher; ``--batch-neighbours``/``--parallel-workers`` reuse the SA batching infrastructure.
- ``fhops solve-tabu ...`` — run the Tabu Search prototype (`--tabu-tenure`, `--stall-limit`, `--batch-neighbours`, `--parallel-workers`) and export telemetry consistent with SA runs.
- ``fhops bench suite --include-ils`` — add ILS rows to the benchmark summary (CSV/JSON). Combine with ``--include-tabu`` for full solver comparisons.
- ``python scripts/render_benchmark_plots.py tmp/benchmarks/summary.csv`` — turn benchmark summaries into comparison charts for documentation (see :doc:`../howto/benchmarks`).
- ``fhops bench suite --include-ils --include-tabu --out-dir tmp/benchmarks_compare`` — generate the richer comparison columns (best heuristic solver, objective gaps, runtime ratios).
- ``fhops solve-heur ... --profile explore`` — apply a named solver profile that bundles presets, batching, and optional multi-start settings (see ``fhops bench suite --list-profiles``).
- ``fhops bench suite --profile mobilisation`` — reuse the same profile defaults across SA/ILS/Tabu when benchmarking; explicit CLI overrides still win.

The evaluation output should include `mobilisation_cost=6.0` and `sequencing_violation_count=0`
if the regression baseline is satisfied.

Dataset command cheat sheet
---------------------------

``fhops dataset estimate-productivity`` exposes every productivity preset that now carries detailed
docstrings (see :mod:`fhops.cli.dataset`). Use the following guide to jump from the CLI role flag to
the corresponding API surface:

.. list-table::
   :header-rows: 1
   :widths: 18 37 20 25

   * - CLI ``--machine-role``
     - When to pick it
     - Key CLI options
     - Docstring to consult
   * - ``forwarder``
     - Cut-to-length or shortwood workflows needing payload/distance regressions.
     - ``--forwarder-model`` + payload/length inputs listed in the CLI help.
     - :func:`fhops.productivity.forwarder_bc.estimate_forwarder_productivity_bc`
   * - ``grapple_skidder``
     - Full-tree or salvage skidding (ADV6N7, Han 2018, ADV1N12 presets).
     - ``--grapple-skidder-model`` with extraction distance, payload, utilisation overrides as needed.
     - :func:`fhops.productivity.grapple_bc.estimate_grapple_skidder_productivity_adv6n7`
   * - ``grapple_yarder`` / ``skyline_yarder``
     - Cable-running or standing skyline studies (TR125/127, FNCY12, TN173, Hi-Skid).
     - ``--model`` plus the lateral/log-count options described in the skyline helper docstrings.
     - :mod:`fhops.productivity.cable_logging`
   * - ``loader`` / ``loader_forwarder``
     - Loader-forwarder or Barko/Hypro presets (ADV5N1, ADV2N26, TN261, Barko 450).
     - ``--loader-model`` with payload/utilisation/delay switches.
     - :mod:`fhops.productivity.loader`
   * - ``helicopter_longline``
     - Longline or direct-to-water transfer flights with payload/delay modelling.
     - ``--helicopter-model`` / ``--helicopter-preset`` plus distance/payload overrides.
     - :func:`fhops.productivity.estimate_helicopter_longline_productivity`
   * - ``shovel_logger``
     - Sessions & Boston serpentine shovel logging.
     - ``--shovel-*`` swing/payload options (defaults seeded by the preset).
     - :func:`fhops.cli.dataset._evaluate_shovel_logger_result`

Each function linked above now documents parameter units, accepted ranges, default templates, and the
structure of the result dataclasses. Run ``sphinx-build -b html docs _build/html -W`` after adding
new roles so these cross-references stay valid.

Log-hauling commands
--------------------

The optional hauling layer adds a `fhops haul` command group:

.. code-block:: bash

   fhops haul validate examples/hauling/scenario.yaml
   fhops haul plan examples/hauling/scenario.yaml --out-dir runs/haul-plan --time-limit 60
   fhops haul schedule examples/hauling/scenario.yaml --out-dir runs/haul-day1 \
     --flow-dir runs/haul-plan --day 1 --solver sa --sa-iters 5000
   fhops haul solve examples/hauling/scenario.yaml --out-dir runs/haul-solve \
     --day 1 --solver mip --time-limit 60
   fhops haul report runs/haul-solve --out runs/haul-solve/report.md

- ``plan`` solves Phase I: cost, contractor workload, or goal programming.
- ``schedule`` solves Phase II/III for one day; it loads an existing Phase I run or solves Phase I
  in memory.
- ``solve`` runs both phases and writes a merged summary.
- ``report`` renders a Markdown report from the stable CSV/JSON artefacts.

See :doc:`../howto/hauling` for the data contract, output schemas, and integration notes.


Web commands
------------

The optional web service adds:

.. code-block:: bash

   fhops web --runs runs/ --port 8765
   fhops web --runs runs/ --allow-agent --agent-timeout 120
   fhops web --runs runs/ --lidar-bundles /path/to/femic/bundles
   fhops web manifest runs/run-a --status optimal
   fhops web discover runs/

See :doc:`../howto/web` for the three planning lenses, run discovery, the `run.json` manifest, and
the security defaults.
