DHDRL commited on
Commit
103ccfb
·
verified ·
1 Parent(s): bc36a84

Update fair_adversarial_validation_framework.py

Browse files
Files changed (1) hide show
  1. fair_adversarial_validation_framework.py +156 -382
fair_adversarial_validation_framework.py CHANGED
@@ -14,11 +14,11 @@ from collections import defaultdict
14
  import matplotlib.pyplot as plt
15
  import seaborn as sns
16
 
17
- # Add this import for the wrapper
18
  from gru_env_wrappers import GRUStateManager
19
 
 
20
  # ============================================================================
21
- # TEST CASE DEFINITIONS
22
  # ============================================================================
23
 
24
  @dataclass
@@ -32,7 +32,8 @@ class FairTestCase:
32
  env_config_modifier: Optional[Any] = None
33
  expected_behavior: str = ""
34
  pass_threshold: float = 0.80
35
-
 
36
  @dataclass
37
  class FairTestResult:
38
  """Results from a fair test case"""
@@ -51,89 +52,51 @@ class FairTestResult:
51
 
52
 
53
  # ============================================================================
54
- # FAIR PERTURBATION GENERATORS
55
  # ============================================================================
56
 
57
  class FairPerturbations:
58
  """Realistic perturbations that respect model assumptions"""
59
-
60
  @staticmethod
61
  def realistic_sensor_noise(observation: Dict, noise_std: float = 0.01) -> Dict:
62
- """
63
- Add small Gaussian noise simulating realistic sensor imperfections.
64
-
65
- noise_std=0.01 represents ~1% measurement uncertainty
66
- This is what you'd see from real metrology equipment.
67
- """
68
  obs = observation.copy()
69
  belief_map = obs['belief_map'].copy()
70
-
71
- # Only add noise to non-zero regions (actual wafer)
72
  wafer_mask = obs.get('wafer_map', np.ones_like(belief_map)) > 0
73
-
74
  noise = np.random.normal(0, noise_std, belief_map.shape)
75
  belief_map = belief_map + (noise * wafer_mask)
76
-
77
- # Preserve probability semantics
78
  obs['belief_map'] = np.clip(belief_map, 0.0, 1.0)
79
  return obs
80
-
81
  @staticmethod
82
  def calibration_drift(observation: Dict, drift_factor: float = 0.05) -> Dict:
83
- """
84
- Simulate systematic calibration drift (e.g., tool aging).
85
-
86
- drift_factor=0.05 means beliefs are systematically 5% off.
87
- This represents gradual tool degradation.
88
- """
89
  obs = observation.copy()
90
  belief_map = obs['belief_map'].copy()
91
-
92
- # Systematic scaling (not random)
93
  drift = 1.0 + np.random.uniform(-drift_factor, drift_factor)
94
  belief_map = belief_map * drift
95
-
96
  obs['belief_map'] = np.clip(belief_map, 0.0, 1.0)
97
  return obs
98
-
99
  @staticmethod
100
  def local_degradation(observation: Dict, affected_ratio: float = 0.1) -> Dict:
101
- """
102
- Simulate localized tool degradation affecting part of wafer.
103
-
104
- affected_ratio=0.1 means 10% of wafer has degraded sensing.
105
- This represents edge-of-wafer effects or local contamination.
106
- """
107
  obs = observation.copy()
108
  belief_map = obs['belief_map'].copy()
109
  H, W = belief_map.shape
110
-
111
- # Create localized degradation zone (e.g., one quadrant)
112
  if np.random.random() < 0.5:
113
- # Edge degradation
114
  margin = int(H * 0.1)
115
- belief_map[:margin, :] *= 0.8 # 20% reduced sensitivity
116
  belief_map[-margin:, :] *= 0.8
117
  else:
118
- # Quadrant degradation
119
  belief_map[:H//2, :W//2] *= 0.85
120
-
121
  obs['belief_map'] = np.clip(belief_map, 0.0, 1.0)
122
  return obs
123
-
124
  @staticmethod
125
  def quantization_noise(observation: Dict, bits: int = 8) -> Dict:
126
- """
127
- Simulate ADC quantization (realistic for real sensors).
128
-
129
- bits=8 means 256 discrete levels (standard ADC resolution).
130
- """
131
  obs = observation.copy()
132
  belief_map = obs['belief_map'].copy()
133
-
134
  levels = 2 ** bits
135
  quantized = np.round(belief_map * levels) / levels
136
-
137
  obs['belief_map'] = quantized
138
  return obs
139
 
@@ -144,68 +107,64 @@ class FairPerturbations:
144
 
145
  class FairEnvModifiers:
146
  """Realistic environment modifications"""
147
-
148
  @staticmethod
149
  def fab_variation_defect_rate(base_rate: float = 0.03, variation: float = 0.3):
150
- """
151
- Simulate normal fab variation in defect rate.
152
-
153
- variation=0.3 means ±30% from baseline
154
- Example: 3% baseline → 2.1% to 3.9% range
155
- """
156
  return base_rate * (1.0 + np.random.uniform(-variation, variation))
157
-
158
  @staticmethod
159
  def budget_efficiency_test(base_budget: int, efficiency: float = 0.8):
160
- """
161
- Test with reduced budget (simulating faster throughput requirement).
162
-
163
- efficiency=0.8 means 80% of normal budget (20% faster needed)
164
- """
165
  return int(base_budget * efficiency)
166
-
167
  @staticmethod
168
  def cost_pressure(base_cost: float, multiplier: float = 1.5):
169
- """
170
- Simulate cost pressure (inspection became more expensive).
171
-
172
- multiplier=1.5 means 50% cost increase
173
- """
174
  return base_cost * multiplier
175
 
176
 
177
  # ============================================================================
178
- # FAIR ADVERSARIAL TEST SUITE
179
  # ============================================================================
180
 
181
  class FairAdversarialTestSuite:
182
- """Fair, realistic adversarial validation"""
183
-
184
- def __init__(self, model, env_factory, output_dir: str = "./fair_adversarial_results"):
185
- """
186
- Args:
187
- model: Trained SB3 model
188
- env_factory: Function that creates fresh environment (critical!)
189
- output_dir: Where to save results
190
- """
191
  self.model = model
192
- self.env_factory = env_factory # Function, not instance!
193
  self.output_dir = Path(output_dir)
194
  self.output_dir.mkdir(exist_ok=True, parents=True)
195
-
 
196
  self.test_cases = self._define_fair_tests()
197
- self.results = []
198
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  def _define_fair_tests(self) -> List[FairTestCase]:
200
- """Define fair, realistic test cases"""
201
-
202
  tests = []
203
-
204
- # =================================================================
205
- # PRODUCTION LEVEL: What you'd see in normal fab operation
206
- # Expected: 90-98% performance
207
- # =================================================================
208
-
209
  tests.extend([
210
  FairTestCase(
211
  name="baseline_clean",
@@ -231,10 +190,7 @@ class FairAdversarialTestSuite:
231
  description="±10% defect rate variation",
232
  category="distribution_robustness",
233
  difficulty="production",
234
- env_config_modifier=lambda config: {
235
- **config,
236
- 'prior_belief': 0.1
237
- },
238
  expected_behavior="Handle normal fab variation",
239
  pass_threshold=0.90
240
  ),
@@ -260,12 +216,8 @@ class FairAdversarialTestSuite:
260
  pass_threshold=0.90
261
  ),
262
  ])
263
-
264
- # =================================================================
265
- # STRESS LEVEL: Challenging but realistic scenarios
266
- # Expected: 75-90% performance
267
- # =================================================================
268
-
269
  tests.extend([
270
  FairTestCase(
271
  name="sensor_noise_2pct",
@@ -281,10 +233,7 @@ class FairAdversarialTestSuite:
281
  description="±30% defect rate variation",
282
  category="distribution_robustness",
283
  difficulty="stress",
284
- env_config_modifier=lambda config: {
285
- **config,
286
- 'prior_belief': 0.1
287
- },
288
  expected_behavior="Adapt to significant fab shifts",
289
  pass_threshold=0.75
290
  ),
@@ -331,12 +280,8 @@ class FairAdversarialTestSuite:
331
  pass_threshold=0.85
332
  ),
333
  ])
334
-
335
- # =================================================================
336
- # EXTREME LEVEL: Edge cases and breaking points
337
- # Expected: 60-75% performance (degradation is acceptable)
338
- # =================================================================
339
-
340
  tests.extend([
341
  FairTestCase(
342
  name="sensor_noise_5pct",
@@ -352,10 +297,7 @@ class FairAdversarialTestSuite:
352
  description="2x normal defect rate",
353
  category="distribution_robustness",
354
  difficulty="extreme",
355
- env_config_modifier=lambda config: {
356
- **config,
357
- 'prior_belief': 0.06
358
- },
359
  expected_behavior="Adapt to crisis scenario",
360
  pass_threshold=0.60
361
  ),
@@ -386,236 +328,177 @@ class FairAdversarialTestSuite:
386
  pass_threshold=0.60
387
  ),
388
  ])
389
-
390
  return tests
391
-
392
  def run_test_case(self, test_case: FairTestCase, num_episodes: int = 30) -> FairTestResult:
393
- """Run a single fair test case"""
394
-
395
  print(f"\n{'='*80}")
396
  print(f"Running: {test_case.name}")
397
  print(f"Category: {test_case.category} | Difficulty: {test_case.difficulty}")
398
  print(f"Description: {test_case.description}")
399
  print(f"Pass Threshold: {test_case.pass_threshold:.2%}")
400
  print(f"{'='*80}")
401
-
402
- catch_rates = []
403
- rewards = []
404
- step_counts = []
405
-
406
  for episode in range(num_episodes):
407
  if episode == 0 or (episode + 1) % 10 == 0:
408
  print(f" Starting episode {episode + 1}/{num_episodes}...")
409
- # CREATE FRESH ENVIRONMENT (critical!)
410
  env = self.env_factory()
411
-
412
- # Apply GRUStateManager if not already in factory (defensive)
413
  if not isinstance(env, GRUStateManager):
414
  env = GRUStateManager(env, policy=self.model.policy)
415
-
416
- # Apply environment config modifications if specified
417
  if test_case.env_config_modifier:
418
- # Get modified config
419
  base_config = {
420
- 'grid_size': env.unwrapped.config.grid_size,
421
- 'inspection_budget': env.unwrapped.config.inspection_budget,
422
- 'inspection_cost': env.unwrapped.config.inspection_cost,
423
- 'prior_belief': env.unwrapped.config.prior_belief,
424
  }
425
- modified_config = test_case.env_config_modifier(base_config)
426
-
427
- # Apply modifications
428
- for key, value in modified_config.items():
429
  if hasattr(env.unwrapped.config, key):
430
  setattr(env.unwrapped.config, key, value)
431
  if key == 'inspection_budget':
432
  env.unwrapped.current_budget = value
433
-
434
- # Reset environment (wrapper handles GRU reset internally)
435
  obs, info = env.reset()
436
  last_catch_rate = 0.0
437
-
438
- episode_reward = 0
439
  steps = 0
440
  done = False
441
  MAX_STEPS = 5000
 
442
  while not done and steps < MAX_STEPS:
443
- # Apply perturbation if specified
444
  if test_case.perturbation_fn:
445
  obs = test_case.perturbation_fn(obs)
446
-
447
- # Predict (GRUStateManager handles GRU state internally)
448
  action, _ = self.model.predict(obs, deterministic=True)
449
-
450
  if steps == 0:
451
  print(f" [DEBUG] First action: {action}, type: {type(action)}")
452
-
453
- # Step environment
454
  obs, reward, terminated, truncated, info = env.step(action)
455
-
456
  episode_reward += reward
457
  steps += 1
458
  last_catch_rate = info.get('catch_rate', last_catch_rate)
459
  done = terminated or truncated
460
-
461
  if steps % 100 == 0:
462
  print(f" [Step {steps}] budget_left={info.get('remaining_budget', '?')}")
463
-
464
  if steps >= MAX_STEPS:
465
  print(f" ⚠️ Episode hit max steps ({MAX_STEPS})")
 
466
  if episode == 0 or (episode + 1) % 10 == 0:
467
  print(f" ✓ Episode {episode + 1} complete: {steps} steps, reward={episode_reward:.2f}")
468
-
469
- # Record results - read from info BEFORE soft_reset clears detected_defects
470
- catch_rate = last_catch_rate # Use tracked value, not post-reset info
471
-
472
- catch_rates.append(catch_rate)
473
  rewards.append(episode_reward)
474
  step_counts.append(steps)
475
  env.close()
476
-
477
  if (episode + 1) % 10 == 0:
478
- print(f" Episode {episode+1}/{num_episodes} - "
479
- f"Catch Rate: {catch_rate:.3f}, Reward: {episode_reward:.1f}")
480
-
481
  # Compute statistics
482
- avg_catch_rate = np.mean(catch_rates)
483
- std_catch_rate = np.std(catch_rates)
484
- min_catch_rate = np.min(catch_rates)
485
- max_catch_rate = np.max(catch_rates)
486
- avg_reward = np.mean(rewards)
487
- avg_steps = np.mean(step_counts)
488
-
489
- pass_status = avg_catch_rate >= test_case.pass_threshold
490
-
491
  result = FairTestResult(
492
  test_name=test_case.name,
493
  category=test_case.category,
494
  difficulty=test_case.difficulty,
495
- catch_rate=avg_catch_rate,
496
  avg_reward=avg_reward,
497
  avg_steps=avg_steps,
498
  pass_status=pass_status,
499
- std_catch_rate=std_catch_rate,
500
- min_catch_rate=min_catch_rate,
501
- max_catch_rate=max_catch_rate,
502
  pass_threshold=test_case.pass_threshold
503
  )
504
-
505
  status = "✅ PASS" if pass_status else "❌ FAIL"
506
- print(f"\n{status} - Catch Rate: {avg_catch_rate:.3f} "
507
- f"(threshold: {test_case.pass_threshold:.3f})")
508
- print(f"Stats: μ={avg_catch_rate:.3f}, σ={std_catch_rate:.3f}, "
509
- f"min={min_catch_rate:.3f}, max={max_catch_rate:.3f}")
510
-
511
  return result
512
-
513
  def run_all_tests(self, num_episodes_per_test: int = 30):
514
- """Run all fair test cases"""
515
-
516
  print(f"\n{'#'*80}")
517
  print(f"FAIR ADVERSARIAL VALIDATION TEST SUITE")
518
  print(f"Total Tests: {len(self.test_cases)}")
519
  print(f"Episodes per Test: {num_episodes_per_test}")
 
520
  print(f"{'#'*80}\n")
521
-
522
  start_time = time.time()
523
-
524
  for i, test_case in enumerate(self.test_cases, 1):
 
 
 
 
525
  print(f"\n[Test {i}/{len(self.test_cases)}]")
526
  result = self.run_test_case(test_case, num_episodes_per_test)
527
  self.results.append(result)
528
-
 
 
529
  elapsed = time.time() - start_time
530
-
531
  print(f"\n{'#'*80}")
532
  print(f"FAIR ADVERSARIAL VALIDATION COMPLETE")
533
  print(f"Total Time: {elapsed/60:.1f} minutes")
534
  print(f"{'#'*80}\n")
535
-
536
  self._generate_summary()
537
  self._save_results()
538
  self._generate_visualizations()
539
-
540
  def _generate_summary(self):
541
- """Generate comprehensive summary"""
542
-
543
  print(f"\n{'='*80}")
544
  print("FAIR ADVERSARIAL VALIDATION SUMMARY")
545
  print(f"{'='*80}\n")
546
-
547
- # Overall statistics
 
 
 
548
  total = len(self.results)
549
  passed = sum(1 for r in self.results if r.pass_status)
550
- pass_rate = passed / total if total > 0 else 0
551
  avg_catch = np.mean([r.catch_rate for r in self.results])
552
-
553
  print(f"Overall Pass Rate: {pass_rate:.1%} ({passed}/{total})")
554
- print(f"Average Catch Rate: {avg_catch:.3f}")
555
- print()
556
-
557
- # By difficulty level
558
- print("Performance by Difficulty:")
559
- print("-" * 80)
560
-
561
  for difficulty in ["production", "stress", "extreme"]:
562
  diff_results = [r for r in self.results if r.difficulty == difficulty]
563
  if not diff_results:
564
  continue
565
-
566
- diff_passed = sum(1 for r in diff_results if r.pass_status)
567
- diff_total = len(diff_results)
568
- diff_pass_rate = diff_passed / diff_total
569
- diff_avg_catch = np.mean([r.catch_rate for r in diff_results])
570
-
571
- status = "✅" if diff_pass_rate >= 0.8 else "⚠️" if diff_pass_rate >= 0.5 else "❌"
572
- print(f"{status} {difficulty.upper():12s} | "
573
- f"Pass: {diff_pass_rate:5.1%} ({diff_passed}/{diff_total}) | "
574
- f"Avg Catch: {diff_avg_catch:.3f}")
575
-
576
- print()
577
-
578
- # By category
579
- print("Performance by Category:")
580
- print("-" * 80)
581
-
582
- categories = defaultdict(list)
583
- for r in self.results:
584
- categories[r.category].append(r)
585
-
586
- for category, results in sorted(categories.items()):
587
- cat_avg = np.mean([r.catch_rate for r in results])
588
- cat_passed = sum(1 for r in results if r.pass_status)
589
- cat_total = len(results)
590
-
591
- print(f"{category:25s} | Avg Catch: {cat_avg:.3f} | "
592
- f"Pass: {cat_passed}/{cat_total}")
593
-
594
- print()
595
-
596
- # Failed tests
597
- failed = [r for r in self.results if not r.pass_status]
598
- if failed:
599
- print("Failed Tests:")
600
- print("-" * 80)
601
- for r in failed:
602
- print(f"❌ {r.test_name:30s} | "
603
- f"Catch: {r.catch_rate:.3f} (need {r.pass_threshold:.3f}) | "
604
- f"{r.difficulty}")
605
- else:
606
- print("✅ All tests passed!")
607
-
608
  print(f"\n{'='*80}\n")
609
-
610
  def _save_results(self):
611
- """Save results to JSON"""
612
-
613
  results_dict = {
614
  'summary': {
615
  'total_tests': len(self.results),
616
  'passed_tests': sum(1 for r in self.results if r.pass_status),
617
- 'pass_rate': sum(1 for r in self.results if r.pass_status) / len(self.results),
618
- 'avg_catch_rate': float(np.mean([r.catch_rate for r in self.results])),
619
  'timestamp': time.time()
620
  },
621
  'test_results': [
@@ -635,158 +518,49 @@ class FairAdversarialTestSuite:
635
  for r in self.results
636
  ]
637
  }
638
-
639
  output_file = self.output_dir / 'fair_adversarial_results.json'
640
  with open(output_file, 'w') as f:
641
- json.dump(results_dict, f, indent=2, default=lambda o: bool(o) if hasattr(o, "item") else o)
642
-
643
  print(f"✅ Results saved to: {output_file}")
644
-
645
  def _generate_visualizations(self):
646
- """Generate visualization plots"""
647
-
648
  if not self.results:
649
  return
650
-
651
  sns.set_style("whitegrid")
652
-
653
- # 1. Performance by Difficulty
654
  fig, ax = plt.subplots(figsize=(10, 6))
655
-
656
  difficulties = ['production', 'stress', 'extreme']
657
- diff_data = {d: [] for d in difficulties}
658
-
659
- for r in self.results:
660
- if r.difficulty in diff_data:
661
- diff_data[r.difficulty].append(r.catch_rate)
662
-
663
- positions = []
664
- data_to_plot = []
665
- labels = []
666
-
667
- for i, diff in enumerate(difficulties):
668
- if diff_data[diff]:
669
- positions.append(i)
670
- data_to_plot.append(diff_data[diff])
671
- labels.append(diff.capitalize())
672
-
673
- bp = ax.boxplot(data_to_plot, positions=positions, labels=labels,
674
- patch_artist=True, widths=0.6)
675
-
676
- # Color boxes
677
- colors = ['lightgreen', 'orange', 'lightcoral']
678
- for patch, color in zip(bp['boxes'], colors[:len(bp['boxes'])]):
679
- patch.set_facecolor(color)
680
-
681
- ax.set_ylabel('Catch Rate', fontsize=12)
682
- ax.set_xlabel('Difficulty Level', fontsize=12)
683
- ax.set_title('Fair Adversarial Testing - Performance by Difficulty',
684
- fontsize=14, fontweight='bold')
685
- ax.set_ylim([0, 1.05])
686
- ax.grid(True, alpha=0.3)
687
-
688
- plt.tight_layout()
689
- plt.savefig(self.output_dir / 'performance_by_difficulty.png', dpi=300)
690
- plt.close()
691
-
692
- # 2. Individual Test Results
693
- fig, ax = plt.subplots(figsize=(12, 10))
694
-
695
- test_names = [r.test_name for r in self.results]
696
- catch_rates = [r.catch_rate for r in self.results]
697
- thresholds = [r.pass_threshold for r in self.results]
698
- pass_statuses = [r.pass_status for r in self.results]
699
-
700
- y_pos = np.arange(len(test_names))
701
-
702
- # Plot bars
703
- colors = ['green' if p else 'red' for p in pass_statuses]
704
- bars = ax.barh(y_pos, catch_rates, color=colors, alpha=0.6)
705
-
706
- # Plot thresholds
707
- ax.scatter(thresholds, y_pos, color='blue', marker='|', s=200,
708
- linewidths=3, label='Pass Threshold', zorder=3)
709
-
710
- ax.set_yticks(y_pos)
711
- ax.set_yticklabels(test_names, fontsize=9)
712
- ax.set_xlabel('Catch Rate', fontsize=12)
713
- ax.set_title('Fair Adversarial Testing - Individual Results',
714
- fontsize=14, fontweight='bold')
715
- ax.set_xlim([0, 1.05])
716
- ax.legend()
717
- ax.grid(True, alpha=0.3, axis='x')
718
-
719
- plt.tight_layout()
720
- plt.savefig(self.output_dir / 'individual_test_results.png', dpi=300, bbox_inches='tight')
721
- plt.close()
722
-
723
- # 3. Category Performance
724
- fig, ax = plt.subplots(figsize=(12, 6))
725
-
726
- categories = defaultdict(list)
727
- for r in self.results:
728
- categories[r.category].append(r.catch_rate)
729
-
730
- cat_names = list(categories.keys())
731
- cat_means = [np.mean(rates) for rates in categories.values()]
732
- cat_stds = [np.std(rates) for rates in categories.values()]
733
-
734
- bars = ax.bar(cat_names, cat_means, yerr=cat_stds, capsize=5, alpha=0.7)
735
-
736
- # Color based on performance
737
- for bar, mean in zip(bars, cat_means):
738
- if mean >= 0.85:
739
- bar.set_color('green')
740
- elif mean >= 0.70:
741
- bar.set_color('orange')
742
- else:
743
- bar.set_color('red')
744
-
745
- ax.set_ylabel('Average Catch Rate', fontsize=12)
746
- ax.set_xlabel('Category', fontsize=12)
747
- ax.set_title('Fair Adversarial Testing - Performance by Category',
748
- fontsize=14, fontweight='bold')
749
- ax.set_ylim([0, 1.05])
750
- plt.xticks(rotation=45, ha='right')
751
- ax.grid(True, alpha=0.3, axis='y')
752
-
753
- plt.tight_layout()
754
- plt.savefig(self.output_dir / 'performance_by_category.png', dpi=300)
755
- plt.close()
756
-
757
  print(f"✅ Visualizations saved to: {self.output_dir}")
758
 
759
 
760
  # ============================================================================
761
- # MAIN EXECUTION
762
  # ============================================================================
763
 
764
  def main():
765
- """Example usage"""
766
-
767
- print("""
768
- ╔════════════════════════════════════════════════════════════════╗
769
- ║ ║
770
- ║ FAIR ADVERSARIAL VALIDATION FRAMEWORK ║
771
- ║ ║
772
- ║ Tests realistic, production-relevant scenarios ║
773
- ║ Provides interpretable, actionable results ║
774
- ║ ║
775
- ╚════════════════════════════════════════════════════════════════╝
776
- """)
777
-
778
- print("\nThis framework tests:")
779
- print(" ✅ Realistic sensor noise (not random corruption)")
780
- print(" ✅ Normal fab variation (not 3x jumps)")
781
- print(" ✅ Efficiency improvements (not crisis scenarios)")
782
- print(" ✅ Production-relevant perturbations")
783
- print()
784
- print("Expected performance ranges:")
785
- print(" • Production tests: 90-98% catch rate")
786
- print(" • Stress tests: 75-90% catch rate")
787
- print(" • Extreme tests: 60-75% catch rate")
788
- print()
789
 
790
 
791
  if __name__ == "__main__":
792
- main()
 
14
  import matplotlib.pyplot as plt
15
  import seaborn as sns
16
 
 
17
  from gru_env_wrappers import GRUStateManager
18
 
19
+
20
  # ============================================================================
21
+ # DATA CLASSES
22
  # ============================================================================
23
 
24
  @dataclass
 
32
  env_config_modifier: Optional[Any] = None
33
  expected_behavior: str = ""
34
  pass_threshold: float = 0.80
35
+
36
+
37
  @dataclass
38
  class FairTestResult:
39
  """Results from a fair test case"""
 
52
 
53
 
54
  # ============================================================================
55
+ # PERTURBATION GENERATORS
56
  # ============================================================================
57
 
58
  class FairPerturbations:
59
  """Realistic perturbations that respect model assumptions"""
60
+
61
  @staticmethod
62
  def realistic_sensor_noise(observation: Dict, noise_std: float = 0.01) -> Dict:
 
 
 
 
 
 
63
  obs = observation.copy()
64
  belief_map = obs['belief_map'].copy()
 
 
65
  wafer_mask = obs.get('wafer_map', np.ones_like(belief_map)) > 0
 
66
  noise = np.random.normal(0, noise_std, belief_map.shape)
67
  belief_map = belief_map + (noise * wafer_mask)
 
 
68
  obs['belief_map'] = np.clip(belief_map, 0.0, 1.0)
69
  return obs
70
+
71
  @staticmethod
72
  def calibration_drift(observation: Dict, drift_factor: float = 0.05) -> Dict:
 
 
 
 
 
 
73
  obs = observation.copy()
74
  belief_map = obs['belief_map'].copy()
 
 
75
  drift = 1.0 + np.random.uniform(-drift_factor, drift_factor)
76
  belief_map = belief_map * drift
 
77
  obs['belief_map'] = np.clip(belief_map, 0.0, 1.0)
78
  return obs
79
+
80
  @staticmethod
81
  def local_degradation(observation: Dict, affected_ratio: float = 0.1) -> Dict:
 
 
 
 
 
 
82
  obs = observation.copy()
83
  belief_map = obs['belief_map'].copy()
84
  H, W = belief_map.shape
 
 
85
  if np.random.random() < 0.5:
 
86
  margin = int(H * 0.1)
87
+ belief_map[:margin, :] *= 0.8
88
  belief_map[-margin:, :] *= 0.8
89
  else:
 
90
  belief_map[:H//2, :W//2] *= 0.85
 
91
  obs['belief_map'] = np.clip(belief_map, 0.0, 1.0)
92
  return obs
93
+
94
  @staticmethod
95
  def quantization_noise(observation: Dict, bits: int = 8) -> Dict:
 
 
 
 
 
96
  obs = observation.copy()
97
  belief_map = obs['belief_map'].copy()
 
98
  levels = 2 ** bits
99
  quantized = np.round(belief_map * levels) / levels
 
100
  obs['belief_map'] = quantized
101
  return obs
102
 
 
107
 
108
  class FairEnvModifiers:
109
  """Realistic environment modifications"""
110
+
111
  @staticmethod
112
  def fab_variation_defect_rate(base_rate: float = 0.03, variation: float = 0.3):
 
 
 
 
 
 
113
  return base_rate * (1.0 + np.random.uniform(-variation, variation))
114
+
115
  @staticmethod
116
  def budget_efficiency_test(base_budget: int, efficiency: float = 0.8):
 
 
 
 
 
117
  return int(base_budget * efficiency)
118
+
119
  @staticmethod
120
  def cost_pressure(base_cost: float, multiplier: float = 1.5):
 
 
 
 
 
121
  return base_cost * multiplier
122
 
123
 
124
  # ============================================================================
125
+ # FAIR ADVERSARIAL TEST SUITE (with resume support)
126
  # ============================================================================
127
 
128
  class FairAdversarialTestSuite:
129
+ """Fair, realistic adversarial validation with resume capability"""
130
+
131
+ def __init__(self, model, env_factory, output_dir: str = "./fair_adversarial_results", start_test: int = 1):
 
 
 
 
 
 
132
  self.model = model
133
+ self.env_factory = env_factory
134
  self.output_dir = Path(output_dir)
135
  self.output_dir.mkdir(exist_ok=True, parents=True)
136
+ self.start_test = max(1, start_test)
137
+
138
  self.test_cases = self._define_fair_tests()
139
+ self.results: List[FairTestResult] = self._load_progress()
140
+
141
+ print(f"✅ FairAdversarialTestSuite initialized with {len(self.test_cases)} tests. "
142
+ f"Starting from test #{self.start_test}")
143
+
144
+ def _load_progress(self) -> List[FairTestResult]:
145
+ progress_file = self.output_dir / "validation_progress.json"
146
+ if progress_file.exists():
147
+ try:
148
+ with open(progress_file) as f:
149
+ data = json.load(f)
150
+ last = data.get("last_completed_test", 0)
151
+ print(f"✅ Found previous progress. Last completed test: {last}")
152
+ except Exception as e:
153
+ print(f"Warning: Could not read progress file: {e}")
154
+ return []
155
+
156
+ def _save_progress(self, last_completed: int):
157
+ progress_file = self.output_dir / "validation_progress.json"
158
+ with open(progress_file, 'w') as f:
159
+ json.dump({
160
+ "last_completed_test": last_completed,
161
+ "timestamp": time.time()
162
+ }, f, indent=2)
163
+
164
  def _define_fair_tests(self) -> List[FairTestCase]:
 
 
165
  tests = []
166
+
167
+ # ==================== PRODUCTION ====================
 
 
 
 
168
  tests.extend([
169
  FairTestCase(
170
  name="baseline_clean",
 
190
  description="±10% defect rate variation",
191
  category="distribution_robustness",
192
  difficulty="production",
193
+ env_config_modifier=lambda config: {**config, 'prior_belief': 0.1},
 
 
 
194
  expected_behavior="Handle normal fab variation",
195
  pass_threshold=0.90
196
  ),
 
216
  pass_threshold=0.90
217
  ),
218
  ])
219
+
220
+ # ==================== STRESS ====================
 
 
 
 
221
  tests.extend([
222
  FairTestCase(
223
  name="sensor_noise_2pct",
 
233
  description="±30% defect rate variation",
234
  category="distribution_robustness",
235
  difficulty="stress",
236
+ env_config_modifier=lambda config: {**config, 'prior_belief': 0.1},
 
 
 
237
  expected_behavior="Adapt to significant fab shifts",
238
  pass_threshold=0.75
239
  ),
 
280
  pass_threshold=0.85
281
  ),
282
  ])
283
+
284
+ # ==================== EXTREME ====================
 
 
 
 
285
  tests.extend([
286
  FairTestCase(
287
  name="sensor_noise_5pct",
 
297
  description="2x normal defect rate",
298
  category="distribution_robustness",
299
  difficulty="extreme",
300
+ env_config_modifier=lambda config: {**config, 'prior_belief': 0.06},
 
 
 
301
  expected_behavior="Adapt to crisis scenario",
302
  pass_threshold=0.60
303
  ),
 
328
  pass_threshold=0.60
329
  ),
330
  ])
331
+
332
  return tests
333
+
334
  def run_test_case(self, test_case: FairTestCase, num_episodes: int = 30) -> FairTestResult:
 
 
335
  print(f"\n{'='*80}")
336
  print(f"Running: {test_case.name}")
337
  print(f"Category: {test_case.category} | Difficulty: {test_case.difficulty}")
338
  print(f"Description: {test_case.description}")
339
  print(f"Pass Threshold: {test_case.pass_threshold:.2%}")
340
  print(f"{'='*80}")
341
+
342
+ catch_rates, rewards, step_counts = [], [], []
343
+
 
 
344
  for episode in range(num_episodes):
345
  if episode == 0 or (episode + 1) % 10 == 0:
346
  print(f" Starting episode {episode + 1}/{num_episodes}...")
347
+
348
  env = self.env_factory()
 
 
349
  if not isinstance(env, GRUStateManager):
350
  env = GRUStateManager(env, policy=self.model.policy)
351
+
 
352
  if test_case.env_config_modifier:
 
353
  base_config = {
354
+ 'grid_size': getattr(env.unwrapped.config, 'grid_size', 64),
355
+ 'inspection_budget': getattr(env.unwrapped.config, 'inspection_budget', 3000),
356
+ 'inspection_cost': getattr(env.unwrapped.config, 'inspection_cost', 1.0),
357
+ 'prior_belief': getattr(env.unwrapped.config, 'prior_belief', 0.1),
358
  }
359
+ modified = test_case.env_config_modifier(base_config)
360
+ for key, value in modified.items():
 
 
361
  if hasattr(env.unwrapped.config, key):
362
  setattr(env.unwrapped.config, key, value)
363
  if key == 'inspection_budget':
364
  env.unwrapped.current_budget = value
365
+
 
366
  obs, info = env.reset()
367
  last_catch_rate = 0.0
368
+ episode_reward = 0.0
 
369
  steps = 0
370
  done = False
371
  MAX_STEPS = 5000
372
+
373
  while not done and steps < MAX_STEPS:
 
374
  if test_case.perturbation_fn:
375
  obs = test_case.perturbation_fn(obs)
376
+
 
377
  action, _ = self.model.predict(obs, deterministic=True)
 
378
  if steps == 0:
379
  print(f" [DEBUG] First action: {action}, type: {type(action)}")
380
+
 
381
  obs, reward, terminated, truncated, info = env.step(action)
 
382
  episode_reward += reward
383
  steps += 1
384
  last_catch_rate = info.get('catch_rate', last_catch_rate)
385
  done = terminated or truncated
386
+
387
  if steps % 100 == 0:
388
  print(f" [Step {steps}] budget_left={info.get('remaining_budget', '?')}")
389
+
390
  if steps >= MAX_STEPS:
391
  print(f" ⚠️ Episode hit max steps ({MAX_STEPS})")
392
+
393
  if episode == 0 or (episode + 1) % 10 == 0:
394
  print(f" ✓ Episode {episode + 1} complete: {steps} steps, reward={episode_reward:.2f}")
395
+
396
+ catch_rates.append(last_catch_rate)
 
 
 
397
  rewards.append(episode_reward)
398
  step_counts.append(steps)
399
  env.close()
400
+
401
  if (episode + 1) % 10 == 0:
402
+ print(f" Episode {episode+1}/{num_episodes} - Catch Rate: {last_catch_rate:.3f}, Reward: {episode_reward:.1f}")
403
+
 
404
  # Compute statistics
405
+ avg_catch = float(np.mean(catch_rates))
406
+ std_catch = float(np.std(catch_rates))
407
+ min_catch = float(np.min(catch_rates))
408
+ max_catch = float(np.max(catch_rates))
409
+ avg_reward = float(np.mean(rewards))
410
+ avg_steps = float(np.mean(step_counts))
411
+
412
+ pass_status = avg_catch >= test_case.pass_threshold
413
+
414
  result = FairTestResult(
415
  test_name=test_case.name,
416
  category=test_case.category,
417
  difficulty=test_case.difficulty,
418
+ catch_rate=avg_catch,
419
  avg_reward=avg_reward,
420
  avg_steps=avg_steps,
421
  pass_status=pass_status,
422
+ std_catch_rate=std_catch,
423
+ min_catch_rate=min_catch,
424
+ max_catch_rate=max_catch,
425
  pass_threshold=test_case.pass_threshold
426
  )
427
+
428
  status = "✅ PASS" if pass_status else "❌ FAIL"
429
+ print(f"\n{status} - Catch Rate: {avg_catch:.3f} (threshold: {test_case.pass_threshold:.3f})")
430
+ print(f"Stats: μ={avg_catch:.3f}, σ={std_catch:.3f}, min={min_catch:.3f}, max={max_catch:.3f}")
431
+
 
 
432
  return result
433
+
434
  def run_all_tests(self, num_episodes_per_test: int = 30):
 
 
435
  print(f"\n{'#'*80}")
436
  print(f"FAIR ADVERSARIAL VALIDATION TEST SUITE")
437
  print(f"Total Tests: {len(self.test_cases)}")
438
  print(f"Episodes per Test: {num_episodes_per_test}")
439
+ print(f"Starting from Test #{self.start_test}")
440
  print(f"{'#'*80}\n")
441
+
442
  start_time = time.time()
443
+
444
  for i, test_case in enumerate(self.test_cases, 1):
445
+ if i < self.start_test:
446
+ print(f"⏭️ Skipping Test {i}/{len(self.test_cases)}: {test_case.name}")
447
+ continue
448
+
449
  print(f"\n[Test {i}/{len(self.test_cases)}]")
450
  result = self.run_test_case(test_case, num_episodes_per_test)
451
  self.results.append(result)
452
+
453
+ self._save_progress(i) # Save progress after every completed test
454
+
455
  elapsed = time.time() - start_time
 
456
  print(f"\n{'#'*80}")
457
  print(f"FAIR ADVERSARIAL VALIDATION COMPLETE")
458
  print(f"Total Time: {elapsed/60:.1f} minutes")
459
  print(f"{'#'*80}\n")
460
+
461
  self._generate_summary()
462
  self._save_results()
463
  self._generate_visualizations()
464
+
465
  def _generate_summary(self):
 
 
466
  print(f"\n{'='*80}")
467
  print("FAIR ADVERSARIAL VALIDATION SUMMARY")
468
  print(f"{'='*80}\n")
469
+
470
+ if not self.results:
471
+ print("No results to summarize.")
472
+ return
473
+
474
  total = len(self.results)
475
  passed = sum(1 for r in self.results if r.pass_status)
476
+ pass_rate = passed / total
477
  avg_catch = np.mean([r.catch_rate for r in self.results])
478
+
479
  print(f"Overall Pass Rate: {pass_rate:.1%} ({passed}/{total})")
480
+ print(f"Average Catch Rate: {avg_catch:.3f}\n")
481
+
 
 
 
 
 
482
  for difficulty in ["production", "stress", "extreme"]:
483
  diff_results = [r for r in self.results if r.difficulty == difficulty]
484
  if not diff_results:
485
  continue
486
+ d_passed = sum(1 for r in diff_results if r.pass_status)
487
+ d_total = len(diff_results)
488
+ d_rate = d_passed / d_total
489
+ d_avg = np.mean([r.catch_rate for r in diff_results])
490
+ status = "✅" if d_rate >= 0.8 else "⚠️" if d_rate >= 0.5 else "❌"
491
+ print(f"{status} {difficulty.upper():12s} | Pass: {d_rate:5.1%} ({d_passed}/{d_total}) | Avg Catch: {d_avg:.3f}")
492
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  print(f"\n{'='*80}\n")
494
+
495
  def _save_results(self):
 
 
496
  results_dict = {
497
  'summary': {
498
  'total_tests': len(self.results),
499
  'passed_tests': sum(1 for r in self.results if r.pass_status),
500
+ 'pass_rate': sum(1 for r in self.results if r.pass_status) / len(self.results) if self.results else 0,
501
+ 'avg_catch_rate': float(np.mean([r.catch_rate for r in self.results])) if self.results else 0,
502
  'timestamp': time.time()
503
  },
504
  'test_results': [
 
518
  for r in self.results
519
  ]
520
  }
521
+
522
  output_file = self.output_dir / 'fair_adversarial_results.json'
523
  with open(output_file, 'w') as f:
524
+ json.dump(results_dict, f, indent=2)
525
+
526
  print(f"✅ Results saved to: {output_file}")
527
+
528
  def _generate_visualizations(self):
 
 
529
  if not self.results:
530
  return
531
+
532
  sns.set_style("whitegrid")
533
+
534
+ # Performance by Difficulty
535
  fig, ax = plt.subplots(figsize=(10, 6))
 
536
  difficulties = ['production', 'stress', 'extreme']
537
+ diff_data = {d: [r.catch_rate for r in self.results if r.difficulty == d] for d in difficulties}
538
+ data_to_plot = [diff_data[d] for d in difficulties if diff_data[d]]
539
+ labels = [d.capitalize() for d in difficulties if diff_data[d]]
540
+
541
+ if data_to_plot:
542
+ bp = ax.boxplot(data_to_plot, labels=labels, patch_artist=True)
543
+ colors = ['lightgreen', 'orange', 'lightcoral']
544
+ for patch, color in zip(bp['boxes'], colors[:len(bp['boxes'])]):
545
+ patch.set_facecolor(color)
546
+ ax.set_ylabel('Catch Rate')
547
+ ax.set_title('Performance by Difficulty')
548
+ ax.set_ylim([0, 1.05])
549
+ plt.tight_layout()
550
+ plt.savefig(self.output_dir / 'performance_by_difficulty.png', dpi=300)
551
+ plt.close()
552
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
  print(f"✅ Visualizations saved to: {self.output_dir}")
554
 
555
 
556
  # ============================================================================
557
+ # MAIN (for direct testing)
558
  # ============================================================================
559
 
560
  def main():
561
+ print("FAIR ADVERSARIAL VALIDATION FRAMEWORK")
562
+ print("Use via: python run_fair_adversarial_validation.py --start_test X")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
 
564
 
565
  if __name__ == "__main__":
566
+ main()