Download src/drilling_physics.py from rasitcesur/Manufacturing-MoE: direct link, hf CLI and curl.
- Browser
- Download file 8.73 kB
-
https://huggingface.co/rasitcesur/Manufacturing-MoE/resolve/main/src/drilling_physics.py
- Command line
-
hf download hf://rasitcesur/Manufacturing-MoE/src/drilling_physics.py
-
curl -L -o drilling_physics.py https://huggingface.co/rasitcesur/Manufacturing-MoE/resolve/main/src/drilling_physics.py
8.73 kB
| """Restricted G17/G90/G94 LinuxCNC-style G81/G82/G83 interpreter surrogate. | |
| This is NOT LinuxCNC, a firmware emulator, or a measured power simulator. | |
| Only supports the explicit subset generated by generate_drilling.py. Fails closed | |
| on unsupported G-codes, offset modes, coordinates or parameter combinations. | |
| For each canned cycle a source G-code block maps to multiple internal motions. | |
| """ | |
| from __future__ import annotations | |
| import math,re | |
| from dataclasses import dataclass | |
| WORDS=re.compile(r'([A-Z])\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+))',re.I) | |
| PAREN=re.compile(r'\([^)]*\)|;[^\n]*') | |
| COMPONENTS=['electronics','axis_hold','axis_x','axis_y','axis_z','spindle_idle','cutting_load','coolant'] | |
| MATERIALS={'AL6061':(.00270,1.45),'C45':(.00785,3.70),'POM':(.00141,.70)} | |
| def motion_time(length,mm_min,accel): | |
| if length<1e-12:return 0. | |
| v=mm_min/60.;d=v*v/accel | |
| return 2*math.sqrt(length/accel) if length<d else 2*v/accel+(length-d)/v | |
| class State: | |
| x:float=0. | |
| y:float=0. | |
| z:float=5. | |
| rate:float=400. | |
| rpm:float=0. | |
| spindle:bool=False | |
| coolant:bool=False | |
| motion:str='G0' | |
| cycle:str|None=None | |
| retract:str='G98' | |
| r:float|None=None | |
| bottom:float|None=None | |
| q:float|None=None | |
| p:float|None=None | |
| old_z:float|None=None | |
| def simulate(lines,profile,*,accel=620.,rapid=6000.,clearance=.254, | |
| base_power=39.,hold_power=19.,spindle_base=48.,spindle_linear=.011, | |
| cutting_factor=1.,coolant_power=80.,axis_factor=1.): | |
| """Return one event per (possibly modal) G-code block; no target CSV is read.""" | |
| material=profile['material'];rho,k=MATERIALS[material];diam=float(profile['tool_diameter_mm']) | |
| assert 0<diam<50 | |
| ztop=float(profile.get('stock_top_z_mm',0.));safe_z=float(profile.get('initial_z_mm',5.)) | |
| state=State(z=safe_z) | |
| result=[] | |
| for line in lines: | |
| stripped=PAREN.sub('',line.upper()).strip() | |
| if not stripped:continue | |
| pairs=[(letter,float(value)) for letter,value in WORDS.findall(stripped)] | |
| if not pairs:continue | |
| words={a:b for a,b in pairs};gs=[int(v) for a,v in pairs if a=='G'];ms=[int(v) for a,v in pairs if a=='M'] | |
| allowed_g={0,1,4,17,21,40,49,80,81,82,83,90,94,98,99} | |
| if any(g not in allowed_g for g in gs):raise ValueError('Unsupported drilling G-code: '+line) | |
| if any(m not in (2,3,4,5,8,9) for m in ms):raise ValueError('Unsupported drilling M-code: '+line) | |
| if 'F' in words:state.rate=float(words['F']) | |
| if 'S' in words:state.rpm=float(words['S']) | |
| if state.rate<=0:raise ValueError('feed must be positive') | |
| for m in ms: | |
| if m in (3,4):state.spindle=True | |
| elif m==5:state.spindle=False | |
| elif m==8:state.coolant=True | |
| elif m==9:state.coolant=False | |
| for g in gs: | |
| if g in (98,99):state.retract='G'+str(g) | |
| elif g in (0,1):state.motion='G'+str(g);state.cycle=None | |
| elif g==80:state.cycle=None | |
| elif g in (81,82,83): | |
| state.cycle='G'+str(g) | |
| state.old_z=state.z | |
| comps=dict.fromkeys(COMPONENTS,0.) | |
| total_time=0.;length_total=0.;feed_length=0.;volume=0.;segments=0;pecks=0;cycle_dwell=0. | |
| def seg(dist,axis,rapid_move=False,cut=False): | |
| nonlocal total_time,length_total,feed_length,segments | |
| if dist<1.e-9:return | |
| v=rapid if rapid_move else state.rate | |
| dt=motion_time(dist,v,accel) | |
| total_time+=dt;length_total+=dist;segments+=1 | |
| if not rapid_move:feed_length+=dist | |
| # Electronics and hold are included during all nonzero time spans. | |
| comps['electronics']+=base_power*dt/3600. | |
| comps['axis_hold']+=hold_power*dt/3600. | |
| if state.spindle: | |
| comps['spindle_idle']+=(spindle_base+spindle_linear*state.rpm+1.1e-6*state.rpm**2)*dt/3600. | |
| if state.coolant:comps['coolant']+=coolant_power*dt/3600. | |
| speed=dist/max(dt,.001) | |
| comps['axis_'+axis]+=axis_factor*(.45+.24*speed+.002*speed**2)*dt/3600. | |
| def dwell(dt): | |
| nonlocal total_time,cycle_dwell | |
| total_time+=dt;cycle_dwell+=dt | |
| comps['electronics']+=base_power*dt/3600. | |
| comps['axis_hold']+=hold_power*dt/3600. | |
| if state.spindle:comps['spindle_idle']+=(spindle_base+spindle_linear*state.rpm+1.1e-6*state.rpm**2)*dt/3600. | |
| if state.coolant:comps['coolant']+=coolant_power*dt/3600. | |
| op='STATE' | |
| if 4 in gs: | |
| if 'P' not in words or words['P']<0:raise ValueError('G4 needs P>=0') | |
| op='G4';dwell(float(words['P'])) | |
| elif state.cycle and (('X' in words) or ('Y' in words) or (any(g in (81,82,83) for g in gs))): | |
| op=state.cycle | |
| # G-code cycling: X/Y modal, R and Z modal. G98 uses old Z; G99 uses R. | |
| xx=words.get('X',state.x);yy=words.get('Y',state.y) | |
| if 'R' in words:state.r=float(words['R']) | |
| if 'Z' in words:state.bottom=float(words['Z']) | |
| if 'Q' in words:state.q=float(words['Q']) | |
| if 'P' in words:state.p=float(words['P']) | |
| if state.r is None or state.bottom is None or state.bottom>=state.r or state.bottom>=ztop:raise ValueError('invalid R/Z in drilling cycle') | |
| if op=='G83' and (state.q is None or state.q<=0):raise ValueError('G83 requires Q>0') | |
| if op=='G82' and (state.p is None or state.p<0):raise ValueError('G82 requires P>=0') | |
| if not state.spindle:raise ValueError('Drilling without spindle') | |
| # XY traverse at current Z; lift to R if needed. | |
| if state.z<state.r:seg(state.r-state.z,'z',True);state.z=state.r | |
| seg(math.hypot(xx-state.x,yy-state.y),'x',True) | |
| state.x,state.y=xx,yy | |
| if state.z>state.r:seg(state.z-state.r,'z',True);state.z=state.r | |
| target=state.bottom | |
| depth=ztop-target | |
| volume=math.pi/4*diam*diam*depth | |
| if op in ('G81','G82'): | |
| seg(state.r-target,'z',False,True);state.z=target | |
| if op=='G82':dwell(state.p) | |
| else: | |
| previous_depth=state.r | |
| while previous_depth>target+1e-9: | |
| next_z=max(target,previous_depth-state.q) | |
| if previous_depth==state.r: | |
| seg(state.r-next_z,'z',False,True) | |
| else: | |
| approach=min(state.r,previous_depth+clearance) | |
| seg(state.r-approach,'z',True) | |
| seg(approach-next_z,'z',False,True) | |
| previous_depth=next_z;pecks+=1 | |
| if previous_depth>target+1e-9: | |
| seg(state.r-previous_depth,'z',True) | |
| state.z=target | |
| end_z=state.old_z if state.retract=='G98' and state.old_z is not None and state.old_z>state.r else state.r | |
| seg(end_z-state.z,'z',True);state.z=end_z | |
| # Drill removes one cylinder per location; each hole is unique in this generated subset. | |
| comps['cutting_load']+=k*cutting_factor*volume/3600. | |
| elif state.cycle and ('Z' in words or 'R' in words): | |
| raise ValueError('Modal cycle must provide hole position') | |
| elif any(g in (0,1) for g in gs) or (any(a in words for a in 'XYZ') and state.cycle is None): | |
| op=state.motion | |
| x,y,z=(words.get('X',state.x),words.get('Y',state.y),words.get('Z',state.z)) | |
| seg(math.hypot(x-state.x,y-state.y),'x',state.motion=='G0') | |
| seg(abs(z-state.z),'z',state.motion=='G0') | |
| state.x,state.y,state.z=x,y,z | |
| if 'M3' in stripped or 'M4' in stripped:op='M3' | |
| if 'M5' in stripped:op='M5' | |
| if 'M8' in stripped:op='M8' | |
| if 'M9' in stripped:op='M9' | |
| if 'M2' in stripped:op='M2' | |
| result.append({'gcode':stripped,'command':op,'time_s':total_time,'energy_Wh':sum(comps.values()), | |
| 'material_removed_mm3':volume,'material_g':volume*rho,'energy_components':comps, | |
| 'motion_length_mm':length_total,'feed_motion_mm':feed_length, | |
| 'segments':segments,'peck_count':pecks,'dwell_s':cycle_dwell, | |
| 'spindle_rpm':state.rpm,'feed_mm_min':state.rate, | |
| 'tool_diameter_mm':diam,'depth_mm':(-state.bottom) if state.bottom is not None else 0., | |
| 'x_after_mm':state.x,'y_after_mm':state.y,'z_after_mm':state.z, | |
| 'spindle_on':int(state.spindle),'coolant_on':int(state.coolant), | |
| 'cycle_type':op if op in ('G81','G82','G83') else ''}) | |
| return result | |