fhops.optimization Package
The optimisation stack converts fhops.scenario.contract.Problem objects into either Pyomo MIP
models or heuristic schedules. Use these modules when:
Running simulated annealing / ILS / Tabu heuristics from
fhops.optimization.heuristics.Building the operational problem context (
fhops.optimization.operational_problem.build_operational_problem()) consumed by the operational MILP (fhops.model.milp.driver.solve_operational_milp()) and the heuristics.
The exact model is the operational MILP (fhops.model.milp, fhops solve-mip-operational),
the formulation documented in the FHOPS paper:
from fhops.scenario.io import load_scenario
from fhops.scenario.contract import Problem
from fhops.optimization.operational_problem import build_operational_problem
from fhops.model.milp.driver import solve_operational_milp
pb = Problem.from_scenario(load_scenario("examples/tiny7/scenario.yaml"))
ctx = build_operational_problem(pb)
result = solve_operational_milp(ctx.bundle, time_limit=300, context=ctx)
if result["has_solution"]:
print(result["outcome"], result["objective"], len(result["assignments"]))
else:
# "infeasible", "no_solution" (time limit) or "error" (see result["solver_error"])
print(result["outcome"], result["solver_error"])
Deprecated legacy MIP (1.0.1, #127)
fhops.optimization.mip.solve_mip() and fhops.optimization.mip.builder.build_model()
remain importable for API compatibility but emit
LegacyMipDeprecationWarning (a
DeprecationWarning, shown for calls from scripts/notebooks and under pytest):
solve_mip(pb, time_limit=60, driver="auto", debug=False)delegates to the operational MILP (solve_with_operational_milp()). It keeps the 1.0.1 result contract (objective,assignments,has_solution,outcome,solver_status,termination_condition,solver_error,warnings; plusproduction,warm_startandsolver), never raises for infeasible models, time limits or solver errors, and raisesSolverUnavailableonly for a missing solver andValueErrorfor an unknown driver. Legacydrivernames map to operational solvers viaoperational_solver_for_driver()(auto→ Gurobi when installed, else HiGHS; HiGHS drivers →highs;gurobi/gurobi-appsi/gurobi-direct→gurobi/appsi_gurobi/gurobi_direct). Objectives are operational MILP objectives, not comparable with 1.0.0solve_mipvalues.build_model(pb)still builds the legacy day-level Pyomo model, which no solver uses. Its loader-buffer constraint demands−batchm³ of buffer in the first shift, so it is infeasible for every scenario with a loader role (every bundled example, also at v1.0.0). Usefhops.model.milp.operational.build_operational_model()instead.
Optimization layer (MIP builders, heuristics, constraints).
- fhops.optimization.build_model(pb)[source]
Build the legacy FHOPS day-level MIP model (deprecated).
Deprecated since version 1.0.1: Emits
fhops.optimization.mip.deprecation.LegacyMipDeprecationWarning. The model is infeasible whenever a block’s harvest system has a loader role with machines (thesystem_loader_bufferrows demand−batchm³ of buffer in the first shift), which includes the bundled tiny7, small21 and med42 examples. Use the operational MILP (fhops.model.milp.operational.build_operational_model(),fhops.model.milp.driver.solve_operational_milp()).- Parameters:
pb (Problem) –
fhops.scenario.contract.Problemproduced byProblem.from_scenario. The helper must include the full shift list (pb.shifts) so the model can build the (machine, block, day, shift) assignment tensor.- Returns:
Fully constructed model containing decision variables for assignments/production, optional transition binaries (when mobilisation or transition penalties are enabled), and the objective/constraints described in the FHOPS roadmap.
- Return type:
pyomo.ConcreteModel
Notes
The builder purposely mirrors the documented objective weights:
ObjectiveWeights.production– coefficient for total production.ObjectiveWeights.mobilisation– coefficient for mobilisation spend derived from transition binaries.ObjectiveWeights.transitions– optional penalty for the number of transitions itself.ObjectiveWeights.landing_surplus– enables soft landing-capacity overages using surplus variables.
Any change to this function should be reflected in
docs/howto/thesis_eval.rstand the MIP section of the API docs.Scenario.locked_assignmentsare fixed on the assignment variables: a lock withoutshift_idfixes every shift of its day, a lock withshift_idfixes only that slot.Scenario.initial_stateis not modelled by this legacy builder (aUserWarningis emitted); use the operational MILP (fhops.model.milp.driver.solve_operational_milp(),fhops solve-mip-operational) when carrying state into a horizon.
- fhops.optimization.solve_mip(pb, time_limit=60, driver='auto', debug=False)[source]
Solve
pbwith the operational MILP (deprecated legacy entry point).Deprecated since version 1.0.1: Emits
fhops.optimization.mip.deprecation.LegacyMipDeprecationWarningand delegates tosolve_with_operational_milp()(i.e.fhops.model.milp.driver.solve_operational_milp()). Until 1.0.1 this function solved the legacy day-level MIP, which is infeasible for every scenario with a loader role (#124, #127). Callfhops.model.milp.driver.solve_operational_milp()directly.- Parameters:
pb (Problem) –
fhops.scenario.contract.Problembuilt withProblem.from_scenario.time_limit (int) – Solver wall-clock limit in seconds (default 60).
driver (str) – Legacy driver name, mapped by
operational_solver_for_driver():"auto"(Gurobi when installed, otherwise — or after a Gurobi solver error — HiGHS),"highs"/"highs-appsi"/"appsi"/"highs-exec"/"exec"(HiGHS),"gurobi","gurobi-appsi"(appsi_gurobi) or"gurobi-direct"(gurobi_direct).debug (bool) – Stream the solver log to stdout and print the selected solver.
- Returns:
Same contract as in 1.0.1 (#124); the function never raises because a model is infeasible, a limit was reached without an incumbent, or an available solver failed. Keys:
objective(float | None)Operational MILP objective of the loaded solution (not comparable with 1.0.0 legacy objectives);
Nonewithout a feasible solution. A solve stopped by a limit that holds a feasible incumbent reports that incumbent.assignments(DataFrame)Columns
machine_id, block_id, day, shift_id, assigned, production; one row per assigned slot or slot with production > 1e-6. Empty (same columns) without a solution.has_solution(bool)Truewhen a feasible solution was loaded into the model.outcome(str)"optimal","feasible"(incumbent at a limit),"infeasible","no_solution"(limit reached without an incumbent) or"error".solver_status,termination_condition(str)Pyomo solver status and termination condition.
solver_error(str | None)Set when the solver failed rather than proving infeasibility.
warnings(list[str])Driver notes (e.g. a Gurobi failure under
driver="auto"before the HiGHS retry) followed by the operational driver’s lock warnings.production(float),warm_start(dict),solver(str)Added by the delegation (see
solve_with_operational_milp()).
- Return type:
- Raises:
SolverUnavailable – When the requested solver is not installed (or, for
driver="auto", neither Gurobi nor HiGHS is available).ValueError – For an unknown
drivername.
- Warns:
LegacyMipDeprecationWarning – On every call.
Examples
>>> res = solve_mip(pb, time_limit=60) >>> # preferred: >>> from fhops.model.milp.driver import solve_operational_milp >>> from fhops.optimization.operational_problem import build_operational_problem >>> ctx = build_operational_problem(pb) >>> res = solve_operational_milp(ctx.bundle, time_limit=60, context=ctx)
Pyomo builder for the legacy FHOPS day-level MIP (deprecated since 1.0.1, #127).
The builder is kept importable for API compatibility only. It is infeasible for every scenario
whose harvest system has a loader role with machines (see
fhops.optimization.mip.deprecation), and no FHOPS solver uses it any more: solve_mip,
fhops solve-mip, fhops benchmark and the ILS hybrid step all run the operational MILP
(fhops.model.milp).
- fhops.optimization.mip.builder.build_model(pb)[source]
Build the legacy FHOPS day-level MIP model (deprecated).
Deprecated since version 1.0.1: Emits
fhops.optimization.mip.deprecation.LegacyMipDeprecationWarning. The model is infeasible whenever a block’s harvest system has a loader role with machines (thesystem_loader_bufferrows demand−batchm³ of buffer in the first shift), which includes the bundled tiny7, small21 and med42 examples. Use the operational MILP (fhops.model.milp.operational.build_operational_model(),fhops.model.milp.driver.solve_operational_milp()).- Parameters:
pb (Problem) –
fhops.scenario.contract.Problemproduced byProblem.from_scenario. The helper must include the full shift list (pb.shifts) so the model can build the (machine, block, day, shift) assignment tensor.- Returns:
Fully constructed model containing decision variables for assignments/production, optional transition binaries (when mobilisation or transition penalties are enabled), and the objective/constraints described in the FHOPS roadmap.
- Return type:
pyomo.ConcreteModel
Notes
The builder purposely mirrors the documented objective weights:
ObjectiveWeights.production– coefficient for total production.ObjectiveWeights.mobilisation– coefficient for mobilisation spend derived from transition binaries.ObjectiveWeights.transitions– optional penalty for the number of transitions itself.ObjectiveWeights.landing_surplus– enables soft landing-capacity overages using surplus variables.
Any change to this function should be reflected in
docs/howto/thesis_eval.rstand the MIP section of the API docs.Scenario.locked_assignmentsare fixed on the assignment variables: a lock withoutshift_idfixes every shift of its day, a lock withshift_idfixes only that slot.Scenario.initial_stateis not modelled by this legacy builder (aUserWarningis emitted); use the operational MILP (fhops.model.milp.driver.solve_operational_milp(),fhops solve-mip-operational) when carrying state into a horizon.
Deprecated solve_mip entry point (delegates to the operational MILP since 1.0.1, #127).
This module backs solve_mip() and the fhops solve-mip / fhops benchmark CLI
commands. Since FHOPS 1.0.1 (#127) they solve the operational MILP
(fhops.model.milp.driver.solve_operational_milp(), the formulation documented in the
SoftwareX paper) through solve_with_operational_milp(); the legacy --driver names are
mapped to operational solver names by operational_solver_for_driver().
The legacy day-level MIP (fhops.optimization.mip.builder.build_model()) is infeasible for
every scenario with a loader role (see fhops.optimization.mip.deprecation). Its 1.0.1 (#124)
solve path is kept as the private _solve_legacy_mip to reproduce that finding; no public entry
point uses it.
- exception fhops.optimization.mip.highs_driver.SolverUnavailable[source]
Bases:
RuntimeErrorRaised when a requested solver backend is not installed or cannot be created.
This is the only solver-related exception
solve_mip()raises; failures of an available solver are reported through the result’ssolver_errorinstead.
- fhops.optimization.mip.highs_driver.operational_solver_for_driver(driver)[source]
Map a legacy MIP
drivername to the operational MILP solver name(s) to try.- Parameters:
driver (str) – Legacy driver name (case-insensitive):
auto,highs,appsi,highs-appsi,exec,highs-exec,gurobi,gurobi-appsiorgurobi-direct.- Returns:
SolverFactorynames in the order they are tried:("gurobi", "highs")forauto(Gurobi when installed, otherwise or after a Gurobi failure HiGHS — the 1.0.0autorule);("highs",)for every HiGHS driver (seeded solves still go throughappsi_highs, seefhops.model.milp.driver.solve_operational_milp());("gurobi",),("appsi_gurobi",)or("gurobi_direct",)for the Gurobi drivers.- Return type:
- Raises:
ValueError – For an unknown driver name.
- fhops.optimization.mip.highs_driver.solve_mip(pb, time_limit=60, driver='auto', debug=False)[source]
Solve
pbwith the operational MILP (deprecated legacy entry point).Deprecated since version 1.0.1: Emits
fhops.optimization.mip.deprecation.LegacyMipDeprecationWarningand delegates tosolve_with_operational_milp()(i.e.fhops.model.milp.driver.solve_operational_milp()). Until 1.0.1 this function solved the legacy day-level MIP, which is infeasible for every scenario with a loader role (#124, #127). Callfhops.model.milp.driver.solve_operational_milp()directly.- Parameters:
pb (Problem) –
fhops.scenario.contract.Problembuilt withProblem.from_scenario.time_limit (int) – Solver wall-clock limit in seconds (default 60).
driver (str) – Legacy driver name, mapped by
operational_solver_for_driver():"auto"(Gurobi when installed, otherwise — or after a Gurobi solver error — HiGHS),"highs"/"highs-appsi"/"appsi"/"highs-exec"/"exec"(HiGHS),"gurobi","gurobi-appsi"(appsi_gurobi) or"gurobi-direct"(gurobi_direct).debug (bool) – Stream the solver log to stdout and print the selected solver.
- Returns:
Same contract as in 1.0.1 (#124); the function never raises because a model is infeasible, a limit was reached without an incumbent, or an available solver failed. Keys:
objective(float | None)Operational MILP objective of the loaded solution (not comparable with 1.0.0 legacy objectives);
Nonewithout a feasible solution. A solve stopped by a limit that holds a feasible incumbent reports that incumbent.assignments(DataFrame)Columns
machine_id, block_id, day, shift_id, assigned, production; one row per assigned slot or slot with production > 1e-6. Empty (same columns) without a solution.has_solution(bool)Truewhen a feasible solution was loaded into the model.outcome(str)"optimal","feasible"(incumbent at a limit),"infeasible","no_solution"(limit reached without an incumbent) or"error".solver_status,termination_condition(str)Pyomo solver status and termination condition.
solver_error(str | None)Set when the solver failed rather than proving infeasibility.
warnings(list[str])Driver notes (e.g. a Gurobi failure under
driver="auto"before the HiGHS retry) followed by the operational driver’s lock warnings.production(float),warm_start(dict),solver(str)Added by the delegation (see
solve_with_operational_milp()).
- Return type:
- Raises:
SolverUnavailable – When the requested solver is not installed (or, for
driver="auto", neither Gurobi nor HiGHS is available).ValueError – For an unknown
drivername.
- Warns:
LegacyMipDeprecationWarning – On every call.
Examples
>>> res = solve_mip(pb, time_limit=60) >>> # preferred: >>> from fhops.model.milp.driver import solve_operational_milp >>> from fhops.optimization.operational_problem import build_operational_problem >>> ctx = build_operational_problem(pb) >>> res = solve_operational_milp(ctx.bundle, time_limit=60, context=ctx)
- fhops.optimization.mip.highs_driver.solve_with_operational_milp(pb, time_limit=60, driver='auto', debug=False)[source]
Solve the operational MILP for
pbusing a legacydrivername (no deprecation).This is the implementation behind
solve_mip(),fhops solve-mipand the MIP step offhops benchmark. It builds the operational problem (fhops.optimization.operational_problem.build_operational_problem()) and callsfhops.model.milp.driver.solve_operational_milp()withsolverfromoperational_solver_for_driver(),time_limit,tee=debugand the operational context. Equivalent tofhops solve-mip-operational SCENARIO --solver <solver> --time-limit <time_limit>.- Parameters:
pb (Problem) –
fhops.scenario.contract.Problembuilt withProblem.from_scenario.time_limit (int | None) – Solver wall-clock limit in seconds (default 60;
Noneleaves the solver default).driver (str) – Legacy driver name (see
operational_solver_for_driver()).debug (bool) – Stream the solver log to stdout and print the selected solver.
- Returns:
The
fhops.model.milp.driver.solve_operational_milp()result (objective,production,assignments,has_solution,outcome,solver_status,termination_condition,solver_error,warnings,warm_start) plussolver(theSolverFactoryname that produced it). Underdriver="auto"a Gurobi run that reportssolver_erroris retried with HiGHS and the failure is prepended towarnings.- Return type:
- Raises:
SolverUnavailable – When no candidate solver is available.
ValueError – For an unknown
drivername (raised before the model is built).
Deprecation notices for the legacy day-level MIP (FHOPS 1.0.1, #127).
The legacy builder (fhops.optimization.mip.builder.build_model()) is infeasible for every
scenario whose harvest system has a loader role with machines: its loader-buffer constraint
Σ_{≤s} prod_loader ≤ Σ_{<s} prod_prereq − batch cannot hold in the first shift (found in #124;
the bundled tiny7, small21 and med42 examples are proven infeasible by HiGHS, also at v1.0.0).
The canonical, published formulation is the operational MILP
(fhops.model.milp, fhops solve-mip-operational); solve_mip / fhops solve-mip
delegate to it and the builder is kept importable only for API compatibility.
- exception fhops.optimization.mip.deprecation.LegacyMipDeprecationWarning[source]
Bases:
DeprecationWarningWarning emitted by the deprecated legacy MIP entry points.
Emitted (
stacklevel=2) byfhops.optimization.mip.solve_mip()andfhops.optimization.mip.builder.build_model(). It subclassesDeprecationWarning, so Python shows it for calls made from__main__(scripts, notebooks) and under test runners; thefhops solve-mipCLI prints its own one-line notice instead.
Simulated annealing heuristic for FHOPS.
- class fhops.optimization.heuristics.sa.Schedule(plan, matrix=<factory>, mobilisation_cache=<factory>, dirty_machines=<factory>, block_remaining_cache=None, role_remaining_cache=None, dirty_blocks=<factory>, block_slots=<factory>, dirty_slots=<factory>, slot_production=<factory>, repair_state=None, watch_stats=None)[source]
Bases:
objectMachine assignment plan storing both dict and array views.
- Parameters:
- repair_state: RepairState | None
- fhops.optimization.heuristics.sa.solve_sa(pb, iters=2000, seed=42, operators=None, operator_weights=None, batch_size=None, max_workers=None, cooling_rate=0.999, restart_interval=None, telemetry_log=None, telemetry_context=None, watch_sink=None, watch_interval=None, watch_metadata=None, watch_debug=False, use_local_repairs=False, objective_weight_overrides=None, milp_objective=None, pre_repair=True, commitment_front=0.0, incremental_repair=True, adaptive_cooling=False, cooling_window=None, cooling_initial_acceptance=0.9, cooling_delta=0.1)[source]
Solve the scheduling problem with simulated annealing.
- Parameters:
pb (fhops.scenario.contract.Problem) – Parsed scenario context describing machines, blocks, and shifts.
iters (int, default=2000) – Number of annealing iterations. Higher values increase runtime and solution quality.
seed (int, default=42) – RNG seed used for deterministic runs.
operators (list[str] | None) – Optional list of operator names to enable (default: all registered operators).
operator_weights (dict[str, float] | None) – Optional weight overrides for operators (values
<= 0disable an operator).batch_size (int | None) – When set, sample up to
batch_sizeneighbour candidates per iteration.Noneor<= 1keeps the sequential single-candidate behaviour.max_workers (int | None) – Maximum worker threads for evaluating batched neighbours.
None/<=1keeps sequential scoring.cooling_rate (float, default=0.999) – Multiplicative cooling factor applied each iteration (0 < rate < 1). Larger values cool more slowly. Ignored when
adaptive_coolingis set.adaptive_cooling (bool, default=False) – Use the Aarts and Van Laarhoven (1985) adaptive temperature schedule instead of geometric cooling. The first
cooling_windowiterations are a warm-up (every move accepted) that sets the initial temperature from the average uphill move; the temperature is then decremented from the spread of the scores seen. Tunes itself to the scenario.cooling_window (int | None, optional) – Iterations per temperature under
adaptive_cooling.Noneauto-scales to64capped at half ofiters.cooling_initial_acceptance (float, default=0.9) – Acceptance ratio the adaptive warm-up targets when setting the initial temperature (
(0, 1)). Ignored unlessadaptive_coolingis set.cooling_delta (float, default=0.1) – Distance from stationarity the adaptive decrement allows (positive). Ignored unless
adaptive_coolingis set.restart_interval (int | None, optional) – Number of consecutive non-accepting iterations before restarting from the greedy seed.
Noneauto-scales tomax(1000, iters / 5).telemetry_log (str | pathlib.Path | None) – Optional telemetry JSONL path. When provided, solver progress and final metrics are logged.
telemetry_context (dict[str, Any] | None) – Additional context merged into telemetry records (scenario metadata, tuner info, etc.).
watch_sink (SnapshotSink | None, optional) – Optional callback that receives
fhops.telemetry.watch.Snapshotupdates for live dashboards. When omitted, no live progress is emitted.watch_interval (int | None, optional) – Iteration interval between snapshot emissions. Defaults to
max(1, iters / 200)when a sink is provided.watch_metadata (dict[str, str] | None) – Additional metadata (e.g., scenario/solver labels) attached to each snapshot.
watch_debug (bool, default=False) – When
Truecapture sequencing debug stats for watch snapshots (adds overhead).use_local_repairs (bool, default=False) – When
Truerepairs only the slots touched by a candidate before scoring. This is an approximate mode: the dirty-slot repair does not refill every idle slot, so a candidate’s search score can differ from a full evaluation of the same plan. The final schedule is always re-scored with a full repair before reporting.incremental_repair (bool, default=True) – Resume each candidate’s repair from the accepted schedule’s per-slot state at its earliest changed slot instead of repairing the whole horizon. Provably identical to the full repair and faster on large scenarios (~1.15x on large84); it falls back to the full repair automatically in the modes where that equivalence is not guaranteed (
batch_sizewith parallel workers,pre_repair=False, oruse_local_repairs=True).objective_weight_overrides (dict[str, float] | None, optional) – Override scenario objective weights (keys:
production,mobilisation,transitions,landing_surplus).Nonekeeps the scenario weights, except for the scenarios listed inAUTO_OBJECTIVE_WEIGHT_OVERRIDES(FHOPS Tiny7/Small21: production 1.0, mobilisation 0.2, transitions 0.1, soft landing capacity 0.05), which are applied automatically and recorded inmeta(#140).milp_objective (float | None, optional) – Reference MILP objective used for gap reporting (best - MILP) in watch/telemetry output.
Noneskips gap metrics.pre_repair (bool)
commitment_front (float)
- Returns:
Dictionary with the following keys:
objective(float)Fresh
evaluate_schedule()score of the returned schedule (higher is better); it equals a re-evaluation ofassignments.assignments(pandas.DataFrame)Assignment matrix with columns
machine_id, block_id, day, shift_id, assigned.meta(dict[str, Any])Telemetry payload including
operatorsweights, optionaloperators_stats, and bookkeeping such asproposalsortelemetry_run_id.
- Return type: