diff --git a/CHANGE_LOG.md b/CHANGE_LOG.md index e7584ce4..b16587f6 100644 --- a/CHANGE_LOG.md +++ b/CHANGE_LOG.md @@ -1,3 +1,30 @@ +# 2026-06-14 — v1.0.0 GA preflight CI gate +- Started the `issue-15-v100-green-ci` branch for #15 under the v1.0.0 GA release issue tree and restored the local lint/type/test gate that current `main` would hit after the existing Ruff-format failure. +- Let Ruff normalize the pending formatting drift, including the release-blocking playback aggregate/test formatting and the processor-loader signature formatting found during the GA audit. +- Migrated CLI/productivity/scenario string enums from `str, Enum` to `StrEnum` so the current Ruff `UP042` gate passes without suppressions. +- Tightened MyPy compatibility after the enum migration by string-coercing harvest-system override values before enum construction and casting the Typer `click_type` option through `Any` to isolate the current Typer stub mismatch. +- Hardened `tests/test_analyze_tuner_reports.py` subprocess calls to use `sys.executable`, preventing local/CI false failures when bare `python` points outside the pytest environment. +- Fixed the `docs/howto/optimization_formulation.rst` heading underline so the Sphinx warning-as-error build succeeds once Pandoc is available. +- Commands executed: + - `git checkout -b issue-15-v100-green-ci origin/main` + - `PYTHONPATH=/tmp/fhops-release-audit-tools python -m ruff format src/fhops/productivity/processor_loader.py` + - `python -m venv .venv` + - `.venv/bin/python -m pip install --upgrade pip` + - `.venv/bin/python -m pip install -e '.[dev]' -r docs/requirements.txt pre-commit` + - `.venv/bin/ruff format src tests` *(initial run reformatted 3 files)* + - `.venv/bin/ruff check src tests` *(initial run failed on 44 pre-existing `UP042` findings; fixed by `StrEnum` migration)* + - `.venv/bin/mypy src` *(initial reruns exposed enum override narrowing and Typer stub issues; fixed in this changeset)* + - `.venv/bin/ruff format src tests` + - `.venv/bin/ruff check src tests` + - `.venv/bin/mypy src` + - `.venv/bin/pytest tests/test_analyze_tuner_reports.py` *(failed before `sys.executable` fix, then passed)* + - `.venv/bin/pytest` *(initial full run failed only on the tuner-report subprocess environment mismatch; rerun passed: 299 passed, 210 skipped, 61 warnings)* + - `.venv/bin/pre-commit run --all-files` + - `.venv/bin/sphinx-build -b html docs _build/html -W` *(failed locally because Pandoc was not installed in the environment)* + - `.venv/bin/python -m pip install pypandoc_binary` + - `PYPANDOC_PANDOC=$(...) .venv/bin/sphinx-build -b html docs _build/html -W` *(still failed because nbsphinx requires `pandoc` on `PATH`)* + - `PANDOC_DIR=$(...) PATH="$PANDOC_DIR:$PATH" .venv/bin/sphinx-build -b html docs _build/html -W` *(first run exposed the heading underline warning; rerun passed)* + # 2026-03-22 — Canonical operational MILP formulation sync (FHOPS + thesis chapter module) - Created the `feature/fhops-operational-formulation-sync` firewall branch and added a canonical formulation source module at `docs/softwarex/manuscript/sections/includes/fhops_operational_formulation.md`, then generated synchronized outputs: - `docs/softwarex/manuscript/sections/includes/fhops_operational_formulation.tex` diff --git a/ROADMAP.md b/ROADMAP.md index 1e692840..21d2da8f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -128,6 +128,7 @@ before proposing new work. ## Detailed Next Steps 1. **Release Candidate Prep (`notes/release_candidate_prep.md`, `AGENTS.md`, `notes/cli_docs_plan.md`)** - Lock feature set, refresh install/docs, and draft release notes + Hatch-based packaging checklist ahead of the public milestone. + - 2026-06-14: v1.0.0 GA issue tree opened; first child branch (`issue-15-v100-green-ci`) is restoring the green CI/local verification gate before release metadata changes. 2. **Metaheuristic Roadmap (`notes/metaheuristic_roadmap.md`)** - Prioritise SA refinements, operator registry work, and benchmarking comparisons with the new harness (including shift-aware neighbourhoods). 3. **Shift-Based Scheduling Refactor (`notes/modular_reorg_plan.md`, `notes/mip_model_plan.md`, `notes/simulation_eval_plan.md`)** diff --git a/docs/howto/optimization_formulation.rst b/docs/howto/optimization_formulation.rst index ebf2aa98..a34e5433 100644 --- a/docs/howto/optimization_formulation.rst +++ b/docs/howto/optimization_formulation.rst @@ -1,5 +1,5 @@ Operational Optimization Formulation -=================================== +==================================== This page documents the canonical FHOPS operational MILP formulation used by the Pyomo implementation in ``fhops.model.milp.operational``. diff --git a/notes/release_candidate_prep.md b/notes/release_candidate_prep.md index 5666868c..2d79e61c 100644 --- a/notes/release_candidate_prep.md +++ b/notes/release_candidate_prep.md @@ -1,12 +1,14 @@ # Release Candidate Prep Plan Date: 2025-11-16 -Status: Draft — drive the v0.x RC process. +Status: Active — v1.0.0 GA preflight in progress for SoftwareX submission. ## Objectives - Freeze scope and polish docs/install instructions for the first FHOPS release candidate. - Adopt Hatch for packaging/publishing (mirroring ws3 workflows) and ensure PyPI metadata is accurate. - Produce changelog/release notes, version bumps, and verification checklists before tagging. +- Promote the public package from the current `1.0.0a2` prerelease line to a clean `1.0.0` + GA release that SoftwareX reviewers can install and cite. ## Tasks 1. **Versioning & Hatch wiring** @@ -34,6 +36,10 @@ Status: Draft — drive the v0.x RC process. 6. **Automation** - [x] Add GitHub Actions job template for ``hatch build`` verification (triggered on tags) — see `.github/workflows/release-build.yml`. - [x] Prepare release checklist in ``AGENTS.md`` (Hatch build/publish cadence documented under Release workflow). + - [ ] Restore green `main` CI before the v1.0.0 version bump/tag. + - 2026-06-14: issue #15 branch fixes current Ruff formatting drift, modernises `StrEnum` + lint blockers, and hardens tuner-report subprocess tests so the local lint/type/test gate + can pass under the release verification environment. 7. **Publishing (TestPyPI → PyPI)** - [x] Dry run using TestPyPI: diff --git a/src/fhops/cli/dataset.py b/src/fhops/cli/dataset.py index e93332c9..0d113d2a 100644 --- a/src/fhops/cli/dataset.py +++ b/src/fhops/cli/dataset.py @@ -6,7 +6,7 @@ from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass, replace from datetime import UTC, datetime -from enum import Enum +from enum import StrEnum from functools import cache from pathlib import Path from typing import Any, Literal, TypeVar, cast @@ -752,7 +752,7 @@ class DatasetRef: } -class ProductivityMachineRole(str, Enum): +class ProductivityMachineRole(StrEnum): """Machine roles supported by the productivity command.""" FELLER_BUNCHER = "feller_buncher" @@ -766,7 +766,7 @@ class ProductivityMachineRole(str, Enum): HELICOPTER_LONGLINE = "helicopter_longline" -class ShovelSlopeClass(str, Enum): +class ShovelSlopeClass(StrEnum): """Slope direction buckets used by shovel logging presets.""" DOWNHILL = "downhill" @@ -774,14 +774,14 @@ class ShovelSlopeClass(str, Enum): UPHILL = "uphill" -class ShovelBunching(str, Enum): +class ShovelBunching(StrEnum): """Bunching assumptions for shovel logging datasets.""" FELLER_BUNCHED = "feller_bunched" HAND_SCATTERED = "hand_scattered" -class CTLHarvesterModel(str, Enum): +class CTLHarvesterModel(StrEnum): """CTL harvester regressions.""" ADV6N10 = "adv6n10" @@ -790,14 +790,14 @@ class CTLHarvesterModel(str, Enum): KELLOGG1994 = "kellogg1994" -class CableSkiddingModel(str, Enum): +class CableSkiddingModel(StrEnum): """Ünver-Okan cable skidding regressions.""" UNVER_SPSS = "unver-spss" UNVER_ROBUST = "unver-robust" -class GrappleYarderModel(str, Enum): +class GrappleYarderModel(StrEnum): """Supported grapple yarder regressions.""" SR54 = "sr54" @@ -1105,7 +1105,7 @@ def _estimate_tn98_manual_falling(species: str, dbh_cm: float) -> dict[str, Any] } -class SkylineProductivityModel(str, Enum): +class SkylineProductivityModel(StrEnum): """Supported skyline productivity regressions.""" LEE_UPHILL = "lee-uphill" @@ -1184,7 +1184,7 @@ def _skyline_cost_role(model: SkylineProductivityModel) -> str | None: return _SKYLINE_COST_ROLES.get(model) -class RoadsideProcessorModel(str, Enum): +class RoadsideProcessorModel(StrEnum): """Supported roadside processor regressions exposed through the CLI dataset helpers.""" BERRY2019 = "berry2019" @@ -1217,14 +1217,14 @@ class RoadsideProcessorModel(str, Enum): } -class ADV5N6StemSource(str, Enum): +class ADV5N6StemSource(StrEnum): """Stem origin used by the ADV5N6 processor presets.""" LOADER_FORWARDED = "loader_forwarded" GRAPPLE_YARDED = "grapple_yarded" -class ADV5N6ProcessingMode(str, Enum): +class ADV5N6ProcessingMode(StrEnum): """Processing deck modes for ADV5N6 coastal scenarios.""" COLD = "cold" @@ -1232,14 +1232,14 @@ class ADV5N6ProcessingMode(str, Enum): LOW_VOLUME = "low_volume" -class ADV7N3Machine(str, Enum): +class ADV7N3Machine(StrEnum): """ADV7N3 processor machines modelled in the dataset.""" HYUNDAI_210 = "hyundai_210" JOHN_DEERE_892 = "john_deere_892" -class TN103Scenario(str, Enum): +class TN103Scenario(StrEnum): """TN-103 Caterpillar DL221 case-study scenarios.""" AREA_A = "area_a_feller_bunched" @@ -1248,7 +1248,7 @@ class TN103Scenario(str, Enum): COMBINED_HIGH_UTIL = "combined_high_util" -class TR106Scenario(str, Enum): +class TR106Scenario(StrEnum): """TR-106 lodgepole pine processor scenarios.""" CASE1187_OCTNOV = "case1187_octnov" @@ -1258,7 +1258,7 @@ class TR106Scenario(str, Enum): KP40_CAT_EL180 = "kp40_caterpillar_el180" -class TR87Scenario(str, Enum): +class TR87Scenario(StrEnum): """TR-87 Timberjack TJ90 observe/test scenarios.""" DAY_SHIFT = "tj90_day_shift" @@ -1268,7 +1268,7 @@ class TR87Scenario(str, Enum): BOTH_PROCESSORS_WAIT_ADJUSTED = "tj90_both_processors_wait_adjusted" -class TN166Scenario(str, Enum): +class TN166Scenario(StrEnum): """TN-166 telescopic-boom processor scenarios.""" GRAPPLE_YARDED = "grapple_yarded" @@ -1276,28 +1276,28 @@ class TN166Scenario(str, Enum): MIXED_SHIFT = "mixed_shift" -class LabelleProcessorSpecies(str, Enum): +class LabelleProcessorSpecies(StrEnum): """Species covered by the Labelle hardwood processor regressions.""" SPRUCE = "spruce" BEECH = "beech" -class LabelleProcessorTreatment(str, Enum): +class LabelleProcessorTreatment(StrEnum): """Silviculture treatments used in Labelle et al. studies.""" CLEAR_CUT = "clear_cut" SELECTIVE_CUT = "selective_cut" -class Labelle2016TreeForm(str, Enum): +class Labelle2016TreeForm(StrEnum): """Tree-form classes from Labelle et al. (2016).""" ACCEPTABLE = "acceptable" UNACCEPTABLE = "unacceptable" -class Labelle2017Variant(str, Enum): +class Labelle2017Variant(StrEnum): """Polynomial/power variants published in Labelle et al. (2017).""" POLY1 = "poly1" @@ -1306,7 +1306,7 @@ class Labelle2017Variant(str, Enum): POWER2 = "power2" -class Labelle2018Variant(str, Enum): +class Labelle2018Variant(StrEnum): """Labelle et al. (2018) beech/spruce regression variants.""" RW_POLY1 = "rw_poly1" @@ -1315,21 +1315,21 @@ class Labelle2018Variant(str, Enum): CT_POLY2 = "ct_poly2" -class ProcessorCarrier(str, Enum): +class ProcessorCarrier(StrEnum): """Carrier classes for processor regressions (purpose-built vs excavator).""" PURPOSE_BUILT = "purpose_built" EXCAVATOR = "excavator" -class SpinelliOperation(str, Enum): +class SpinelliOperation(StrEnum): """Operation type for Spinelli et al. (2010) processors.""" HARVEST = "harvest" PROCESS = "process" -class SpinelliStandType(str, Enum): +class SpinelliStandType(StrEnum): """Stand types captured in the Spinelli et al. dataset.""" FOREST = "forest" @@ -1337,7 +1337,7 @@ class SpinelliStandType(str, Enum): COPPICE = "coppice" -class SpinelliCarrier(str, Enum): +class SpinelliCarrier(StrEnum): """Carrier types used in Spinelli et al. (2010).""" PURPOSE_BUILT = "purpose_built" @@ -1346,14 +1346,14 @@ class SpinelliCarrier(str, Enum): TRACTOR = "tractor" -class SpinelliHead(str, Enum): +class SpinelliHead(StrEnum): """Processor head types from Spinelli et al. (2010).""" ROLLER = "roller" STROKE = "stroke" -class SpinelliSpecies(str, Enum): +class SpinelliSpecies(StrEnum): """Species groups from Spinelli et al. (2010).""" CONIFER = "conifer" @@ -1361,7 +1361,7 @@ class SpinelliSpecies(str, Enum): OTHER_HARDWOOD = "other_hardwood" -class LoaderProductivityModel(str, Enum): +class LoaderProductivityModel(StrEnum): """Loader productivity presets available via the CLI dataset helper.""" TN261 = "tn261" @@ -1371,28 +1371,28 @@ class LoaderProductivityModel(str, Enum): KIZHA2020 = "kizha2020" -class LoaderAdv5N1SlopeClass(str, Enum): +class LoaderAdv5N1SlopeClass(StrEnum): """Slope classes for the ADV5N1 loader-forwarder regression.""" ZERO_TO_TEN = "0_10" ELEVEN_TO_THIRTY = "11_30" -class LoaderBarkoScenario(str, Enum): +class LoaderBarkoScenario(StrEnum): """Barko 450 loader scenarios bundled in the dataset.""" GROUND_SKID_BLOCK = "ground_skid_block" CABLE_YARD_BLOCK = "cable_yard_block" -class LoaderHotColdMode(str, Enum): +class LoaderHotColdMode(StrEnum): """Kizha et al. (2020) loader modes (hot vs cold yarding).""" HOT = "hot" COLD = "cold" -class SkidderSpeedProfileOption(str, Enum): +class SkidderSpeedProfileOption(StrEnum): """Speed profile presets for grapple skidder travel-time overrides.""" LEGACY = "legacy" @@ -1415,14 +1415,14 @@ class SkidderSpeedProfileOption(str, Enum): } -class RunningSkylineVariant(str, Enum): +class RunningSkylineVariant(StrEnum): """Longline yarder variants from McNeel (2000).""" YARDER_A = "yarder_a" YARDER_B = "yarder_b" -class GrappleSkidderModel(str, Enum): +class GrappleSkidderModel(StrEnum): """Supported grapple-skidder regressions.""" HAN_LOP_AND_SCATTER = "lop_and_scatter" @@ -2651,7 +2651,7 @@ def _apply_skidder_system_defaults( value = overrides.get("grapple_skidder_model") if value and not user_supplied.get("grapple_skidder_model", False): try: - model = GrappleSkidderModel(value) + model = GrappleSkidderModel(str(value)) used = True except ValueError as exc: # pragma: no cover - validated by CI raise ValueError(f"Unknown grapple skidder model override: {value}") from exc @@ -2777,7 +2777,7 @@ def _apply_forwarder_system_defaults( value = overrides.get("forwarder_model") if value and not user_supplied.get("forwarder_model", False): try: - model = ForwarderBCModel(value) + model = ForwarderBCModel(str(value)) used = True except ValueError as exc: raise ValueError(f"Unknown forwarder model override: {value}") from exc @@ -2808,14 +2808,14 @@ def _apply_processor_system_defaults( value = overrides.get("processor_model") if value and not user_supplied.get("processor_model", False): try: - processor_model = RoadsideProcessorModel(value) + processor_model = RoadsideProcessorModel(str(value)) used = True except ValueError as exc: raise ValueError(f"Unknown processor model override: {value}") from exc value = overrides.get("processor_adv7n3_machine") if value and not user_supplied.get("processor_adv7n3_machine", False): try: - processor_adv7n3_machine = ADV7N3Machine(value) + processor_adv7n3_machine = ADV7N3Machine(str(value)) used = True except ValueError as exc: raise ValueError(f"Unknown ADV7N3 processor override: {value}") from exc @@ -3164,7 +3164,7 @@ def maybe_float( value = overrides.get("skyline_model") if value and not user_supplied.get("model", False): try: - model = SkylineProductivityModel(value) + model = SkylineProductivityModel(str(value)) used = True except ValueError as exc: raise ValueError(f"Unknown skyline model override '{value}'.") from exc @@ -3237,7 +3237,7 @@ def maybe_float( value = overrides.get("skyline_running_variant") if value and not user_supplied.get("running_yarder_variant", False): try: - running_variant = RunningSkylineVariant(value) + running_variant = RunningSkylineVariant(str(value)) used = True except ValueError as exc: raise ValueError(f"Unknown running-skyline variant override '{value}'.") from exc @@ -3329,7 +3329,7 @@ def _apply_helicopter_system_defaults( value = overrides.get("helicopter_model") if value and not user_supplied.get("helicopter_model", False): try: - model = HelicopterLonglineModel(value) + model = HelicopterLonglineModel(str(value)) used = True except ValueError as exc: raise ValueError(f"Unknown helicopter model override '{value}'.") from exc @@ -3546,7 +3546,7 @@ def _apply_grapple_yarder_system_defaults( value = overrides.get("grapple_yarder_model") if value and not user_supplied.get("grapple_yarder_model", False): try: - model = GrappleYarderModel(value) + model = GrappleYarderModel(str(value)) used = True except ValueError as exc: raise ValueError(f"Unknown grapple yarder model override '{value}'.") from exc diff --git a/src/fhops/cli/main.py b/src/fhops/cli/main.py index 6a7a5ca5..52330650 100644 --- a/src/fhops/cli/main.py +++ b/src/fhops/cli/main.py @@ -85,7 +85,7 @@ app.add_typer(dataset_app, name="dataset") app.add_typer(plan_app, name="plan") console = Console() -KPI_MODE = click.Choice(["basic", "extended"], case_sensitive=False) +KPI_MODE: click.ParamType = click.Choice(["basic", "extended"], case_sensitive=False) TUNING_BUNDLE_ALIASES: dict[str, list[tuple[str, Path]]] = { "baseline": [ @@ -827,7 +827,7 @@ def solve_heur_cmd( "--kpi-mode", help="Control verbosity of KPI output (basic|extended).", show_choices=True, - click_type=KPI_MODE, + click_type=cast(Any, KPI_MODE), ), batch_neighbours: int = typer.Option( 1, @@ -1233,7 +1233,7 @@ def solve_ils_cmd( "--kpi-mode", help="Control verbosity of KPI output (basic|extended).", show_choices=True, - click_type=KPI_MODE, + click_type=cast(Any, KPI_MODE), ), show_operator_stats: bool = typer.Option( False, "--show-operator-stats", help="Print per-operator stats after solving." @@ -1561,7 +1561,7 @@ def solve_tabu_cmd( "--kpi-mode", help="Control verbosity of KPI output (basic|extended).", show_choices=True, - click_type=KPI_MODE, + click_type=cast(Any, KPI_MODE), ), show_operator_stats: bool = typer.Option( False, "--show-operator-stats", help="Print per-operator stats." @@ -1804,7 +1804,7 @@ def evaluate( "--kpi-mode", help="Control verbosity of KPI output (basic|extended).", show_choices=True, - click_type=KPI_MODE, + click_type=cast(Any, KPI_MODE), ), ): """Compute KPI summaries for a schedule CSV and print them to the console. diff --git a/src/fhops/evaluation/playback/aggregates.py b/src/fhops/evaluation/playback/aggregates.py index f62e497b..d6091425 100644 --- a/src/fhops/evaluation/playback/aggregates.py +++ b/src/fhops/evaluation/playback/aggregates.py @@ -180,9 +180,9 @@ def machine_utilisation_summary(shift_df: pd.DataFrame) -> pd.DataFrame: .reset_index(drop=True) ) aggregated["utilisation_ratio"] = aggregated.apply( - lambda row: row["total_hours"] / row["available_hours"] - if row["available_hours"] > 0 - else None, + lambda row: ( + row["total_hours"] / row["available_hours"] if row["available_hours"] > 0 else None + ), axis=1, ) return aggregated diff --git a/src/fhops/productivity/cable_logging.py b/src/fhops/productivity/cable_logging.py index 75bbca92..5f75c798 100644 --- a/src/fhops/productivity/cable_logging.py +++ b/src/fhops/productivity/cable_logging.py @@ -7,7 +7,7 @@ import warnings from collections.abc import Mapping from dataclasses import dataclass -from enum import Enum +from enum import StrEnum from functools import lru_cache from pathlib import Path @@ -29,7 +29,7 @@ _TR125_LATERAL_RANGE = (0.0, 50.0) -class Fncy12ProductivityVariant(str, Enum): +class Fncy12ProductivityVariant(StrEnum): """Variants of the FNCY12 Thunderbird TMY45 productivity study.""" OVERALL = "overall" @@ -437,7 +437,7 @@ def estimate_running_skyline_productivity_mcneel2000( return _m3_per_pmh_from_minutes(payload_m3, cycle_minutes) -class HelicopterLonglineModel(str, Enum): +class HelicopterLonglineModel(StrEnum): """Supported helicopter models used in FPInnovations longline studies.""" LAMA = "lama" diff --git a/src/fhops/productivity/forwarder_bc.py b/src/fhops/productivity/forwarder_bc.py index 7fe0af12..29e6df7c 100644 --- a/src/fhops/productivity/forwarder_bc.py +++ b/src/fhops/productivity/forwarder_bc.py @@ -11,7 +11,7 @@ import math from collections.abc import Mapping from dataclasses import dataclass, field -from enum import Enum +from enum import StrEnum from fhops.productivity.eriksson2014 import ( estimate_forwarder_productivity_final_felling, @@ -32,7 +32,7 @@ from fhops.productivity.laitila2020 import estimate_brushwood_harwarder_productivity -class ForwarderBCModel(str, Enum): +class ForwarderBCModel(StrEnum): """Forwarder regressions wired into FHOPS' dataset helpers. Each enum value corresponds to a published study or FPInnovations bulletin: diff --git a/src/fhops/productivity/ghaffariyan2019.py b/src/fhops/productivity/ghaffariyan2019.py index 08f5bab8..655e3475 100644 --- a/src/fhops/productivity/ghaffariyan2019.py +++ b/src/fhops/productivity/ghaffariyan2019.py @@ -3,10 +3,10 @@ from __future__ import annotations import math -from enum import Enum +from enum import StrEnum -class ALPACASlopeClass(str, Enum): +class ALPACASlopeClass(StrEnum): """Slope buckets described by Ghaffariyan et al. (2019).""" FLAT = "flat" # <10% diff --git a/src/fhops/productivity/kellogg_bettinger1994.py b/src/fhops/productivity/kellogg_bettinger1994.py index e54dacc8..f0e186ed 100644 --- a/src/fhops/productivity/kellogg_bettinger1994.py +++ b/src/fhops/productivity/kellogg_bettinger1994.py @@ -2,10 +2,10 @@ from __future__ import annotations -from enum import Enum +from enum import StrEnum -class LoadType(str, Enum): +class LoadType(StrEnum): """Forwarded product mix in the Kellogg & Bettinger thinning study.""" SAWLOG = "sawlog" diff --git a/src/fhops/productivity/lahrsen2025.py b/src/fhops/productivity/lahrsen2025.py index c52181bf..337ac260 100644 --- a/src/fhops/productivity/lahrsen2025.py +++ b/src/fhops/productivity/lahrsen2025.py @@ -4,7 +4,7 @@ from collections.abc import Mapping from dataclasses import dataclass, field -from enum import Enum +from enum import StrEnum import numpy as np @@ -17,7 +17,7 @@ pacal = None -class LahrsenModel(str, Enum): +class LahrsenModel(StrEnum): """Available coefficient sets from Lahrsen (2025).""" DAILY = "daily" diff --git a/src/fhops/productivity/skidder_ft.py b/src/fhops/productivity/skidder_ft.py index d101b229..048f7df9 100644 --- a/src/fhops/productivity/skidder_ft.py +++ b/src/fhops/productivity/skidder_ft.py @@ -5,20 +5,20 @@ import json import math from dataclasses import dataclass, field -from enum import Enum +from enum import StrEnum from functools import lru_cache from pathlib import Path from typing import Any, cast -class Han2018SkidderMethod(str, Enum): +class Han2018SkidderMethod(StrEnum): """Harvesting method definitions from Han et al. (2018).""" LOP_AND_SCATTER = "lop_and_scatter" WHOLE_TREE = "whole_tree" -class TrailSpacingPattern(str, Enum): +class TrailSpacingPattern(StrEnum): """Trail-network layouts from FPInnovations TN285 (ghost-trail study).""" NARROW_13_15M = "narrow_13_15m" @@ -26,14 +26,14 @@ class TrailSpacingPattern(str, Enum): DOUBLE_GHOST_27M = "double_ghost_27m" -class DeckingCondition(str, Enum): +class DeckingCondition(StrEnum): """Decking/landing preparation states from ADV4N21 (loader-forward vs. skidder).""" CONSTRAINED = "constrained_decking" PREPARED = "prepared_decking" -class ADV6N7DeckingMode(str, Enum): +class ADV6N7DeckingMode(StrEnum): """Decking variants from FPInnovations Advantage Vol. 6 No. 7.""" SKIDDER = "skidder" diff --git a/src/fhops/scenario/contract/models.py b/src/fhops/scenario/contract/models.py index 47568a01..a346369f 100644 --- a/src/fhops/scenario/contract/models.py +++ b/src/fhops/scenario/contract/models.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import date -from enum import Enum +from enum import StrEnum from pydantic import BaseModel, ValidationInfo, field_validator, model_validator @@ -61,7 +61,7 @@ def _non_negative(cls, value: float) -> float: Day = int # 1..D -class SalvageProcessingMode(str, Enum): +class SalvageProcessingMode(StrEnum): STANDARD_MILL = "standard_mill" PORTABLE_MILL = "portable_mill" IN_WOODS_CHIPPING = "in_woods_chipping" diff --git a/tests/test_analyze_tuner_reports.py b/tests/test_analyze_tuner_reports.py index 0f48a793..6096f3bf 100644 --- a/tests/test_analyze_tuner_reports.py +++ b/tests/test_analyze_tuner_reports.py @@ -1,6 +1,7 @@ from __future__ import annotations import subprocess +import sys from pathlib import Path import pandas as pd @@ -54,7 +55,7 @@ def test_analyze_tuner_reports_cli(tmp_path: Path): summary_md = tmp_path / "summary.md" cmd = [ - "python", + sys.executable, "scripts/analyze_tuner_reports.py", "--report", f"baseline={report_a}", @@ -188,7 +189,7 @@ def _persist_history_snapshot( delta_md = tmp_path / "history_delta.md" cmd = [ - "python", + sys.executable, "scripts/analyze_tuner_reports.py", "--report", f"baseline={history_dir / '2024-11-02.csv'}", diff --git a/tests/test_run_tuning_benchmarks.py b/tests/test_run_tuning_benchmarks.py index bdaf961a..ceb30333 100644 --- a/tests/test_run_tuning_benchmarks.py +++ b/tests/test_run_tuning_benchmarks.py @@ -47,9 +47,9 @@ def _normalise(df: pd.DataFrame, *, drop_bundle: bool = True) -> pd.DataFrame: result = df.copy() if "scenario" in result.columns: result["scenario"] = result["scenario"].apply( - lambda value: value.split(":", 1)[1] - if isinstance(value, str) and ":" in value - else value + lambda value: ( + value.split(":", 1)[1] if isinstance(value, str) and ":" in value else value + ) ) if drop_bundle and "bundle" in result.columns: result = result.drop(columns=["bundle"])