Skip to content

Allocation Parameters

The allocation engine uses seven distinct parameter types, each stored as a JSONB row in {schema}.allocation_parameter_set. This mirrors the project's existing parameter pattern (forecast_parameter_set, meio_parameter_set) and allows per-segment overrides with independent versioning per concern.


Parameter Types at a Glance

parameter_type Controls One or many per scenario
allocation_priority Priority scores per class + business weight multipliers one global row
allocation_starvation Anti-starvation bonus and priority escalation one global row
allocation_reservation Future supply protection fences one global row
allocation_fairness Proportional share enforcement across demand groups one global row
allocation_routing Supply source filtering (hard block or soft penalty) many — demands reference by routing_policy_id
allocation_aging Score bonus curve for overdue demands many — demands reference by aging_policy_id
allocation_lateness Score penalty curve for late supply many — demands reference by lateness_curve_id

Resolution Order

For global types (priority, starvation, reservation, fairness):

  1. Look for a row linked to the demand's segment via allocation_parameter_set_segment
  2. Fall back to the row where is_default = TRUE for this scenario

For policy types (routing, aging, lateness): all rows for the scenario are loaded into the engine's HashMap; each demand references the right policy by its ID field at scoring time.

The resolved configuration snapshot is stored verbatim in allocation_run.resolved_config for full auditability and replay.


Scoring Formula

Each feasible (demand, supply) candidate receives a composite score:

total_score =
    priority_component        ← PriorityScores lookup (with optional starvation escalation)
  + aging_component           ← AgingPolicy curve + starvation bonus
  + lateness_component        ← LatenessCurve (supply arrival delay relative to demand due date)
  + strategic_component       ← demand.strategic_weight × strategic_weight_factor
  + revenue_component         ← demand.revenue_weight   × revenue_weight_factor
  + criticality_component     ← demand.criticality_weight × criticality_weight_factor
  − routing_penalty           ← RoutingPolicy soft violation
  − fairness_penalty          ← FairnessTracker over-share
  − reservation_penalty       ← ReservationEngine protection

Candidates are sorted highest score first; ties are broken deterministically by (demand_id, supply_id). The greedy pass then allocates supply in that order.


allocation_priority

Controls the base score for each priority class and the global multipliers for the three business weight dimensions.

{
  "priority_scores": {
    "aog":      10000,
    "critical":  5000,
    "premium":   2000,
    "standard":  1000,
    "low":        100
  },
  "strategic_weight_factor":   1.0,
  "revenue_weight_factor":     1.0,
  "criticality_weight_factor": 1.0
}

Fields

Field Type Default Description
priority_scores.aog float 10 000 Score for AOG (Aircraft on Ground) demands — highest urgency
priority_scores.critical float 5 000 Score for Critical demands
priority_scores.premium float 2 000 Score for Premium demands
priority_scores.standard float 1 000 Score for Standard demands
priority_scores.low float 100 Score for Low-priority demands
strategic_weight_factor float 1.0 Global multiplier on demand.strategic_weight
revenue_weight_factor float 1.0 Global multiplier on demand.revenue_weight
criticality_weight_factor float 1.0 Global multiplier on demand.criticality_weight

Priority class values (serde)

The Rust enum uses SCREAMING_SNAKE_CASE serialization: "AOG", "CRITICAL", "PREMIUM", "STANDARD", "LOW".

Segment use-case

Give an MRO-critical segment a higher strategic_weight_factor to amplify the strategic importance already encoded on each demand without changing the global priority scores.


allocation_starvation

Prevents low-priority demands from being perpetually starved by higher-priority ones. Once a demand has waited beyond escalation_threshold_days, it receives an urgency bonus and can optionally be promoted one priority level.

{
  "enabled": true,
  "escalation_threshold_days": 14,
  "score_per_extra_day":       50.0,
  "max_starvation_bonus":    3000.0,
  "auto_upgrade_priority":     false
}

Fields

Field Type Default Description
enabled bool true Enable/disable anti-starvation entirely
escalation_threshold_days int 14 Days overdue before starvation bonus activates
score_per_extra_day float 50.0 Bonus score per day beyond the threshold
max_starvation_bonus float 3 000 Cap on total bonus (0 = uncapped)
auto_upgrade_priority bool false When true, promote the demand one PriorityClass level

Starvation bonus formula

excess_days   = max(0, backlog_days − escalation_threshold_days)
bonus         = min(excess_days × score_per_extra_day, max_starvation_bonus)

Segment use-case

AOG-prone segments might use escalation_threshold_days: 7 (escalate faster) while standard segments keep the default of 14.


allocation_reservation

Protects supply for future high-priority demands arriving within a lookahead window. Lower-priority demands that attempt to consume reserved supply incur a score penalty.

{
  "enabled": true,
  "lookahead_days":          14,
  "min_protected_priority":  "PREMIUM",
  "penalty":               2000.0,
  "safety_margin":            0.10
}

Fields

Field Type Default Description
enabled bool Enable/disable reservation fencing
lookahead_days int 14 How far ahead to scan for demands that need protection
min_protected_priority string "PREMIUM" Protect supply for demands at or above this priority class
penalty float 2 000 Score penalty for lower-priority demands targeting reserved supply
safety_margin float 0.10 Extra fraction reserved on top of raw demand quantity (10% = 1.1×)

How fences work

The ReservationEngine scans all future demands within lookahead_days. For each demand at or above min_protected_priority, it creates a fence on the matching supply (same item_id + location_id), reserving qty × (1 + safety_margin).

During the greedy pass, reserved quantities are subtracted from the available supply pool upfront. Lower-priority demands can still allocate against fenced supply but receive penalty deducted from their score.

Penalty is soft

Reservation is a score penalty, not a hard block. An extremely high-priority starved demand can still win against a fence if its overall score is high enough.


allocation_fairness

Enforces proportional allocation across demand groups (channel, customer class, program, etc.) to prevent any single group from capturing a disproportionate share of scarce supply.

{
  "enabled": true,
  "group_field":   "channel",
  "target_shares": { "retail": 0.40, "wholesale": 0.35, "direct": 0.25 },
  "tolerance":     0.05,
  "penalty_per_over_unit": 50.0,
  "hard_cap": false
}

Fields

Field Type Default Description
enabled bool Enable/disable fairness enforcement
group_field string "channel" Demand field defining the group: channel, customer_class, program, region
target_shares object {} Target weight per group. Empty = equal shares across all groups
tolerance float 0.10 Maximum overshoot before penalty activates (0.05 = 5%)
penalty_per_over_unit float 50.0 Score penalty per unit when a group exceeds its share
hard_cap bool false When true, fully block a group once it exceeds target_share + tolerance

Penalty formula

actual_share  = group_allocated / total_allocated
overshoot     = max(0, actual_share − target_share − tolerance)
penalty       = penalty_per_over_unit × overshoot × proposed_qty

Segment use-case

Use hard_cap: true for regulated industries where proportional distribution is legally required. Use soft penalties (hard_cap: false) for commercial fairness guidelines where some deviation is acceptable.


allocation_routing (one row per policy)

Controls which supply sources a demand is permitted or preferred to use. Assigned to demands via routing_policy_id.

{
  "policy_id":               "domestic_only",
  "hard_mode":               true,
  "allowed_locations":       ["WH_US_EAST", "WH_US_WEST"],
  "forbidden_pools":         ["quarantine"],
  "routing_violation_penalty": 100.0
}

Fields

Field Type Description
policy_id string Unique identifier referenced by Demand.routing_policy_id
hard_mode bool true = routing violations make the candidate infeasible (filtered in Pass 5); false = soft penalty only
allowed_locations array Supply must be at one of these locations (empty = all allowed)
forbidden_pools array Supply must not be in any of these inventory pools
routing_violation_penalty float Score penalty per soft violation (default 100.0)

Hard vs soft mode

  • Hard mode (hard_mode: true): the candidate is removed during Pass 5 (filter hard constraints). The demand will be reported as unfulfilled with reason "All supply candidates blocked by hard routing/pool constraints".
  • Soft mode (hard_mode: false): the candidate survives but routing_violation_penalty is deducted from its score. It may still be allocated if no better option exists.

allocation_aging (one row per policy)

Controls how urgency accumulates as a demand ages overdue. Assigned to demands via aging_policy_id.

{
  "policy_id":  "linear_10",
  "mode":       "linear",
  "multiplier": 10.0,
  "exponent":   1.0
}

Fields

Field Type Description
policy_id string Unique identifier referenced by Demand.aging_policy_id
mode string none, linear, or exponential
multiplier float Scale factor
exponent float Exponent for exponential mode (ignored for linear/none)

Mode formulas

Mode Score contribution
none 0
linear multiplier × backlog_days
exponential multiplier × backlog_days ^ exponent

The aging component is added to the starvation bonus before being included in total_score. A demand with no aging_policy_id uses the default_aging_policy_id from AllocationConfig if one is set, otherwise contributes 0.


allocation_lateness (one row per curve)

Penalizes supply that arrives later than the demand's due date. Used to prefer earlier supply when multiple options are available for the same demand. Assigned to demands via lateness_curve_id.

{
  "curve_id":    "standard_lateness",
  "breakpoints": [[0, 0], [5, 10], [10, 50], [20, 500], [30, 2000]]
}

Fields

Field Type Description
curve_id string Unique identifier referenced by Demand.lateness_curve_id
breakpoints array of [days, penalty] Piecewise-linear curve. Linear interpolation between points; flat extrapolation beyond the last.

How lateness is computed

delay_days = max(0, supply.available_date − demand.required_date)
penalty    = curve.interpolate(delay_days)

The lateness_component in ScoreBreakdown is this penalty subtracted from the total score, making earlier supply more attractive relative to late supply.

A demand with no lateness_curve_id uses default_lateness_curve_id from AllocationConfig if set, otherwise contributes 0.


Segment Overrides

Any global parameter type can be overridden per segment by creating an additional row linked to the segment via allocation_parameter_set_segment:

INSERT INTO {schema}.allocation_parameter_set
    (scenario_id, parameter_type, name, is_default, params)
VALUES
    (1, 'allocation_starvation', 'Aggressive starvation — AOG segment', false,
     '{"enabled":true,"escalation_threshold_days":7,"score_per_extra_day":100,"max_starvation_bonus":5000,"auto_upgrade_priority":true}')
RETURNING id;

INSERT INTO {schema}.allocation_parameter_set_segment (parameter_set_id, segment_id)
VALUES (<returned_id>, <aog_segment_id>);

Resolution walks: segment-specific row → scenario default (is_default = TRUE).


Seeding via Python

import json, psycopg2

conn = psycopg2.connect(...)
cur  = conn.cursor()
SCENARIO_ID = 1

def seed(param_type, name, params, is_default=True):
    cur.execute(
        """INSERT INTO scenario.allocation_parameter_set
               (scenario_id, parameter_type, name, is_default, params)
           VALUES (%s, %s, %s, %s, %s)""",
        (SCENARIO_ID, param_type, name, is_default, json.dumps(params))
    )

# Global types
seed("allocation_priority",    "Default priority",    {"priority_scores": {"aog":10000,"critical":5000,"premium":2000,"standard":1000,"low":100},"strategic_weight_factor":1.0,"revenue_weight_factor":1.0,"criticality_weight_factor":1.0})
seed("allocation_starvation",  "Default starvation",  {"enabled":True,"escalation_threshold_days":14,"score_per_extra_day":50.0,"max_starvation_bonus":3000.0,"auto_upgrade_priority":False})
seed("allocation_reservation", "Default reservation", {"enabled":True,"lookahead_days":14,"min_protected_priority":"PREMIUM","penalty":2000.0,"safety_margin":0.10})
seed("allocation_fairness",    "Default fairness",    {"enabled":False,"group_field":"channel","target_shares":{},"tolerance":0.10,"penalty_per_over_unit":50.0,"hard_cap":False})

# Policy types (is_default=False — demands ref by ID)
seed("allocation_routing",  "Domestic only",     {"policy_id":"domestic_only","hard_mode":True,"allowed_locations":["WH_US_EAST","WH_US_WEST"],"forbidden_pools":["quarantine"],"routing_violation_penalty":100.0}, is_default=False)
seed("allocation_aging",    "Linear ×10",        {"policy_id":"linear_10","mode":"linear","multiplier":10.0,"exponent":1.0}, is_default=False)
seed("allocation_lateness", "Standard lateness", {"curve_id":"standard_lateness","breakpoints":[[0,0],[5,10],[10,50],[20,500],[30,2000]]}, is_default=False)

conn.commit()