diff --git a/monte_carlo_clutter_rev5_app.m b/monte_carlo_clutter_rev5_app.m new file mode 100644 index 0000000..91f1401 --- /dev/null +++ b/monte_carlo_clutter_rev5_app.m @@ -0,0 +1,78 @@ +function [monte_carlo_clutter_loss]=monte_carlo_clutter_rev5_app(app,reliability_range,sort_clutter_loss,rand_numbers) +%MONTE_CARLO_CLUTTER_REV5_APP Correctness-first optimized Monte Carlo clutter interpolation. +% rev5 goals: +% 1) preserve rev3 output contract and units exactly; +% 2) reduce per-TX loop overhead with shape-safe vectorized interpolation; +% 3) keep RNG-free, call-site-compatible interface for rev11-based pipelines. + +DEBUG_CHECKS=false; + +[num_tx,~]=size(sort_clutter_loss); + +[reliability_range,sort_idx]=sort(reliability_range); +sort_clutter_loss=sort_clutter_loss(:,sort_idx); + +monte_carlo_clutter_loss=NaN(num_tx,1); +rel_min=min(reliability_range); +rel_max=max(reliability_range); + +if rel_min==rel_max + monte_carlo_clutter_loss=sort_clutter_loss(:,1); +else + rand_numbers=rand_numbers(:); + rand_numbers=min(max(rand_numbers,rel_min),rel_max); + + ind_prev=nearestpoint_app(app,rand_numbers,reliability_range,'previous'); + ind_next=nearestpoint_app(app,rand_numbers,reliability_range,'next'); + + idx_nan_prev=isnan(ind_prev); + if any(idx_nan_prev) + ind_prev(idx_nan_prev)=1; + end + + idx_nan_next=isnan(ind_next); + if any(idx_nan_next) + ind_next(idx_nan_next)=length(reliability_range); + end + + prev_rel=reliability_range(ind_prev); + next_rel=reliability_range(ind_next); + remainder=rand_numbers-prev_rel; + span=next_rel-prev_rel; + + % Match rev3 semantics: when span==0, subtract term becomes NaN and is reset to 0. + ratio=remainder./span; + ratio(~isfinite(ratio))=0; + + idx_prev=sub2ind(size(sort_clutter_loss),(1:num_tx)',ind_prev); + idx_next=sub2ind(size(sort_clutter_loss),(1:num_tx)',ind_next); + + prev_loss=sort_clutter_loss(idx_prev); + next_loss=sort_clutter_loss(idx_next); + + temp_diff_Pr=prev_loss-next_loss; + subtract_Pr=temp_diff_Pr.*ratio; + subtract_Pr(~isfinite(subtract_Pr))=0; + + monte_carlo_clutter_loss=prev_loss-subtract_Pr; +end + +if DEBUG_CHECKS + if ~isequal(size(monte_carlo_clutter_loss),[num_tx,1]) + error('monte_carlo_clutter_rev5_app:ShapeMismatch', ... + 'Expected [%d x 1] clutter output, got [%d x %d].', ... + num_tx,size(monte_carlo_clutter_loss,1),size(monte_carlo_clutter_loss,2)); + end +end + +if any(isnan(monte_carlo_clutter_loss)) + 'NaN Error with monte_carlo_pr_dBm'; %#ok + pause; +end + +if any(isinf(monte_carlo_clutter_loss)) + inf_idx=find(isinf(monte_carlo_clutter_loss)); + monte_carlo_clutter_loss(inf_idx)=0; +end + +end diff --git a/profile_subchunk_agg_check_maxazi_rev18_real.m b/profile_subchunk_agg_check_maxazi_rev18_real.m new file mode 100644 index 0000000..10c151f --- /dev/null +++ b/profile_subchunk_agg_check_maxazi_rev18_real.m @@ -0,0 +1,199 @@ +function results = profile_subchunk_agg_check_maxazi_rev18_real(app,cell_aas_dist_data,array_bs_azi_data,radar_beamwidth,min_azimuth,max_azimuth,base_protection_pts,point_idx,on_list_bs,cell_sim_chunk_idx,rand_seed1,agg_check_reliability,on_full_Pr_dBm,clutter_loss,custom_antenna_pattern,sub_point_idx) +%PROFILE_SUBCHUNK_AGG_CHECK_MAXAZI_REV18_REAL +% Profile rev18 and compare clutter timing directly against golden rev11. + +must_exist('subchunk_agg_check_maxazi_rev11','MissingRev11'); +must_exist('subchunk_agg_check_maxazi_rev18','MissingRev18'); + +opts=struct(); +opts.AziChunkRev11=128; +opts.AziChunkRev18=128; +opts.TopN=20; +opts.EnableDetailBuiltin=true; +opts.MaterialDropThreshold=0.20; + +fprintf('\n=== PROFILE REV18 (REAL INPUTS, WITH REV11 BASELINE) ===\n'); +fprintf('AZI_CHUNK rev11: %d | rev18: %d\n',opts.AziChunkRev11,opts.AziChunkRev18); + +% Measure rev11 profile first as golden runtime baseline on identical inputs. +[baseline_tbl,baseline_wall_s,baseline_top]=run_profile_once(@subchunk_agg_check_maxazi_rev11,opts.AziChunkRev11, ... + opts.EnableDetailBuiltin,app,cell_aas_dist_data,array_bs_azi_data,radar_beamwidth, ... + min_azimuth,max_azimuth,base_protection_pts,point_idx,on_list_bs,cell_sim_chunk_idx, ... + rand_seed1,agg_check_reliability,on_full_Pr_dBm,clutter_loss,custom_antenna_pattern,sub_point_idx); + +% Measure rev18 profile on the same inputs. +[tbl,wall_runtime_s,top_total]=run_profile_once(@subchunk_agg_check_maxazi_rev18,opts.AziChunkRev18, ... + opts.EnableDetailBuiltin,app,cell_aas_dist_data,array_bs_azi_data,radar_beamwidth, ... + min_azimuth,max_azimuth,base_protection_pts,point_idx,on_list_bs,cell_sim_chunk_idx, ... + rand_seed1,agg_check_reliability,on_full_Pr_dBm,clutter_loss,custom_antenna_pattern,sub_point_idx); + +fprintf('\nTop contributors by total time (rev18):\n'); +disp(top_total); + +% Required reporting targets for rev18. +key=struct(); +key.subchunk_agg_check_maxazi_rev18=summarize_rows(tbl,match_rows(tbl,'subchunk_agg_check_maxazi_rev18'),wall_runtime_s); +key.monte_carlo_clutter_rev5_app=summarize_rows(tbl,match_rows(tbl,'monte_carlo_clutter_rev5_app'),wall_runtime_s); +key.monte_carlo_Pr_dBm_rev2_app=summarize_rows(tbl,match_rows(tbl,'monte_carlo_Pr_dBm_rev2_app'),wall_runtime_s); + +if exist('monte_carlo_super_bs_eirp_dist_rev5','file')==2 + eirp_pattern='monte_carlo_super_bs_eirp_dist_rev5'; +else + eirp_pattern='monte_carlo_super_bs_eirp_dist'; +end +key.monte_carlo_super_bs_eirp_dist_valid=summarize_rows(tbl,match_rows(tbl,eirp_pattern),wall_runtime_s); +key.nearestpoint_app=summarize_rows(tbl,match_rows(tbl,'nearestpoint_app'),wall_runtime_s); +key.db2pow=summarize_rows(tbl,match_rows(tbl,'db2pow'),wall_runtime_s); + +% Matching rev11 timing keys for direct baseline comparisons. +base=struct(); +base.subchunk_agg_check_maxazi_rev11=summarize_rows(baseline_tbl,match_rows(baseline_tbl,'subchunk_agg_check_maxazi_rev11'),baseline_wall_s); +base.monte_carlo_clutter_rev3_app=summarize_rows(baseline_tbl,match_rows(baseline_tbl,'monte_carlo_clutter_rev3_app'),baseline_wall_s); +base.monte_carlo_Pr_dBm_rev2_app=summarize_rows(baseline_tbl,match_rows(baseline_tbl,'monte_carlo_Pr_dBm_rev2_app'),baseline_wall_s); +base.monte_carlo_super_bs_eirp_dist_valid=summarize_rows(baseline_tbl,match_rows(baseline_tbl,eirp_pattern),baseline_wall_s); +base.nearestpoint_app=summarize_rows(baseline_tbl,match_rows(baseline_tbl,'nearestpoint_app'),baseline_wall_s); +base.db2pow=summarize_rows(baseline_tbl,match_rows(baseline_tbl,'db2pow'),baseline_wall_s); + +fprintf('\nSummary timing table (requested functions):\n'); +print_row('subchunk_agg_check_maxazi_rev18',key.subchunk_agg_check_maxazi_rev18); +print_row('monte_carlo_clutter_rev5_app',key.monte_carlo_clutter_rev5_app); +print_row('monte_carlo_Pr_dBm_rev2_app',key.monte_carlo_Pr_dBm_rev2_app); +print_row(eirp_pattern,key.monte_carlo_super_bs_eirp_dist_valid); +print_row('nearestpoint_app',key.nearestpoint_app); +print_row('db2pow',key.db2pow); + +clutter_drop_frac=(base.monte_carlo_clutter_rev3_app.total_time_s-key.monte_carlo_clutter_rev5_app.total_time_s) ... + /max(base.monte_carlo_clutter_rev3_app.total_time_s,eps); +material_clutter_drop=clutter_drop_frac>=opts.MaterialDropThreshold; + +fprintf('\nRuntime comparison vs rev11 baseline (same run harness):\n'); +fprintf(' subchunk total: rev11=%.6f s | rev18=%.6f s | speedup=%.3fx\n', ... + baseline_wall_s,wall_runtime_s,baseline_wall_s/max(wall_runtime_s,eps)); +fprintf(' clutter helper: rev11 rev3=%.6f s | rev18 rev5=%.6f s | drop=%.2f%%\n', ... + base.monte_carlo_clutter_rev3_app.total_time_s,key.monte_carlo_clutter_rev5_app.total_time_s,100*clutter_drop_frac); +if material_clutter_drop + fprintf(' MATERIAL clutter helper drop vs rev11: YES\n'); +else + fprintf(' MATERIAL clutter helper drop vs rev11: NO\n'); +end + +focus_names={'monte_carlo_Pr_dBm_rev2_app',eirp_pattern,'monte_carlo_clutter_rev5_app','nearestpoint_app','db2pow'}; +focus_times=[key.monte_carlo_Pr_dBm_rev2_app.total_time_s, ... + key.monte_carlo_super_bs_eirp_dist_valid.total_time_s, ... + key.monte_carlo_clutter_rev5_app.total_time_s, ... + key.nearestpoint_app.total_time_s, ... + key.db2pow.total_time_s]; +[~,top_idx]=max(focus_times); +new_top_bottleneck=focus_names{top_idx}; +fprintf(' New top bottleneck (among requested targets): %s\n',new_top_bottleneck); + +results=struct(); +results.options=opts; +results.rev11_wall_runtime_s=baseline_wall_s; +results.rev18_wall_runtime_s=wall_runtime_s; +results.speedup_rev11_over_rev18=baseline_wall_s/max(wall_runtime_s,eps); +results.top_by_total_rev11=baseline_top; +results.top_by_total_rev18=top_total; +results.summary_rev11=base; +results.summary_rev18=key; +results.clutter_drop_fraction_vs_rev11=clutter_drop_frac; +results.material_clutter_drop_vs_rev11=material_clutter_drop; +results.new_top_bottleneck=new_top_bottleneck; +results.full_profile_table_rev11=baseline_tbl; +results.full_profile_table_rev18=tbl; + +end + +function [tbl,wall_runtime_s,top_total]=run_profile_once(fhandle,azi_chunk,enable_detail_builtin,app,cell_aas_dist_data,array_bs_azi_data,radar_beamwidth,min_azimuth,max_azimuth,base_protection_pts,point_idx,on_list_bs,cell_sim_chunk_idx,rand_seed1,agg_check_reliability,on_full_Pr_dBm,clutter_loss,custom_antenna_pattern,sub_point_idx) +profile off; +profile clear; +if enable_detail_builtin + profile('-memory','off','-detail','builtin'); +end +profile on; + +wall_tic=tic; +out=fhandle(app,cell_aas_dist_data,array_bs_azi_data,radar_beamwidth,min_azimuth,max_azimuth, ... + base_protection_pts,point_idx,on_list_bs,cell_sim_chunk_idx,rand_seed1,agg_check_reliability, ... + on_full_Pr_dBm,clutter_loss,custom_antenna_pattern,sub_point_idx,azi_chunk); %#ok +wall_runtime_s=toc(wall_tic); + +profile off; +pinfo=profile('info'); +if ~isfield(pinfo,'FunctionTable') || isempty(pinfo.FunctionTable) + error('profile_subchunk_agg_check_maxazi_rev18_real:EmptyProfile', ... + 'MATLAB profile did not return function timing data.'); +end + +tbl=build_profile_table(pinfo.FunctionTable); +[~,idx_total]=sort(tbl.TotalTime_s,'descend','MissingPlacement','last'); +top_n=min(20,height(tbl)); +top_total=tbl(idx_total(1:top_n),:); +end + +function tbl=build_profile_table(ft) +n=numel(ft); +name_col=cell(n,1); +total_col=zeros(n,1); +self_col=zeros(n,1); +calls_col=zeros(n,1); +for i=1:n + name_col{i}=safe_get(ft(i),{'FunctionName','CompleteName','FileName'},''); + total_col(i)=safe_get(ft(i),{'TotalTime'},NaN); + self_col(i)=safe_get(ft(i),{'SelfTime'},NaN); + calls_col(i)=safe_get(ft(i),{'NumCalls'},NaN); +end +tbl=table(name_col,total_col,self_col,calls_col, ... + 'VariableNames',{'Function','TotalTime_s','SelfTime_s','NumCalls'}); +end + +function rows=match_rows(tbl,pattern) +rows=false(height(tbl),1); +for i=1:height(tbl) + if contains(tbl.Function{i},pattern,'IgnoreCase',true) + rows(i)=true; + end +end +end + +function s=summarize_rows(tbl,rows,wall_runtime_s) +if ~any(rows) + s=struct('visible',false,'num_rows',0,'total_time_s',0,'self_time_s',0, ... + 'calls',0,'pct_of_wall',0,'matches',{{}}); + return; +end +s=struct(); +s.visible=true; +s.num_rows=nnz(rows); +s.total_time_s=sum(tbl.TotalTime_s(rows),'omitnan'); +s.self_time_s=sum(tbl.SelfTime_s(rows),'omitnan'); +s.calls=sum(tbl.NumCalls(rows),'omitnan'); +s.pct_of_wall=100*s.total_time_s/max(wall_runtime_s,eps); +s.matches=tbl.Function(rows); +end + +function print_row(label,s) +if s.visible + fprintf(' %-42s total=%10.6f s | self=%10.6f s | calls=%g\n', ... + label,s.total_time_s,s.self_time_s,s.calls); +else + fprintf(' %-42s not visible in current profiler table\n',label); +end +end + +function val=safe_get(s,keys,default_val) +val=default_val; +for k=1:numel(keys) + if isfield(s,keys{k}) + val=s.(keys{k}); + return; + end +end +end + +function must_exist(fname,errid) +if exist(fname,'file')~=2 + error(['profile_subchunk_agg_check_maxazi_rev18_real:' errid], ... + '%s.m was not found on MATLAB path.',fname); +end +end diff --git a/subchunk_agg_check_maxazi_rev18.m b/subchunk_agg_check_maxazi_rev18.m new file mode 100644 index 0000000..d819e33 --- /dev/null +++ b/subchunk_agg_check_maxazi_rev18.m @@ -0,0 +1,119 @@ +function [sub_array_agg_check_mc_dBm]=subchunk_agg_check_maxazi_rev18(app,cell_aas_dist_data,array_bs_azi_data,radar_beamwidth,min_azimuth,max_azimuth,base_protection_pts,point_idx,on_list_bs,cell_sim_chunk_idx,rand_seed1,agg_check_reliability,on_full_Pr_dBm,clutter_loss,custom_antenna_pattern,sub_point_idx,varargin) +%SUBCHUNK_AGG_CHECK_MAXAZI_REV18 Fresh clutter-focused optimization branch. +% rev18 is a rev11-semantic branch that only swaps clutter helper +% (rev3 -> rev5) and is intended to be validated fail-closed against rev11. + +% Tuning knob: larger chunks can improve compute throughput but may increase peak memory. +AZI_CHUNK_DEFAULT=128; +DEBUG_CHECKS=false; +azi_chunk=AZI_CHUNK_DEFAULT; +if ~isempty(varargin) + azi_chunk=varargin{1}; +end +azi_chunk=max(1,round(azi_chunk)); + +array_aas_dist_data=cell_aas_dist_data{2}; +aas_dist_azimuth=cell_aas_dist_data{1}; +mod_azi_diff_bs=array_bs_azi_data(:,4); + +% Off-axis EIRP lookup at BS-relative azimuth. +nn_azi_idx=nearestpoint_app(app,mod_azi_diff_bs,aas_dist_azimuth); +super_array_bs_eirp_dist=array_aas_dist_data(nn_azi_idx,:); + +% Simulation azimuth grid. +[array_sim_azimuth,num_sim_azi]=calc_sim_azimuths_rev3_360_azimuths_app(app,radar_beamwidth,min_azimuth,max_azimuth); + +% BS->point azimuths. +sim_pt=base_protection_pts(point_idx,:); +bs_azimuth=azimuth(sim_pt(1),sim_pt(2),on_list_bs(:,1),on_list_bs(:,2)); + +% MC iteration indices for this sub-point. +sub_mc_idx=cell_sim_chunk_idx{sub_point_idx}; %#ok +num_mc_idx=length(sub_mc_idx); +num_bs=length(bs_azimuth); +sub_array_agg_check_mc_dBm=NaN(num_mc_idx,1); + +% ------------------------------------------------------------------------- +% STEP 1: MC random pre-generation using a single RNG seeding call. +% Draw in [rel_min, rel_max] for PR, EIRP, clutter random reliabilities. +% ------------------------------------------------------------------------- +rel_min=min(agg_check_reliability); +rel_max=max(agg_check_reliability); + +if rel_min==rel_max + rand_pr_all=repmat(rel_min,num_bs,num_mc_idx); + rand_eirp_all=rand_pr_all; + rand_clutter_all=rand_pr_all; +else + rng(rand_seed1); + rel_span=(rel_max-rel_min); + rand_pr_all=rel_min+rel_span.*rand(num_bs,num_mc_idx); + rand_eirp_all=rel_min+rel_span.*rand(num_bs,num_mc_idx); + rand_clutter_all=rel_min+rel_span.*rand(num_bs,num_mc_idx); +end + +% ------------------------------------------------------------------------- +% STEP 2: Precompute off-axis gain matrix once for all (bs,sim_azimuth). +% Keep nearestpoint semantics stable. +% ------------------------------------------------------------------------- +pat_az=mod(custom_antenna_pattern(:,1),360); +pat_gain=custom_antenna_pattern(:,2); +[pat_az_unique,ia_unique]=unique(pat_az,'stable'); +pat_gain_unique=pat_gain(ia_unique); + +off_axis_gain_matrix=NaN(num_bs,num_sim_azi); +for azimuth_idx=1:1:num_sim_azi + sim_azimuth=array_sim_azimuth(azimuth_idx); + rel_az=mod(bs_azimuth-sim_azimuth,360); + ant_deg_idx=nearestpoint_app(app,rel_az,pat_az_unique); + off_axis_gain_matrix(:,azimuth_idx)=pat_gain_unique(ant_deg_idx); +end + +% ------------------------------------------------------------------------- +% STEP 3: RNG-free MC pathloss terms for each MC realization. +% ------------------------------------------------------------------------- +sort_monte_carlo_pr_dBm_all=NaN(num_bs,num_mc_idx); +for loop_idx=1:1:num_mc_idx + pre_sort_monte_carlo_pr_dBm=monte_carlo_Pr_dBm_rev2_app(app,agg_check_reliability,on_full_Pr_dBm,rand_pr_all(:,loop_idx)); + rand_norm_eirp=monte_carlo_super_bs_eirp_dist_rev5(app,super_array_bs_eirp_dist,agg_check_reliability,rand_eirp_all(:,loop_idx)); + monte_carlo_clutter_loss=monte_carlo_clutter_rev5_app(app,agg_check_reliability,clutter_loss,rand_clutter_all(:,loop_idx)); + + sort_monte_carlo_pr_dBm_all(:,loop_idx)=pre_sort_monte_carlo_pr_dBm+rand_norm_eirp-monte_carlo_clutter_loss; +end + +% ------------------------------------------------------------------------- +% STEP 4: Aggregate over BS in watts, convert back to dBm, then max over az. +% ------------------------------------------------------------------------- +for loop_idx=1:1:num_mc_idx + base_mc=sort_monte_carlo_pr_dBm_all(:,loop_idx); + max_azi_agg=-Inf; + + for azi_start=1:azi_chunk:num_sim_azi + azi_end=min(azi_start+azi_chunk-1,num_sim_azi); + chunk_gain=off_axis_gain_matrix(:,azi_start:azi_end); + sort_temp_mc_dBm=base_mc+chunk_gain; + + if DEBUG_CHECKS + if any(isnan(sort_temp_mc_dBm),'all') + error('subchunk_agg_check_maxazi_rev18:NaNTempDbm','NaN detected in sort_temp_mc_dBm'); + end + end + + binary_sort_mc_watts=db2pow(sort_temp_mc_dBm)/1000; + if DEBUG_CHECKS + if any(isnan(binary_sort_mc_watts),'all') + error('subchunk_agg_check_maxazi_rev18:NaNWatt','NaN detected in binary_sort_mc_watts'); + end + end + + azimuth_agg_dBm_chunk=pow2db(sum(binary_sort_mc_watts,1,'omitnan')*1000); + chunk_max=max(azimuth_agg_dBm_chunk,[],'omitnan'); + if chunk_max>max_azi_agg + max_azi_agg=chunk_max; + end + end + + sub_array_agg_check_mc_dBm(loop_idx,1)=max_azi_agg; +end + +end diff --git a/validate_subchunk_agg_check_maxazi_rev11_rev18_statistical.m b/validate_subchunk_agg_check_maxazi_rev11_rev18_statistical.m new file mode 100644 index 0000000..9c75ecb --- /dev/null +++ b/validate_subchunk_agg_check_maxazi_rev11_rev18_statistical.m @@ -0,0 +1,159 @@ +function results = validate_subchunk_agg_check_maxazi_rev11_rev18_statistical(app,cell_aas_dist_data,array_bs_azi_data,radar_beamwidth,min_azimuth,max_azimuth,base_protection_pts,point_idx,on_list_bs,cell_sim_chunk_idx,rand_seed1,agg_check_reliability,on_full_Pr_dBm,clutter_loss,custom_antenna_pattern,sub_point_idx) +%VALIDATE_SUBCHUNK_AGG_CHECK_MAXAZI_REV11_REV18_STATISTICAL +% Fail-closed statistical validation of rev18 against golden rev11 only. + +must_exist('subchunk_agg_check_maxazi_rev11','MissingRev11'); +must_exist('subchunk_agg_check_maxazi_rev18','MissingRev18'); + +opts=struct(); +opts.AziChunkRev11=128; +opts.AziChunkRev18=128; +opts.NumTimingRuns=3; +opts.CoreAbsDrift_dB=0.250; +opts.CoreRelDrift=0.020; +opts.TailAbsDrift_dB=0.150; +opts.TailRelDrift=0.010; + +fprintf('\n=== VALIDATION: REV11 vs REV18 (STATISTICAL) ===\n'); +fprintf('Thresholds (fail-closed): core=max(%.3f dB, %.3f*|rev11|), tail=max(%.3f dB, %.3f*|rev11|)\n', ... + opts.CoreAbsDrift_dB,opts.CoreRelDrift,opts.TailAbsDrift_dB,opts.TailRelDrift); + +runtime11=zeros(opts.NumTimingRuns,1); +runtime18=zeros(opts.NumTimingRuns,1); +out11=[]; +out18=[]; +for run_idx=1:opts.NumTimingRuns + t=tic; + out11=subchunk_agg_check_maxazi_rev11(app,cell_aas_dist_data,array_bs_azi_data, ... + radar_beamwidth,min_azimuth,max_azimuth,base_protection_pts,point_idx,on_list_bs, ... + cell_sim_chunk_idx,rand_seed1,agg_check_reliability,on_full_Pr_dBm,clutter_loss, ... + custom_antenna_pattern,sub_point_idx,opts.AziChunkRev11); + runtime11(run_idx)=toc(t); + + t=tic; + out18=subchunk_agg_check_maxazi_rev18(app,cell_aas_dist_data,array_bs_azi_data, ... + radar_beamwidth,min_azimuth,max_azimuth,base_protection_pts,point_idx,on_list_bs, ... + cell_sim_chunk_idx,rand_seed1,agg_check_reliability,on_full_Pr_dBm,clutter_loss, ... + custom_antenna_pattern,sub_point_idx,opts.AziChunkRev18); + runtime18(run_idx)=toc(t); +end + +x11=out11(:); +x18=out18(:); +mask=isfinite(x11) & isfinite(x18); +x11=x11(mask); +x18=x18(mask); +if isempty(x11) + error('validate_subchunk_agg_check_maxazi_rev11_rev18_statistical:NoFiniteSamples', ... + 'No finite paired samples available for comparison.'); +end + +metric_names={'mean','std','min','max','median','p90','p95','p99'}; +tail_metrics={'p95','p99'}; +qvals=[0.90 0.95 0.99]; + +s11=summary_stats(x11,qvals); +s18=summary_stats(x18,qvals); + +drift=struct(); +pass_vec=true(size(metric_names)); +for k=1:numel(metric_names) + m=metric_names{k}; + v11=s11.(m); + v18=s18.(m); + abs_diff=abs(v18-v11); + + is_tail=any(strcmp(m,tail_metrics)); + if is_tail + allowed=max(opts.TailAbsDrift_dB,opts.TailRelDrift*abs(v11)); + else + allowed=max(opts.CoreAbsDrift_dB,opts.CoreRelDrift*abs(v11)); + end + + pass_here=abs_diff<=allowed; + pass_vec(k)=pass_here; + drift.(m)=struct('rev11',v11,'rev18',v18,'abs_diff',abs_diff, ... + 'allowed_abs',allowed,'is_tail',is_tail,'pass',pass_here); +end + +overall_pass=all(pass_vec); + +fprintf('\nRuntime summary (seconds):\n'); +fprintf(' rev11 runs: [%s]\n',fmt_vec(runtime11)); +fprintf(' rev18 runs: [%s]\n',fmt_vec(runtime18)); +fprintf(' rev11 mean: %.6f\n',mean(runtime11)); +fprintf(' rev18 mean: %.6f\n',mean(runtime18)); +fprintf(' speedup (rev11/rev18): %.3fx\n',mean(runtime11)/max(mean(runtime18),eps)); + +fprintf('\nMetric drift summary:\n'); +for k=1:numel(metric_names) + m=metric_names{k}; + d=drift.(m); + fprintf(' %-7s | rev11=%10.4f | rev18=%10.4f | abs=%8.4f | allow=%8.4f | %s\n', ... + m,d.rev11,d.rev18,d.abs_diff,d.allowed_abs,passfail(d.pass)); +end + +fprintf('\nUpper-tail emphasis:\n'); +for k=1:numel(tail_metrics) + m=tail_metrics{k}; + d=drift.(m); + fprintf(' %-7s | rev11=%10.4f | rev18=%10.4f | abs=%8.4f | allow=%8.4f | %s\n', ... + m,d.rev11,d.rev18,d.abs_diff,d.allowed_abs,passfail(d.pass)); +end + +if overall_pass + fprintf('\nPASS / FAIL: PASS\n'); +else + fprintf('\nPASS / FAIL: FAIL\n'); + error('validate_subchunk_agg_check_maxazi_rev11_rev18_statistical:DriftExceeded', ... + 'Fail-closed: rev18 drift exceeded rev11 thresholds.'); +end + +results=struct(); +results.options=opts; +results.n_samples=numel(x11); +results.runtime_rev11_s=runtime11; +results.runtime_rev18_s=runtime18; +results.runtime_mean_rev11_s=mean(runtime11); +results.runtime_mean_rev18_s=mean(runtime18); +results.speedup_rev11_over_rev18=mean(runtime11)/max(mean(runtime18),eps); +results.summary_rev11=s11; +results.summary_rev18=s18; +results.metrics=metric_names; +results.drift=drift; +results.pass=overall_pass; + +end + +function s=summary_stats(x,qvals) +s=struct(); +s.mean=mean(x,'omitnan'); +s.std=std(x,0,'omitnan'); +s.min=min(x,[],'omitnan'); +s.max=max(x,[],'omitnan'); +s.median=median(x,'omitnan'); +q=quantile(x,qvals); +s.p90=q(1); +s.p95=q(2); +s.p99=q(3); +end + +function must_exist(fname,errid) +if exist(fname,'file')~=2 + error(['validate_subchunk_agg_check_maxazi_rev11_rev18_statistical:' errid], ... + '%s.m was not found on MATLAB path.',fname); +end +end + +function txt=fmt_vec(v) +txt=sprintf('%.6f ',v); +txt=strtrim(txt); +end + +function txt=passfail(tf) +if tf + txt='PASS'; +else + txt='FAIL'; +end +end