d62dfd13a8
- add skill package and SKILL.md with AM workflow, guardrails, and output structure - add technical reference corpus (DfAM, fatigue, defects, process parameters, compliance, cost) - add materials-db.json with polymer/metal data, roughness/post-processing ranges, and selection guides - add CLI tools: select_material.py and postprocess_route.py for material ranking and post-processing route generation
344 lines
14 KiB
Python
344 lines
14 KiB
Python
#!/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()
|