"""Research-only analytic surrogate for continuous-band METAL SAWING (cut-off). Supported: G21/G90/G94 G0/G1 G4 P M3/M5/M8/M9/M2, S in m/min, F in mm/min, vertical Z feed, mm/seconds/Wh/g. Rectangular solid or round solid. Not a controller, validated machine model, wear model, or measured electrical data. Teacher-specific machine gain is PRIVATE and never included in prediction inputs. Mechanism decomposition: tooth shear + edge ploughing + sliding/rubbing; the sum represents mechanical blade load, not additional spindle idle electrical power. """ from __future__ import annotations import math,re,json import numpy as np COMPONENTS=('electronics','hydraulic_hold','blade_drive_idle','blade_acceleration', 'cutting_shear','ploughing','tooth_rubbing','feed_axis','band_guides', 'coolant','chip_conveyor','control_aux') DENSITY={'C45':.00785,'AL6061':.00270,'304SS':.00800,'CAST_IRON':.00720} RESISTANCE={'C45':1.0,'AL6061':.49,'304SS':1.32,'CAST_IRON':.82} WORD=re.compile(r'([A-Z])\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+))',re.I) COMMENT=re.compile(r'\([^)]*\)|;[^\n]*') def motion_time(mm,speed_mm_min,accel): if mm<=0:return 0. v=max(.01,speed_mm_min/60.);a=max(.01,accel) return 2*math.sqrt(mm/a) if mmlo)) removed_area=max(0.,cross_section_area(hi,profile)-cross_section_area(lo,profile)) if cut else 0. vol=kerf*removed_area total_removed+=vol sweep=hi-lo if cut else 0. wavg=removed_area/sweep if sweep>1e-12 else 0. vfeed=sweep/max(t,1.e-8) # mm/s vblade=band_speed/60. # m/s (S specified m/min) fz=(vfeed*60)*tooth_pitch_mm/max(1.,band_speed*1000.) # mm / tooth engaged=wavg/max(tooth_pitch_mm,1.e-5) # Flexible empirical 'mechanistic' approximation. Constants are NOT # published/experimentally calibrated force coefficients. blade_factor=.84 if blade_type=='CARBIDE' else 1. u_shear=(1.7*res*blade_factor)*(1.+.015/(fz+.015)) u_plough=(.70*res*blade_factor)/(1.+fz/.015) in_contact=bool(on and motion=='G1' and move and -nz>0 and z>=-height-.01 and nz<=0) # Friction/rubbing on engaged teeth even on near-zero-volume contact. rubbing_power=(8.+.42*engaged + .12*band_speed)*res*blade_factor if in_contact else 0. p=np.zeros(len(COMPONENTS),np.float64) if t>0: p[0]=(54.+13.*gain)*t/3600. p[1]=(92.+21.*gain)*t/3600. if on:p[2]=(195.+2.7*band_speed+.006*band_speed**2)*gain*t/3600. if 3 in mcodes and not was_on: # Approximate drive/band kinetic energy; not included in idle load. p[3]=(.5*float(profile['equivalent_blade_mass_kg'])*vblade*vblade)/(3600*.77)*gain p[4]=u_shear*vol/3600. p[5]=u_plough*vol/3600. p[6]=rubbing_power*t/3600. if move: axis_v=dist/max(t,.0001) p[7]=(11.+.8*axis_v+.012*axis_v*axis_v)*gain*t/3600. if on:p[8]=(31.+.18*band_speed)*t/3600. if cool:p[9]=(82.+12.*gain)*t/3600. if on:p[10]=64.*t/3600. p[11]=26.*t/3600. command='G4' if 4 in gcodes else (motion if move else 'M'+str(mcodes[0]) if mcodes else 'STATE') events.append({'command':command,'time_s':t,'energy_Wh':float(p.sum()),'component_energy_Wh':p.tolist(), 'material_g':vol*density,'material_removed_mm3':vol,'cut_width_avg_mm':wavg, 'mean_engaged_teeth':engaged,'feed_per_tooth_mm':fz,'band_speed_m_min':band_speed, 'feed_mm_min':feed,'z_after_mm':nz,'motion_mm':dist,'contact':in_contact, 'kerf_mm':kerf,'height_mm':height,'cut_depth_mm':sweep,'total_volume_mm3':total_removed, 'blade_acceleration_event':bool(3 in mcodes and not was_on)}) z=nz return events