Ground-Based System Simulation
==============================

``fhops.ground_system`` layers a deterministic grapple-skidder and loader queue
on top of the existing single-machine tree simulation. It is a reference
execution model for one block, one feller-buncher, one or more skidders, and one
loader. It does not replace the block/day MILP or claim field-calibrated system
productivity.

Command
-------

.. code-block:: bash

   fhops tree system scenario.yaml \
     --block B01 \
     --feller H1 \
     --skidder H2 \
     --skidder H3 \
     --processor H5 \
     --processor H6 \
     --loader H4 \
     --out build/system

The scenario must contain the named machines with compatible roles:

- feller: ``feller_buncher``, ``harvester``, or ``hand_faller``;
- skidder: ``grapple_skidder`` or ``skidder``;
- processor: ``roadside_processor`` or ``processor``; repeat
  ``--processor`` to simulate a parallel processing deck;
- loader: ``loader``.

The feller-buncher is processed with
:func:`fhops.spatial_machine.simulate_block`; its processed-tree events are
grouped into bunches of approximately ``--bunch-volume`` cubic metres. Skidders
are dispatched greedily to the earliest available bunch and always return to
the landing. If ``--processor`` is supplied, the roadside processor processes
each skidder load at the landing before it reaches the loader. The loader
processes processor output in arrival order. Without ``--processor``, loads go
directly to the loader.

Cycle-time controls are exposed on the CLI:

- ``--feller-reach`` and ``--feller-speed``;
- ``--skidder-capacity``, ``--skidder-empty-speed``,
  ``--skidder-loaded-speed``, ``--skidder-load-fixed``,
  ``--skidder-load-per-log``, ``--skidder-unload-fixed``, and
  ``--skidder-unload-per-log``;
- ``--loader-process-fixed`` and ``--loader-process-per-log``.

Processor selection can be explicit:

- ``--processor-model tn103_area_a`` (default, 35.0 m³/SMH)
- ``--processor-model tn103_combined`` (20.4 m³/SMH)
- ``--processor-model adv5n6_cold`` (49.0 m³/SMH)
- ``--processor-model custom --processor-rate <m3/SMH>``

The selected model and per-machine productivity are written to provenance.
For stand-specific calibration, pass a JSON processor-spec file:

.. code-block:: json

   {
     "H5": {"seconds_per_m3": 72.0, "fixed_s": 30.0},
     "H6": {"seconds_per_m3": 36.0, "fixed_s": 30.0}
   }

.. code-block:: bash

   fhops tree system scenario.yaml --processor H5 --processor H6 \
     --processor-specs processor_specs.json ...

`seconds_per_m3` is productive time per cubic metre; `fixed_s` is the fixed
per-load setup. Explicit specs are recorded as `custom_specs`.

``--processor-productivity-multiplier`` scales processor output without changing
the selected model or explicit spec. Values above 1 shorten fixed,
per-cubic-metre, and per-log processor time; values below 1 lengthen them. The
multiplier is recorded under ``rate_provenance.processor`` and is used by the
Studio operator-policy translation.

The default ``--rate-model calibrated`` uses:

- the Han et al. (2018) whole-tree grapple-skidder regression with the GNSS
  cable-skidder speed profile (SK) used as a travel override;
- the Barko 450 TN-46 ground-skid loader result. The loader's recorded
  ``avg_volume_per_shift_m3`` is converted to seconds per cubic metre with an
  assumed 8-hour shift.

``--rate-model reference`` uses the fixed ``MachineSpec`` cycle times instead.
The calibrated rates are provenance-bearing reference values, not universal or
field-validated machine rates. The default skidder capacity is 40 m³ so
tree-length loads from the mature demo are not silently refused; set it to the
actual machine payload for operational work.

``--landing-x`` and ``--landing-y`` set the primary landing in the scenario CRS.
When omitted, the mean tree position is used.

For multiple landing choices, repeat ``--landing`` with ``id,x,y`` (or just
``x,y``). Each bunch is routed to the landing with the lowest travel-time proxy
(``distance / speed_factor``), so terrain grade and speed can make a slightly
longer route preferable. The first landing is the primary landing in result
metadata. Inventory and capacity are tracked per landing. By default the
processor and loader are one shared crew; assign dedicated fleets with:

.. code-block:: bash

   --landing-loader main:H4 --landing-processor main:H5 \
   --landing-loader north:H4-B --landing-processor north:H5-B

Each landing receiving loads needs one loader. Machines must exist in the
scenario and may not be assigned to more than one landing.

Truck fleets can also be split per landing:

.. code-block:: bash

   --landing-truck main:2 --landing-truck north:1

When per-landing counts are supplied, every landing receives its own truck
availability list and truck IDs such as ``T-main-01``. Blank/omitted counts use
the shared ``--truck-count`` fleet.

Per-machine cycle overrides can be supplied as JSON mappings:

.. code-block:: bash

   --loader-specs loader_specs.json \
   --skidder-specs skidder_specs.json \
   --processor-specs processor_specs.json

Loader/skidder mappings use the ``MachineSpec`` cycle fields; processor mappings
use ``seconds_per_m3``, ``fixed_s``, and ``per_log_s``. A skidder/forwarder spec
may include ``productivity_m3_per_smh`` to derive cycle time from a calibrated
forwarder rate instead of the grapple-skidder regression.

Per-landing truck equipment can be overridden with a JSON mapping:

.. code-block:: bash

   --landing-truck-specs landing_trucks.json

Supported fields include ``capacity_m3``, ``cycle_time_s``, ``count``,
``load_time_s``, ``arrival_interval_s``, ``one_way_distance_m``, and
``speed_mps``. Mill service time and bay count remain global.

Optional terrain routing:

.. code-block:: bash

   fhops tree system scenario.yaml      --block B01 --feller H1 --skidder H2 --loader H3      --dem dem.tif --road road.geojson      --max-skidder-grade 30 --terrain-step 10

``--dem`` samples elevation along each skidder route and refuses routes above
``--max-skidder-grade``. ``--road`` reads GeoJSON LineStrings; a single line uses
projected corridor routing, while a MultiLineString is treated as a connected
road network and routed with a shortest path. Network edge costs account for
DEM-derived grade/speed when a DEM is supplied, so a longer flatter branch can
beat a shorter steep one. DEM and road coordinates must use the same CRS as the
inventory. Install the optional dependencies with ``pip install "fhops[lidar]"``.
Studio blocks runs with disconnected road components and warns when a landing is
more than 30 m from the network.

Shift and trucking controls:

.. code-block:: bash

   fhops tree system scenario.yaml      --block B01 --feller H1 --skidder H2 --loader H3      --shift-days 7 --shifts-per-day 2 --hours-per-shift 10      --truck-capacity 30 --truck-cycle 3600 --truck-count 3      --truck-load-time 300 --landing-capacity 120

``--shift-days`` enables discrete operating windows. Tasks that do not fit in
the remaining window are delayed to the next window; a task is not split across
a shift boundary, and the run fails if no window can hold it. When
``--shift-days`` is omitted, time is continuous.

Machine downtime can be added inside the shift calendar:

.. code-block:: bash

   fhops tree system scenario.yaml      --block B01 --feller H1 --skidder H2 --processor H5 --loader H4      --shift-days 12 --shifts-per-day 2 --hours-per-shift 10      --downtime H2:24:2:breakdown --downtime H4:48:1:maintenance

``MACHINE:START_HOURS:DURATION_HOURS[:REASON]`` times are absolute from the
simulation start. A task that overlaps a machine downtime window waits until the
window ends. Downtime requires ``--shift-days``.

A seeded stochastic breakdown model can generate downtime windows instead:

.. code-block:: bash

   fhops tree system scenario.yaml      --block B01 --feller H1 --skidder H2 --processor H5 --loader H4      --shift-days 12 --shifts-per-day 2 --hours-per-shift 10      --failure-rate 0.02 --repair-hours 2 --repair-cv 0.5      --breakdown-seed 20261029

Failures are exponential in machine operating hours; repair durations are
lognormal. Repairs are serialized through one repair crew. The seed and
failure/repair summary are written to the event/summary provenance. This is a
reference reliability model, not a spare-parts or maintenance-policy model.

``--truck-capacity`` enables a log-truck fleet and landing inventory. Loader
output adds to landing inventory; trucks are dispatched when a payload is
available. If landing inventory would exceed ``--landing-capacity``, the loader
waits for a truck. Partial truck loads are allowed to make space. Truck loading
respects the active shift window; ``--truck-arrival-interval`` staggers initial
truck availability. If ``--truck-route-distance`` is supplied, the round-trip
time is derived from one-way distance/speed plus ``--mill-service`` and queued
through ``--mill-bays`` service bays. Truck events, mill wait time, and landing
inventory are written to the JSON and summary.

Outputs
-------

- ``system_events.csv`` — machine timeline with travel, load, unload, processor,
  and loader segments.
- ``system_events.json`` — the same event stream plus tree processing records,
  truck events, machine types, landing, shift windows, and duration; suitable
  for an external viewer.
- ``tree_processing.csv`` — one row per processed tree.
- ``system_summary.json`` — tree count, processed volume, makespan, machine
  utilisation, shift/downtime/truck summaries, landing inventory, warnings, and
  caveats.

Determinism and invariants
--------------------------

Given the same scenario, inventory, machine parameters, and bunch volume, the
system ID and event timeline are deterministic. The model records each
processed tree once, uses only the feller-buncher's processed events, and
reports unreachable trees separately. Skidder capacity is checked against each
bunch; a load larger than capacity raises an error rather than silently
overloading the machine.

Limits
------

- One block, one landing, one feller-buncher, one optional processor, one loader.
- DEM and road routing are optional; single corridors use projection, while
  MultiLineStrings use a small connected-network shortest path. This is not a
  full forest-road scale/landing-search model.
- Skidder and loader cycle times are configurable defaults, not calibrated
  regressions.
- Shift windows are fixed and tasks are not split across them.
- Trucking blocks the loader on landing capacity; arrivals are deterministic and
  staggered by a fixed interval. Route time is a constant-speed distance model;
  there is no road network, scale, or adaptive dispatch policy.
- Processor rates are selected from TN-103/ADV5N6 reference models or a custom
  rate; they are not calibrated to the demo stands.
- Downtime windows are absolute and user-supplied; stochastic breakdowns are
  seeded and serialized through one repair crew.
- No spare-parts, maintenance-policy, or multi-block routing model.
- Tree inventories must be reviewed separately; this module does not validate
  stem-model calibration.
- The JSON timeline is data only; FHOPS does not ship a renderer.

Committed example
-----------------

``examples/tree_system/`` is a compact synthetic end-to-end example with 12
trees, one block, one feller-buncher, two skidders, one processor, one loader,
one truck, and a prebuilt glTF/viewer artifact.
