File size: 8,732 Bytes
547ac30 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | """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
@dataclass
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
|