File size: 10,859 Bytes
0c8aa0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# repair_table_data.py
# Full Iowa State "Accumulated Repair Costs as a Percentage of New List Price" (Table 3)
# Source figure: American Society of Agricultural Engineers (1996).
# Provides: REPAIR_TABLE, get_repair_fraction(machine, hours), list_machines()

from typing import Dict, Tuple, Optional, List
import re

# ----------------------------
# 1) Canonical table (fractions, not percents)
# ----------------------------
REPAIR_TABLE: Dict[str, Dict[int, float]] = {
    # --- Tractors ---
    "Two-wheel drive tractor": {
        1000: 0.01, 2000: 0.03, 3000: 0.06, 4000: 0.11, 5000: 0.18,
        6000: 0.25, 7000: 0.34, 8000: 0.45, 9000: 0.57, 10000: 0.70
    },
    "Four-wheel drive tractor": {
        1000: 0.00, 2000: 0.01, 3000: 0.03, 4000: 0.05, 5000: 0.08,
        6000: 0.11, 7000: 0.15, 8000: 0.19, 9000: 0.24, 10000: 0.30
    },

    # --- Tillage Implements ---
    "Moldboard plow": {200:0.02, 400:0.06, 600:0.12, 800:0.19, 1000:0.29,
                       1200:0.40, 1400:0.53, 1600:0.68, 1800:0.84, 2000:1.01},
    "Heavy-duty disk": {200:0.01, 400:0.04, 600:0.08, 800:0.12, 1000:0.18,
                        1200:0.25, 1400:0.32, 1600:0.40, 1800:0.49, 2000:0.58},
    "Tandem disk": {200:0.01, 400:0.04, 600:0.08, 800:0.12, 1000:0.18,
                    1200:0.25, 1400:0.32, 1600:0.40, 1800:0.49, 2000:0.58},
    "Chisel plow": {200:0.03, 400:0.08, 600:0.14, 800:0.20, 1000:0.28,
                    1200:0.36, 1400:0.45, 1600:0.54, 1800:0.64, 2000:0.74},
    "Field cultivator": {200:0.03, 400:0.07, 600:0.13, 800:0.20, 1000:0.27,
                         1200:0.35, 1400:0.43, 1600:0.52, 1800:0.61, 2000:0.71},
    "Harrow": {200:0.02, 400:0.05, 600:0.08, 800:0.12, 1000:0.16,
               1200:0.22, 1400:0.29, 1600:0.37, 1800:0.46, 2000:0.56},
    "Roller-packer, mulcher": {200:0.02, 400:0.05, 600:0.08, 800:0.12, 1000:0.16,
                               1200:0.22, 1400:0.29, 1600:0.37, 1800:0.46, 2000:0.56},
    "Rotary hoe": {200:0.02, 400:0.06, 600:0.11, 800:0.17, 1000:0.23,
                   1200:0.29, 1400:0.37, 1600:0.44, 1800:0.52, 2000:0.60},
    "Row crop cultivator": {200:0.00, 400:0.02, 600:0.06, 800:0.10, 1000:0.17,
                            1200:0.25, 1400:0.36, 1600:0.48, 1800:0.62, 2000:0.78},

    # --- Harvesters & Hay ---
    "Corn picker": {200:0.00, 400:0.02, 600:0.04, 800:0.08, 1000:0.14,
                    1200:0.21, 1400:0.30, 1600:0.41, 1800:0.54, 2000:0.69},
    "Combine (pull)": {200:0.00, 400:0.02, 600:0.04, 800:0.08, 1000:0.14,
                       1200:0.21, 1400:0.30, 1600:0.41, 1800:0.54, 2000:0.69},
    "Potato harvester": {200:0.02, 400:0.05, 600:0.09, 800:0.14, 1000:0.19,
                         1200:0.25, 1400:0.32, 1600:0.39, 1800:0.46, 2000:0.54},
    "Mower-conditioner": {200:0.01, 400:0.04, 600:0.08, 800:0.13, 1000:0.18,
                          1200:0.24, 1400:0.31, 1600:0.38, 1800:0.46, 2000:0.55},
    "Mower-conditioner (rotary)": {200:0.01, 400:0.04, 600:0.08, 800:0.13, 1000:0.18,
                                   1200:0.24, 1400:0.31, 1600:0.38, 1800:0.46, 2000:0.55},
    "Rake": {200:0.02, 400:0.05, 600:0.09, 800:0.12, 1000:0.16,
             1200:0.22, 1400:0.27, 1600:0.33, 1800:0.39, 2000:0.45},
    "Rectangular baler": {200:0.01, 400:0.03, 600:0.06, 800:0.10, 1000:0.15,
                          1200:0.20, 1400:0.26, 1600:0.32, 1800:0.38, 2000:0.45},
    "Large square baler": {200:0.01, 400:0.03, 600:0.06, 800:0.10, 1000:0.15,
                           1200:0.20, 1400:0.26, 1600:0.32, 1800:0.38, 2000:0.45},
    "Forage harvester (pull)": {200:0.01, 400:0.03, 600:0.07, 800:0.10, 1000:0.15,
                                1200:0.20, 1400:0.26, 1600:0.32, 1800:0.38, 2000:0.45},

    "Forage harvester (SP)": {300:0.00, 600:0.01, 900:0.02, 1200:0.04, 1500:0.07,
                              1800:0.10, 2100:0.13, 2400:0.17, 2700:0.22, 3000:0.27},
    "Combine (SP)": {300:0.00, 600:0.01, 900:0.02, 1200:0.04, 1500:0.07,
                     1800:0.10, 2100:0.13, 2400:0.17, 2700:0.22, 3000:0.27},
    "Windrower (SP)": {300:0.01, 600:0.02, 900:0.04, 1200:0.09, 1500:0.15,
                       1800:0.23, 2100:0.32, 2400:0.42, 2700:0.53, 3000:0.66},
    "Cotton picker (SP)": {300:0.01, 600:0.04, 900:0.09, 1200:0.15, 1500:0.23,
                           1800:0.32, 2100:0.42, 2400:0.53, 2700:0.66, 3000:0.79},

    # --- Other Implements / Misc ---
    "Mower (sickle)": {100:0.01, 200:0.03, 300:0.06, 400:0.10, 500:0.14,
                       600:0.19, 700:0.25, 800:0.31, 900:0.38, 1000:0.46},
    "Mower (rotary)": {100:0.00, 200:0.02, 300:0.04, 400:0.07, 500:0.11,
                       600:0.16, 700:0.22, 800:0.28, 900:0.36, 1000:0.44},
    "Large round baler": {100:0.01, 200:0.03, 300:0.06, 400:0.10, 500:0.15,
                          600:0.20, 700:0.26, 800:0.33, 900:0.40, 1000:0.48},
    "Sugar beet harvester": {100:0.03, 200:0.07, 300:0.12, 400:0.18, 500:0.24,
                             600:0.30, 700:0.37, 800:0.44, 900:0.51, 1000:0.59},
    "Rotary tiller": {100:0.01, 200:0.03, 300:0.06, 400:0.09, 500:0.13,
                      600:0.17, 700:0.22, 800:0.27, 900:0.33, 1000:0.40},
    "Row crop planter": {100:0.00, 200:0.01, 300:0.03, 400:0.05, 500:0.07,
                         600:0.09, 700:0.11, 800:0.15, 900:0.20, 1000:0.26},
    "Grain drill": {100:0.01, 200:0.03, 300:0.06, 400:0.09, 500:0.13,
                    600:0.19, 700:0.26, 800:0.32, 900:0.40, 1000:0.47},
    "Fertilizer spreader": {100:0.03, 200:0.08, 300:0.13, 400:0.19, 500:0.26,
                            600:0.32, 700:0.40, 800:0.47, 900:0.55, 1000:0.63},

    # --- Sprayers & Wagons ---
    "Boom-type sprayer": {200:0.05, 400:0.12, 600:0.21, 800:0.31, 1000:0.41,
                          1200:0.52, 1400:0.63, 1600:0.76, 1800:0.88, 2000:1.01},
    "Air-carrier sprayer": {200:0.02, 400:0.05, 600:0.09, 800:0.14, 1000:0.20,
                            1200:0.27, 1400:0.34, 1600:0.42, 1800:0.51, 2000:0.61},
    "Bean puller-windrower": {200:0.02, 400:0.05, 600:0.09, 800:0.14, 1000:0.20,
                              1200:0.27, 1400:0.34, 1600:0.42, 1800:0.51, 2000:0.61},
    "Stalk chopper": {200:0.03, 400:0.08, 600:0.14, 800:0.20, 1000:0.28,
                      1200:0.36, 1400:0.45, 1600:0.54, 1800:0.64, 2000:0.74},
    "Forage blower": {200:0.01, 400:0.04, 600:0.09, 800:0.15, 1000:0.22,
                      1200:0.29, 1400:0.37, 1600:0.46, 1800:0.56, 2000:0.67},
    "Wagon": {200:0.01, 400:0.04, 600:0.07, 800:0.11, 1000:0.16,
              1200:0.21, 1400:0.27, 1600:0.34, 1800:0.41, 2000:0.49},
    "Forage wagon": {200:0.02, 400:0.06, 600:0.10, 800:0.14, 1000:0.19,
                     1200:0.24, 1400:0.29, 1600:0.35, 1800:0.41, 2000:0.47},
}

# ----------------------------
# 2) Name normalization / synonyms
# ----------------------------
_CANON = {k.lower(): k for k in REPAIR_TABLE.keys()}

_SYNONYMS = {
    # tractors
    "2wd": "Two-wheel drive tractor",
    "two wheel drive": "Two-wheel drive tractor",
    "two-wheel drive": "Two-wheel drive tractor",
    "two wheel drive tractor": "Two-wheel drive tractor",

    "4wd": "Four-wheel drive tractor",
    "four wheel drive": "Four-wheel drive tractor",
    "four-wheel drive": "Four-wheel drive tractor",
    "four wheel drive tractor": "Four-wheel drive tractor",

    # common short names
    "tractor": "Two-wheel drive tractor",  # default if just "tractor"
    "sp forage harvester": "Forage harvester (SP)",
    "sp combine": "Combine (SP)",
    "sp windrower": "Windrower (SP)",
}

# --- add these course-specific aliases (repair table name mapping) ---
_SYNONYMS.update({
    # fertilizer equipment
    "fertilizer sprayer": "Fertilizer spreader",
    "fert sprayer": "Fertilizer spreader",
    "fert spreader": "Fertilizer spreader",
    "fertiliser sprayer": "Fertilizer spreader",
    "fertiliser spreader": "Fertilizer spreader",

    # disks
    "offset disk": "Heavy-duty disk",
    "offset disc": "Heavy-duty disk",
    "hd disk": "Heavy-duty disk",
    "heavy duty disk": "Heavy-duty disk",
    "heavy-duty disc": "Heavy-duty disk",

    # grain drill
    "drill seeder": "Grain drill",
    "drill": "Grain drill",
    "seeder": "Grain drill",
})


def _normalize_name(s: str) -> Optional[str]:
    key = re.sub(r"\s+", " ", s.strip().lower())
    if key in _CANON:
        return _CANON[key]
    if key in _SYNONYMS:
        return _SYNONYMS[key]
    # try loose contains
    for alias, canon in _SYNONYMS.items():
        if alias in key:
            return canon
    # last resort: pick a canonical that fully appears
    for canon_lower, canon in _CANON.items():
        if canon_lower in key:
            return canon
    return None

# ----------------------------
# 3) Interpolation / extrapolation
# ----------------------------
def _sorted_points(machine: str) -> List[Tuple[int, float]]:
    pts = list(REPAIR_TABLE[machine].items())
    pts.sort(key=lambda x: x[0])
    return pts

def _interp(x0: float, y0: float, x1: float, y1: float, x: float) -> float:
    if x1 == x0:
        return y0
    return y0 + (y1 - y0) * ((x - x0) / (x1 - x0))

def get_repair_fraction(machine_name: str, hours: float) -> Tuple[float, str, Tuple[int, float], Tuple[int, float]]:
    """
    Returns:
      fraction (0..1+), canonical_machine_name, (x_lo,y_lo), (x_hi,y_hi)
    Uses linear interpolation between nearest table points. Extrapolates gently
    beyond edges (linear using last two points).
    """
    if hours < 0:
        hours = 0.0

    canon = _normalize_name(machine_name) or "Two-wheel drive tractor"
    pts = _sorted_points(canon)
    xs = [p[0] for p in pts]

    # below range
    if hours <= xs[0]:
        x0, y0 = pts[0]
        x1, y1 = pts[1]
        y = _interp(x0, y0, x1, y1, hours)
        return max(0.0, y), canon, (x0, y0), (x1, y1)

    # above range
    if hours >= xs[-1]:
        x0, y0 = pts[-2]
        x1, y1 = pts[-1]
        y = _interp(x0, y0, x1, y1, hours)
        return max(0.0, y), canon, (x0, y0), (x1, y1)

    # interior: find bracketing points
    for i in range(1, len(pts)):
        x0, y0 = pts[i-1]
        x1, y1 = pts[i]
        if x0 <= hours <= x1:
            y = _interp(x0, y0, x1, y1, hours)
            return max(0.0, y), canon, (x0, y0), (x1, y1)

    # should not reach here
    x0, y0 = pts[0]
    x1, y1 = pts[1]
    return y0, canon, (x0, y0), (x1, y1)

# ----------------------------
# 4) Utilities
# ----------------------------
def list_machines() -> List[str]:
    """Return the list of canonical machine names available in the table."""
    return sorted(REPAIR_TABLE.keys())

def describe_machine_match(name: str) -> str:
    """Debug helper: shows which canonical name a user string maps to."""
    c = _normalize_name(name)
    if not c:
        return f"No match for '{name}'. Known: {', '.join(list_machines()[:8])}…"
    return f"'{name}' → '{c}'"