| |
| |
| |
| |
|
|
| from typing import Dict, Tuple, Optional, List |
| import re |
|
|
| |
| |
| |
| REPAIR_TABLE: Dict[str, Dict[int, float]] = { |
| |
| "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 |
| }, |
|
|
| |
| "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}, |
|
|
| |
| "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}, |
|
|
| |
| "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}, |
|
|
| |
| "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}, |
| } |
|
|
| |
| |
| |
| _CANON = {k.lower(): k for k in REPAIR_TABLE.keys()} |
|
|
| _SYNONYMS = { |
| |
| "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", |
|
|
| |
| "tractor": "Two-wheel drive tractor", |
| "sp forage harvester": "Forage harvester (SP)", |
| "sp combine": "Combine (SP)", |
| "sp windrower": "Windrower (SP)", |
| } |
|
|
| |
| _SYNONYMS.update({ |
| |
| "fertilizer sprayer": "Fertilizer spreader", |
| "fert sprayer": "Fertilizer spreader", |
| "fert spreader": "Fertilizer spreader", |
| "fertiliser sprayer": "Fertilizer spreader", |
| "fertiliser spreader": "Fertilizer spreader", |
|
|
| |
| "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", |
|
|
| |
| "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] |
| |
| for alias, canon in _SYNONYMS.items(): |
| if alias in key: |
| return canon |
| |
| for canon_lower, canon in _CANON.items(): |
| if canon_lower in key: |
| return canon |
| return None |
|
|
| |
| |
| |
| 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] |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| x0, y0 = pts[0] |
| x1, y1 = pts[1] |
| return y0, canon, (x0, y0), (x1, y1) |
|
|
| |
| |
| |
| 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}'" |
|
|