"""Independent stateful *approximate* physics features (no label access, not Marlin native). Handles synchronous temperature waits, asynchronous setpoints, motion lengths, lookahead angle hints, and energy = integrated predicted grid power. The C++ labeler is invoked only by dataset generation, never by this inference module. """ import math,re,json import numpy as np RE=re.compile(r'([A-Z])([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)') CODE=['G0','G1','G2','G3','G4','M104','M109','M140','M190','M106','M107','M204','M205','G90','G91','M82','M83','G92','M220','M221','M400','OTHER','M105','M114','M115','M119','G21','M17','M18','M84'] CMAP={k:i+1 for i,k in enumerate(CODE)} def parse(line): s=line.upper().split(';')[0].strip();t=s.split() if not t:return 'OTHER',{} c={'G00':'G0','G01':'G1','G02':'G2','G03':'G3'}.get(t[0],t[0]);a={k:float(v) for k,v in RE.findall(' '.join(t[1:]))} return (c if c in CMAP else 'OTHER'),a def motion_info(code,a,pos,feed,absolute=True): old=np.array(pos[:3],dtype=float); end=old.copy() for j,k in enumerate('XYZ'): if k in a:end[j]=a[k] if absolute else end[j]+a[k] vec=end-old;dist=float(np.linalg.norm(vec));arc_r=0.;sweep=0. if code in ('G2','G3') and ('I' in a or 'J' in a): cx=old[0]+a.get('I',0.);cy=old[1]+a.get('J',0.) r=math.hypot(old[0]-cx,old[1]-cy);end_r=math.hypot(end[0]-cx,end[1]-cy) if r>1e-7 and abs(r-end_r)0 and self.nozzle0 and self.bedstart if not heating and not allow_cooling:return 0.,0. e=0.;t=0. while t<900.: current=self.nozzle if which=='nozzle' else self.bed if heating and current>=target-.8:break if not heating and current<=target+.8:break e+=self.step(.1,False);t+=.1 return t,e def planned_motion_times(rows, p, parsed): """Offline multi-block velocity planning: approximate junction and accel passes. This planner is independently implemented and does not query label targets. It is a deliberately simplified model (arcs are not firmware-segmented). """ state=np.array([p['start_x'],p['start_y'],p['start_z']],float) feed=1800.;ap=float(p['accel_print']);at=float(p['accel_travel']);jerk=float(p['jerk']); absolute=True;speed_mult=1.;events=[] for idx,(c,a) in enumerate(parsed): if c=='G90':absolute=True if c=='G91':absolute=False if c=='G92': for ax,k in enumerate('XYZ'): if k in a:state[ax]=a[k] if c=='M204': ap=a.get('P',a.get('S',ap));at=a.get('T',a.get('S',at)) if c=='M205':jerk=a.get('X',jerk) if c=='M220':speed_mult=a.get('S',100)/100 if c in ('G0','G1','G2','G3'): new,vect,dist,v,radius,sweep=motion_info(c,a,state,feed,absolute) if 'F' in a:feed=a['F'] v*=speed_mult;accel=at if c=='G0' else ap if radius>0: # For arc, represent local start/end tangents; full toolpath is curved. center=state[:2]+[a.get('I',0.),a.get('J',0.)] rs=state[:2]-center;re=new[:2]-center tangent_start=np.array([-rs[1],rs[0],0.]);tangent_end=np.array([-re[1],re[0],0.]) if c=='G2':tangent_start*=-1;tangent_end*=-1 vstart=tangent_start/(np.linalg.norm(tangent_start)+1.e-12) vend=tangent_end/(np.linalg.norm(tangent_end)+1.e-12) else: vend=vstart=vect/(np.linalg.norm(vect)+1.e-12) if dist>1e-8:events.append(dict(i=idx,d=dist,v=v,a=max(accel,1.),jerk=jerk, enter_dir=vstart,exit_dir=vend,arc=(radius>0))) state=new result={};block=[] def flush(b): if not b:return n=len(b);limits=np.zeros(n+1,dtype=float) for j in range(1,n): a=b[j-1];q=b[j] diff=np.abs(q['v']*q['enter_dir']-a['v']*a['exit_dir']) multiplier=min(1.,min(a['jerk'],q['jerk'])/(max(diff[0],diff[1],.000001))) limits[j]=max(0.,min(a['v'],q['v'])*multiplier) vel=limits.copy() for j in range(n-1,-1,-1): ev=b[j];vel[j]=min(vel[j],math.sqrt(max(0,vel[j+1]**2+2*ev['a']*ev['d']))) for j in range(n): ev=b[j];vel[j+1]=min(vel[j+1],math.sqrt(max(0,vel[j]**2+2*ev['a']*ev['d']))) for j,ev in enumerate(b): result[ev['i']]=(trapezoid(ev['d'],ev['v'],ev['a'],vel[j],vel[j+1]),vel[j],vel[j+1]) last=-1 for ev in events: if last>=0 and any(parsed[j][0] in ('G4','M109','M190','M400') for j in range(last+1,ev['i'])): flush(block);block=[] block.append(ev);last=ev['i'] flush(block) return result def state_features(program_rows): """Return (codes[T], raw_features[T,F], prior[T,3]) for ONE whole G-code program. No labels are touched. Offline lookahead uses at most next 2 commands. """ rows=sorted(program_rows,key=lambda r:int(r['step_index'])) p=json.loads(rows[0]['initial_profile']);thermal=Thermal(p) pos=np.array([p['start_x'],p['start_y'],p['start_z']],float) feed=1800.;acc_print=float(p['accel_print']);acc_travel=float(p['accel_travel']);jerk=float(p['jerk']);flow=1.;speed_mult=1. absolute=True;extruder_abs=True;e_pos=0.;ret_debt=0.;prev_vec=np.zeros(3);prev_feed=0. parsed=[parse(r['gcode']) for r in rows] plans=planned_motion_times(rows,p,parsed) codes=[];feats=[];priors=[] for i,(r,(c,a)) in enumerate(zip(rows,parsed)): prev=parsed[max(0,i-1)][0] if i else 'OTHER' nextc=parsed[min(i+1,len(rows)-1)][0] if i+11e-8 and np.linalg.norm(nextdir)>1e-8: cos_next=float(np.dot(vec,nextdir)/(np.linalg.norm(vec)*np.linalg.norm(nextdir))) if np.linalg.norm(vec)>1e-8 and np.linalg.norm(prev_vec)>1e-8: cos_prev=float(np.dot(vec,prev_vec)/(np.linalg.norm(vec)*np.linalg.norm(prev_vec))) de=0. if c in ('G0','G1','G2','G3') and 'E' in a: e_new=(a['E'] if extruder_abs else e_pos+a['E']) de=(e_new-e_pos)*flow; e_pos=e_new accel=acc_travel if c=='G0' else acc_print # Approximate lookahead junction: geometry-dependent exit/entry speed. est_exit=0.;est_entry=0. if c in ('G0','G1','G2','G3'): if nextdist>1e-8: est_exit=min(v,nextfeed,jerk/(max(.1,math.sqrt(max(0,2-2*cos_next))))) if prev_feed>0 and np.linalg.norm(prev_vec)>1e-8: est_entry=min(v,prev_feed,jerk/(max(.1,math.sqrt(max(0,2-2*cos_prev))))) tt=0.;ee=0.;mm=0. if c in ('G0','G1','G2','G3'): # Arc motion's segmentation can shift time vs lumped arc estimate. plan=plans.get(i) if plan is not None: tt,est_entry,est_exit=plan else: tt=trapezoid(length,v,accel,est_entry,est_exit) ee=thermal.advance(tt,True) if de<0:ret_debt+=-de elif de>0: unretract=min(de,ret_debt);ret_debt-=unretract if length>1e-9: mm=(de-unretract)*math.pi*(1.75/2)**2*1.24/1000. pos=newpos;prev_vec=vec.copy();prev_feed=v elif c=='G4': tt=max(0.,a.get('P',0)/1000+a.get('S',0));ee=thermal.advance(tt,False) prev_feed=0. elif c in ('M104','M109'): thermal.nt=a.get('S',a.get('R',thermal.nt)) if c=='M109':tt,ee=thermal.wait('nozzle',thermal.nt,'R' in a) prev_feed=0. elif c in ('M140','M190'): thermal.bt=a.get('S',a.get('R',thermal.bt)) if c=='M190':tt,ee=thermal.wait('bed',thermal.bt,'R' in a) prev_feed=0. elif c=='M106':thermal.fan=a.get('S',255.) elif c=='M107':thermal.fan=0. elif c=='M204': acc_print=a.get('P',a.get('S',acc_print));acc_travel=a.get('T',a.get('S',acc_travel)) elif c=='M205':jerk=a.get('X',jerk) elif c=='M220':speed_mult=a.get('S',100)/100 elif c=='M221':flow=a.get('S',100)/100 elif c=='G90':absolute=True elif c=='G91':absolute=False elif c=='M82':extruder_abs=True elif c=='M83':extruder_abs=False elif c=='G92': for ax,k in enumerate('XYZ'): if k in a:pos[ax]=a[k] if 'E' in a:e_pos=a['E'] # A compact physically meaningful feature vector, including state. f=[float(i)/max(1,len(rows)-1),float(CMAP.get(prev,CMAP['OTHER'])),float(CMAP.get(nextc,CMAP['OTHER'])), math.log1p(length),math.log1p(v),math.log1p(accel),math.log1p(jerk), cos_next,cos_prev,math.log1p(nextdist),math.log1p(nextfeed), math.log1p(est_entry),math.log1p(est_exit),math.log1p(radius),sweep, de,float(max(0.,de)),float(ret_debt),float(thermal.fan)/255, (thermal.nozzle-thermal.ambient)/200,(thermal.bed-thermal.ambient)/100, (thermal.nt-thermal.nozzle)/200,(thermal.bt-thermal.bed)/100, thermal.ambient/30,pos[0]/235,pos[1]/235,pos[2]/250, acc_print/1000,acc_travel/1000,speed_mult,flow, float(c=='G0'),float(c=='G1'),float(c in ('G2','G3')), float(c in ('M109','M190')),float(c=='G4'), math.log1p(max(tt,0.)),math.log1p(max(ee,0.)*3600)] codes.append(CMAP.get(c,CMAP['OTHER']));feats.append(f);priors.append([max(0.,tt),max(0.,ee),max(0.,mm)]) return np.array(codes,np.int64),np.array(feats,np.float32),np.array(priors,np.float32)