fhops.evaluation Package
The evaluation layer turns solver assignments into KPI-rich reports. It houses deterministic playback, stochastic extensions, and KPI calculators. Use it to:
Convert solver outputs into shift/day summaries (
fhops.evaluation.playback.run_playback()).Compute KPI bundles for CLI reports or notebooks (
fhops.evaluation.metrics.kpis.compute_kpis()).Export CSV/Parquet/Markdown summaries for documentation or telemetry.
Example:
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
from fhops.evaluation import compute_kpis
scenario = load_scenario("examples/tiny7/scenario.yaml")
problem = Problem.from_scenario(scenario)
ctx = build_operational_problem(problem)
mip_res = solve_operational_milp(ctx.bundle, time_limit=60, context=ctx)
kpis = compute_kpis(problem, mip_res["assignments"])
print(kpis["total_production"], kpis["mobilisation_cost"])
(fhops.optimization.mip.solve_mip() is a deprecated wrapper around the same operational MILP
since FHOPS 1.0.1.)
Evaluation layer (playback, metrics, reporting).
- class fhops.evaluation.DaySummary(day, available_hours=0.0, total_hours=0.0, production_units=0.0, mobilisation_cost=0.0, completed_blocks=0, idle_hours=None, blackout_conflicts=0, sequencing_violations=0, utilisation_ratio=None, sample_id=0, downtime_hours=0.0, downtime_events=0, weather_severity_total=0.0)[source]
Bases:
objectAggregated metrics per day across machines.
- Parameters:
day (int)
available_hours (float)
total_hours (float)
production_units (float)
mobilisation_cost (float)
completed_blocks (int)
idle_hours (float | None)
blackout_conflicts (int)
sequencing_violations (int)
utilisation_ratio (float | None)
sample_id (int)
downtime_hours (float)
downtime_events (int)
weather_severity_total (float)
- class fhops.evaluation.DowntimeEvent(config)[source]
Bases:
objectRemove part or all of a machine-shift’s production to simulate downtime.
- Parameters:
config (DowntimeEventConfig) – Probability, duration distribution (hours),
max_concurrentand role filter.
Notes
Eligible rows are assignments with
assigned > 0whose machine role passestarget_machine_roles. Sampling order (deterministic for a given RNG state):Days are visited in ascending order.
Selection on each day: with
max_concurrentset,rng.choicedrawsmin(max_concurrent, n)of the day’sneligible rows without replacement (probabilityonly acts as an on/off switch); otherwise onerng.random()per eligible row (in frame order) selects the row when<= probability.For each selected row (in selection order) one
rng.normal(mean_duration_hours, std_duration_hours)draw gives the durationd, clipped to[0, shift_hours].
shift_hoursfollows deterministic playback (shift_hours_resolver()): the matchingtimeline.shiftsdefinition, else the machine’sdaily_hoursdivided by its number of shifts that day. With0 < d < shift_hoursthe row’s production is multiplied by1 - d / shift_hours; withd == shift_hours(or unknown shift hours) the shift is lost entirely (assigned = 0,production = 0);d == 0leaves the row untouched. Affected rows get_downtime = 1,_downtime_hours = dand_downtime_lost(the proposed volume removed, m³) so playback summaries report the sampled downtime hours andcompute_kpis()the lost volume.Row updates are positional: the caller’s index (which may contain duplicate labels) is ignored and restored on the returned copy.
- class fhops.evaluation.DowntimeEventConfig(*, enabled=True, seed_offset=0, probability=0.15, mean_duration_hours=4.0, std_duration_hours=1.5, max_concurrent=None, target_machine_roles=None)[source]
Bases:
SamplingEventConfigDescribes downtime sampling parameters for machines.
- Parameters:
probability (float, default=0.15) – Probability in
[0, 1]that an eligible machine-shift assignment is hit by downtime. Ignored for selection whenmax_concurrentis set (see below);0disables the event.mean_duration_hours (float, default=4.0) – Mean of the Normal distribution used to sample the downtime duration (hours,
>= 0).std_duration_hours (float, default=1.5) – Standard deviation of the downtime duration (hours,
>= 0).0makes every downtime last exactlymean_duration_hours(clipped to the shift length).max_concurrent (int | None, default=None) – When set, exactly
min(max_concurrent, n)of theneligible assignments on each day are hit, chosen uniformly without replacement; whenNoneeach assignment is hit independently withprobability. Must be positive when provided.target_machine_roles (list[str] | None, default=None) – Restrict downtime to machines whose
roleis in this list (Nonetargets all).enabled (bool)
seed_offset (int)
Notes
The sampled duration
dis clipped to[0, shift_hours]and removes the fractiond / shift_hoursof the assignment’s production. A full-shift loss (d == shift_hours) cancels the assignment. Seefhops.evaluation.playback.stochastic.DowntimeEvent.- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class fhops.evaluation.EnsembleResult(base_result, samples)[source]
Bases:
objectAggregate of the baseline deterministic playback plus stochastic samples.
- Parameters:
base_result (PlaybackResult)
samples (list[PlaybackSample])
- base_result: PlaybackResult
- samples: list[PlaybackSample]
- class fhops.evaluation.KPIResult(totals=<factory>, shift_calendar=None, day_calendar=None, sequencing_debug=None)[source]
Bases:
Mapping[str,float|int|str]Structured KPI bundle with optional shift/day calendar attachments.
- Parameters:
- DAY_COLUMNS: ClassVar[tuple[str, ...]] = ('day', 'sample_id', 'production_units', 'total_hours', 'idle_hours', 'mobilisation_cost', 'completed_blocks', 'blackout_conflicts', 'sequencing_violations', 'available_hours', 'utilisation_ratio', 'downtime_hours', 'downtime_events', 'weather_severity_total')
- SHIFT_COLUMNS: ClassVar[tuple[str, ...]] = ('day', 'shift_id', 'machine_id', 'machine_role', 'sample_id', 'production_units', 'total_hours', 'idle_hours', 'mobilisation_cost', 'sequencing_violations', 'blackout_conflicts', 'available_hours', 'utilisation_ratio', 'downtime_hours', 'downtime_events', 'weather_severity_total')
- class fhops.evaluation.LandingShockConfig(*, enabled=True, seed_offset=0, probability=0.1, capacity_multiplier_range=(0.4, 0.8), duration_days=1, target_landing_ids=None)[source]
Bases:
SamplingEventConfigParameterises landing congestion shocks reducing capacity.
- Parameters:
probability (float, default=0.1) – Probability in
[0, 1]that a shock starts at a landing on a given calendar day of the scenario horizon (0disables the event).capacity_multiplier_range (tuple[float, float], default=(0.4, 0.8)) – Ascending
(low, high)bounds (both> 0) of the uniform distribution from which each shock’s production multiplier is drawn.duration_days (int, default=1) – Number of consecutive calendar days each shock lasts, starting on its start day (
>= 1).target_landing_ids (list[str] | None, default=None) – Restrict shocks to these landings (
Noneuses every scenario landing).enabled (bool)
seed_offset (int)
Notes
Every assignment on a block served by a shocked landing has its production scaled by the shock multiplier on each affected day; overlapping shocks use the minimum multiplier. See
fhops.evaluation.playback.stochastic.LandingShockEvent.Shocks start independently for each landing and calendar day, so the expected fraction of landing-days under a shock is
1 - (1 - probability) ** duration_days(daysd < duration_daysat the start of the horizon usedinstead ofduration_days). The defaults (probability=0.1,duration_days=1) shock 10 % of landing-days, with a mean multiplier of(low + high) / 2 = 0.6on those days.- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class fhops.evaluation.LandingShockEvent(config)[source]
Bases:
objectReduce landing throughput via random multi-day shocks.
- Parameters:
config (LandingShockConfig) – Start probability, multiplier range, duration (days) and optional landing filter.
Notes
Sampling order (deterministic for a given RNG state): landings are visited in the order of
target_landing_ids(orscenario.landings); for each landing, every calendar day1..num_daysof the scenario horizon is visited in ascending order and onerng.random()draw starts a shock when<= probability. A started shock immediately draws its multiplier withrng.uniform(low, high)and coversduration_daysconsecutive days starting on its start day. When shocks overlap, the minimum multiplier applies on each day.Every still-assigned row whose block is served by a shocked landing on an affected day has its current production multiplied by the shock multiplier (so effects compose with earlier events) and records
_landing_multiplier. Row updates are positional (the caller’s index is ignored and restored on the returned copy).The expected fraction of landing-days under a shock is
1 - (1 - probability) ** duration_days(ignoring the horizon start, where fewer start days can cover a day); seeLandingShockConfig.- apply(context, assignments, base_production)[source]
Return a copy of
assignmentswith landing shocks applied (see class notes).
- sample_multipliers(context)[source]
Sample landing shocks and return the per-(landing, day) multiplier map.
- Parameters:
context (SamplingContext) – Sample context providing the scenario and RNG (consumed in the documented order).
- Returns:
Mapping
(landing_id, day) -> multiplierfor every day covered by at least one shock (minimum multiplier across overlapping shocks). Days without a shock are absent.- Return type:
- class fhops.evaluation.PlaybackConfig(respect_blackouts=True, infer_missing_shifts=True, include_idle_records=False)[source]
Bases:
objectTuning options for deterministic playback execution.
- class fhops.evaluation.PlaybackEvent(*args, **kwargs)[source]
Bases:
ProtocolStochastic event modifying assignments before playback summarisation.
- class fhops.evaluation.PlaybackRecord(day, shift_id, machine_id, block_id, hours_worked=None, production_units=None, mobilisation_cost=None, blackout_hit=False, landing_id=None, machine_role=None, downtime=False, weather_severity=None, metadata=<factory>, sample_id=0, downtime_hours=None)[source]
Bases:
objectAtomic shift-level record produced by deterministic playback.
Notes
hours_workedis the productive time of the machine-shift (hours);production_unitsis the delivered volume (m³) after sequencing caps.downtimeflags machine-shifts hit by stochastic downtime anddowntime_hoursstores the sampled hours lost (Nonewhen unknown, in which case summaries fall back tohours_worked).- Parameters:
day (int)
shift_id (str)
machine_id (str)
block_id (str | None)
hours_worked (float | None)
production_units (float | None)
mobilisation_cost (float | None)
blackout_hit (bool)
landing_id (str | None)
machine_role (str | None)
downtime (bool)
weather_severity (float | None)
sample_id (int)
downtime_hours (float | None)
- class fhops.evaluation.PlaybackResult(records, shift_summaries, day_summaries, config, sequencing_debug=None, sample_id=0, delivered_total=0.0, remaining_work_total=0.0)[source]
Bases:
objectContainer grouping playback outputs.
Notes
delivered_totalis the volume (m³) delivered by terminal roles andremaining_work_totalthe volume (m³) ofBlock.work_requiredstill undelivered.run_playback()always sets both from the sequencing tracker; for an empty plandelivered_total == 0andremaining_work_total == sum(Block.work_required)(the field defaults only apply to hand-built instances).- Parameters:
records (Sequence[PlaybackRecord])
shift_summaries (Sequence[ShiftSummary])
day_summaries (Sequence[DaySummary])
config (PlaybackConfig)
sample_id (int)
delivered_total (float)
remaining_work_total (float)
- config: PlaybackConfig
- day_summaries: Sequence[DaySummary]
- records: Sequence[PlaybackRecord]
- shift_summaries: Sequence[ShiftSummary]
- class fhops.evaluation.PlaybackSample(sample_id, result)[source]
Bases:
objectContainer pairing a stochastic sample ID with its
PlaybackResult.- Parameters:
sample_id (int)
result (PlaybackResult)
- result: PlaybackResult
- class fhops.evaluation.SamplingConfig(*, samples=10, base_seed=123, downtime=<factory>, weather=<factory>, landing=<factory>)[source]
Bases:
BaseModelTop-level configuration for stochastic playback ensembles.
- Parameters:
samples (int)
base_seed (int)
downtime (DowntimeEventConfig)
weather (WeatherEventConfig)
landing (LandingShockConfig)
- downtime: DowntimeEventConfig
- landing: LandingShockConfig
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- weather: WeatherEventConfig
- class fhops.evaluation.SamplingContext(problem, sample_id, rng, config)[source]
Bases:
objectPer-sample execution context.
- Parameters:
problem (Problem)
sample_id (int)
rng (Generator)
config (SamplingConfig)
- config: SamplingConfig
- problem: Problem
- rng: Generator
- class fhops.evaluation.SamplingEventConfig(*, enabled=True, seed_offset=0)[source]
Bases:
BaseModelBase configuration shared by all stochastic events.
- Parameters:
enabled (bool, default=True) – Whether
run_stochastic_playback()applies the event when it builds the default event list.seed_offset (int, default=0) – Deprecated (FHOPS 1.0.1) and ignored: every event of sample
idraws from the shared generatornumpy.random.default_rng(base_seed + i)in the order downtime → weather → landing shocks (FHOPS 1.0.0 also ignored this field). Setting a non-zero value emits aDeprecationWarning;0(e.g. a round trip of a dumped default config) does not.
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class fhops.evaluation.ShiftSummary(day, shift_id, machine_id, machine_role=None, available_hours=0.0, total_hours=0.0, production_units=0.0, mobilisation_cost=0.0, idle_hours=None, blackout_conflicts=0, sequencing_violations=0, utilisation_ratio=None, sample_id=0, downtime_hours=0.0, downtime_events=0, weather_severity_total=0.0)[source]
Bases:
objectAggregated metrics per machine/shift.
- Parameters:
day (int)
shift_id (str)
machine_id (str)
machine_role (str | None)
available_hours (float)
total_hours (float)
production_units (float)
mobilisation_cost (float)
idle_hours (float | None)
blackout_conflicts (int)
sequencing_violations (int)
utilisation_ratio (float | None)
sample_id (int)
downtime_hours (float)
downtime_events (int)
weather_severity_total (float)
- class fhops.evaluation.WeatherEvent(config)[source]
Bases:
objectScale production on days hit by sampled weather spells.
- Parameters:
config (WeatherEventConfig) – Day probability, severity levels, spell length and optional shift filter.
Notes
Sampling order: for each distinct assignment day in ascending order, one
rng.random()draw starts a spell when<= day_probability; a started spell draws its severity level withrng.integers. Each spell coversimpact_window_daysconsecutive days; overlapping spells keep the maximum severity. Affected, still-assigned rows have their current production multiplied by1 - severity(so effects compose with earlier events) and record_weather_severityand_weather_lost(the proposed volume removed, m³).correlated_daysis deprecated and ignored. Row updates are positional (the caller’s index is ignored and restored on the returned copy).
- class fhops.evaluation.WeatherEventConfig(*, enabled=True, seed_offset=0, day_probability=0.2, severity_levels=<factory>, correlated_days=True, impact_window_days=1, affected_shifts=None)[source]
Bases:
SamplingEventConfigCaptures stochastic weather impacts affecting production rates.
- Parameters:
day_probability (float, default=0.2) – Probability in
[0, 1]that a weather spell starts on a given assignment day.severity_levels (dict[str, float]) – Mapping
label -> severity; a spell picks one level uniformly at random and scales affected production by1 - severity(severity in[0, 1]).correlated_days (bool, default=True) – Deprecated (FHOPS 1.0.1) and ignored. Explicitly setting a non-default value (
False) emits aDeprecationWarning; the defaultTrue(e.g. a round trip of a dumped config or an older syntheticmetadata.yaml) does not. Useimpact_window_daysto model multi-day spells.impact_window_days (int, default=1) – Number of consecutive calendar days covered by each spell (
>= 1); overlapping spells keep the highest severity per day.affected_shifts (list[str] | None, default=None) – Restrict weather impacts to these shift IDs (
Noneaffects every shift).enabled (bool)
seed_offset (int)
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- fhops.evaluation.assignments_to_records(problem, assignments)[source]
Convert solver assignments dataframe into playback records.
- Parameters:
problem (fhops.scenario.contract.Problem) – Problem wrapping the scenario being replayed.
assignments (pandas.DataFrame) – Rows with
machine_id,block_id,day,shift_id(optional only for single-shift scenarios, default"S1"; seenormalise_shift_ids()),assigned(rows withassigned <= 0are skipped) andproduction(volume, m³; when missing the production rate capped by remaining work is used). The frame index is ignored. Stochastic events may add the private columns_downtime(flag),_downtime_hours(sampled downtime hours),_downtime_lost(proposed volume removed by downtime, m³),_weather_severityand_weather_lost(proposed volume removed by weather, m³).
- Returns:
Iterator over chronologically ordered records. The iterator exposes a
sequencing_trackerattribute holding theSequencingTrackerused to cap production.- Return type:
Iterator[PlaybackRecord]
- Raises:
ValueError – When required columns are missing, or when the scenario has several shifts per day and a replayed row has no
shift_id(normalise_shift_ids()).
Notes
Rows flagged by downtime keep their record even when the whole shift is lost (
assigned == 0): such records carryproduction_units = 0,hours_worked = 0anddowntime_hoursequal to the lost shift hours, and they bypass the sequencing tracker and mobilisation costing (the machine did not work). Partially lost shifts reporthours_worked = shift_hours - downtime_hours.shift_hourscomes fromshift_hours_resolver(). When the private_downtime_lost/_weather_lostcolumns are present, their values are copied tometadata["downtime_production_lost"]/metadata["weather_production_lost"](m³) for record-level loss KPIs.A row without a
productionvalue in a slot that a scenario lock (ScheduleLock) pins to the row’s block is replayed as a locked slot (#158): it proposesmin(rate, remaining)capped byScheduleLock.productionwhen set, produces what the staged input allows, and idles without a sequencing violation when the input or a head-start/truckload threshold is missing (as the operational MILP and the heuristics treat locked slots).When
Scenario.initial_stateis set, the sequencing tracker starts from the carried-in staged inventory, role remaining volumes, and role shift counts, and each machine’s mobilisation tracking starts at itslast_block_idso the first move to a different block is charged (matching the operational MILP and the heuristics).An empty plan (
assignmentsisNone, has no rows, or has no row withassigned > 0) yields no records but still exposes a freshsequencing_tracker, so callers see the fullBlock.work_requiredas remaining and nothing delivered. Empty frames are accepted without the required-column check.
- fhops.evaluation.compute_kpis(pb, assignments)[source]
Compute production, mobilisation, utilisation, and sequencing KPIs from assignments.
- Parameters:
pb (fhops.scenario.contract.Problem) – Problem wrapping the scenario whose blocks define
work_required(m³).assignments (pandas.DataFrame) – Solver assignments (
machine_id,block_id,dayand optionalshift_id,assigned,production) replayed through deterministic playback. May be empty (e.g. a solver run that found no solution); see Notes.
- Returns:
Scalar KPI totals. Volume KPIs are in m³, the units of
Block.work_required:total_productionis the volume delivered by each block’s terminal role andremaining_work_total/staged_productionthe volume still to deliver.- Return type:
Notes
total_productionis always the playbackdelivered_total;remaining_work_totalissum(Block.work_required) - total_production(tracked per block by playback). Both respect the scenario as given, so for a rolling-horizon window with carried-forwardwork_requiredthe total is the reduced volume.Empty plans (no rows, or no row with
assigned > 0) and partial plans are evaluated like any other plan, which gives for an empty plan:total_production = 0,remaining_work_total = staged_production = sum(Block.work_required),completed_blocks = 0;makespan_day = 0andmakespan_shift = "N/A";utilisation_ratio_mean_day/utilisation_ratio_weighted_dayare0(every available scenario day is idle) and the shift/machine/role utilisation keys are absent (there are no worked machine-shifts);sequencing_violation_*counts are0,sequencing_violation_breakdownis"none"andsequencing_clean_blockscounts every harvest-system block (no block has a violation, including blocks the plan never touches);the mobilisation, downtime, and weather keys are absent (they are only emitted when non-zero).
FHOPS ≤ 1.0.0 reported
total_production = sum(Block.work_required)andremaining_work_total = 0for an empty assignment frame (#108).Downtime and weather loss KPIs (emitted only for frames carrying stochastic event columns, e.g. a sample produced by
run_stochastic_playback()) are record-level sums (FHOPS 1.0.1, #116):downtime_production_loss_est— Σ over downtime records of the proposed volume (m³) the downtime event removed from that record (_downtime_lost: the full proposed volume for a cancelled shift,volume × d / shift_hoursfor a partial loss). Records without that column userate(machine, block) × min(downtime_hours / shift_hours, 1).weather_production_loss_est— Σ over weather-hit records of the proposed volume (m³) removed by weather (_weather_lost=volume × severity); without that column,production_units × severity / (1 − severity).weather_hours_est— Σ over weather-hit records ofseverity × shift_hours.
shift_hoursfollowsshift_hours_resolver(). The losses are measured on the proposed production of each machine-shift, before sequencing caps, so they include upstream (non-terminal) volume; they are not the change intotal_production, which also reflects knock-on sequencing effects. FHOPS 1.0.0 estimated both as hours × (delivered terminal volume / recorded hours of every role), which does not match the volume the events actually removed.
- fhops.evaluation.compute_makespan_metrics(problem, shift_df, *, fallback_days=None, fallback_shift_keys=None)[source]
Compute makespan metrics (latest productive day/shift) given playback summaries.
- fhops.evaluation.compute_utilisation_metrics(shift_df, day_df=None)[source]
Compute utilisation KPI metrics from shift/day playback DataFrames.
- fhops.evaluation.day_dataframe(result)[source]
Return the day-level playback summaries as a DataFrame.
- Parameters:
result (PlaybackResult)
- Return type:
DataFrame
- fhops.evaluation.day_dataframe_from_ensemble(ensemble, *, include_base=False)[source]
Concatenate day-level summaries from an ensemble.
- Parameters:
ensemble (EnsembleResult)
include_base (bool)
- Return type:
DataFrame
- fhops.evaluation.export_playback(shift_df, day_df, *, shift_csv=None, day_csv=None, shift_parquet=None, day_parquet=None, summary_md=None)[source]
Write playback summaries to the requested outputs.
- Parameters:
shift_df (DataFrame) – DataFrames produced by
fhops.evaluation.playback.aggregates.shift_dataframe()andday_dataframe().day_df (DataFrame) – DataFrames produced by
fhops.evaluation.playback.aggregates.shift_dataframe()andday_dataframe().shift_csv (Path | None) – Optional paths for CSV exports.
day_csv (Path | None) – Optional paths for CSV exports.
shift_parquet (Path | None) – Optional paths for Parquet exports (requires
pyarroworfastparquet).day_parquet (Path | None) – Optional paths for Parquet exports (requires
pyarroworfastparquet).summary_md (Path | None) – Optional Markdown summary export (produced via
render_markdown_summary()).
- Returns:
Scalar metrics from
playback_summary_metrics()(samples, total production, etc.).- Return type:
- fhops.evaluation.machine_utilisation_summary(shift_df)[source]
Aggregate shift summaries into per-machine utilisation metrics.
- Parameters:
shift_df (DataFrame) – DataFrame produced by
shift_dataframe()or*_from_ensemble; must includetotal_hoursandavailable_hourscolumns.- Returns:
One row per
(sample_id, machine_id)with total/available hours, utilisation ratio, production, mobilisation cost, and constraint violation counts.- Return type:
pandas.DataFrame
- fhops.evaluation.playback_summary_metrics(shift_df, day_df)[source]
Compute scalar summary metrics (production, hours, mobilisation, utilisation).
- fhops.evaluation.render_markdown_summary(shift_df, day_df, metrics=None)[source]
Return a Markdown-formatted playback summary.
- fhops.evaluation.run_playback(problem, assignments, *, config=None, sample_id=0)[source]
Convert solver assignments into playback records and aggregated summaries.
- Parameters:
problem (fhops.scenario.contract.Problem) – Problem wrapping the scenario being replayed (including any
initial_state).assignments (pandas.DataFrame) – Assignment rows accepted by
assignments_to_records(). May be empty (no rows, no columns) or contain onlyassigned <= 0rows.config (PlaybackConfig | None, default=None) – Playback options;
NoneusesPlaybackConfigdefaults.sample_id (int, default=0) – Identifier stamped on the summaries (stochastic playback sample index).
- Returns:
Records, shift/day summaries, the tracker’s
sequencing_debugsnapshot,delivered_totalandremaining_work_total(m³).- Return type:
Notes
An empty plan yields no records and no shift summaries; day summaries still list the available hours of every scenario day (utilisation
0).delivered_totalis0andremaining_work_totalequalssum(Block.work_required)(FHOPS ≤ 1.0.0 reported0remaining for an empty frame, which made a plan with no assignments look complete).
- fhops.evaluation.run_stochastic_playback(problem, assignments, *, sampling_config, events=None)[source]
Run stochastic playback over multiple samples.
- Parameters:
problem (fhops.scenario.contract.Problem) – Problem wrapping the scenario being replayed.
assignments (pandas.DataFrame) – Deterministic assignments (
machine_id,block_id,day, optionalshift_id,assigned,production).sampling_config (SamplingConfig) – Number of samples,
base_seedand per-event configuration.events (Iterable[PlaybackEvent] | None, default=None) – Custom events applied in the given order;
Nonebuilds the enabled default events in the order downtime → weather → landing shocks.
- Returns:
base_result(deterministic playback ofassignments) and onePlaybackSampleper sample.- Return type:
Notes
Sample
iseedsnumpy.random.default_rng(sampling_config.base_seed + i); every event consumes that generator in turn, following the sampling order documented onDowntimeEvent,WeatherEventandLandingShockEvent. Each sample starts from the deterministic baseline production and events compose multiplicatively.
- fhops.evaluation.schedule_to_records(problem, schedule)[source]
Convert a heuristic Schedule plan into playback records.
- Parameters:
problem (Problem)
schedule (object)
- Return type:
- fhops.evaluation.shift_dataframe(result)[source]
Return the shift-level playback summaries as a DataFrame.
- Parameters:
result (PlaybackResult) –
PlaybackResultreturned byfhops.evaluation.playback.core.run_playback().- Return type:
DataFrame
- fhops.evaluation.shift_dataframe_from_ensemble(ensemble, *, include_base=False)[source]
Concatenate shift summaries from a stochastic ensemble.
- Parameters:
ensemble (EnsembleResult) – Result of
fhops.evaluation.playback.stochastic.run_stochastic_playback().include_base (bool) – When
True, include the base deterministic result in addition to samples.
- Return type:
DataFrame
- fhops.evaluation.summarise_days(shift_summaries, availability_map, completed_by_day, sample_id=0)[source]
Aggregate playback results to day-level summaries.
- Parameters:
shift_summaries (Iterable[ShiftSummary]) – Iterable produced by
summarise_shifts().availability_map (dict[tuple[int, str, str], float]) – Same map passed to
summarise_shifts; used to recover day-level availability totals.completed_by_day (dict[int, set[str]]) – Mapping
day -> set(block_id)indicating which blocks completed on each day (consumed for KPI reporting).sample_id (int) – Propagated through stochastic playback.
- Return type:
- fhops.evaluation.summarise_shifts(records, availability_map, *, include_idle=False, sample_id=0)[source]
Aggregate playback records to machine/shift summaries.
- Parameters:
records (Iterable[PlaybackRecord]) – Iterator of
PlaybackRecordinstances produced byassignments_to_records.availability_map (dict[tuple[int, str, str], float]) – Mapping
(day, shift_id, machine_id) -> available_hourscomputed via_compute_shift_availability; used to seed summaries and compute utilisation ratios.include_idle (bool) – When
True, emit summaries for every availability entry even if no work occurred (resulting intotal_hours = 0but preserving the availability baseline).sample_id (int) – Identifier propagated through stochastic playback so downstream aggregations can group rows.
- Return type:
Notes
downtime_hourssums each downtime record’s sampleddowntime_hours(falling back tohours_workedwhen the record carries no sampled value) anddowntime_eventscounts those records.
KPI helpers for FHOPS schedules.
- class fhops.evaluation.metrics.kpis.KPIResult(totals=<factory>, shift_calendar=None, day_calendar=None, sequencing_debug=None)[source]
Bases:
Mapping[str,float|int|str]Structured KPI bundle with optional shift/day calendar attachments.
- Parameters:
- DAY_COLUMNS: ClassVar[tuple[str, ...]] = ('day', 'sample_id', 'production_units', 'total_hours', 'idle_hours', 'mobilisation_cost', 'completed_blocks', 'blackout_conflicts', 'sequencing_violations', 'available_hours', 'utilisation_ratio', 'downtime_hours', 'downtime_events', 'weather_severity_total')
- SHIFT_COLUMNS: ClassVar[tuple[str, ...]] = ('day', 'shift_id', 'machine_id', 'machine_role', 'sample_id', 'production_units', 'total_hours', 'idle_hours', 'mobilisation_cost', 'sequencing_violations', 'blackout_conflicts', 'available_hours', 'utilisation_ratio', 'downtime_hours', 'downtime_events', 'weather_severity_total')
- day_calendar: DataFrame | None
- get(k[, d]) D[k] if k in D, else d. d defaults to None.[source]
- shift_calendar: DataFrame | None
- to_dict()[source]
Return the scalar KPI totals as a plain dictionary.
- fhops.evaluation.metrics.kpis.compute_kpis(pb, assignments)[source]
Compute production, mobilisation, utilisation, and sequencing KPIs from assignments.
- Parameters:
pb (fhops.scenario.contract.Problem) – Problem wrapping the scenario whose blocks define
work_required(m³).assignments (pandas.DataFrame) – Solver assignments (
machine_id,block_id,dayand optionalshift_id,assigned,production) replayed through deterministic playback. May be empty (e.g. a solver run that found no solution); see Notes.
- Returns:
Scalar KPI totals. Volume KPIs are in m³, the units of
Block.work_required:total_productionis the volume delivered by each block’s terminal role andremaining_work_total/staged_productionthe volume still to deliver.- Return type:
Notes
total_productionis always the playbackdelivered_total;remaining_work_totalissum(Block.work_required) - total_production(tracked per block by playback). Both respect the scenario as given, so for a rolling-horizon window with carried-forwardwork_requiredthe total is the reduced volume.Empty plans (no rows, or no row with
assigned > 0) and partial plans are evaluated like any other plan, which gives for an empty plan:total_production = 0,remaining_work_total = staged_production = sum(Block.work_required),completed_blocks = 0;makespan_day = 0andmakespan_shift = "N/A";utilisation_ratio_mean_day/utilisation_ratio_weighted_dayare0(every available scenario day is idle) and the shift/machine/role utilisation keys are absent (there are no worked machine-shifts);sequencing_violation_*counts are0,sequencing_violation_breakdownis"none"andsequencing_clean_blockscounts every harvest-system block (no block has a violation, including blocks the plan never touches);the mobilisation, downtime, and weather keys are absent (they are only emitted when non-zero).
FHOPS ≤ 1.0.0 reported
total_production = sum(Block.work_required)andremaining_work_total = 0for an empty assignment frame (#108).Downtime and weather loss KPIs (emitted only for frames carrying stochastic event columns, e.g. a sample produced by
run_stochastic_playback()) are record-level sums (FHOPS 1.0.1, #116):downtime_production_loss_est— Σ over downtime records of the proposed volume (m³) the downtime event removed from that record (_downtime_lost: the full proposed volume for a cancelled shift,volume × d / shift_hoursfor a partial loss). Records without that column userate(machine, block) × min(downtime_hours / shift_hours, 1).weather_production_loss_est— Σ over weather-hit records of the proposed volume (m³) removed by weather (_weather_lost=volume × severity); without that column,production_units × severity / (1 − severity).weather_hours_est— Σ over weather-hit records ofseverity × shift_hours.
shift_hoursfollowsshift_hours_resolver(). The losses are measured on the proposed production of each machine-shift, before sequencing caps, so they include upstream (non-terminal) volume; they are not the change intotal_production, which also reflects knock-on sequencing effects. FHOPS 1.0.0 estimated both as hours × (delivered terminal volume / recorded hours of every role), which does not match the volume the events actually removed.