cernenv-trainer / server /tasks /scenarios.py
anugrah55's picture
Update CERNenv Space
5f78183 verified
"""Built-in physics scenarios + procedural sampling.
Each scenario binds a hidden ``LatentParticle`` truth and a public
``TaskSpec`` (search window, available channels, resource budgets, expected
findings, paper references). Curated scenarios are inspired by famous LHC
discoveries; procedural ones randomise mass, channel, width and budgets to
build a curriculum.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional
import numpy as np
from models import (
DetectorChannel,
ExpectedFinding,
PaperReference,
TOOL_REGISTRY,
TaskSpec,
)
from server.simulator.latent_state import (
DetectorState,
FullLatentState,
LatentParticle,
ResourceState,
)
@dataclass
class Scenario:
name: str
difficulty: str
task: TaskSpec
latent: FullLatentState
def fresh_latent(self) -> FullLatentState:
# Pydantic deep-copy so the env can mutate freely
return self.latent.model_copy(deep=True)
# ── Curated, story-driven scenarios ──────────────────────────────────────
def _higgs_like_scenario() -> Scenario:
particle = LatentParticle(
name="HiggsLike",
mass_gev=125.0,
width_gev=0.004,
spin=0,
parity="+",
cross_section_fb=55.0,
decay_branching={
"diphoton": 0.0023,
"dilepton_ee": 0.00003,
"dilepton_mumu": 0.00022,
"four_lepton": 0.000125,
"bb": 0.58,
"dijet": 0.30,
},
primary_channel="diphoton",
)
detector = DetectorState(
detector_resolution_gev=1.5,
pileup_mu=30.0,
trigger_efficiency=0.85,
)
resources = ResourceState(
budget_total_musd=120.0,
luminosity_total_fb=300.0,
time_limit_days=365.0,
)
latent = FullLatentState(
particle=particle,
detector=detector,
resources=resources,
rng_seed=125,
)
task = TaskSpec(
problem_statement=(
"An anomalous excess at ~125 GeV is rumoured in early 13 TeV runs. "
"Plan a campaign to confirm or refute a Standard-Model Higgs-like scalar. "
"Pick channels, allocate luminosity, fit, and submit a calibrated discovery claim."
),
target_collider="LHC",
mass_search_window_gev=[100.0, 200.0],
budget_limit_musd=120.0,
luminosity_budget_fb=300.0,
time_limit_days=365.0,
prior_observations=[
"Earlier Tevatron data shows a mild diphoton excess near 125 GeV.",
"ATLAS/CMS rumour mills suggest a 4ℓ excess at low mass.",
],
success_criteria=[
"Identify a resonance within 1 GeV of the truth.",
"Reach ≥5σ local significance.",
"Submit confidence consistent with calibration.",
],
paper_references=[
PaperReference(
title="Observation of a new particle in the search for the SM Higgs boson",
arxiv_id="1207.7214",
doi="10.1016/j.physletb.2012.08.020",
),
],
expected_findings=[
ExpectedFinding(finding="Diphoton resonance at ~125 GeV", category="discovery"),
ExpectedFinding(finding="Spin-0, even parity", category="property"),
],
difficulty="medium",
available_tools=list(TOOL_REGISTRY.keys()),
)
return Scenario(name="higgs_like_125", difficulty="medium", task=task, latent=latent)
def _hidden_zprime_scenario() -> Scenario:
particle = LatentParticle(
name="ZPrime",
mass_gev=600.0,
width_gev=18.0,
spin=1,
parity="-",
cross_section_fb=12.0,
decay_branching={
"diphoton": 0.0,
"dilepton_ee": 0.04,
"dilepton_mumu": 0.04,
"four_lepton": 0.0,
"bb": 0.20,
"dijet": 0.70,
},
primary_channel="dilepton_mumu",
)
detector = DetectorState(
detector_resolution_gev=8.0,
pileup_mu=45.0,
trigger_efficiency=0.78,
qcd_background_strength=1.2,
)
resources = ResourceState(
budget_total_musd=140.0,
luminosity_total_fb=200.0,
time_limit_days=400.0,
)
latent = FullLatentState(
particle=particle, detector=detector, resources=resources, rng_seed=600,
)
task = TaskSpec(
problem_statement=(
"Run-2 dilepton spectra hint at a high-mass excess. Hunt for a heavy "
"Z'-like vector resonance and characterise spin-1, parity-odd hypothesis."
),
mass_search_window_gev=[300.0, 1500.0],
budget_limit_musd=140.0,
luminosity_budget_fb=200.0,
time_limit_days=400.0,
prior_observations=[
"High-pT dilepton tail shows a 2.7σ shoulder near 600 GeV.",
"Dijet smooth-fit residuals consistent with the same window.",
],
success_criteria=[
"Identify a high-mass dilepton/dijet resonance.",
"Constrain spin to be vector (1).",
"Report calibrated mass within 5% and ≥4σ significance.",
],
paper_references=[
PaperReference(
title="Search for high-mass dilepton resonances at the LHC",
arxiv_id="1903.06248",
),
],
expected_findings=[
ExpectedFinding(finding="Heavy Z'-like dilepton resonance", category="discovery"),
ExpectedFinding(finding="Spin-1, parity-odd", category="property"),
],
difficulty="hard",
available_tools=list(TOOL_REGISTRY.keys()),
)
return Scenario(name="hidden_zprime_600", difficulty="hard", task=task, latent=latent)
def _diboson_resonance_scenario() -> Scenario:
particle = LatentParticle(
name="Graviton",
mass_gev=750.0,
width_gev=45.0,
spin=2,
parity="+",
cross_section_fb=6.0,
decay_branching={
"diphoton": 0.06,
"dilepton_ee": 0.005,
"dilepton_mumu": 0.005,
"four_lepton": 0.001,
"bb": 0.15,
"dijet": 0.70,
},
primary_channel="diphoton",
)
detector = DetectorState(
detector_resolution_gev=12.0,
pileup_mu=50.0,
trigger_efficiency=0.80,
)
resources = ResourceState(
budget_total_musd=110.0,
luminosity_total_fb=180.0,
time_limit_days=350.0,
)
latent = FullLatentState(
particle=particle, detector=detector, resources=resources, rng_seed=750,
)
task = TaskSpec(
problem_statement=(
"A faint γγ excess at 750 GeV stirred the field briefly in 2015-2016. "
"Re-investigate with the modern luminosity budget and decide if it is "
"real or a fluctuation."
),
mass_search_window_gev=[400.0, 1200.0],
budget_limit_musd=110.0,
luminosity_budget_fb=180.0,
time_limit_days=350.0,
prior_observations=[
"Public CMS/ATLAS data show a 2-3σ diphoton bump near 750 GeV.",
"Theory papers proposed graviton, scalar singlet, and SM-fluctuation explanations.",
],
success_criteria=[
"Decide between discovery and fluctuation with calibrated confidence.",
],
paper_references=[
PaperReference(
title="Search for resonant production of high-mass diphoton pairs",
arxiv_id="1606.04093",
),
],
expected_findings=[
ExpectedFinding(finding="Possible diphoton resonance near 750 GeV", category="discovery"),
],
difficulty="hard",
available_tools=list(TOOL_REGISTRY.keys()),
)
return Scenario(name="diphoton_750", difficulty="hard", task=task, latent=latent)
def _easy_diphoton_scenario() -> Scenario:
"""Generous budgets, narrow scalar, single obvious channel."""
particle = LatentParticle(
name="EasyScalar",
mass_gev=160.0,
width_gev=0.5,
spin=0,
parity="+",
cross_section_fb=120.0,
decay_branching={
"diphoton": 0.05,
"dilepton_ee": 0.001,
"dilepton_mumu": 0.005,
"four_lepton": 0.0001,
"bb": 0.50,
"dijet": 0.30,
},
primary_channel="diphoton",
)
detector = DetectorState(
detector_resolution_gev=2.0,
pileup_mu=20.0,
trigger_efficiency=0.9,
)
resources = ResourceState(
budget_total_musd=200.0,
luminosity_total_fb=400.0,
time_limit_days=500.0,
)
latent = FullLatentState(
particle=particle, detector=detector, resources=resources, rng_seed=160,
)
task = TaskSpec(
problem_statement=(
"Tutorial scenario: discover a narrow scalar that decays cleanly to "
"two photons. Resources are abundant; focus on running a clean pipeline."
),
mass_search_window_gev=[80.0, 300.0],
budget_limit_musd=200.0,
luminosity_budget_fb=400.0,
time_limit_days=500.0,
success_criteria=[
"Identify the diphoton peak and submit a calibrated 5σ claim.",
],
expected_findings=[
ExpectedFinding(finding="Diphoton scalar near 160 GeV", category="discovery"),
],
difficulty="easy",
available_tools=list(TOOL_REGISTRY.keys()),
)
return Scenario(name="easy_diphoton_160", difficulty="easy", task=task, latent=latent)
CURATED_SCENARIOS: List[Scenario] = [
_easy_diphoton_scenario(),
_higgs_like_scenario(),
_hidden_zprime_scenario(),
_diboson_resonance_scenario(),
]
# ── Procedural sampler ───────────────────────────────────────────────────
_DIFFICULTY_TIERS = {
"easy": {"mass_lo": 90.0, "mass_hi": 250.0, "xsec_lo": 80.0, "xsec_hi": 150.0, "res": 1.5, "budget": 200.0, "lumi": 400.0},
"medium": {"mass_lo": 100.0, "mass_hi": 600.0, "xsec_lo": 25.0, "xsec_hi": 80.0, "res": 3.0, "budget": 150.0, "lumi": 300.0},
"hard": {"mass_lo": 250.0, "mass_hi": 1500.0, "xsec_lo": 5.0, "xsec_hi": 25.0, "res": 8.0, "budget": 110.0, "lumi": 200.0},
}
def _procedural_scenario(difficulty: str, rng: np.random.Generator) -> Scenario:
tier = _DIFFICULTY_TIERS.get(difficulty, _DIFFICULTY_TIERS["medium"])
mass = float(rng.uniform(tier["mass_lo"], tier["mass_hi"]))
xsec = float(rng.uniform(tier["xsec_lo"], tier["xsec_hi"]))
spin = int(rng.choice([0, 1, 2]))
parity = str(rng.choice(["+", "-"]))
primary = str(rng.choice([c.value for c in DetectorChannel]))
branching = {c.value: 0.001 for c in DetectorChannel}
branching[primary] = float(rng.uniform(0.02, 0.6))
# normalise so it sums to ~1
total = sum(branching.values())
branching = {k: v / total for k, v in branching.items()}
particle = LatentParticle(
name=f"Mystery_{int(mass)}GeV",
mass_gev=mass,
width_gev=float(rng.uniform(0.5, 30.0) if difficulty != "easy" else rng.uniform(0.05, 2.0)),
spin=spin,
parity=parity,
cross_section_fb=xsec,
decay_branching=branching,
primary_channel=primary,
)
detector = DetectorState(
detector_resolution_gev=tier["res"],
pileup_mu=float(rng.uniform(20.0, 60.0)),
trigger_efficiency=float(rng.uniform(0.7, 0.92)),
qcd_background_strength=float(rng.uniform(0.8, 1.3)),
)
resources = ResourceState(
budget_total_musd=tier["budget"],
luminosity_total_fb=tier["lumi"],
time_limit_days=float(rng.uniform(300.0, 500.0)),
)
latent = FullLatentState(
particle=particle, detector=detector, resources=resources,
rng_seed=int(rng.integers(1, 1_000_000)),
)
window_lo = max(50.0, mass - 200.0)
window_hi = mass + 300.0
task = TaskSpec(
problem_statement=(
f"Procedural ({difficulty}): a hidden resonance lives somewhere in "
f"[{window_lo:.0f}, {window_hi:.0f}] GeV. Discover and characterise it."
),
mass_search_window_gev=[window_lo, window_hi],
budget_limit_musd=tier["budget"],
luminosity_budget_fb=tier["lumi"],
time_limit_days=resources.time_limit_days,
difficulty=difficulty,
available_tools=list(TOOL_REGISTRY.keys()),
success_criteria=[
"Discover the hidden resonance with a calibrated mass and channel.",
],
)
return Scenario(
name=f"procedural_{difficulty}_{int(mass)}",
difficulty=difficulty,
task=task,
latent=latent,
)
def sample_scenario(
*,
difficulty: Optional[str] = None,
name: Optional[str] = None,
seed: Optional[int] = None,
) -> Scenario:
rng = np.random.default_rng(seed)
if name:
for s in CURATED_SCENARIOS:
if s.name == name:
fresh = Scenario(
name=s.name,
difficulty=s.difficulty,
task=s.task,
latent=s.fresh_latent(),
)
if seed is not None:
fresh.latent.rng_seed = int(seed)
return fresh
if difficulty in {"easy", "medium", "hard"}:
# mix curated + procedural
curated_pool = [s for s in CURATED_SCENARIOS if s.difficulty == difficulty]
if curated_pool and rng.random() < 0.4:
picked = curated_pool[int(rng.integers(0, len(curated_pool)))]
return Scenario(
name=picked.name,
difficulty=picked.difficulty,
task=picked.task,
latent=picked.fresh_latent(),
)
return _procedural_scenario(difficulty, rng)
# default: random difficulty
diff = str(rng.choice(["easy", "medium", "hard"]))
return _procedural_scenario(diff, rng)
__all__ = ["CURATED_SCENARIOS", "Scenario", "sample_scenario"]