YashKikani24/yield-shield
YieldShield
A multi-step OpenEnv environment simulating semiconductor fab yield excursion investigations. The agent plays a yield engineer chasing a hidden process excursion across hundreds of process steps — it must localize the root cause with costly inspections, then disposition the lot (SCRAP / REWORK / SHIP) before running out of a token budget.
Built for the Meta PyTorch × Scaler School of Technology OpenEnv Hackathon.
🔗 Live on Hugging Face Spaces: YashKikani24/YieldShield 📦 Source on GitHub: Yash-Kikani-24/YieldShield
Grader Variance (the Key Signal)
Plugging different agents into the same environment produces meaningfully different scores — exactly what a benchmark should do:
Three-tier staircase on a single task: 0.33 → 0.66 → 0.93. Task 3 trap variance: 0.54 vs 0.86. This spread comes purely from agent reasoning quality, not luck.
What Makes This Different
Most RL benchmarks evaluate on static maps or toy puzzles. This environment tests active industrial decision-making under partial observability:
- Red-herring trap: Task 3 seeds a "ghost step" that blares loud equipment alarms and anomalous process-log readings but has zero yield impact. Agents that chase the loudest signal get 0 on root cause — agents that read the wafer-pattern first converge on the real step.
- Asymmetric disposition costs: Shipping a lot that should have been scrapped (customer escape) scores 0.0; over-cautious scrapping scores 0.4. Mirrors real fab economics where one bad chip at a customer costs more than wasted silicon.
- Resource constraints: 10–22 token budget across 7 action types, each costing 0.5–3 tokens.
- Reproducible ground truth: Every episode's
true_excursion_step,true_excursion_pattern,true_disposition, and ghost step are deterministic functions of a single seed. - Three difficulty tiers: 50 / 150 / 300 process steps with increasing Gaussian noise and, on hard, the ghost-step trap.
Quick Start
import requests
BASE_URL = "http://localhost:7860"
# Start a new episode
r = requests.post(f"{BASE_URL}/reset", json={
"session_id": "demo-1",
"seed": 42,
"task_id": "task1",
})
obs = r.json()
print(f"Lot loaded: {obs['message']}")
# Lot loaded: New lot loaded. Task=task1. 50 process steps. Wafer die-yield=78.1%. Budget=10.0 tokens. Investigate and disposition.
# Cross-reference a machine type (1 token)
r = requests.post(f"{BASE_URL}/step", json={
"session_id": "demo-1",
"action": {
"action_type": "cross_reference_lot",
"parameters": {"machine_id": "LITHO"},
},
})
# Confirm with a diagnostic (0.5 tokens, ground-truth)
r = requests.post(f"{BASE_URL}/step", json={
"session_id": "demo-1",
"action": {
"action_type": "run_diagnostic",
"parameters": {"machine_id": "LITHO"},
},
})
# Make a disposition (ends episode)
r = requests.post(f"{BASE_URL}/step", json={
"session_id": "demo-1",
"action": {
"action_type": "disposition_lot",
"parameters": {"decision": "REWORK", "root_cause_step": 35},
},
})
print(f"Reward: {r.json()['reward']}")
# Reveal the hidden ground truth and score the episode
state = requests.get(f"{BASE_URL}/state", params={"session_id": "demo-1"}).json()
print(f"True excursion step: {state['true_excursion_step']}")
scores = requests.get(f"{BASE_URL}/grader", params={"session_id": "demo-1"}).json()
print(f"Composite score: {scores['composite']}")No local setup needed — point your client at the live Space:
BASE_URL=https://yashkikani24-yieldshield.hf.spaceBuilding & Running
Docker (Recommended)
docker build -t yieldShield .
docker run -p 7860:7860 yieldShield
curl http://localhost:7860/health
# {"status":"healthy","environment":"YieldShield","version":"1.0.0"}Local Development
pip install -r requirements.txt
uvicorn server.yieldshield_app:app --host 0.0.0.0 --port 7860Running the Heuristic Demo
python inference_demo.py # spawns its own server
python inference_demo.py --no-server --host http://127.0.0.1:7860 # uses running server
python inference_demo.py --seed 100 # try a different seedRuns 6 heuristic agents across all 3 tasks and prints the composite-score spread.
Environment Details
Episode Structure
Each episode is a multi-step investigation:
POST /reset— generates an episode with a hidden excursion step, wafer pattern, and (on task3) a decoy ghost stepPOST /step— agent takes actions (inspect, query logs, cross-reference, diagnose, disposition, escalate)- Episode ends on disposition, escalate, budget exhausted, or hitting max steps
GET /state— reveals the true excursion step, pattern, and correct dispositionGET /grader— scores the episode (works for completed or in-progress episodes)
Tasks
Actions
Enums (all categorical parameters are strict enums — no free-text strings):
MachineType= LITHO, ETCH, DEPOSITION, POLISH, DOPINGParamID= TEMPERATURE, CHAMBERPRESSURE, GASFLOW, PLASMA_POWERDispositionDecision= SCRAP, REWORK, SHIPEscalationReason= CONTRADICTORYLOGS, OUTOFBUDGET, NOPATTERN
Sending an invalid enum value returns HTTP 422, never 500.
Observation Fields
Reward
Step-level shaped rewards guide learning. Terminal dispositions are scored by the grader, not the shaped reward.
Grading
The composite grader scores an episode — completed or in-progress. All scores are clamped to (0.01, 0.99) — the platform requires strictly between 0 and 1. Incomplete episodes (no terminal action yet) return scores where agent_final_step_guess and agent_final_disposition are treated as None/incorrect, yielding a low but valid score.
composite = clamp(0.45 × rca + 0.30 × disposition + 0.25 × efficiency, 0.01, 0.99)
Baseline Scores
Six heuristic agents across all three tasks (from inference_demo.py):
Three-tier variance on task1 alone: 0.33 → 0.66 → 0.93. Task 3 red-herring variance: 0.54 vs 0.86. Overall spread: 0.60 — satisfying the random ≈ 0.2-0.3, mediocre ≈ 0.5-0.7, strong ≈ 0.85+ requirement.
The strong agent uses only query_process_log (0.5 tokens each) for a coarse scan + refinement — 7 probes total, leaving 70% budget for efficiency credit. The key insight: cross_reference_lot (1 token each) is for machine-type identification on Task 3; on Task 1 (clean single-excursion), the cheap log probes reach the true step faster and cheaper.
API Reference
Project Structure
YieldShield/
├── fab_sim/ # Synthetic fab simulation
│ ├── wafer_map.py # 16×16 pattern generators (RING, SCRATCH, CENTER_HEAVY, ...)
│ ├── process_flow.py # L/E/D-cycle process step roster generator
│ └── excursion_gen.py # EpisodeFactory — combines pattern + flow + hidden ground truth
├── server/
│ ├── models.py # Pydantic OpenEnv models + all action enums
│ ├── environment.py # YieldShieldEnvironment (reset / step / state)
│ ├── action_handlers.py # One handler per ActionType + dispatcher
│ └── yieldShield_app.py # FastAPI server (6 endpoints, port 7860)
├── graders/
│ └── composite_grader.py # 3-component weighted grader
├── tasks/
│ ├── task1_easy.json # 50 steps, budget 10, σ=0.05
│ ├── task2_medium.json # 150 steps, budget 18, σ=0.15
│ └── task3_hard.json # 300 steps, budget 22, σ=0.30, ghost-step enabled
├── inference.py # 5 heuristic agents + variance demo
├── Dockerfile # HF Spaces entrypoint (port 7860)
├── requirements.txt # Python runtime deps
├── info.txt # Full project explanation
├── test.txt # All test commands (unit / HTTP / quality checks / docker)
└── README.md # This fileDeployment Specs
Limitations
- No mid-episode dynamics: The wafer map is frozen at reset — excursions can't evolve or cascade into new defects during the investigation.
- Single lot per episode: No multi-lot batching, no concurrent investigations.
- Fixed action cost model: Tool costs are static, not noise- or history-dependent.
- Categorical excursion patterns only: Six spatial pattern families; no continuous interpolation between them.
- No vision modality: Agents see the 6-zone wafer summary, not the raw 16×16 grid.
Use Cases
- LLM Evaluation: Benchmark industrial reasoning and red-herring resistance on multi-step investigation tasks.
- RL Agent Training: Multi-step action spaces with shaped rewards, asymmetric terminal costs, and ground-truth grading.
- Agentic Tool-Use Research: Every action is a typed enum-parameter tool call — ideal for function-calling / tool-use evaluations.
- Operations Research Simulation: Budget-constrained diagnostic decision-making under partial observability.
Tech Stack
FastAPI · Uvicorn · Pydantic v2 · NumPy · SciPy · Docker · Hugging Face Spaces · OpenEnv
Learn More
- OpenEnv Documentation
- Meta PyTorch × Scaler OpenEnv Hackathon
info.md— detailed project writeuptest.md— every test command used to verify the build
Agent Interaction Flow
sequenceDiagram
participant Agent
participant YieldShield as YieldShield (Port 7860)
Agent->>YieldShield: POST /reset {task_id: "task3", seed: 45}
YieldShield-->>Agent: YieldObservation: 6-zone yield map + 300-step process flow
Note over Agent,YieldShield: Agent reads yield_map_zones — identifies spatial pattern
Agent->>YieldShield: POST /step {cross_reference_lot, machine_id: "ETCH"}
YieldShield-->>Agent: failure_rate: 7/10 (elevated — true machine)
Agent->>YieldShield: POST /step {cross_reference_lot, machine_id: "LITHO"}
YieldShield-->>Agent: failure_rate: 1/10 (ghost machine looks healthy here)
Agent->>YieldShield: POST /step {run_diagnostic, machine_id: "ETCH"}
YieldShield-->>Agent: confirmed_fault: true
Agent->>YieldShield: POST /step {query_process_log, step_id: 167, param: "TEMPERATURE"}
YieldShield-->>Agent: anomaly_score: 0.91, flag: CRITICAL, reward: +0.10
Agent->>YieldShield: POST /step {disposition_lot, decision: "SCRAP", root_cause_step: 167}
YieldShield-->>Agent: YieldObservation (done: true)
Agent->>YieldShield: GET /grader?session_id=...
YieldShield-->>Agent: {rca: 1.0, disp: 1.0, eff: 0.45, composite: 0.86}Sample Evaluation Run
Actual output of python inference_demo.py --seed 42:
--- task1-random task=task1 seed=42 ---
actions taken : 2
budget remaining : 9.50 / 10.0
agent guess / truth : step 0 vs 35
disposition guess/truth: SHIP vs REWORK
scores : {"root_cause_accuracy": 0.0, "disposition_quality": 0.3, "efficiency": 0.95, "composite": 0.3275}
--- task1-mediocre task=task1 seed=42 ---
actions taken : 4
budget remaining : 7.00 / 10.0
agent guess / truth : step 26 vs 35
disposition guess/truth: REWORK vs REWORK
scores : {"root_cause_accuracy": 0.4, "disposition_quality": 1.0, "efficiency": 0.7, "composite": 0.655}
--- task1-strong task=task1 seed=42 ---
actions taken : 7
budget remaining : 7.00 / 10.0
agent guess / truth : step 35 vs 35
disposition guess/truth: REWORK vs REWORK
scores : {"root_cause_accuracy": 1.0, "disposition_quality": 1.0, "efficiency": 0.7, "composite": 0.925}
--- task3-bad task=task3 seed=45 ---
actions taken : 2
budget remaining : 21.00 / 22.0
agent guess / truth : step 213 vs 167 (ghost=213)
disposition guess/truth: SCRAP vs SCRAP
scores : {"root_cause_accuracy": 0.0, "disposition_quality": 1.0, "efficiency": 0.9545, "composite": 0.5386}
--- task3-good task=task3 seed=45 ---
actions taken : 20
budget remaining : 10.00 / 22.0
agent guess / truth : step 167 vs 167 (ghost=213)
disposition guess/truth: SCRAP vs SCRAP
scores : {"root_cause_accuracy": 1.0, "disposition_quality": 1.0, "efficiency": 0.4545, "composite": 0.8636}
=================== SUMMARY ===================
task1-random task=task1 rca=0.00 disp=0.30 eff=0.95 COMPOSITE=0.33
task1-mediocre task=task1 rca=0.40 disp=1.00 eff=0.70 COMPOSITE=0.66
task1-strong task=task1 rca=1.00 disp=1.00 eff=0.70 COMPOSITE=0.93
task2-dual task=task2 rca=1.00 disp=1.00 eff=0.69 COMPOSITE=0.92
task3-bad task=task3 rca=0.00 disp=1.00 eff=0.95 COMPOSITE=0.54
task3-good task=task3 rca=1.00 disp=1.00 eff=0.45 COMPOSITE=0.86task3-bad commits to step 213 — the ghost step with the loudest alarms. task3-good ignores it and finds step 167, the true excursion, via cross-reference + log-probe scan.
FAQ / Design Rationale
Q: Why do `order_inspection` responses only show real zones at the EXACT true step? Spatial evidence is intentionally a discrete signal. The shaped reward (+0.15 within ±5 steps) gives a gradient; the actual zone data appears only at exact match. This forces the agent to combine multiple probe types instead of greedy-climbing on inspection severity.
Q: Why isn't `order_inspection` the most efficient action? It costs 1–3 tokens and gives no useful signal away from the true step. The cheap query_process_log (0.5 tokens) returns a Beta-distributed anomaly score everywhere. On Task 1, a 7-probe log scan costs 3.5 tokens and finds the true step, leaving 65% of the budget for the efficiency sub-score. On Task 3, combine cross_reference_lot (machine-type identification) with log probes to avoid the ghost-step trap.
Q: Why is Beta(2,8) background noise spiked to Beta(5,5) at 12% probability? Without occasional mid-range noise, a single CLEAN reading would be conclusive. The 12% spike forces corroboration across multiple probes rather than halting on the first low anomaly score.
Q: Why is per-query RNG seeded per-step rather than using a global env RNG? So two replays of the same action sequence produce byte-identical probe values regardless of step ordering. Required for debugging, reproducibility validation, and off-policy evaluation.
Q: Why is SHIP-when-SCRAP scored 0.01 instead of partial credit? It mirrors real fab economics: one bad chip reaching a customer triggers recalls and reputation damage — costs that dwarf any wasted silicon from over-scrapping. The asymmetric matrix creates a hard incentive to err on the side of caution. The score is 0.01 rather than 0.0 because the platform requires all scores to be strictly between 0 and 1.
License
BSD-3-Clause License (see OpenEnv LICENSE)
