File size: 6,860 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
"""Job-level energy features (10 independently supervised subsystems).

Inputs: complete G-code, machine profile, INFERENCE-PREDICTED command durations.
Outputs: one feature vector and one physically integrated baseline Wh per device/JOB.
No teacher duration or measured energy enters feature extraction. The physical
integration uses command chronology to obtain duty/regime exposure, but does
NOT make any neural energy prediction per G-code line.

All feature engineering is based on known G-code, machine metadata, estimated
motion time, and simulated (not observed) temperatures/duty. NOT calibrated.
"""
import json
import numpy as np
from physics import parse
from component_power import COMPONENTS,DEFAULT_POWER,simulate_components,MOTION

FEAT_NAMES={
 'electronics':['baseline_Wh','total_s','nominal_W','n_commands'],
 'stepper_hold':['baseline_Wh','total_s','hold_active_s_est','nominal_W','n_commands'],
 'motor_x':['baseline_Wh','motion_s','axis_mm_est','axis_mean_v','axis_rms_v','axis_peak_v','nominal_W','n_moves','total_s','accel_travel'],
 'motor_y':['baseline_Wh','motion_s','axis_mm_est','axis_mean_v','axis_rms_v','axis_peak_v','nominal_W','n_moves','total_s','accel_travel'],
 'motor_z':['baseline_Wh','motion_s','axis_mm_est','axis_mean_v','axis_rms_v','axis_peak_v','nominal_W','n_moves','total_s','accel_print'],
 'motor_e':['baseline_Wh','motion_s','axis_mm_est','axis_mean_v','axis_rms_v','axis_peak_v','nominal_W','n_extrusion_moves','total_s','accel_print'],
 'heater_nozzle':['baseline_Wh','total_s','nozzle_heating_s_est','target_temp_s','setpoint_mean_C','start_C','ambient_C','nominal_W','thermal_capacity','thermal_loss','fan_level_s','n_target_changes','extrusion_mm'],
 'heater_bed':['baseline_Wh','total_s','bed_heating_s_est','target_temp_s','setpoint_mean_C','start_C','ambient_C','nominal_W','thermal_capacity','thermal_loss','n_target_changes'],
 'fan_part':['baseline_Wh','total_s','fan_pwm_time_s','fan_active_s','nominal_W','fan_transition_n'],
 'fan_hotend':['baseline_Wh','total_s','hotend_on_time_s_est','nominal_W','nozzle_target_time_Cs','nozzle_start_C'],
}

# Feature magnitudes are log1p transformed, except unscaled temperatures may be
# zero or negative. Train-only scalers are used afterward.
def summary(rows,durations):
    t=np.asarray(durations,dtype=np.float64).reshape(-1)
    if len(t)!=len(rows) or len(rows)==0 or not np.all(np.isfinite(t)) or np.any(t<0):
        raise ValueError('Invalid sequence durations')
    profile=json.loads(rows[0]['initial_profile']) if isinstance(rows[0]['initial_profile'],str) else rows[0]['initial_profile']
    hw={**DEFAULT_POWER,**profile.get('power_hardware',{})}
    energy_line,power_line,vel=simulate_components(rows,t)
    base=np.asarray(energy_line,dtype=np.float64).sum(axis=0)
    assert base.shape==(len(COMPONENTS),)
    commands=[parse(r['gcode']) for r in rows]
    move=np.asarray([c in MOTION for c,_ in commands])
    nmove=int(move.sum());total=float(t.sum());move_s=float(t[move].sum())
    axis_len=np.sum(vel*t[:,None],axis=0)
    axis_mean=np.sum(vel*t[:,None],axis=0)/max(1.e-8,move_s)
    axis_rms=np.sqrt(np.sum(vel**2*t[:,None],axis=0)/max(1.e-8,move_s))
    axis_max=np.max(vel,axis=0)
    # G-code state-history (setpoint & PWM); no labels or sensor truth involved.
    noz_sp=float(profile.get('nozzle_start',profile.get('ambient',23)))
    bed_sp=float(profile.get('bed_start',profile.get('ambient',23)))
    fan=float(profile.get('fan_start',0.))
    fan_pwm_s=fan_s=noz_sp_time=bed_sp_time=0.
    n_noz=n_bed=n_fan=0
    noz_heating_s=bed_heating_s=hotend_on_s=0.
    for (c,a),dt,wh in zip(commands,t,energy_line):
        if c in ('M104','M109'):
            nxt=float(a.get('S',a.get('R',noz_sp)))
            n_noz+=int(abs(nxt-noz_sp)>1e-8);noz_sp=nxt
        if c in ('M140','M190'):
            nxt=float(a.get('S',a.get('R',bed_sp)))
            n_bed+=int(abs(nxt-bed_sp)>1e-8);bed_sp=nxt
        if c=='M106':
            nxt=max(0.,min(255.,a.get('S',255.)));n_fan+=int(abs(fan-nxt)>1e-8);fan=nxt
        if c=='M107':
            n_fan+=int(fan>0);fan=0.
        noz_sp_time+=noz_sp*dt
        bed_sp_time+=bed_sp*dt
        fan_pwm_s+=dt*fan/255.
        fan_s+=dt*(fan>0)
        # Estimated energy exposure, from reference physics, not measured label.
        noz_heating_s+=float(wh[COMPONENTS.index('heater_nozzle')])*3600/max(hw['nozzle_w'],1.e-7)
        bed_heating_s+=float(wh[COMPONENTS.index('heater_bed')])*3600/max(hw['bed_w'],1.e-7)
        hotend_on_s+=float(wh[COMPONENTS.index('fan_hotend')])*3600/max(hw['hotend_fan_w'],1.e-7)
    hold_s=base[1]*3600/max(hw['stepper_hold_w'],1.e-7)
    shared={}
    shared['electronics']=[base[0],total,hw['electronics_w'],len(t)]
    shared['stepper_hold']=[base[1],total,hold_s,hw['stepper_hold_w'],len(t)]
    for i,k in enumerate(['motor_x','motor_y','motor_z','motor_e']):
        kk=('x','y','z','e')[i]
        nominal=hw[f'{kk}_motor_w_at_'+('60' if i<2 else '5')]
        shared[k]=[base[2+i],move_s,axis_len[i],axis_mean[i],axis_rms[i],axis_max[i],nominal,
                   int((vel[:,i]>0).sum()) if i==3 else nmove,total,
                   float(profile.get('accel_travel' if i<2 else 'accel_print',500))]
    shared['heater_nozzle']=[base[6],total,noz_heating_s,noz_sp_time,
          noz_sp_time/max(total,1.e-8),float(profile['nozzle_start']),float(profile['ambient']),hw['nozzle_w'],
          hw['nozzle_cap_j_c'],hw['nozzle_loss_w_c'],fan_pwm_s,n_noz,axis_len[3]]
    shared['heater_bed']=[base[7],total,bed_heating_s,bed_sp_time,
          bed_sp_time/max(total,1.e-8),float(profile['bed_start']),float(profile['ambient']),hw['bed_w'],
          hw['bed_cap_j_c'],hw['bed_loss_w_c'],n_bed]
    shared['fan_part']=[base[8],total,fan_pwm_s,fan_s,hw['part_fan_w'],n_fan]
    shared['fan_hotend']=[base[9],total,hotend_on_s,hw['hotend_fan_w'],noz_sp_time,float(profile['nozzle_start'])]
    features={k:np.asarray(shared[k],dtype=np.float32) for k in COMPONENTS}
    for k in COMPONENTS:
        if len(features[k])!=len(FEAT_NAMES[k]):raise AssertionError(k)
        if not np.all(np.isfinite(features[k])):raise ValueError('Nonfinite feature '+k)
    return {'x':features,'base':base.astype('float32'),'time_s':total,
            'n_moves':nmove,
            'regimes':{'motion_time_s':move_s,'heater_nozzle_s_est':noz_heating_s,'heater_bed_s_est':bed_heating_s,
                'fan_pwm_equiv_s':fan_pwm_s,'axis_distance_mm_est':axis_len.tolist()}}


def fit_scalers(examples):
    return {k:(np.stack([e['summary']['x'][k] for e in examples]).mean(0).astype('float32'),
               np.maximum(np.stack([e['summary']['x'][k] for e in examples]).std(0),.001).astype('float32'))
            for k in COMPONENTS}

def normalized(x,scalers):
    return {k:np.clip((x[k]-scalers[k][0])/scalers[k][1],-8,8).astype('float32') for k in COMPONENTS}