pakfit-backend / backend /engine.py
Ham-462's picture
initial-deploy
bb29672
Raw
History Blame Contribute Delete
10.4 kB
# ─────────────────────────────────────────────────────────────────────────────
# PakFit Engine v1
# Weighted, garment-aware fit scoring algorithm for Pakistani Eastern wear
# Author: [Your Name]
# ─────────────────────────────────────────────────────────────────────────────
# ── Measurement weights per garment type ─────────────────────────────────────
# Based on J. and Almirah published size charts
# Weights reflect how hard each measurement is to fix after stitching
WEIGHTS = {
"Kameez": {
"chest": 0.35, # Most important β€” cannot fix after stitching
"shoulder": 0.20, # Defines silhouette β€” very hard to fix
"sleeve": 0.18, # Short sleeves cannot be extended
"collar": 0.15, # Visible immediately β€” hard to adjust
"length": 0.12, # Tailor can shorten cheaply
},
"Kurta": {
"chest": 0.35,
"shoulder": 0.20,
"sleeve": 0.18,
"collar": 0.15,
"length": 0.12,
},
"Waistcoat": {
"chest": 0.42, # No sleeves β€” chest weight increased
"shoulder": 0.28, # No sleeves β€” shoulder weight increased
"length": 0.30, # Length carries remaining weight
},
"Shalwar": {
"waist": 0.60, # Primary measurement for Shalwar
"length": 0.40, # Confirmed from J. and Almirah charts
# No chest, hip, or thigh β€” brands do not publish these
},
}
# ── Tolerance thresholds (inches) ────────────────────────────────────────────
# Maximum difference at which a measurement still scores above zero
TOLERANCES = {
"chest": 6.0,
"shoulder": 3.0,
"sleeve": 2.5,
"collar": 2.0,
"length": 4.0,
"waist": 5.0,
}
# ── Directional sleeve penalty ────────────────────────────────────────────────
# Short sleeves are worse than long β€” cannot extend after stitching
SLEEVE_SHORT_THRESHOLD = 0.75 # inches shorter than buyer arm
SLEEVE_SHORT_PENALTY = 8.0 # points deducted from FitScore
# ── Shalwar length formula ────────────────────────────────────────────────────
# Calculated from buyer height β€” buyer never needs to measure this
SHALWAR_LENGTH_RATIO = 0.595 # height_cm Γ— 0.595 = ideal length in inches
SHALWAR_STYLE_ADJUSTMENTS = {
"traditional": 1.0, # Falls to ankle β€” add 1 inch
"churidar": 2.0, # Bunches at ankle β€” add 2 inches
"trouser": 0.0, # Exact ankle length
}
# ─────────────────────────────────────────────────────────────────────────────
# CORE FUNCTIONS
# ─────────────────────────────────────────────────────────────────────────────
def calculate_shalwar_length(height_cm, style="traditional"):
"""
Calculate ideal Shalwar length from buyer height.
Buyer never needs to measure Shalwar length separately.
"""
height_inches = height_cm / 2.54
base_length = height_inches * SHALWAR_LENGTH_RATIO
adjustment = SHALWAR_STYLE_ADJUSTMENTS.get(style.lower(), 1.0)
return round(base_length + adjustment, 1)
def match_score(body_val, garment_val, measurement):
"""
Compute how closely a garment measurement matches a buyer body measurement.
Returns a score between 0 and 1.
1.0 = perfect match
0.0 = difference equals or exceeds tolerance threshold
No ease is added β€” Pakistani brand charts already include manufacturing ease.
"""
tolerance = TOLERANCES.get(measurement, 4.0)
difference = abs(body_val - garment_val)
score = max(0.0, 1.0 - (difference / tolerance))
return score
def apply_directional_penalty(buyer, garment_row, base_score):
"""
Apply directional sleeve penalty.
Short sleeves on a Kameez cannot be extended β€” penalise more heavily.
Long sleeves can be rolled up β€” no penalty.
"""
penalty = 0.0
buyer_sleeve = buyer.get("sleeve", None)
garment_sleeve = garment_row.get("sleeve", None)
if buyer_sleeve and garment_sleeve:
shortfall = buyer_sleeve - garment_sleeve
if shortfall > SLEEVE_SHORT_THRESHOLD:
penalty += SLEEVE_SHORT_PENALTY
return base_score - penalty
def fit_score(buyer, garment_row, garment_type):
"""
Compute FitScore for one size option.
Compares buyer body measurements to garment published measurements.
Returns score from 0 to 100.
"""
weights = WEIGHTS.get(garment_type, WEIGHTS["Kameez"])
raw_score = 0.0
for measurement, weight in weights.items():
buyer_val = buyer.get(measurement)
garment_val = garment_row.get(measurement)
# Skip if either value is missing
if buyer_val is None or garment_val is None:
continue
raw_score += weight * match_score(buyer_val, garment_val, measurement)
# Convert to 0-100 scale
score_100 = raw_score * 100.0
# Apply directional sleeve penalty for Kameez and Kurta
if garment_type in ["Kameez", "Kurta"]:
score_100 = apply_directional_penalty(buyer, garment_row, score_100)
return round(max(0.0, score_100), 1)
def apply_fit_preference_tiebreaker(results, fit_pref, garment_type):
"""
Fit preference tiebreaker β€” only applied when two sizes score within 5 points.
Fit preference does NOT add inches to measurements.
It only determines which direction to lean when sizes are nearly equal.
slim β†’ prefer smaller garment
loose β†’ prefer larger garment
regular β†’ prefer closest absolute match (default)
"""
if len(results) < 2:
return results
top_score = results[0]["score"]
second_score = results[1]["score"]
# Only apply tiebreaker if scores are within 5 points
if abs(top_score - second_score) > 5.0:
return results
# Primary measurement to use for tiebreaker
primary = "chest" if garment_type != "Shalwar" else "waist"
if fit_pref == "slim":
# Prefer smaller garment
results.sort(key=lambda x: x["row"].get(primary, 99))
elif fit_pref == "loose":
# Prefer larger garment
results.sort(key=lambda x: -x["row"].get(primary, 0))
# regular = keep current order (closest absolute match already at top)
return results
def recommend_size(buyer_measurements, size_chart, garment_type, fit_pref="regular"):
"""
Main recommendation function.
Takes buyer body measurements and brand size chart data.
Returns ranked list of sizes with FitScores.
buyer_measurements: dict with keys like chest, waist, shoulder, sleeve, collar, length
size_chart: list of dicts, each representing one size row from the brand chart
garment_type: "Kameez", "Shalwar", "Waistcoat", "Kurta"
fit_pref: "slim", "regular", or "loose"
"""
if not size_chart:
return []
results = []
for row in size_chart:
score = fit_score(buyer_measurements, row, garment_type)
results.append({
"size": row.get("size_label", "Unknown"),
"score": score,
"confidence": f"{score:.0f}%",
"row": row,
})
# Sort by score β€” highest first
results.sort(key=lambda x: -x["score"])
# Apply fit preference tiebreaker
results = apply_fit_preference_tiebreaker(results, fit_pref, garment_type)
return results
# ─────────────────────────────────────────────────────────────────────────────
# QUICK TEST β€” run this file directly to verify the engine works
# python3 engine.py
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("Testing PakFit Engine v1...\n")
# Real J. Kameez size chart data
j_kameez = [
{"size_label": "XS", "chest": 22, "shoulder": 17, "sleeve": 23, "collar": 14.5, "length": 39.5},
{"size_label": "S", "chest": 23, "shoulder": 17.5, "sleeve": 23.5, "collar": 15, "length": 40.75},
{"size_label": "M", "chest": 24, "shoulder": 18.5, "sleeve": 24.25, "collar": 16, "length": 42.25},
{"size_label": "L", "chest": 25, "shoulder": 19.5, "sleeve": 25, "collar": 17, "length": 44},
{"size_label": "XL", "chest": 27, "shoulder": 20.5, "sleeve": 25.5, "collar": 18, "length": 45.25},
]
# Test buyer with 24-inch chest (garment measurement)
buyer = {
"chest": 24,
"shoulder": 18.5,
"sleeve": 24,
"collar": 16,
"length": 42,
}
print("Buyer measurements:", buyer)
print("Garment type: Kameez")
print("Fit preference: regular\n")
results = recommend_size(buyer, j_kameez, "Kameez", "regular")
print("Results (ranked by FitScore):")
for r in results:
print(f" Size {r['size']:>3} β€” FitScore: {r['score']:>5.1f}%")
print(f"\nRecommendation: Size {results[0]['size']} β€” {results[0]['confidence']} confidence")
# Test Shalwar length calculation
print("\n--- Shalwar length test ---")
height_cm = 175
length = calculate_shalwar_length(height_cm, "traditional")
print(f"Buyer height: {height_cm}cm β†’ Ideal Shalwar length: {length} inches")
print("\nEngine test complete.")