#!/usr/bin/env python3 """ AM Material Selector — Additive Manufacturing Expert Skill Filters and ranks materials from the database based on engineering requirements. Usage: python3 select_material.py --help """ import json, argparse, sys from pathlib import Path DB_PATH = Path(__file__).parent.parent / "references" / "materials-db.json" def load_db(): with open(DB_PATH) as f: return json.load(f) def parse_range(val): """Converts 'min-max' or a single number into (min, max).""" if val is None: return None, None s = str(val) if '-' in s: parts = s.split('-') return float(parts[0]), float(parts[1]) return float(s), float('inf') def score_material(mat, req): """Computes a 0-100 score for a material against requirements. Returns (score, reasons, disqualified).""" score = 100 reasons = [] disqualified = [] mech = mat.get('mechanical', {}) thermal = mat.get('thermal', {}) flags = mat.get('flags', {}) ra = mat.get('surface_roughness', {}) # --- ELIMINATION FILTERS --- # Service temperature if req.get('T_service'): T = req['T_service'] T_max = thermal.get('T_max_service', 9999) if isinstance(T_max, str): T_max = float(T_max.replace('>','').replace('~','')) if T_max < T: disqualified.append(f"T_max_service={T_max}°C < required {T}°C") # Process if req.get('process'): proc = req['process'].upper() mat_procs = [p.upper() for p in mat.get('processes', [])] if not any(proc in p or p in proc for p in mat_procs): disqualified.append(f"process {req['process']} not available (processes: {mat.get('processes')})") # Biocompatibility if req.get('biocompatible'): if not flags.get('biocompatible', False): disqualified.append("biocompatibility required but not available") # UV resistant if req.get('uv_resistant'): if not flags.get('uv_resistant', False): disqualified.append("UV resistance required but not guaranteed") # Minimum UTS if req.get('UTS_min') and disqualified == []: uts_min_req = req['UTS_min'] uts_max_mat = mech.get('UTS_max') or mech.get('UTS_asbuilt_max') or mech.get('UTS_LPBF_max') or 0 if uts_max_mat < uts_min_req: disqualified.append(f"UTS_max={uts_max_mat} MPa < required {uts_min_req} MPa") # Target roughness (Ra) — check if achievable if req.get('Ra_target') and disqualified == []: Ra_req = req['Ra_target'] Ra_asbuilt = ra.get('Ra_asbuilt_typical', 50) postproc = ra.get('postprocess_achievable', {}) Ra_achievable_min = Ra_asbuilt best_method = "as-built" for method, vals in postproc.items(): if isinstance(vals, dict) and 'Ra_min' in vals: if vals['Ra_min'] < Ra_achievable_min: Ra_achievable_min = vals['Ra_min'] best_method = method if Ra_achievable_min > Ra_req: disqualified.append(f"achievable Ra_min={Ra_achievable_min}µm > target {Ra_req}µm even with post-processing") else: if Ra_asbuilt > Ra_req: reasons.append(f"target Ra {Ra_req}µm achievable with {best_method} (as-built: {Ra_asbuilt}µm)") if disqualified: return 0, reasons, disqualified # --- POSITIVE SCORING --- # Temperature: more margin = better if req.get('T_service'): T = req['T_service'] T_max = thermal.get('T_max_service', 9999) if isinstance(T_max, (int, float)): margin = T_max - T if margin > 100: score += 10 reasons.append(f"excellent thermal margin (+{margin:.0f}°C)") elif margin > 30: score += 5 # UTS: margin against requirement if req.get('UTS_min'): uts_min_req = req['UTS_min'] uts_max_mat = mech.get('UTS_max') or mech.get('UTS_asbuilt_max') or mech.get('UTS_LPBF_max') or 0 if uts_max_mat > 0: ratio = uts_max_mat / uts_min_req if ratio > 2: score += 10 elif ratio > 1.5: score += 5 # As-built Ra: if it already meets the target without post-processing -> advantage if req.get('Ra_target'): Ra_asbuilt = ra.get('Ra_asbuilt_typical', 50) if Ra_asbuilt <= req['Ra_target']: score += 15 reasons.append(f"as-built Ra ({Ra_asbuilt}µm) already meets the target — no additional post-processing") # Printability difficulty_map = { "easy": 10, "moderate": 5, "difficult": 0, "very difficult": -5, "extreme": -10 } diff = mat.get('print_difficulty', '') score += difficulty_map.get(diff, 0) # Cost cost = mat.get('cost_relative', 5) if cost <= 2: score += 8 reasons.append(f"low cost (index {cost})") elif cost <= 5: score += 4 elif cost > 15: score -= 10 # Isotropy aniso = mech.get('anisotropy_Z_factor', 1.0) if aniso >= 0.9: score += 5 reasons.append(f"good isotropy (Z/XY = {aniso})") # Quantity if req.get('quantity') and req['quantity'] > 100: procs = mat.get('processes', []) if any(p in ['SLS', 'MJF'] for p in procs): score += 8 reasons.append("process well-suited for large series") if any(p == 'LPBF' for p in procs) and req['quantity'] > 50: score -= 5 # LPBF is expensive for large volumes return min(score, 100), reasons, [] def recommend(req, top_n=5): db = load_db() all_mats = db['polymers'] + db['metals'] results = [] eliminated = [] for mat in all_mats: score, reasons, disqualified = score_material(mat, req) if disqualified: eliminated.append({'id': mat['id'], 'name': mat['name'], 'why': disqualified}) else: results.append({ 'id': mat['id'], 'name': mat['name'], 'processes': mat.get('processes', []), 'score': score, 'reasons': reasons, 'cost_relative': mat.get('cost_relative', '?'), 'Ra_asbuilt': mat.get('surface_roughness', {}).get('Ra_asbuilt_typical', '?'), 'T_max': mat.get('thermal', {}).get('T_max_service', '?'), 'UTS_max': (mat.get('mechanical', {}).get('UTS_max') or mat.get('mechanical', {}).get('UTS_asbuilt_max') or mat.get('mechanical', {}).get('UTS_LPBF_max') or '?'), 'postprocess_for_Ra': _ra_strategy(mat, req.get('Ra_target')) }) results.sort(key=lambda x: x['score'], reverse=True) return results[:top_n], eliminated def _ra_strategy(mat, Ra_target): """Returns the post-processing strategy to reach target Ra.""" if Ra_target is None: return None ra = mat.get('surface_roughness', {}) Ra_asbuilt = ra.get('Ra_asbuilt_typical', 50) if Ra_asbuilt <= Ra_target: return f"as-built is sufficient (Ra_typical={Ra_asbuilt}µm)" postproc = ra.get('postprocess_achievable', {}) strategies = [] for method, vals in postproc.items(): if isinstance(vals, dict) and 'Ra_min' in vals: if vals['Ra_min'] <= Ra_target: note = vals.get('note', '') strategies.append(f"{method} → Ra≥{vals['Ra_min']}µm" + (f" ({note})" if note else "")) if strategies: return "; or ".join(strategies) return f"target Ra={Ra_target}µm NOT achievable (min={min((v['Ra_min'] for v in postproc.values() if isinstance(v,dict) and 'Ra_min' in v), default=Ra_asbuilt)}µm)" def print_report(results, eliminated, req): print("\n" + "="*70) print(" AM MATERIAL SELECTOR — Results") print("="*70) print(f"\n Requirements analyzed:") for k, v in req.items(): if v is not None: print(f" {k}: {v}") print(f"\n Materials evaluated: {len(results)+len(eliminated)}") print(f" Eligible: {len(results)} | Eliminated: {len(eliminated)}") print("\n" + "-"*70) print(" TOP CANDIDATES (sorted by score)") print("-"*70) for i, r in enumerate(results): print(f"\n [{i+1}] {r['name']} (score: {r['score']}/100)") print(f" Processes: {', '.join(r['processes'])}") print(f" UTS_max: {r['UTS_max']} MPa | T_max: {r['T_max']}°C | Ra_asbuilt: {r['Ra_asbuilt']}µm | Cost: {r['cost_relative']}/10") if r['postprocess_for_Ra']: print(f" Ra strategy: {r['postprocess_for_Ra']}") if r['reasons']: for reason in r['reasons'][:3]: print(f" ✓ {reason}") if eliminated: print("\n" + "-"*70) print(f" ELIMINATED ({len(eliminated)}) — main reasons") print("-"*70) shown = 0 for e in eliminated[:10]: print(f" ✗ {e['name']}: {'; '.join(e['why'][:2])}") shown += 1 if len(eliminated) > shown: print(f" ... and {len(eliminated)-shown} more") print("\n" + "="*70 + "\n") def main(): parser = argparse.ArgumentParser(description="AM Material Selector — filters materials by engineering requirements") parser.add_argument('--T', type=float, help="Max service temperature (°C)") parser.add_argument('--UTS', type=float, help="Minimum required UTS (MPa)") parser.add_argument('--Ra', type=float, help="Target surface roughness Ra (µm)") parser.add_argument('--process', type=str, help="Specific process (FDM, SLS, LPBF, etc.)") parser.add_argument('--bio', action='store_true', help="Biocompatibility required") parser.add_argument('--uv', action='store_true', help="UV resistance required") parser.add_argument('--qty', type=int, help="Quantity (pcs)") parser.add_argument('--top', type=int, default=5, help="Number of results (default: 5)") parser.add_argument('--json', action='store_true', help="JSON output") args = parser.parse_args() req = { 'T_service': args.T, 'UTS_min': args.UTS, 'Ra_target': args.Ra, 'process': args.process, 'biocompatible':args.bio or None, 'uv_resistant': args.uv or None, 'quantity': args.qty } req = {k: v for k, v in req.items() if v} if not req: print("ERROR: specify at least one requirement. Use --help for parameters.") sys.exit(1) results, eliminated = recommend(req, top_n=args.top) if args.json: print(json.dumps({'results': results, 'eliminated': eliminated}, indent=2)) else: print_report(results, eliminated, req) if __name__ == '__main__': main()