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):
- Look for a row linked to the demand's segment via
allocation_parameter_set_segment - Fall back to the row where
is_default = TRUEfor 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 butrouting_violation_penaltyis 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.
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()
Related Pages¶
- Allocation Engine Internals — detailed algorithm walkthrough, all Rust structs, CH output tables
- ALLOCATION_ENGINE.md — quick-reference card
- User Guide — Allocation — UI walkthrough