murnanedaniel commited on
Commit
1e0a3c9
·
verified ·
1 Parent(s): a483eeb

sync from main @ f4b392cb

Browse files
Files changed (3) hide show
  1. README.md +45 -7
  2. app.py +365 -0
  3. requirements.txt +3 -0
README.md CHANGED
@@ -1,12 +1,50 @@
1
  ---
2
- title: Colliderml Simulation Form
3
- emoji: 🔥
4
- colorFrom: gray
5
- colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.12.0
 
8
  app_file: app.py
9
- pinned: false
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: ColliderML Simulation
3
+ emoji: ⚛️
4
+ colorFrom: indigo
5
+ colorTo: pink
6
  sdk: gradio
7
+ sdk_version: 5.50.0
8
+ python_version: "3.12"
9
  app_file: app.py
10
+ pinned: true
11
+ license: apache-2.0
12
+ hf_oauth: true
13
  ---
14
 
15
+ # ColliderML Simulation Service
16
+
17
+ Submit custom ColliderML simulation requests without installing anything
18
+ locally. This Space is a thin frontend for the ColliderML backend — it
19
+ sends the same payloads that the `colliderml` pip package sends and
20
+ returns the same results.
21
+
22
+ ## Usage
23
+
24
+ 1. Click **Sign in with HuggingFace** (you'll get 10 free credits on your first sign-in).
25
+ 2. Choose a physics channel, number of events, and pileup.
26
+ 3. Click **Submit**. You'll be given a request ID and an email when the job
27
+ completes.
28
+ 4. Use the **Chat** tab if you'd rather describe what you need in plain English —
29
+ the Claude-powered agent estimates the compute cost, checks your balance,
30
+ and asks for confirmation before submitting.
31
+
32
+ ## Environment variables
33
+
34
+ | Variable | Purpose |
35
+ |-----------------------|--------------------------------------------------------------------------------------------|
36
+ | `COLLIDERML_BACKEND` | Backend URL (default: `https://api.colliderml.com`) |
37
+ | `ANTHROPIC_API_KEY` | Required for the Chat tab. Without it the chat tab shows a "not configured" message. |
38
+
39
+ ## See also
40
+
41
+ - [Event display](https://huggingface.co/spaces/CERN/colliderml-event-display) — visualise existing datasets
42
+ - [Leaderboard](https://huggingface.co/spaces/CERN/colliderml-leaderboard) — benchmarks and credits
43
+ - [`colliderml` pip package](https://pypi.org/project/colliderml/) — same capabilities from the CLI
44
+ - [Remote simulation guide](https://opendatadetector.github.io/ColliderML/guide/remote-simulation)
45
+
46
+ ## Deployment
47
+
48
+ Synced automatically from `spaces/simulation-form/` on the
49
+ [OpenDataDetector/ColliderML](https://github.com/OpenDataDetector/ColliderML)
50
+ repo via the `sync-spaces.yml` workflow.
app.py ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ColliderML Simulation Service — Gradio HuggingFace Space.
3
+
4
+ Two tabs:
5
+ 1. Simulation form — pick channel/events/pileup, submit, track status.
6
+ 2. Chat agent — natural-language interface via Anthropic Claude with
7
+ tool use (calls the same backend under the hood).
8
+
9
+ Authentication is HuggingFace OAuth. The OAuth token is forwarded as a
10
+ bearer token to the backend, which verifies it and does all the real work.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from typing import Optional
17
+
18
+ import gradio as gr
19
+ import requests
20
+
21
+ BACKEND_URL = os.environ.get("COLLIDERML_BACKEND", "https://api.colliderml.com").rstrip("/")
22
+
23
+ CHANNELS = [
24
+ "higgs_portal",
25
+ "ttbar",
26
+ "zmumu",
27
+ "zee",
28
+ "diphoton",
29
+ "jets",
30
+ "susy_gmsb",
31
+ "hidden_valley",
32
+ "zprime",
33
+ "single_muon",
34
+ ]
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Backend helpers
39
+ # ---------------------------------------------------------------------------
40
+ def _headers(token: str) -> dict:
41
+ return {"Authorization": f"Bearer {token}"}
42
+
43
+
44
+ def fetch_me(token: str) -> dict:
45
+ r = requests.get(f"{BACKEND_URL}/v1/me", headers=_headers(token), timeout=20)
46
+ r.raise_for_status()
47
+ return r.json()
48
+
49
+
50
+ def submit_simulation(
51
+ token: str,
52
+ channel: str,
53
+ events: int,
54
+ pileup: int,
55
+ seed: int,
56
+ ) -> dict:
57
+ r = requests.post(
58
+ f"{BACKEND_URL}/v1/simulate",
59
+ json={"channel": channel, "events": events, "pileup": pileup, "seed": seed},
60
+ headers=_headers(token),
61
+ timeout=60,
62
+ )
63
+ if r.status_code >= 400:
64
+ raise RuntimeError(f"Backend error {r.status_code}: {r.text}")
65
+ return r.json()
66
+
67
+
68
+ def fetch_request(token: str, request_id: str) -> dict:
69
+ r = requests.get(
70
+ f"{BACKEND_URL}/v1/requests/{request_id}",
71
+ headers=_headers(token),
72
+ timeout=20,
73
+ )
74
+ r.raise_for_status()
75
+ return r.json()
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # Simulation tab handlers
80
+ # ---------------------------------------------------------------------------
81
+ def login_display(oauth_token: Optional[gr.OAuthToken]):
82
+ if oauth_token is None:
83
+ return "Not signed in. Click **Sign in with HuggingFace** above."
84
+ try:
85
+ me = fetch_me(oauth_token.token)
86
+ except Exception as e:
87
+ return f"Error fetching profile: {e}"
88
+ return (
89
+ f"**Signed in as `{me['hf_username']}`** \n"
90
+ f"Credits: **{me['credits']:.2f}**"
91
+ )
92
+
93
+
94
+ def on_submit(
95
+ channel: str,
96
+ events: int,
97
+ pileup: int,
98
+ seed: int,
99
+ oauth_token: Optional[gr.OAuthToken],
100
+ ):
101
+ if oauth_token is None:
102
+ return "Please sign in with HuggingFace first.", None
103
+ try:
104
+ result = submit_simulation(oauth_token.token, channel, events, pileup, seed)
105
+ except Exception as e:
106
+ return f"Error: {e}", None
107
+
108
+ message = (
109
+ f"**Submitted!** \n"
110
+ f"- Request ID: `{result['request_id']}` \n"
111
+ f"- State: `{result['state']}` \n"
112
+ f"- Est. credits: **{result['credits_charged']:.2f}** \n"
113
+ f"- Est. completion: ~{result['estimated_completion_seconds'] // 60} min \n"
114
+ )
115
+ if result.get("cached"):
116
+ message += "- *This request was deduplicated against a cached result.*\n"
117
+ if result.get("output_hf_repo"):
118
+ message += f"- Output: https://huggingface.co/datasets/{result['output_hf_repo']}\n"
119
+ return message, result["request_id"]
120
+
121
+
122
+ def on_poll(request_id: str, oauth_token: Optional[gr.OAuthToken]):
123
+ if not request_id:
124
+ return "No request ID. Submit a job first."
125
+ if oauth_token is None:
126
+ return "Please sign in with HuggingFace first."
127
+ try:
128
+ data = fetch_request(oauth_token.token, request_id)
129
+ except Exception as e:
130
+ return f"Error: {e}"
131
+ out = (
132
+ f"**Request `{data['id']}`** \n"
133
+ f"- State: `{data['state']}` \n"
134
+ f"- Channel: {data['channel']} \n"
135
+ f"- Events: {data['events']} (pileup={data['pileup']}) \n"
136
+ )
137
+ if data.get("output_hf_repo"):
138
+ out += f"- Output: https://huggingface.co/datasets/{data['output_hf_repo']}\n"
139
+ if data.get("error_message"):
140
+ out += f"- Error: {data['error_message']}\n"
141
+ return out
142
+
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # Chat agent
146
+ # ---------------------------------------------------------------------------
147
+ CHAT_TOOLS = [
148
+ {
149
+ "name": "estimate_compute",
150
+ "description": "Estimate the node-hours (credits) needed for a simulation.",
151
+ "input_schema": {
152
+ "type": "object",
153
+ "properties": {
154
+ "channel": {"type": "string", "enum": CHANNELS},
155
+ "events": {"type": "integer", "minimum": 1, "maximum": 100000},
156
+ "pileup": {"type": "integer", "minimum": 0, "maximum": 200},
157
+ },
158
+ "required": ["channel", "events"],
159
+ },
160
+ },
161
+ {
162
+ "name": "submit_simulation",
163
+ "description": (
164
+ "Actually submit a simulation request to NERSC. Deducts credits from "
165
+ "the user's balance. Only call after the user has confirmed the parameters."
166
+ ),
167
+ "input_schema": {
168
+ "type": "object",
169
+ "properties": {
170
+ "channel": {"type": "string", "enum": CHANNELS},
171
+ "events": {"type": "integer", "minimum": 1, "maximum": 100000},
172
+ "pileup": {"type": "integer", "minimum": 0, "maximum": 200},
173
+ "seed": {"type": "integer", "default": 42},
174
+ },
175
+ "required": ["channel", "events"],
176
+ },
177
+ },
178
+ {
179
+ "name": "check_balance",
180
+ "description": "Check the user's current credit balance.",
181
+ "input_schema": {"type": "object", "properties": {}},
182
+ },
183
+ ]
184
+
185
+
186
+ def _tool_call(name: str, arguments: dict, oauth_token) -> dict:
187
+ """Execute a tool call against the backend, return a result dict."""
188
+ if oauth_token is None:
189
+ return {"error": "Not signed in"}
190
+ try:
191
+ if name == "check_balance":
192
+ return fetch_me(oauth_token.token)
193
+ if name == "estimate_compute":
194
+ # Dry-run cost estimate that mirrors the backend's cap module.
195
+ from math import ceil
196
+ base = {
197
+ "higgs_portal": 60.0, "ttbar": 90.0, "zmumu": 30.0,
198
+ "zee": 30.0, "diphoton": 30.0, "jets": 45.0,
199
+ "susy_gmsb": 60.0, "hidden_valley": 60.0,
200
+ "zprime": 60.0, "single_muon": 5.0,
201
+ }.get(arguments["channel"], 60.0)
202
+ overhead = 300.0 if arguments["channel"] in (
203
+ "ttbar", "susy_gmsb", "hidden_valley", "zprime"
204
+ ) else 0.0
205
+ pu = arguments.get("pileup", 0)
206
+ seconds = overhead + base * arguments["events"] * (1 + pu / 50)
207
+ credits = round(seconds / 3600, 2)
208
+ return {
209
+ "channel": arguments["channel"],
210
+ "events": arguments["events"],
211
+ "pileup": pu,
212
+ "estimated_credits": credits,
213
+ "estimated_minutes": ceil(seconds / 60),
214
+ }
215
+ if name == "submit_simulation":
216
+ return submit_simulation(
217
+ oauth_token.token,
218
+ arguments["channel"],
219
+ arguments["events"],
220
+ arguments.get("pileup", 0),
221
+ arguments.get("seed", 42),
222
+ )
223
+ except Exception as e:
224
+ return {"error": str(e)}
225
+ return {"error": f"unknown tool {name}"}
226
+
227
+
228
+ def chat_respond(history, message, oauth_token: "gr.OAuthToken | None" = None):
229
+ if not os.environ.get("ANTHROPIC_API_KEY"):
230
+ history.append({"role": "user", "content": message})
231
+ history.append({
232
+ "role": "assistant",
233
+ "content": "Chat agent is not configured on this Space (ANTHROPIC_API_KEY not set).",
234
+ })
235
+ return history, ""
236
+
237
+ try:
238
+ import anthropic
239
+ except ImportError:
240
+ history.append({"role": "user", "content": message})
241
+ history.append({
242
+ "role": "assistant",
243
+ "content": "anthropic package not installed on this Space.",
244
+ })
245
+ return history, ""
246
+
247
+ history = history + [{"role": "user", "content": message}]
248
+
249
+ client = anthropic.Anthropic()
250
+ system = (
251
+ "You are a helpful assistant for researchers using ColliderML. "
252
+ "You can estimate compute costs, check the user's credit balance, "
253
+ "and submit simulation requests on their behalf. Always confirm "
254
+ "cost and parameters before calling submit_simulation. "
255
+ "1 credit ≈ 100 pu0 events or 20 pu200 events. Users start with 10 credits."
256
+ )
257
+
258
+ messages = [
259
+ {"role": m["role"], "content": m["content"]}
260
+ for m in history if m["role"] in ("user", "assistant")
261
+ ]
262
+
263
+ # Tool-use loop
264
+ for _ in range(5):
265
+ response = client.messages.create(
266
+ model="claude-sonnet-4-6",
267
+ max_tokens=1024,
268
+ system=system,
269
+ tools=CHAT_TOOLS,
270
+ messages=messages,
271
+ )
272
+ if response.stop_reason != "tool_use":
273
+ text = "".join(
274
+ block.text for block in response.content if hasattr(block, "text")
275
+ )
276
+ history.append({"role": "assistant", "content": text})
277
+ return history, ""
278
+
279
+ tool_results = []
280
+ for block in response.content:
281
+ if block.type == "tool_use":
282
+ result = _tool_call(block.name, block.input, oauth_token)
283
+ tool_results.append({
284
+ "type": "tool_result",
285
+ "tool_use_id": block.id,
286
+ "content": str(result),
287
+ })
288
+
289
+ messages.append({"role": "assistant", "content": response.content})
290
+ messages.append({"role": "user", "content": tool_results})
291
+
292
+ history.append({
293
+ "role": "assistant",
294
+ "content": "(hit tool-use iteration limit)",
295
+ })
296
+ return history, ""
297
+
298
+
299
+ # ---------------------------------------------------------------------------
300
+ # UI
301
+ # ---------------------------------------------------------------------------
302
+ with gr.Blocks(
303
+ title="ColliderML Simulation Service",
304
+ theme=gr.themes.Soft(primary_hue="indigo"),
305
+ ) as demo:
306
+ gr.Markdown(
307
+ """
308
+ # ColliderML Simulation Service
309
+
310
+ Submit custom particle physics simulations to NERSC Perlmutter,
311
+ without installing anything locally.
312
+ """
313
+ )
314
+
315
+ login_btn = gr.LoginButton()
316
+ status_md = gr.Markdown("Not signed in.")
317
+
318
+ demo.load(fn=login_display, inputs=None, outputs=status_md)
319
+
320
+ with gr.Tab("Simulate"):
321
+ with gr.Row():
322
+ with gr.Column(scale=2):
323
+ channel = gr.Dropdown(CHANNELS, value="higgs_portal", label="Physics channel")
324
+ events = gr.Number(value=10, minimum=1, maximum=100_000, label="Events", precision=0)
325
+ pileup = gr.Slider(0, 200, value=0, step=10, label="Pileup")
326
+ seed = gr.Number(value=42, precision=0, label="Seed")
327
+ submit_btn = gr.Button("Submit", variant="primary")
328
+ with gr.Column(scale=3):
329
+ submit_output = gr.Markdown()
330
+ last_request_id = gr.Textbox(label="Last request ID", interactive=False)
331
+ poll_btn = gr.Button("Refresh status")
332
+ poll_output = gr.Markdown()
333
+
334
+ submit_btn.click(
335
+ fn=on_submit,
336
+ inputs=[channel, events, pileup, seed],
337
+ outputs=[submit_output, last_request_id],
338
+ )
339
+ poll_btn.click(
340
+ fn=on_poll,
341
+ inputs=[last_request_id],
342
+ outputs=poll_output,
343
+ )
344
+
345
+ with gr.Tab("Chat"):
346
+ gr.Markdown(
347
+ """
348
+ Describe what you need in plain English — the agent will estimate
349
+ compute, check your balance, and submit the request after you confirm.
350
+
351
+ Example: *"I need 1000 ttbar events with pileup 200 for jet tagging."*
352
+ """
353
+ )
354
+ chatbot = gr.Chatbot(type="messages", height=450)
355
+ msg = gr.Textbox(label="Message", placeholder="Ask me to simulate something...")
356
+ clear = gr.Button("Clear")
357
+
358
+ # Gradio auto-injects gr.OAuthToken into any annotated default arg
359
+ # at runtime; do NOT list it in the explicit inputs.
360
+ msg.submit(chat_respond, [chatbot, msg], [chatbot, msg])
361
+ clear.click(lambda: ([], ""), outputs=[chatbot, msg])
362
+
363
+
364
+ if __name__ == "__main__":
365
+ demo.launch(server_name="0.0.0.0", server_port=7860)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio[oauth]>=5.50
2
+ requests>=2.28
3
+ anthropic>=0.40