Data Contract Guide
This guide summarises the structured inputs FHOPS expects when authoring scenarios. It
builds on the examples/tiny7 and tests/fixtures/regression assets and reflects
recent extensions (mobilisation, geo metadata, crew assignments).
Reference dataset generator
All bundled scenarios (examples/{tiny7,small21,med42,large84}) now originate from a single
deterministic generator: scripts/rebuild_reference_datasets.py. The script samples Lahrsen-style
stand attributes, derives ADV6N7 skidding distances from block geometry, and emits the full CSV + YAML
bundle (blocks, machines, calendar, production rates, landings, mobilisation distances, README). Run:
python scripts/rebuild_reference_datasets.py tiny7 # or small21 / med42 / large84
Use --seed to reproduce specific variants. Regenerate the dataset before updating docs/fixtures so
the CSVs, README stats, and MILP bundles stay in sync across the ladder.
Core Tables (CSV)
Each scenario references a set of CSV files. Required columns and notes:
schema_version: Contract version tag (currently 1.0.0).
Table |
Required Columns |
Notes |
|---|---|---|
|
|
Optional: |
|
|
Optional: |
|
|
|
|
|
|
|
|
|
Cross References & Validators
The Pydantic models enforce consistency:
Blocks reference known landings; harvest-system IDs must exist (see Harvest System Registry for defaults).
Calendar and production rates must reference defined machines/blocks and lie within the scenario horizon.
Mobilisation distances must reference known blocks; mobilisation parameters must reference known machines.
Crew assignments (optional) require unique crew IDs and valid machine IDs.
Optional Extras
Recent helpers enable richer metadata:
MobilisationConfig— per-machine mobilisation costs and block distance matrices.GeoMetadata— optional GeoJSON paths and CRS tags for blocks/landings.CrewAssignment— map crew identifiers to machines/roles for downstream planners.TimelineConfig— shift definitions and blackout windows controlling daily availability.ScheduleLock— pre-assign specific machine/block/day combinations (enforced in MIP & SA).ObjectiveWeights— tweak solver objective weighting (production, mobilisation penalties, transition counts, optional landing-cap slack penalties).Round-the-clock operations — unless you override
machines.csvor the calendar, FHOPS expects continuous availability (24 h/day) and only pauses work when you add blackouts, downtime sampling, or calendar gaps.
ObjectiveWeights fields are optional; omit any you do not need. For example:
objective_weights:
production: 1.0
mobilisation: 0.5
transitions: 2.0
landing_surplus: 3.0
This configuration maximises production while penalising moves between blocks and soft landing
capacity violations. Setting a weight to 0 reverts to the default hard behaviour.
Reference tests/test_contract_validations.py for examples that exercise these validators.
Timeline Example
Add a top-level timeline block in your scenario YAML to describe shifts and blackouts:
timeline:
shifts:
- name: day
hours: 10
shifts_per_day: 1
blackouts:
- start_day: 5
end_day: 7
reason: wildfire risk
days_per_week: 5
The loader converts this into a TimelineConfig instance available via scenario.timeline.
Shift Calendars & Defaults
When you need per-machine shift availability (for example, night crews that only run certain
machines), add a shift_calendar CSV and reference it under data.shift_calendar:
machine_id,day,shift_id,available
H1,1,day,1
H1,1,night,0
H2,1,day,1
H2,1,night,1
shift_id values must match names declared in the timeline’s shifts section; the loader
rejects unknown labels or days outside num_days. Supplying multiple rows for the same
machine/day lets you model overlapping shifts with different availability flags, and you can
leave individual rows at available=0 to block a specific crew without editing the base
timeline. When a shift_calendar is omitted entirely, FHOPS synthesises a single shift named
S1 for every day so legacy day-indexed scenarios continue to work without edits.
Run fhops validate to confirm the calendar files and shift labels line up before solving:
fhops validate examples/tiny7/scenario.yaml
The CLI surfaces precise validation errors (missing columns, bad shift IDs, horizon overruns) so you can correct the dataset without spelunking through Pydantic stack traces.
Schedule Locking
Lock a machine to a block on a given day by adding locked_assignments:
locked_assignments:
- machine_id: YARDER1
block_id: B12
day: 5
Any attempt to reassign that machine/day is blocked in both the MIP builder and the SA heuristic.
GeoJSON Ingestion & Distances
When supplying block or landing geometries:
Provide GeoJSON files with a
FeatureCollectioncontaining polygon features.Each feature must include an
idorproperties.idmatching the block/landing IDs defined in the CSV tables.Use a projected CRS suitable for distance calculations; FHOPS defaults to
EPSG:3005(BC Albers) but any metre-based CRS is acceptable when specified viageo.crs.Store relative paths in
GeoMetadata.block_geojson/landing_geojsonso the CLI tooling can locate the files.
To generate mobilisation distances from geometries, run:
fhops geo distances --blocks blocks.geojson --out mobilisation_distances.csv
The command computes centroid-to-centroid distances (in metres) respecting the CRS. The
resulting CSV aligns with the MobilisationConfig distance format and can be referenced
under scenario.data.mobilisation_distances.
Machine-Rate Defaults & Costing Helper
FHOPS includes a BC-focused rental-rate catalogue under data/machine_rates.json to keep
costing consistent across scenarios:
Each entry records
machine_name,role, owning/operating costs ($/SMH), default utilisation, move-in allowance, citation, and notes. Baseline values combine Dodson et al. (2015) Montana machine-rate data (converted at ~1.33 CAD/USD with diesel ≈ CAD 1.80/L) and Hartley & Han (2007) coastal grapple-yarders to match BC market conditions.Repair and maintenance allowances come from FPInnovations Advantage Vol. 4 No. 23 (2003). The report’s 2002 CAD regression averages are escalated to 2024 CAD using the Statistics Canada Machinery & Equipment CPI (Table 18-10-0005-01), giving a cumulative multiplier of ≈1.56. FPInnovations reports the allowances at cumulative usage classes of 0–5k, 0–10k, 0–15k, 0–20k, and 0–25k operating hours; we treat the 10 000 h bucket as the default because every machine class has data at that point and it best represents a “typical” fleet age. Newer machines (≤5 000 h) and end-of-life machines (≥15 000 h) sit in the neighbouring buckets, so supplying
repair_usage_hourslets you scale the allowance up or down using the published FPInnovations ratios when a scenario deviates from the baseline. These allowances remain optional per role.The CLI exposes the defaults via
fhops dataset estimate-cost:# Deterministic Lahrsen productivity with table defaults fhops dataset estimate-cost --machine-role grapple_yarder \ --avg-stem-size 0.45 --volume-per-ha 320 --stem-density 750 --ground-slope 35
The command prints the chosen role, source, owning/operating/repair split, utilisation, productivity (m³/PMH15), and $/m³. Add
--include-repair/--exclude-repairto toggle the FPInnovations allowance or--usage-hoursto select a different usage-class multiplier (nearest 5 000 h bucket from Table 2). Supplying--dataset <scenario.yaml> --machine <ID>auto-loads the machine’s role,operating_cost(when already populated), andrepair_usage_hoursso recurring reports always respect the dataset defaults. Override components directly via:fhops dataset estimate-cost --machine-role feller_buncher \ --owning-rate 95 --operating-rate 120 --repair-rate 40 \ --productivity 30 --utilisation 0.8
When you also need a road/subgrade allowance, append the TR-28 helper directly to the same command:
fhops dataset estimate-cost --machine-role grapple_yarder \ --productivity 22 --utilisation 0.85 \ --road-machine caterpillar_235_hydraulic_backhoe --road-length-m 150
Supplying
--road-machine(slug or name fromfhops dataset tr28-subgrade) together with--road-length-mprints a CPI-adjusted “TR-28 Road Cost Estimate” table after the machine-cost summary. Use--road-exclude-mobilisationwhen movement is covered elsewhere—the CLI warns in either case and cites the soil-protection guidance from FNRB3 (Cat D7H vs. D7G trial) and ADV4N7 (compaction thresholds). When the scenario already lists road jobs (see below),fhops.dataset estimate-cost --dataset …auto-selects the only row or lets you pick via--road-job-id; attach additional soil warnings by referencingdata/reference/soil_protection_profiles.jsonthrough--road-soil-profile(or via the CSV).
Road Construction Tables & Validation
To keep road-building metadata alongside the rest of the scenario, add an optional road_construction table:
id,machine_slug,road_length_m,include_mobilisation,soil_profile_ids RC1,caterpillar_235_hydraulic_backhoe,150,True,fnrb3_d7h|adv4n7_compactionReference the file under
data.road_constructioninscenario.yaml. Each row requires a uniqueid, a TR-28 machine slug (seefhops dataset tr28-subgrade), the road/subgrade length in metres, and whether the published mobilisation charge should be included by default.soil_profile_ids(pipe- or comma-separated) tie intodata/reference/soil_protection_profiles.jsonso the CLI can print structured reminders (ground-pressure multipliers, compaction thresholds, recommended mitigation). When a scenario contains exactly one row,estimate-cost --dataset ...pulls that entry automatically; specify--road-job-id RC1if multiple road jobs exist or pass--road-machine/--road-length-mto override everything from the command line.
Harvest-system templates now infer these rows from the skyline/cable presets they assign: any block that references a preset with
defined road defaults (see fhops.scheduling.systems.SYSTEM_ROAD_DEFAULTS) gains the matching TR-28 slug/length automatically,
and the synthetic dataset generator writes data/road_construction.csv alongside the other tables so downstream tooling picks up
the entries without manual edits. Run fhops validate after editing the table to catch duplicate IDs or malformed slugs before
passing the scenario to solvers or costing helpers.
Supplying
--rental-ratebypasses the lookup for bespoke studies, butmachines.csvrows should normally use the curated rates (or CLI recomputed totals) so costing/evaluation tools stay aligned.fhops dataset inspect-machine prints the machine metadata and the same default owning/operating/repair breakdown (honouring
repair_usage_hours) so you can audit scenario inputs without running a full cost estimate. You can also inspect the rental table directly via--machine-role(e.g.,fhops dataset inspect-machine --machine-role loader_barko450dumps the TN-46 Barko 450 live-heel loader rate). Historical Appendix II presets such asloader_cat966c_tr45,skidder_tr45, andbulldozer_tr45are exposed the same way so you can compare CPI-normalized 1979 CAD rates against modern entries. When you pair the command with a harvest system such asground_fb_loader_liveheel, the loader job automatically swaps its cost role toloader_barko450so budgeting outputs stay aligned with the TN-46 preset. Add--json-out machine.jsonto capture the same payload for automated QA pipelines. Solver telemetry (solve-heur,solve-ils,solve-tabu,eval-playback) automatically embeds thismachine_costsbundle whenever--telemetry-logis used, so KPI histories and dashboards can trace the assumed repair buckets.
Overriding the defaults follows two common paths:
Quick sensitivity checks – keep the built-in role but pass overrides to the CLI, e.g.
fhops dataset estimate-cost --machine-role grapple_yarder \ --owning-rate 185 --operating-rate 260 --repair-rate 50 \ --avg-stem-size 0.5 --volume-per-ha 280 --stem-density 700 --ground-slope 40
This keeps the source/role metadata but swaps in your costs for the report/run only.
Persistent dataset changes – clone
data/machine_rates.jsonor edit your scenario’smachines.csv. For the JSON table, add a new entry (e.g.,"role": "custom_grapple") and pointmachines.csv.roleat that slug; the loader will backfill operating_cost from your entry. Alternatively, write the full $/SMH intomachines.csv.operating_costfor the specific machine if the rate is project-specific. Both approaches play nicely with the CLI helpers and solver defaults.
When authoring datasets set machines.csv.operating_cost to the all-in rental rate ($/SMH)
for each machine/system. Use the CLI helper (deterministic or distribution-based) to turn
Lahrsen stand descriptors and machine rates into comparable $/m³ for QA, reporting, or solver
inputs.
Skyline Stand Profiles
Arnvik (2024) Appendix 5 stand metadata is bundled under
data/reference/arnvik/appendix5_stands.json and exposed via the CLI:
fhops dataset appendix5-standslists authors/species and the parsed slope/ground descriptors (filter with--author). The parser converts ranges (e.g.,23 (5-55)%) and qualitative labels (Level - Erasmus) into approximate slope percentages.fhops dataset estimate-cable-skiddingevaluates the Ünver-Okan (2020) uphill skidding regressions (spruce stands in north-east Turkey). Supply either--slope-percentor a stand--profile(author name) to pull slope defaults directly from Appendix 5, and treat the results as non-BC placeholders unless you calibrate them.fhops dataset estimate-skyline-productivitywraps the Lee et al. (2018), TR-125, and TR-127 regressions, with optional--tr119-treatmentmultipliers (strip cut / retention levels) so partial-cut scenarios automatically apply the published BC productivity and cost offsets. Lee et al. (2018) regressions are small-scale tethered yarder studies from South Korea (HAM300), while TR-127 block-specific models (tr127-block1 … block6) use the Appendix VII coefficients from the FPInnovations case studies in northwestern BC. TR-112 only publishes descriptive productivity tables, so no regression helper is available for that report yet.Programmatically,
fhops.reference.arnvik_appendix5exposes dataclasses and helper functions so skyline helpers and costing workflows can bind stand descriptors to the new cable productivity models.
Fixture Gallery
tests/data/minimal— smallest possible scenario for smoke-testing the loader.tests/data/typical— multi-block example with mobilisation distances and harvest system IDs.tests/data/invalid— intentionally malformed inputs that surface validation errors.
See also Quickstart for CLI commands and tests/fixtures/regression for a
mobilisation-aware example.