Pybunny commited on
Commit
7835d40
·
verified ·
1 Parent(s): 9897c77

Restore pre-BYOM Space

Browse files
Files changed (1) hide show
  1. app.py +23 -226
app.py CHANGED
@@ -1,53 +1,14 @@
1
  """NILMbench HuggingFace Space.
2
 
3
- Three tabs:
4
-
5
- 1. **Built-in example** run the FaustineCNN baseline on a packaged
6
- 6-second 16 kHz V/I frame from UK-DALE House 2.
7
- 2. **Upload V/I frame** – run FaustineCNN on a user-supplied single frame.
8
- 3. **Benchmark your model** – upload a ``.py`` model definition + a ``.pt``
9
- weights file and score it on the dense UK-DALE House 2 benchmark (full
10
- 60,000 frames; the Space defaults to a 500-frame quick check to stay
11
- within the free-tier compute budget).
12
-
13
- Model weights, classes, and recall-constrained cutoffs for the baseline are
14
- pulled from the HF model repo ``Pybunny/nilmbench-faustine`` at startup.
15
  """
16
 
17
- # ----------------------------------------------------------------------
18
- # Monkey-patch the gradio_client schema walker BEFORE importing gradio.
19
- # In gradio 4.44 / gradio_client 1.5 the walker recurses into
20
- # ``additionalProperties`` without checking whether the value is a bool
21
- # (JSON-Schema allows ``additionalProperties: true``), then crashes with
22
- # ``TypeError: argument of type 'bool' is not iterable``. This brings down
23
- # the / route at startup. Patching the two entry points is enough.
24
- # ----------------------------------------------------------------------
25
- import gradio_client.utils as _gc_utils # noqa: E402
26
-
27
- _orig_get_type = _gc_utils.get_type
28
- _orig_to_python = _gc_utils._json_schema_to_python_type
29
-
30
-
31
- def _safe_get_type(schema):
32
- if isinstance(schema, bool):
33
- return "Any" if schema else "None"
34
- return _orig_get_type(schema)
35
 
36
-
37
- def _safe_to_python(schema, defs):
38
- if isinstance(schema, bool):
39
- return "Any" if schema else "None"
40
- return _orig_to_python(schema, defs)
41
-
42
-
43
- _gc_utils.get_type = _safe_get_type
44
- _gc_utils._json_schema_to_python_type = _safe_to_python
45
-
46
- import importlib.util
47
  import json
48
- import sys
49
- import tempfile
50
- import traceback
51
  from pathlib import Path
52
 
53
  import numpy as np
@@ -58,17 +19,11 @@ import gradio as gr
58
  import matplotlib
59
  matplotlib.use("Agg")
60
  import matplotlib.pyplot as plt
61
- from huggingface_hub import hf_hub_download, snapshot_download
62
-
63
- # nilmbench is installed from the companion GitHub repo (see requirements.txt).
64
- from nilmbench.runner import run_user_model
65
- from nilmbench.benchmark import evaluate_dense
66
- from nilmbench.io.report import render_markdown_report
67
 
68
  HERE = Path(__file__).resolve().parent
69
  EXAMPLES_DIR = HERE / "examples"
70
  MODEL_REPO = "Pybunny/nilmbench-faustine"
71
- DATASET_REPO = "Pybunny/nilmbench-ukdale"
72
 
73
  # UK-DALE House 2 calibration constants (from calibration_house_2.cfg).
74
  V_PER_ADC = 1.88296904357e-7
@@ -79,7 +34,7 @@ I_FACTOR = ADC_FULL_SCALE * I_PER_ADC # ~102.5
79
 
80
 
81
  # ----------------------------------------------------------------------
82
- # Baseline model (self-contained for the single-frame demo)
83
  # ----------------------------------------------------------------------
84
  class FaustineCNN(nn.Module):
85
  def __init__(self, n_categories: int):
@@ -134,7 +89,7 @@ MODEL, CLASSES, CUTOFFS = load_assets()
134
 
135
 
136
  # ----------------------------------------------------------------------
137
- # Single-frame inference (tabs 1 and 2)
138
  # ----------------------------------------------------------------------
139
  def _to_2d_image(vi_norm: np.ndarray) -> torch.Tensor:
140
  if vi_norm.shape != (2, 96000):
@@ -146,6 +101,8 @@ def _to_2d_image(vi_norm: np.ndarray) -> torch.Tensor:
146
  def predict(vi_norm: np.ndarray, aggregate_W: float) -> dict[str, float]:
147
  with torch.no_grad():
148
  scores = MODEL(_to_2d_image(vi_norm)).cpu().numpy().squeeze(0)
 
 
149
  shares = scores / (scores.sum() + 1e-9)
150
  raw = shares * float(aggregate_W)
151
  out = {}
@@ -198,6 +155,9 @@ def make_overview_plot(vi_norm: np.ndarray, preds: dict[str, float],
198
  return fig
199
 
200
 
 
 
 
201
  def list_examples() -> list[str]:
202
  if not EXAMPLES_DIR.exists():
203
  return []
@@ -235,202 +195,39 @@ def run_upload(file_obj, aggregate_W: float):
235
  return make_overview_plot(vi, preds, None), preds
236
 
237
 
238
- # ----------------------------------------------------------------------
239
- # Tab 3: full benchmark, with the user's uploaded model
240
- # ----------------------------------------------------------------------
241
- _BENCHMARK_DATA_DIR: Path | None = None
242
-
243
-
244
- def _ensure_benchmark_data() -> Path:
245
- """Snapshot-download the dense House-2 split (cached after first call)."""
246
- global _BENCHMARK_DATA_DIR
247
- if _BENCHMARK_DATA_DIR is not None:
248
- return _BENCHMARK_DATA_DIR
249
- local = snapshot_download(
250
- repo_id=DATASET_REPO,
251
- repo_type="dataset",
252
- allow_patterns=["benchmark/*", "summary.json", "README.md"],
253
- )
254
- _BENCHMARK_DATA_DIR = Path(local)
255
- return _BENCHMARK_DATA_DIR
256
-
257
-
258
- def _import_user_module(file_path: Path, class_name: str):
259
- """Dynamically import a user-uploaded ``.py`` and return the class."""
260
- spec = importlib.util.spec_from_file_location("user_model_module", file_path)
261
- if spec is None or spec.loader is None:
262
- raise ImportError(f"Could not load module from {file_path}")
263
- mod = importlib.util.module_from_spec(spec)
264
- sys.modules["user_model_module"] = mod
265
- spec.loader.exec_module(mod)
266
- if not hasattr(mod, class_name):
267
- raise AttributeError(
268
- f"Uploaded module has no attribute '{class_name}'. "
269
- f"Available: {[n for n in dir(mod) if not n.startswith('_')]}"
270
- )
271
- return getattr(mod, class_name)
272
-
273
-
274
- def _subset_dataset(data_root: Path, max_frames: int) -> Path:
275
- """Make a temporary benchmark/ directory with the first N frames only.
276
-
277
- Lets us cap compute time on the free Space tier.
278
- """
279
- src = data_root / "benchmark"
280
- n_total = int(np.load(src / "x_vi_6s.npy", mmap_mode="r").shape[0])
281
- if max_frames >= n_total:
282
- return data_root # use full set
283
-
284
- tmp_root = Path(tempfile.mkdtemp(prefix="nilmbench_subset_"))
285
- sub = tmp_root / "benchmark"
286
- sub.mkdir(parents=True)
287
-
288
- x = np.load(src / "x_vi_6s.npy", mmap_mode="r")
289
- np.save(sub / "x_vi_6s.npy", np.asarray(x[:max_frames]))
290
-
291
- lab = np.load(src / "labels_and_index.npz", allow_pickle=True)
292
- sliced = {}
293
- for k in lab.files:
294
- v = lab[k]
295
- if v.ndim >= 1 and v.shape[0] == n_total:
296
- sliced[k] = v[:max_frames]
297
- else:
298
- sliced[k] = v
299
- np.savez_compressed(sub / "labels_and_index.npz", **sliced)
300
- return tmp_root
301
-
302
-
303
- def run_benchmark_upload(model_file, weights_file, class_name: str,
304
- output_kind: str, max_frames: int, batch_size: int):
305
- """Run the user's model on the dense House-2 set and render a report."""
306
- if model_file is None:
307
- return "**Please upload a Python file defining your model.**", None
308
- class_name = (class_name or "Model").strip() or "Model"
309
-
310
- try:
311
- ModelCls = _import_user_module(Path(model_file.name), class_name)
312
- except Exception as exc:
313
- return (f"**Failed to import model class `{class_name}`:**\n\n"
314
- f"```\n{traceback.format_exc()}\n```"), None
315
-
316
- try:
317
- data_root = _ensure_benchmark_data()
318
- except Exception:
319
- return (f"**Could not download benchmark data:**\n\n"
320
- f"```\n{traceback.format_exc()}\n```"), None
321
-
322
- try:
323
- active_root = _subset_dataset(data_root, int(max_frames))
324
- except Exception:
325
- return (f"**Could not prepare data subset:**\n\n"
326
- f"```\n{traceback.format_exc()}\n```"), None
327
-
328
- tmpdir = Path(tempfile.mkdtemp(prefix="nilmbench_report_"))
329
- preds_path = tmpdir / "predictions.npz"
330
-
331
- try:
332
- # We already have the class; rebind via a temporary module name so
333
- # nilmbench.runner's importer can find it.
334
- sys.modules["__nilmbench_user__"] = sys.modules["user_model_module"]
335
- run = run_user_model(
336
- module_spec=f"__nilmbench_user__:{class_name}",
337
- weights_path=weights_file.name if weights_file is not None else None,
338
- data_root=active_root,
339
- out_path=preds_path,
340
- batch_size=int(batch_size),
341
- device="cpu",
342
- output_kind=output_kind,
343
- strict_load=False,
344
- model_name=class_name,
345
- )
346
- except Exception:
347
- return (f"**Model failed during inference:**\n\n"
348
- f"```\n{traceback.format_exc()}\n```"), None
349
-
350
- preds = np.load(preds_path, allow_pickle=True)
351
- result = evaluate_dense(
352
- y_true_W=preds["y_true"].astype(np.float32),
353
- y_pred_W=preds["y_pred"].astype(np.float32),
354
- classes=[str(c) for c in preds["class_names"]],
355
- model_name=class_name,
356
- )
357
-
358
- extra = {
359
- "Model class": class_name,
360
- "Weights file": Path(weights_file.name).name if weights_file else "(none)",
361
- "Frames scored": f"{run.n_frames} / 60,000",
362
- "Output kind": output_kind,
363
- }
364
- md = render_markdown_report(
365
- result,
366
- title=f"NILMbench report — {class_name}",
367
- extra=extra,
368
- )
369
-
370
- score_json_path = tmpdir / "score.json"
371
- score_json_path.write_text(json.dumps(result.to_dict(), indent=2, sort_keys=True))
372
-
373
- return md, str(score_json_path)
374
-
375
-
376
  # ----------------------------------------------------------------------
377
  # UI
378
  # ----------------------------------------------------------------------
379
  def build_ui() -> gr.Blocks:
380
  examples = list_examples()
381
- with gr.Blocks(title="NILMbench") as demo:
382
  gr.Markdown(
383
- "# NILMbench\n"
384
- "Open benchmark for high-frequency NILM regression on UK-DALE 2015 "
385
- "(House 1 House 2). Headline metric: modified Jaccard index "
386
- "**MJ$_{20W}$** with hybrid tolerance.\n\n"
387
  "Source code: <https://github.com/Saharmgh/NILMbench> · "
388
- "Baseline model: <https://huggingface.co/Pybunny/nilmbench-faustine> · "
389
- "Dataset: <https://huggingface.co/datasets/Pybunny/nilmbench-ukdale>"
390
  )
391
  with gr.Tabs():
392
- with gr.TabItem("Single frame · built-in example"):
393
  ex = gr.Dropdown(examples, label="Example frame",
394
  value=examples[0] if examples else None)
395
- btn = gr.Button("Run FaustineCNN", variant="primary")
396
  plot_a = gr.Plot()
397
  lab_a = gr.JSON(label="Predicted power per category (W)")
398
  btn.click(run_example, ex, [plot_a, lab_a])
399
-
400
- with gr.TabItem("Single frame · upload V/I"):
401
  up = gr.File(label="V/I segment (.npy, shape (2, 96000), "
402
  "FLAC-normalised float in [-1, 1])")
403
  agg = gr.Slider(0, 8000, value=300, step=10,
404
  label="Aggregate active power (W)")
405
- btn2 = gr.Button("Run FaustineCNN", variant="primary")
406
  plot_b = gr.Plot()
407
  lab_b = gr.JSON(label="Predicted power per category (W)")
408
  btn2.click(run_upload, [up, agg], [plot_b, lab_b])
409
-
410
- with gr.TabItem("Benchmark your model"):
411
- gr.Markdown(
412
- "Full benchmark scoring is run via the CLI on your "
413
- "machine (uses up to 5 GB of UK-DALE 16 kHz V/I data "
414
- "and tens of minutes on CPU):\n\n"
415
- "```bash\n"
416
- "pip install git+https://github.com/Saharmgh/NILMbench\n"
417
- "nilmbench benchmark \\\n"
418
- " --module my_model:MyModel \\\n"
419
- " --weights ./my_checkpoint.pt \\\n"
420
- " --data hf:Pybunny/nilmbench-ukdale \\\n"
421
- " --out ./report/\n"
422
- "```\n\n"
423
- "See "
424
- "[examples/byom_template.py](https://github.com/Saharmgh/NILMbench/blob/main/examples/byom_template.py) "
425
- "and "
426
- "[docs/TESTING_GUIDE.md](https://github.com/Saharmgh/NILMbench/blob/main/docs/TESTING_GUIDE.md) "
427
- "for the model contract and a step-by-step recipe."
428
- )
429
  return demo
430
 
431
 
432
  if __name__ == "__main__":
433
- # Use bare launch(); HF Spaces auto-detects host/port from env vars.
434
- # The schema bug that breaks /info is already handled by the
435
- # gradio_client monkey-patch at the top of this file.
436
  build_ui().launch()
 
1
  """NILMbench HuggingFace Space.
2
 
3
+ Single-frame demo of the FaustineCNN baseline. Model weights, classes, and
4
+ recall-constrained cutoffs are pulled from the HF model repo
5
+ ``Pybunny/nilmbench-faustine`` at startup. Example frames are bundled with
6
+ the Space so the demo works offline of the laptop.
 
 
 
 
 
 
 
 
7
  """
8
 
9
+ from __future__ import annotations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
 
 
 
 
 
 
 
 
 
 
 
11
  import json
 
 
 
12
  from pathlib import Path
13
 
14
  import numpy as np
 
19
  import matplotlib
20
  matplotlib.use("Agg")
21
  import matplotlib.pyplot as plt
22
+ from huggingface_hub import hf_hub_download
 
 
 
 
 
23
 
24
  HERE = Path(__file__).resolve().parent
25
  EXAMPLES_DIR = HERE / "examples"
26
  MODEL_REPO = "Pybunny/nilmbench-faustine"
 
27
 
28
  # UK-DALE House 2 calibration constants (from calibration_house_2.cfg).
29
  V_PER_ADC = 1.88296904357e-7
 
34
 
35
 
36
  # ----------------------------------------------------------------------
37
+ # Model (self-contained so the Space has no dependency on the nilmbench pkg)
38
  # ----------------------------------------------------------------------
39
  class FaustineCNN(nn.Module):
40
  def __init__(self, n_categories: int):
 
89
 
90
 
91
  # ----------------------------------------------------------------------
92
+ # Inference + plotting
93
  # ----------------------------------------------------------------------
94
  def _to_2d_image(vi_norm: np.ndarray) -> torch.Tensor:
95
  if vi_norm.shape != (2, 96000):
 
101
  def predict(vi_norm: np.ndarray, aggregate_W: float) -> dict[str, float]:
102
  with torch.no_grad():
103
  scores = MODEL(_to_2d_image(vi_norm)).cpu().numpy().squeeze(0)
104
+ # FaustineCNN outputs per-category Bernoulli activations; renormalise
105
+ # across categories to obtain shares, then scale by the aggregate.
106
  shares = scores / (scores.sum() + 1e-9)
107
  raw = shares * float(aggregate_W)
108
  out = {}
 
155
  return fig
156
 
157
 
158
+ # ----------------------------------------------------------------------
159
+ # Gradio handlers
160
+ # ----------------------------------------------------------------------
161
  def list_examples() -> list[str]:
162
  if not EXAMPLES_DIR.exists():
163
  return []
 
195
  return make_overview_plot(vi, preds, None), preds
196
 
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  # ----------------------------------------------------------------------
199
  # UI
200
  # ----------------------------------------------------------------------
201
  def build_ui() -> gr.Blocks:
202
  examples = list_examples()
203
+ with gr.Blocks(title="NILMbench demo") as demo:
204
  gr.Markdown(
205
+ "# NILMbench demo\n"
206
+ "FaustineCNN trained on UK-DALE House 1, applied to a single "
207
+ "6-second 16 kHz V/I segment from House 2. Predicted power is "
208
+ "post-processed with the recall-constrained cutoffs from the paper.\n\n"
209
  "Source code: <https://github.com/Saharmgh/NILMbench> · "
210
+ "Model: <https://huggingface.co/Pybunny/nilmbench-faustine>"
 
211
  )
212
  with gr.Tabs():
213
+ with gr.TabItem("Built-in example"):
214
  ex = gr.Dropdown(examples, label="Example frame",
215
  value=examples[0] if examples else None)
216
+ btn = gr.Button("Run", variant="primary")
217
  plot_a = gr.Plot()
218
  lab_a = gr.JSON(label="Predicted power per category (W)")
219
  btn.click(run_example, ex, [plot_a, lab_a])
220
+ with gr.TabItem("Upload your own"):
 
221
  up = gr.File(label="V/I segment (.npy, shape (2, 96000), "
222
  "FLAC-normalised float in [-1, 1])")
223
  agg = gr.Slider(0, 8000, value=300, step=10,
224
  label="Aggregate active power (W)")
225
+ btn2 = gr.Button("Run", variant="primary")
226
  plot_b = gr.Plot()
227
  lab_b = gr.JSON(label="Predicted power per category (W)")
228
  btn2.click(run_upload, [up, agg], [plot_b, lab_b])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  return demo
230
 
231
 
232
  if __name__ == "__main__":
 
 
 
233
  build_ui().launch()