"""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"