#!/usr/bin/env python3 """ AM Post-Processing Route Planner Given a process, material, and target Ra, generates the optimal post-processing sequence. Usage: python3 postprocess_route.py --material AlSi10Mg --Ra 0.8 --use medical """ import json, argparse, sys from pathlib import Path DB_PATH = Path(__file__).parent.parent / "references" / "materials-db.json" # Post-processing data by process — material-independent POSTPROCESS_CATALOG = { # --- Physical abrasive methods --- "bead_blast": { "label": "Bead/Sand Blasting", "Ra_achievable": (3, 8), "applies_to_processes": ["LPBF", "EBM", "SLS", "MJF", "BJT"], "time_h": 0.1, "cost_relative": 1, "note": "Uniform matte finish. Typical prerequisite for other treatments.", "sequence_priority": 1 }, "vibratory": { "label": "Vibratory/Barrel Finishing", "Ra_achievable": (1.5, 5), "applies_to_processes": ["LPBF", "SLS", "MJF", "BJT"], "time_h": 2, "cost_relative": 2, "note": "Good for batches. Watch thin features (<1mm).", "sequence_priority": 2 }, "shot_peen": { "label": "Shot Peening", "Ra_achievable": (3, 8), "applies_to_processes": ["LPBF", "EBM"], "time_h": 0.5, "cost_relative": 2, "note": "Introduces compressive residual stress -> improves fatigue by 20-40%. AMS 2430 for aerospace.", "sequence_priority": 2, "functional_benefit": "fatigue_improvement" }, "machining_CNC": { "label": "CNC machining (milling/turning)", "Ra_achievable": (0.4, 1.6), "applies_to_processes": ["LPBF", "EBM", "SLS", "BJT", "FDM"], "time_h": 1, "cost_relative": 4, "note": "For accessible specific surfaces. Machining stock must be planned in design (0.5-1.5mm).", "sequence_priority": 3 }, "grinding": { "label": "Grinding", "Ra_achievable": (0.1, 0.4), "applies_to_processes": ["LPBF", "EBM", "BJT"], "applies_to_materials": ["17-4PH", "IN718", "CoCr", "316L"], "time_h": 2, "cost_relative": 5, "note": "For flat sealing surfaces or precision fits.", "sequence_priority": 4 }, "lapping_polishing": { "label": "Lapping / Polishing", "Ra_achievable": (0.025, 0.2), "applies_to_processes": ["LPBF", "EBM", "SLA", "DLP"], "time_h": 3, "cost_relative": 6, "note": "For Ra <0.2um. Surgical surfaces, precision seals, optics.", "sequence_priority": 5 }, "electropolish": { "label": "Electropolishing", "Ra_achievable": (0.2, 1.5), "applies_to_processes": ["LPBF", "BJT"], "applies_to_materials": ["316L", "17-4PH", "Ti-6Al-4V", "IN625", "IN718"], "time_h": 1, "cost_relative": 3, "note": "Excellent for 316L (food/medical). Variable results on Ti. Not recommended on Al.", "sequence_priority": 3, "functional_benefit": "corrosion_resistance" }, "passivation": { "label": "Passivation (nitric/citric acid)", "Ra_achievable": None, "applies_to_processes": ["LPBF", "BJT"], "applies_to_materials": ["316L", "17-4PH", "15-5PH"], "time_h": 0.5, "cost_relative": 1, "note": "Does not change Ra. Restores passive layer and improves corrosion resistance.", "sequence_priority": 5, "functional_benefit": "corrosion_resistance" }, "anodizing": { "label": "Anodizing", "Ra_achievable": None, "applies_to_processes": ["LPBF"], "applies_to_materials": ["AlSi10Mg", "Scalmalloy"], "time_h": 1, "cost_relative": 2, "note": "Does not change Ra. Protection, color, and surface hardness for Al.", "sequence_priority": 5, "functional_benefit": "surface_protection" }, "sanding": { "label": "Manual sanding (abrasive paper)", "Ra_achievable": (0.4, 4), "applies_to_processes": ["FDM", "SLA", "DLP"], "time_h": 0.5, "cost_relative": 1, "grit_sequence": "120 -> 240 -> 400 -> 800 -> 1200 for Ra <1um", "note": "Low cost. Labor-intensive. Not suitable for complex geometries.", "sequence_priority": 2 }, "acetone_smoothing": { "label": "Acetone smoothing (ABS only)", "Ra_achievable": (1.5, 5), "applies_to_processes": ["FDM"], "applies_to_materials": ["ABS"], "time_h": 0.3, "cost_relative": 1, "note": "ABS ONLY. Changes dimensions by +/-0.1-0.3mm; account for tolerances. Toxic vapors.", "sequence_priority": 2 }, "IPA_wash_UV_cure": { "label": "IPA Wash + UV Post-Curing", "Ra_achievable": (1, 6), "applies_to_processes": ["SLA", "DLP", "MSLA"], "time_h": 0.5, "cost_relative": 1, "note": "MANDATORY for resins. IPA 10-15min + UV 900-1200 mJ/cm^2.", "sequence_priority": 1, "mandatory": True }, "dyeing_SLS": { "label": "Dyeing - SLS/MJF", "Ra_achievable": None, "applies_to_processes": ["SLS", "MJF"], "time_h": 1, "cost_relative": 1, "note": "Does not change Ra. Uniform PA12 coloration. Hot process at 80-95C.", "sequence_priority": 3, "functional_benefit": "aesthetics" }, "SLS_coating": { "label": "SLS Coating (Ceracoat, Duracoat)", "Ra_achievable": (3, 6), "applies_to_processes": ["SLS"], "time_h": 0.5, "cost_relative": 2, "note": "Sealing + uniform color for PA12 SLS.", "sequence_priority": 4 }, "HIP": { "label": "HIP (Hot Isostatic Pressing)", "Ra_achievable": None, "applies_to_processes": ["LPBF", "EBM", "BJT"], "time_h": 8, "cost_relative": 8, "note": "Closes internal porosity (<0.1%). Mandatory for biomedical and fatigue-critical parts. 900C / 150MPa / 3h (Ti).", "sequence_priority": 2, "functional_benefit": "density_porosity" } } def load_db(): with open(DB_PATH) as f: return json.load(f) def find_material(mat_id, db): for m in db['polymers'] + db['metals']: if m['id'].lower() == mat_id.lower(): return m return None def plan_route(mat_id, Ra_target, process, use_case=None): db = load_db() mat = find_material(mat_id, db) if not mat: avail = [m['id'] for m in db['polymers']+db['metals']] return None, f"Material '{mat_id}' not found. Available: {', '.join(avail)}" ra_data = mat.get('surface_roughness', {}) Ra_asbuilt = ra_data.get('Ra_asbuilt_typical', 50) postproc_achievable = ra_data.get('postprocess_achievable', {}) mat_process = mat.get('processes', [process]) route = [] reasoning = [] # Step 1: mandatory process steps if any(p in ['SLA','DLP','MSLA'] for p in mat_process): route.append({ "step": 1, "operation": "IPA Wash + UV Post-Curing", "conditions": "IPA 90%+ / 10-15 min agitation; UV 405nm / 900-1200 mJ/cm^2", "Ra_after": Ra_asbuilt, "mandatory": True, "reason": "MANDATORY for resins -> final mechanical properties" }) # Step 2: stress relief (metalli) if any(p in ['LPBF','EBM','BJT'] for p in mat_process): ht = mat.get('heat_treatment', {}) sr = ht.get('stress_relief', {}) if sr: route.append({ "step": len(route)+1, "operation": "Stress Relief termico", "conditions": f"{sr.get('temp_C','?')}°C / {sr.get('time_h','?')}h / {sr.get('atmosphere','?')}", "Ra_after": Ra_asbuilt, "mandatory": sr.get('mandatory', True), "reason": sr.get('timing', "Before removal from build plate") + " -> reduces residual stress and prevents distortion" }) # Step 3: HIP if required by use case if use_case in ['biomedical', 'fatigue_critical', 'pressure_vessel']: if any(p in ['LPBF','EBM'] for p in mat_process): route.append({ "step": len(route)+1, "operation": "HIP (Hot Isostatic Pressing)", "conditions": "~900C / 150 MPa / 3h / Argon (Ti); consult supplier for other materials", "Ra_after": Ra_asbuilt, "mandatory": True, "reason": f"Mandatory for {use_case} -> closes porosity <0.1%, uniform properties" }) # Step 4: specific heat treatment ht = mat.get('heat_treatment', {}) for ht_key, ht_val in ht.items(): if isinstance(ht_val, dict) and ht_val.get('mandatory', False): if ht_key not in ['stress_relief']: route.append({ "step": len(route)+1, "operation": f"Heat Treatment: {ht_key.replace('_',' ').title()}", "conditions": f"{ht_val.get('temp_C','?')}°C / {ht_val.get('time_h','?')}h / {ht_val.get('atmosphere','?')}", "Ra_after": Ra_asbuilt, "mandatory": True, "reason": ht_val.get('warning', ht_val.get('note', 'Required for final mechanical properties')) }) # Step 5: support removal (if required) if mat.get('flags', {}).get('supports_needed', False): route.append({ "step": len(route)+1, "operation": "Support removal", "conditions": "Mechanical (pliers, cutters) + milling for metal supports. EDM for hard-to-access areas.", "Ra_after": Ra_asbuilt, "mandatory": True, "reason": "After heat treatment for metals -> NOT before stress relief" }) # Step 6: Ra route Ra_current = Ra_asbuilt if Ra_target and Ra_current > Ra_target: reasoning.append(f"Ra as-built: {Ra_asbuilt}µm → target: {Ra_target}µm → delta: {Ra_asbuilt-Ra_target:.1f}µm") # Choose optimal strategy candidates = [] for method, vals in postproc_achievable.items(): if isinstance(vals, dict) and 'Ra_min' in vals: if vals['Ra_min'] <= Ra_target: candidates.append((method, vals['Ra_min'], vals)) # Sort by increasing cost cost_map = {m: POSTPROCESS_CATALOG.get(m, {}).get('cost_relative', 3) for m, _, _ in candidates} candidates.sort(key=lambda x: cost_map.get(x[0], 3)) if candidates: best_method, best_Ra_min, best_vals = candidates[0] catalog_entry = POSTPROCESS_CATALOG.get(best_method, {}) route.append({ "step": len(route)+1, "operation": catalog_entry.get('label', best_method.replace('_',' ').title()), "conditions": catalog_entry.get('grit_sequence', catalog_entry.get('note', '')), "Ra_after": best_Ra_min, "mandatory": False, "reason": f"Required to reach target Ra {Ra_target}um (as-built: {Ra_asbuilt}um). Alternative: {candidates[1][0] if len(candidates)>1 else 'none'}" }) Ra_current = best_Ra_min else: reasoning.append(f"WARNING: target Ra {Ra_target}um is not reachable with standard post-processing for {mat_id}") # Step 7: additional functional treatments by use case if use_case == 'food_contact' and mat_id in ['316L']: route.append({ "step": len(route)+1, "operation": "Electropolishing + Passivation", "conditions": "Electropolish in perchloric/acetic acid; nitric passivation 20-25%", "Ra_after": 0.5, "mandatory": True, "reason": "Mandatory for food-contact -> Ra <0.8um + intact passive layer (FDA/EHEDG)" }) elif use_case == 'biomedical' and mat_id in ['316L', 'Ti-6Al-4V']: route.append({ "step": len(route)+1, "operation": "Electropolishing + Biomedical passivation", "conditions": "ASTM F86 or equivalent. Ra <0.8um for contact surfaces.", "Ra_after": 0.4, "mandatory": True, "reason": "Biomedical standard -> biofilm prevention, controlled cell adhesion" }) # Final step: inspection if use_case in ['biomedical', 'fatigue_critical', 'aerospace', 'pressure_vessel']: route.append({ "step": len(route)+1, "operation": "Quality inspection", "conditions": "CT scan (porosity) + CMM (dimensions) + hardness test" + (" + FPI (cracks)" if use_case in ['fatigue_critical','aerospace'] else ""), "Ra_after": Ra_current, "mandatory": True, "reason": f"Mandatory qualification for {use_case} application" }) return route, reasoning def print_route(mat_id, route, reasoning, Ra_target): print("\n" + "="*70) print(f" POST-PROCESSING ROUTE — {mat_id}") if Ra_target: print(f" Target Ra: {Ra_target} µm") print("="*70) if reasoning: print("\n Notes:") for r in reasoning: print(f" {r}") print(f"\n Sequence ({len(route)} steps):") for step in route: flag = " [MANDATORY]" if step.get('mandatory') else " [recommended]" print(f"\n STEP {step['step']}: {step['operation']}{flag}") if step.get('conditions'): print(f" Conditions: {step['conditions']}") if step.get('Ra_after') and Ra_target: print(f" Ra after: ~{step['Ra_after']} µm") print(f" Why: {step['reason']}") print("\n" + "="*70 + "\n") def main(): parser = argparse.ArgumentParser(description="AM Post-Processing Route Planner") parser.add_argument('--material', required=True, help="Material ID (e.g. AlSi10Mg, Ti-6Al-4V, PA12-SLS)") parser.add_argument('--Ra', type=float, help="Final target Ra (µm)") parser.add_argument('--process', default='LPBF',help="AM process used") parser.add_argument('--use', help="Application: biomedical, food_contact, aerospace, fatigue_critical, pressure_vessel") parser.add_argument('--json', action='store_true', help="JSON output") args = parser.parse_args() route, reasoning = plan_route(args.material, args.Ra, args.process, args.use) if route is None: print(f"ERROR: {reasoning}") sys.exit(1) if args.json: import json as _json print(_json.dumps({'material': args.material, 'Ra_target': args.Ra, 'route': route, 'notes': reasoning}, indent=2)) else: print_route(args.material, route, reasoning, args.Ra) if __name__ == '__main__': main()