Spaces:
Sleeping
Sleeping
| # 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}'" | |