"""Deterministic synthetic sensible-cooling response experiment; stdlib only.""" import argparse,csv,json,math from pathlib import Path DEFAULT = {'duration_h':12,'dt_s':10,'initial_temp_c':24,'outside_temp_c':22, 'setpoint_c':24,'band_low_c':23,'band_high_c':25,'capacitance_j_per_k':12000000, 'ua_w_per_k':300,'base_heat_w':800,'light_heat_w':8000,'lights_on_h':2, 'lights_off_h':8,'cooling_w':12000,'kp_per_k':0.6,'ki_per_k_s':0.0004} def validate(c,tau): if set(c)!=set(DEFAULT):raise ValueError('config keys must match config.json') for k,v in {**c,'tau_s':tau}.items(): if isinstance(v,bool) or not isinstance(v,(int,float)) or not math.isfinite(v):raise ValueError(k+': finite number required') if not 020000:raise ValueError('time grid exceeds limits') if c['capacitance_j_per_k']<=0 or c['cooling_w']<=0 or c['ua_w_per_k']<0:raise ValueError('invalid thermal parameters') if any(c[k]<0 for k in ['base_heat_w','light_heat_w','kp_per_k','ki_per_k_s']) or tau<0:raise ValueError('negative capacity/gain/time constant') if not c['band_low_c']0.1:raise ValueError('thermal Euler step too large') def actuator(a,u,dt,tau): """End value and exact interval mean under zero-order-held command.""" if tau==0:return u,u weight=-math.expm1(-dt/tau) return a+(u-a)*weight,u+(a-u)*(tau/dt)*weight def integrate(i,e,raw,c): if 00 or raw>=1 and e<0: return i+c['ki_per_k_s']*e*c['dt_s'] return i def run(c,tau): validate(c,tau);dt=c['dt_s'];t=c['initial_temp_c'];a=0.;integ=0.;prev=0. metrics={'max_temp_c':t,'min_temp_c':t,'outside_band_h':0.,'iae_k_h':0., 'cooling_thermal_kwh':0.,'command_actual_gap_h':0.,'command_total_variation':0., 'heat_balance_residual_j':0.} rows=[] for k in range(round(c['duration_h']*3600/dt)): seconds=k*dt;h=seconds/3600;e=t-c['setpoint_c'];raw=c['kp_per_k']*e+integ;u=max(0.,min(1.,raw)) integ=integrate(integ,e,raw,c);end,avg=actuator(a,u,dt,tau) load=c['base_heat_w']+(c['light_heat_w'] if c['lights_on_h']<=h