Eric-Tsai's picture
Upload 36 files
7a3d380 verified
Raw
History Blame Contribute Delete
5.8 kB
"""Central tuning knobs for the routing optimizer.
All times are integer minutes since midnight; all distances are miles.
"""
# ---------------------------------------------------------------------------
# Travel model (placeholder until you plug in a real drive-time source)
# ---------------------------------------------------------------------------
# Straight-line (haversine) miles are multiplied by a road-circuity factor to
# approximate actual driving distance, then converted to minutes at an
# average speed. Swap distance.py's matrix builder for OSRM / a Distance
# Matrix API when you're ready for real drive times.
CIRCUITY_FACTOR = 1.3
AVG_SPEED_MPH = 35.0
# ---------------------------------------------------------------------------
# Google Maps Routes API (used with --distance-source google)
# ---------------------------------------------------------------------------
# TRAFFIC_UNAWARE: typical drive times, 625 elements/request, cheapest.
# TRAFFIC_AWARE: live+predictive traffic, but requires a departureTime per
# request and allows only 100 elements/request — see google_distance.py
# before switching.
GOOGLE_ROUTING_PREFERENCE = "TRAFFIC_UNAWARE"
# ---------------------------------------------------------------------------
# Claim priorities and drop penalties
# ---------------------------------------------------------------------------
# Every claim node is wrapped in a "disjunction": the solver may leave it
# unassigned (dropped -> rescheduled to a later day) by paying the penalty.
# Penalties are in the same units as the objective's arc cost (travel
# minutes), so a penalty of 600 means "spend up to ~10 extra driving hours
# across the fleet before you're allowed to drop this claim".
#
# Priority 1 = MUST be inspected today. Its penalty is set so high that the
# solver will only ever drop it when it is physically impossible to serve
# (no qualified adjuster, or no feasible slot in anyone's shift). If that
# happens the report flags it loudly as a violation so a human can intervene.
PRIORITY_MUST_TODAY = 1
PRIORITY_HIGH = 2
PRIORITY_NORMAL = 3
DROP_PENALTY = {
PRIORITY_MUST_TODAY: 1_000_000,
PRIORITY_HIGH: 3_000,
PRIORITY_NORMAL: 600,
}
PRIORITY_LABEL = {
PRIORITY_MUST_TODAY: "MUST-TODAY",
PRIORITY_HIGH: "high",
PRIORITY_NORMAL: "normal",
}
# SLA-age escalation: a claim's drop penalty grows the longer it has been
# waiting (optional claims.csv column `age_days`, default 0 - so all
# published benchmark numbers are unchanged unless ages are provided).
# MUST-TODAY claims are already at the ceiling and do not escalate.
SLA_ESCALATION_PER_DAY = 0.25
def effective_penalty(priority: int, age_days: int = 0) -> int:
base = DROP_PENALTY[priority]
if priority == PRIORITY_MUST_TODAY:
return base
return int(base * (1 + SLA_ESCALATION_PER_DAY * max(0, age_days)))
def is_eligible(adjuster, claim, home_to_claim_miles: float) -> bool:
"""The single assignment-eligibility rule, shared by every backend:
the adjuster must hold the claim's peril skill AND, if they have a
service territory (max_radius_miles), the claim must lie within it.
Distances are the road-approximate matrix miles, so the same rule
works with haversine and Google-based matrices."""
if claim.peril not in adjuster.skills:
return False
radius = getattr(adjuster, "max_radius_miles", None)
return radius is None or home_to_claim_miles <= radius
# ---------------------------------------------------------------------------
# Optional schedule features (OR-Tools backend; off by default so that
# benchmarks and documented results remain reproducible)
# ---------------------------------------------------------------------------
# 30-minute lunch starting between 11:30 and 13:30.
LUNCH_BREAK = {"duration": 30, "earliest_start": 690, "latest_start": 810}
# Standard appointment slots the call center offers policyholders (and
# the Call Planner re-offers when someone can't make their original
# window). Three 3-hour chunks by default; edit to match your company's
# slot menu (e.g. two half-day windows).
CALL_SLOTS = [(8 * 60, 11 * 60), (11 * 60, 14 * 60), (14 * 60, 17 * 60)]
# Span cost per minute of route duration when workload balancing is on;
# discourages one adjuster carrying a much longer day than the others.
BALANCE_COEFFICIENT = 2
# ---------------------------------------------------------------------------
# Rolling booking horizon (horizon.py): capacity planning knobs for
# assigning backlog claims to days before each day is routed exactly.
# ---------------------------------------------------------------------------
# Planning estimate of drive time consumed per visit (calibrate from
# your fleet's measured mean leg; the per-day routing repair pass
# corrects estimation errors honestly).
HORIZON_TRAVEL_ALLOWANCE_MIN = 40
# Fraction of shift minutes considered bookable when day-assigning
# (the slack absorbs matrix estimation error and the lunch break).
HORIZON_UTILIZATION = 0.9
# ---------------------------------------------------------------------------
# Solver
# ---------------------------------------------------------------------------
SOLVER_TIME_LIMIT_SECONDS = 15
# Max waiting time allowed at a stop (arriving before the policyholder's
# window opens), in minutes.
MAX_WAIT_MINUTES = 240
# Upper bound for the time dimension (minutes since midnight).
TIME_HORIZON_MINUTES = 24 * 60
# ---------------------------------------------------------------------------
# Synthetic data defaults
# ---------------------------------------------------------------------------
PERILS = ["fire", "flood", "wind", "hail"]
# Claims are scattered around the Houston metro area.
REGION_CENTER = (29.76, -95.37)
REGION_SPREAD_DEG = 0.35
DATA_DIR = "data"