CoolFace
Apppublic

YashKikani24/yield-shield

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
App README

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:

Agent strategyTask**Composite**
Random (one probe → SHIP)task10.33
Mediocre (machine cross-ref, no step localization)task10.66
Strong (cheap log-probe scan, exact refinement)task10.93
Alarm-chaser (falls for red herring)task30.54
Pattern-first (reads wafer zones, resists trap)task30.86

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

python
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:

bash
BASE_URL=https://yashkikani24-yieldshield.hf.space

Building & Running

Docker (Recommended)

bash
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

bash
pip install -r requirements.txt
uvicorn server.yieldshield_app:app --host 0.0.0.0 --port 7860

Running the Heuristic Demo

bash
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 seed

Runs 6 heuristic agents across all 3 tasks and prints the composite-score spread.


Environment Details

Episode Structure

Each episode is a multi-step investigation:

  1. 1.POST /reset — generates an episode with a hidden excursion step, wafer pattern, and (on task3) a decoy ghost step
  2. 2.POST /step — agent takes actions (inspect, query logs, cross-reference, diagnose, disposition, escalate)
  3. 3.Episode ends on disposition, escalate, budget exhausted, or hitting max steps
  4. 4.GET /state — reveals the true excursion step, pattern, and correct disposition
  5. 5.GET /grader — scores the episode (works for completed or in-progress episodes)

Tasks

TaskProcess StepsBudgetMax ActionsNoise σGhost StepKey Challenge
task1 (easy)5010.0150.05—Single clean excursion — binary-search wins
task2 (medium)15018.0250.15—Noisy signal, two excursion candidates, force disambiguation
task3 (hard)30022.0350.30yesRed-herring ghost with loud alarms; true excursion upstream

Actions

ActionParametersCostDescription
order_inspectionstep_id: int, tool: MachineType1 / 3Zone summary at a step (3 tokens if wrong tool)
query_process_logstep_id: int, param: ParamID0.5Beta-distributed anomaly score + flag
query_equipment_historytool_id: MachineType1Recent PM days, 24h alarms, failure rate
cross_reference_lotmachine_id: MachineType1Historical failure rate for a machine type
run_diagnosticmachine_id: MachineType0.5Ground-truth machine-type confirmation
disposition_lotdecision: DispositionDecision, root_cause_step: int0Terminal — commits final decision
escalatereason: EscalationReason0Terminal — escalates to senior engineer

Enums (all categorical parameters are strict enums — no free-text strings):

  • —MachineType = LITHO, ETCH, DEPOSITION, POLISH, DOPING
  • —ParamID = TEMPERATURE, CHAMBERPRESSURE, GASFLOW, PLASMA_POWER
  • —DispositionDecision = SCRAP, REWORK, SHIP
  • —EscalationReason = CONTRADICTORYLOGS, OUTOFBUDGET, NOPATTERN

Sending an invalid enum value returns HTTP 422, never 500.

Observation Fields

FieldDescription
yield_map_zones6-key wafer summary (topleft, topright, bottomleft, bottomright, center, edge_avg)
yield_summarydie_yield_pct, total_dies, dead_dies, noise_sigma
process_flowPublic list of 50–300 steps with step_id, machine_type, alarm_count, nominal_params
inspection_historyAppend-only log of every probe, inspection, and diagnostic
budget_remainingTokens left (float)
available_actionsAll action types the agent may invoke
messageHuman-readable engineer's-note string
rewardShaped per-step reward
doneTrue once a terminal action or penalty fires
step_countActions taken so far

Reward

Step-level shaped rewards guide learning. Terminal dispositions are scored by the grader, not the shaped reward.

EventReward
order_inspection within ±5 of true excursion+0.15
order_inspection on the ghost step (task3)−0.10
query_process_log at the exact true step+0.10
query_process_log within ±5 of true step+0.05
query_process_log at the ghost step−0.05
run_diagnostic confirms the true machine type+0.10
cross_reference_lot on the true machine type+0.05
Budget exhausted (episode ends)−0.5
max_steps reached (episode ends)−0.3
escalate with OUTOFBUDGET / CONTRADICTORY_LOGS+0.1
escalate with NO_PATTERN (gave up)−0.3
Terminal disposition0 (scored by grader)

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.

GraderWeightWhat It Measures
Root Cause Accuracy45%`\guess − true\` ≤ 2 → 0.99; ≤ 5 → 0.7; ≤ 10 → 0.4; else 0.01
Disposition Quality30%Exact match → 0.99; SCRAP-when-REWORK → 0.4 (overkill); SHIP-when-SCRAP → 0.01 (catastrophic escape); SHIP-when-REWORK → 0.3; escalation → 0.3 (budget/contradiction) or 0.01 (no-pattern)
Efficiency25%budget_remaining / budget_total, clamped (0.01, 0.99); 0.01 if budget exhausted

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):

AgentTaskRCADispositionEfficiency**Composite**
Random (one probe → SHIP at step 0)task10.010.300.990.34
Mediocre (cross-ref + mid-step guess)task10.400.990.700.66
Strong (log-probe scan + exact refinement)task10.990.990.700.93
Alarm-chaser (falls for red herring)task30.010.990.950.54
Pattern-first (resists herring)task30.990.990.450.86
Dual-probetask20.990.990.690.92

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

MethodEndpointDescription
POST/resetStart a new episode (session_id, seed, task_id)
POST/stepApply one action (session_id, action)
GET/state?session_id=...Full state including hidden ground truth (grader use)
GET/grader?session_id=...Score a session (completed or in-progress). Without session_id: runs a probe episode for task1 and returns its score. Returns task_id, done, composite, score — all strictly in (0.01, 0.99)
GET/grader/{task_id}Primary grader endpoint per task. Without session_id: runs a fresh probe episode (score ≈ 0.255). With ?session_id=...: scores that session. Always returns {"score": float, ...} strictly in (0.01, 0.99). The grader URL in each task config points here.
POST/gradeScore via POST body (all fields optional): {"session_id":"...","task_id":"..."}. Without session_id runs a probe episode. Returns {"score": float, "composite": float, ...}
GET/tasksList all 3 tasks, each with their grader block
GET/tasks/{task_id}Config for a single task (task1, task2, task3)
GET/healthHealth check — returns {"status": "healthy"}
DELETE/session?session_id=...Drop a session's environment

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 file

Deployment Specs

Value
Compute2 vCPU / 4GB RAM, no GPU required
Port7860
FrameworkFastAPI + Uvicorn + Pydantic v2
InterfaceOpenEnv-compliant HTTP (reset / step / state)
DeploymentDocker on Hugging Face Spaces

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


Agent Interaction Flow

mermaid
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.86

task3-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)