jeffrey1963 commited on
Commit
4273a97
·
verified ·
1 Parent(s): 28349a7

Create salvage_lookup.py

Browse files
Files changed (1) hide show
  1. salvage_lookup.py +81 -0
salvage_lookup.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # salvage_lookup.py
2
+ from typing import Dict, Tuple, Optional
3
+ from salvage_table_data import SALVAGE_TABLE
4
+
5
+ def _interp(x: float, x0: float, x1: float, y0: float, y1: float) -> float:
6
+ if x0 == x1:
7
+ return y0
8
+ # linear interpolation (also used for gentle extrapolation)
9
+ return y0 + (y1 - y0) * ((x - x0) / (x1 - x0))
10
+
11
+ def _interp_age(col: Dict[int, float], age: float) -> float:
12
+ """Interpolate salvage over age (1..20). Extrapolate softly outside."""
13
+ if not col:
14
+ return 0.20 # safety
15
+ ages = sorted(col.keys()) # [1..20]
16
+ if age <= ages[0]:
17
+ # extrapolate with slope of first segment
18
+ a0, a1 = ages[0], ages[1]
19
+ return _interp(age, a0, a1, col[a0], col[a1])
20
+ if age >= ages[-1]:
21
+ a0, a1 = ages[-2], ages[-1]
22
+ return _interp(age, a0, a1, col[a0], col[a1])
23
+ # bracket
24
+ for i in range(len(ages) - 1):
25
+ a0, a1 = ages[i], ages[i+1]
26
+ if a0 <= age <= a1:
27
+ return _interp(age, a0, a1, col[a0], col[a1])
28
+ return list(col.values())[-1]
29
+
30
+ def _bands_for(machine_class: str):
31
+ # tractors have {200,400,600}; combines/forage {100,300,500}
32
+ keys = sorted(SALVAGE_TABLE[machine_class].keys())
33
+ return keys
34
+
35
+ def get_salvage_fraction(machine_class: str, annual_hours: float, age_years: float) -> Optional[float]:
36
+ """
37
+ Bilinear interpolation across annual hours band and age.
38
+ Returns salvage as a fraction of new list price (e.g., 0.31).
39
+ """
40
+ if machine_class not in SALVAGE_TABLE:
41
+ return None
42
+ table = SALVAGE_TABLE[machine_class]
43
+ bands = _bands_for(machine_class)
44
+
45
+ # choose bracketing hour bands (soft extrapolation at ends)
46
+ if annual_hours <= bands[0]:
47
+ lo, hi = bands[0], bands[1] if len(bands) > 1 else bands[0]
48
+ elif annual_hours >= bands[-1]:
49
+ lo, hi = bands[-2], bands[-1] if len(bands) > 1 else bands[0]
50
+ else:
51
+ lo, hi = None, None
52
+ for i in range(len(bands) - 1):
53
+ if bands[i] <= annual_hours <= bands[i+1]:
54
+ lo, hi = bands[i], bands[i+1]
55
+ break
56
+ if lo is None: # fallback
57
+ lo, hi = bands[0], bands[-1]
58
+
59
+ # age-interp in each column
60
+ s_lo = _interp_age(table[lo], age_years)
61
+ s_hi = _interp_age(table[hi], age_years)
62
+
63
+ # hour-interp across bands
64
+ salv = _interp(annual_hours, lo, hi, s_lo, s_hi)
65
+ # clamp to [0.05, 0.95] to avoid silly results
66
+ return max(0.05, min(0.95, salv))
67
+
68
+ def classify_machine_for_salvage(machine_name: str, hp: Optional[float]) -> Optional[str]:
69
+ m = (machine_name or "").lower()
70
+ if "combine" in m or "forage" in m:
71
+ return "combine/forage harvester"
72
+ if "tractor" in m:
73
+ # map by hp
74
+ if hp is None:
75
+ return "80-149 hp tractor" # a reasonable default middle class
76
+ if hp < 80:
77
+ return "30-79 hp tractor"
78
+ if hp < 150:
79
+ return "80-149 hp tractor"
80
+ return "150+ hp tractor"
81
+ return None