rylieweber commited on
Commit
c844e30
·
verified ·
1 Parent(s): 0c8aa0f

Upload salvage_lookup.py

Browse files
Files changed (1) hide show
  1. salvage_lookup.py +155 -0
salvage_lookup.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # salvage_lookup.py — robust salvage class & fraction lookup (Iowa PM 710 tables)
2
+
3
+ from typing import Dict, Optional
4
+ from salvage_table_data import SALVAGE_TABLE
5
+
6
+ # -------- Linear helpers --------
7
+ def _lin(x1, y1, x2, y2, x):
8
+ if x1 == x2:
9
+ return float(y1)
10
+ return float(y1) + (float(y2) - float(y1)) * ((float(x) - float(x1)) / (float(x2) - float(x1)))
11
+
12
+ def _interp_on_sorted(xs, ys, x):
13
+ xs = list(xs); ys = list(ys)
14
+ if len(xs) == 1:
15
+ return float(ys[0])
16
+ if x <= xs[0]:
17
+ return _lin(xs[0], ys[0], xs[1], ys[1], x)
18
+ if x >= xs[-1]:
19
+ return _lin(xs[-2], ys[-2], xs[-1], ys[-1], x)
20
+ for i in range(len(xs) - 1):
21
+ if xs[i] <= x <= xs[i+1]:
22
+ return _lin(xs[i], ys[i], xs[i+1], ys[i+1], x)
23
+ return float(ys[-1])
24
+
25
+ # -------- Table access that tolerates case/spacing --------
26
+ def _get_table_for(mclass: str) -> Optional[Dict]:
27
+ if not mclass:
28
+ return None
29
+ # exact
30
+ t = SALVAGE_TABLE.get(mclass)
31
+ if t is not None:
32
+ return t
33
+ # lower
34
+ t = SALVAGE_TABLE.get(mclass.lower())
35
+ if t is not None:
36
+ return t
37
+ # strip spaces/commas/hyphens variants
38
+ mnorm = "".join(ch for ch in mclass.lower() if ch.isalnum())
39
+ for k in SALVAGE_TABLE.keys():
40
+ knorm = "".join(ch for ch in k.lower() if ch.isalnum())
41
+ if knorm == mnorm:
42
+ return SALVAGE_TABLE[k]
43
+ return None
44
+
45
+ # -------- Public: salvage fraction lookup --------
46
+ def get_salvage_fraction(mclass: Optional[str], annual_hours: float, age_years: float) -> Optional[float]:
47
+ """
48
+ For tractors/combines (banded by annual hours): interpolate across bands, then across age.
49
+ For implements (age-only): ignore annual_hours and interpolate across age.
50
+ Returns fraction of new list price at given age.
51
+ """
52
+ if not mclass:
53
+ return None
54
+
55
+ table = _get_table_for(mclass)
56
+ if table is None:
57
+ return None
58
+
59
+ age = float(int(round(age_years))) # PM 710 ages are integer 1..20
60
+
61
+ # IMPLEMENTS: either {age: frac} or {0:{age:frac}}
62
+ first_val = next(iter(table.values()))
63
+ if isinstance(first_val, (int, float)):
64
+ ages = sorted(table.keys())
65
+ vals = [table[a] for a in ages]
66
+ return float(_interp_on_sorted(ages, vals, age))
67
+ if len(table) == 1 and isinstance(first_val, dict):
68
+ inner = first_val
69
+ ages = sorted(inner.keys())
70
+ vals = [inner[a] for a in ages]
71
+ return float(_interp_on_sorted(ages, vals, age))
72
+
73
+ # TRACTORS / COMBINES: {band_hours: {age: frac}}
74
+ bands = sorted(table.keys())
75
+ def frac_at(band):
76
+ col = table[band]
77
+ ages = sorted(col.keys())
78
+ vals = [col[a] for a in ages]
79
+ return _interp_on_sorted(ages, vals, age)
80
+
81
+ if len(bands) == 1:
82
+ return float(frac_at(bands[0]))
83
+
84
+ # Interpolate across hour bands at the given annual_hours
85
+ if annual_hours <= bands[0]:
86
+ f1, f2 = frac_at(bands[0]), frac_at(bands[1])
87
+ return float(_lin(bands[0], f1, bands[1], f2, annual_hours))
88
+ if annual_hours >= bands[-1]:
89
+ f1, f2 = frac_at(bands[-2]), frac_at(bands[-1])
90
+ return float(_lin(bands[-2], f1, bands[-1], f2, annual_hours))
91
+ for i in range(len(bands) - 1):
92
+ lo, hi = bands[i], bands[i+1]
93
+ if lo <= annual_hours <= hi:
94
+ f_lo, f_hi = frac_at(lo), frac_at(hi)
95
+ return float(_lin(lo, f_lo, hi, f_hi, annual_hours))
96
+
97
+ return None # should not hit
98
+
99
+ # -------- Classifier (one definitive version) --------
100
+ OVERRIDE_MAP = {
101
+ # Your course-specific mapping choice:
102
+ # Treat disks as "plows" so they use the plow salvage column (as you requested).
103
+ "heavy-duty disk": "plows",
104
+ "heavy duty disk": "plows",
105
+ "disk": "plows",
106
+ "disc": "plows",
107
+ }
108
+
109
+ def classify_machine_for_salvage(name: str, hp: Optional[float]) -> Optional[str]:
110
+ """
111
+ Map a free-text equipment string (+ optional hp) to the PM 710 class names used in SALVAGE_TABLE.
112
+ """
113
+ s = (name or "").lower().strip()
114
+
115
+ # 0) explicit overrides first (substring match)
116
+ for key, cls in OVERRIDE_MAP.items():
117
+ if key in s:
118
+ return cls
119
+
120
+ # 1) Tractors (three HP bands)
121
+ if "tractor" in s:
122
+ try:
123
+ h = float(hp) if hp is not None else None
124
+ except Exception:
125
+ h = None
126
+ if h is None:
127
+ return "80-149 hp tractor"
128
+ if h >= 150:
129
+ return "150+ hp tractor"
130
+ if h >= 80:
131
+ return "80-149 hp tractor"
132
+ return "30-79 hp tractor"
133
+
134
+ # 2) Combines / Forage harvesters
135
+ if any(k in s for k in ["combine", "forage harvester", "forage-harvester", "silage harvester"]):
136
+ return "combine/forage harvester"
137
+
138
+ # 3) Implements (Table 1b buckets)
139
+ if any(k in s for k in ["plow", "moldboard"]):
140
+ return "plows"
141
+ if any(k in s for k in ["disk", "disc", "harrow", "chisel", "cultivator", "roller", "mulcher", "rotary hoe", "tiller"]):
142
+ return "other tillage"
143
+ if any(k in s for k in ["planter", "drill", "sprayer", "boom sprayer"]):
144
+ return "planter, drill, sprayer"
145
+ if any(k in s for k in ["mower", "chopper", "windrower"]):
146
+ return "mower, chopper"
147
+ if "baler" in s:
148
+ return "baler"
149
+ if any(k in s for k in ["swather", "rake"]):
150
+ return "swather, rake"
151
+ if any(k in s for k in ["truck", "pickup", "vehicle"]):
152
+ return "vehicle"
153
+
154
+ # 4) Fallback
155
+ return "others"