LukeFP commited on
Commit
49a8dc7
·
1 Parent(s): 4123863

Run on ZeroGPU: add @spaces.GPU entry point

Browse files
Files changed (5) hide show
  1. DEPLOY.md +42 -34
  2. README.md +6 -1
  3. app.py +66 -24
  4. requirements.txt +4 -5
  5. test_local.py +44 -15
DEPLOY.md CHANGED
@@ -1,58 +1,66 @@
1
- # Deploying this to a Space
2
 
3
- ## 1. Create the Space
4
 
5
- On huggingface.co → **New** → **Space**:
6
 
7
- - Owner `LukeFP`, name `physh-topic-classifier`
8
- - SDK **Gradio**, hardware **CPU basic** (free) — EmbeddingGemma-300m runs on
9
- CPU in roughly a second per abstract, so a GPU buys little here
10
- - Visibility public or private, your call
11
 
12
- ## 2. Add the token secret
13
 
14
- `google/embeddinggemma-300m` is gated. Accept the Gemma license while signed in,
15
- create a **read** token, then in the Space: Settings → *Variables and secrets* →
16
- **New secret**, name `HF_TOKEN`, value the token.
 
 
 
17
 
18
- If the model repo is private too, the same token covers it.
 
 
 
 
 
 
 
 
19
 
20
  ## 3. Push
21
 
22
  ```bash
23
- cd /Users/firstprinciplesextralaptop2/code/2026.7/physh-topic-classifier-space
24
-
25
- git init
26
- git remote add origin https://huggingface.co/spaces/LukeFP/physh-topic-classifier
27
- git add app.py requirements.txt README.md .gitignore DEPLOY.md test_local.py
28
- git commit -m "Gradio app for PhySH discipline + concept classification"
29
  git push origin main
30
  ```
31
 
32
- If the Space was created with a README already, `git pull --rebase origin main`
33
- first — the frontmatter in this README is the one you want to keep, since it
34
- carries the `sdk_version` and `app_file` settings.
35
 
36
- The build takes a few minutes, mostly `pip install torch`.
37
 
38
- ## 4. Check it
39
-
40
- Open the Space and run one of the built-in examples. Things to look at:
41
-
42
- - **Predictions look like noise / nothing clears the threshold.** Almost
43
  certainly the embedding prompt. Open *Advanced* and try the other two formats;
44
- the one matching your training pipeline will give confident, coherent labels.
45
  Once you know which, set `DEFAULT_PROMPT` at the top of `app.py`.
46
- - **Error mentioning gated repo or 401.** `HF_TOKEN` is missing, wrong, or the
47
- account behind it hasn't accepted the Gemma license.
48
- - **First request is slow.** Expected — EmbeddingGemma loads lazily on first use
49
- so the Space boots fast. It's cached after that.
 
 
 
 
 
 
 
 
50
 
51
  ## Local smoke test
52
 
53
  Runs the real checkpoints through the full chain with a stubbed embedder, so it
54
- needs no token and no download:
55
 
56
  ```bash
57
- python test_local.py
58
  ```
 
1
+ # Deploying to LukeFP/Physh_Classification
2
 
3
+ The Space repo lives at `~/code/2026.7/Physh_Classification`.
4
 
5
+ ## 1. Add the token secret
6
 
7
+ `google/embeddinggemma-300m` is gated. Accept the Gemma license while signed in,
8
+ create a **read** token, then on the Space page: Settings → *Variables and
9
+ secrets* → **New secret**, name `HF_TOKEN`, value the token. Without it the Space
10
+ boots fine but the first classification fails with a 401.
11
 
12
+ ## 2. Hardware
13
 
14
+ On the free tier, Gradio Spaces run on **ZeroGPU**, which stops the container at
15
+ startup unless it finds at least one `@spaces.GPU` function — the
16
+ `No @spaces.GPU function detected during startup` error. `infer()` in `app.py`
17
+ carries that decorator, so ZeroGPU is satisfied.
18
+
19
+ Constraints ZeroGPU imposes, and how `app.py` meets them:
20
 
21
+ | Constraint | Handling |
22
+ |---|---|
23
+ | `import spaces` must precede `import torch` | It is the first import in `app.py` |
24
+ | Nothing may touch CUDA outside a `@GPU` function | Models load with `device="cpu"`; `.to(device)` happens inside `infer()` |
25
+ | Return values cross a process boundary | `infer()` returns plain `list[float]`, never CUDA tensors |
26
+ | One GPU allocation per call, with a duration budget | `@GPU(duration=60)`; the model is already resident, so only the encode runs |
27
+
28
+ CPU basic (a PRO perk) also works with this code unchanged — `spaces` is an
29
+ optional import and the device is chosen from `torch.cuda.is_available()`.
30
 
31
  ## 3. Push
32
 
33
  ```bash
34
+ cd ~/code/2026.7/Physh_Classification
 
 
 
 
 
35
  git push origin main
36
  ```
37
 
38
+ The build takes a few minutes, most of it `pip install torch`.
 
 
39
 
40
+ ## 4. First checks
41
 
42
+ - **Predictions look like noise, or nothing clears the threshold.** Almost
 
 
 
 
43
  certainly the embedding prompt. Open *Advanced* and try the other two formats;
44
+ the one matching your training pipeline gives confident, coherent labels.
45
  Once you know which, set `DEFAULT_PROMPT` at the top of `app.py`.
46
+ (`~/code/2026/embedding_title_abstract` likely has the answer.)
47
+ - **Error mentioning a gated repo, or a 401.** `HF_TOKEN` is missing, wrong, or
48
+ the account behind it hasn't accepted the Gemma license.
49
+ - **First request is slow, later ones fast.** Expected — EmbeddingGemma loads
50
+ lazily on first use so the Space boots quickly. Cached after that.
51
+
52
+ ## Updating later
53
+
54
+ Retraining only needs a push to
55
+ [`LukeFP/physh_topic_supervised_classifier`](https://huggingface.co/LukeFP/physh_topic_supervised_classifier);
56
+ the Space picks up new weights on its next restart. Only change this repo if the
57
+ *filenames* change — they're the constants at the top of `app.py`.
58
 
59
  ## Local smoke test
60
 
61
  Runs the real checkpoints through the full chain with a stubbed embedder, so it
62
+ needs no token and no model download:
63
 
64
  ```bash
65
+ PHYSH_WEIGHTS_DIR=~/code/2026.7/physh_topic_supervised_classifier python test_local.py
66
  ```
README.md CHANGED
@@ -54,6 +54,11 @@ model page, then add a read token as a Space secret named `HF_TOKEN`
54
  (Settings → Variables and secrets). Without it the Space boots but the first
55
  classification fails.
56
 
 
 
 
 
 
57
  ### Prompt format
58
 
59
  EmbeddingGemma prepends a task-specific prefix, and the prefix used here must
@@ -80,7 +85,7 @@ labelling:
80
  ```python
81
  from gradio_client import Client
82
 
83
- client = Client("LukeFP/physh-topic-classifier")
84
  disciplines, concepts, summary = client.predict(
85
  "Title and abstract…", 0.5, "document — title: none | text: {}", 8,
86
  api_name="/classify",
 
54
  (Settings → Variables and secrets). Without it the Space boots but the first
55
  classification fails.
56
 
57
+ This Space runs on **ZeroGPU**: `infer()` carries the `@spaces.GPU` decorator,
58
+ the models are loaded on CPU in the main process, and device placement happens
59
+ inside the decorated function. The same code runs unchanged on CPU hardware —
60
+ `spaces` is optional at import and `torch.cuda.is_available()` picks the device.
61
+
62
  ### Prompt format
63
 
64
  EmbeddingGemma prepends a task-specific prefix, and the prefix used here must
 
85
  ```python
86
  from gradio_client import Client
87
 
88
+ client = Client("LukeFP/Physh_Classification")
89
  disciplines, concepts, summary = client.predict(
90
  "Title and abstract…", 0.5, "document — title: none | text: {}", 8,
91
  api_name="/classify",
app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- PhySH topic classifier — Gradio Space.
3
 
4
  Pipeline: text ──EmbeddingGemma-300m──> 768-d vector
5
  │
@@ -15,9 +15,24 @@ carry several disciplines and several concepts.
15
 
16
  from __future__ import annotations
17
 
18
- import functools
19
  import os
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  import gradio as gr
22
  import torch
23
  import torch.nn as nn
@@ -32,8 +47,8 @@ DISCIPLINE_CKPT = "discipline_classifier_gemma_20260130_140842.pt"
32
  CONCEPT_CKPT = "concept_conditioned_gemma_20260130_140842.pt"
33
  EMBED_MODEL = "google/embeddinggemma-300m"
34
 
35
- # google/embeddinggemma-300m is a gated repo: set HF_TOKEN as a Space secret,
36
- # using a token from an account that has accepted the Gemma license.
37
  HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
38
 
39
  # Set to a local directory to load the .pt files from disk instead of the Hub.
@@ -102,14 +117,18 @@ _CONDITION_ORDER = [d["discipline_id"] for d in CONCEPT_CKPT_DATA["discipline_la
102
  _HEAD_ORDER = [d["discipline_id"] for d in DISCIPLINE_CKPT_DATA["class_labels"]]
103
  _REMAP = torch.tensor([_HEAD_ORDER.index(i) for i in _CONDITION_ORDER], dtype=torch.long)
104
 
105
-
106
- @functools.lru_cache(maxsize=1)
107
- def get_embedder():
108
- """Loaded on first request rather than at import, so the Space boots quickly
109
- and a missing token surfaces as a readable error instead of a crashed app."""
 
 
110
  from sentence_transformers import SentenceTransformer
111
 
112
- return SentenceTransformer(EMBED_MODEL, token=HF_TOKEN)
 
 
113
 
114
 
115
  # --------------------------------------------------------------------------- #
@@ -117,30 +136,53 @@ def get_embedder():
117
  # --------------------------------------------------------------------------- #
118
 
119
 
120
- @torch.inference_mode()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  def classify(text: str, threshold: float, prompt_choice: str, top_k: int):
122
  text = (text or "").strip()
123
  if not text:
124
  return {}, {}, "Paste some text — a title and abstract work best."
125
 
126
- prompt = PROMPT_TEMPLATES.get(prompt_choice, PROMPT_TEMPLATES[DEFAULT_PROMPT])
127
- # prompt="" stops sentence-transformers from also applying the model's own
128
- # default prefix on top of the one built here.
129
- vector = get_embedder().encode(prompt.format(text), prompt="", convert_to_numpy=True)
130
- embedding = torch.as_tensor(vector, dtype=torch.float32).unsqueeze(0)
131
-
132
- discipline_probs = torch.sigmoid(DISCIPLINE_MODEL(embedding))[0]
133
- conditioned = torch.cat([embedding, discipline_probs[_REMAP].unsqueeze(0)], dim=1)
134
- concept_probs = torch.sigmoid(CONCEPT_MODEL(conditioned))[0]
135
 
136
- disciplines = {DISCIPLINE_LABELS[i]: float(p) for i, p in enumerate(discipline_probs)}
137
- concepts = {CONCEPT_LABELS[i]: float(p) for i, p in enumerate(concept_probs)}
138
 
139
- summary = _summarize(disciplines, concepts, threshold)
140
  return (
141
  dict(sorted(disciplines.items(), key=lambda kv: -kv[1])[:top_k]),
142
  dict(sorted(concepts.items(), key=lambda kv: -kv[1])[:top_k]),
143
- summary,
144
  )
145
 
146
 
 
1
  """
2
+ PhySH topic classifier — Gradio Space (ZeroGPU).
3
 
4
  Pipeline: text ──EmbeddingGemma-300m──> 768-d vector
5
  │
 
15
 
16
  from __future__ import annotations
17
 
 
18
  import os
19
 
20
+ # `spaces` must be imported before torch — it patches CUDA init so the main
21
+ # process stays GPU-free until a @GPU function actually runs. ZeroGPU also scans
22
+ # for at least one decorated function at startup and stops the container if it
23
+ # finds none. The fallback keeps local runs and test_local.py working without it.
24
+ try:
25
+ import spaces
26
+
27
+ GPU = spaces.GPU
28
+ except ImportError: # local development, or CPU hardware
29
+
30
+ def GPU(*dargs, **dkwargs):
31
+ if len(dargs) == 1 and callable(dargs[0]) and not dkwargs:
32
+ return dargs[0]
33
+ return lambda fn: fn
34
+
35
+
36
  import gradio as gr
37
  import torch
38
  import torch.nn as nn
 
47
  CONCEPT_CKPT = "concept_conditioned_gemma_20260130_140842.pt"
48
  EMBED_MODEL = "google/embeddinggemma-300m"
49
 
50
+ # google/embeddinggemma-300m is gated: set HF_TOKEN as a Space *secret*, from an
51
+ # account that has accepted the Gemma license. Never commit the token itself.
52
  HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
53
 
54
  # Set to a local directory to load the .pt files from disk instead of the Hub.
 
117
  _HEAD_ORDER = [d["discipline_id"] for d in DISCIPLINE_CKPT_DATA["class_labels"]]
118
  _REMAP = torch.tensor([_HEAD_ORDER.index(i) for i in _CONDITION_ORDER], dtype=torch.long)
119
 
120
+ # EmbeddingGemma is loaded on CPU in the main process — under ZeroGPU nothing may
121
+ # touch CUDA outside a @GPU function, and the fork inherits this copy for free.
122
+ # A load failure is captured rather than raised so the Space still boots and can
123
+ # report the reason in the UI instead of crash-looping.
124
+ _EMBEDDER = None
125
+ _EMBEDDER_ERROR: str | None = None
126
+ try:
127
  from sentence_transformers import SentenceTransformer
128
 
129
+ _EMBEDDER = SentenceTransformer(EMBED_MODEL, token=HF_TOKEN, device="cpu")
130
+ except Exception as exc: # noqa: BLE001 — surfaced to the user verbatim
131
+ _EMBEDDER_ERROR = f"{type(exc).__name__}: {exc}"
132
 
133
 
134
  # --------------------------------------------------------------------------- #
 
136
  # --------------------------------------------------------------------------- #
137
 
138
 
139
+ @GPU(duration=60)
140
+ def infer(text: str, prompt_template: str) -> tuple[list[float], list[float]]:
141
+ """Embed and run both heads. Returns plain lists — ZeroGPU pickles the return
142
+ value across a process boundary, so nothing CUDA-resident may escape."""
143
+ if _EMBEDDER is None:
144
+ raise gr.Error(
145
+ "EmbeddingGemma failed to load. It is a gated model, so the Space needs "
146
+ "an HF_TOKEN secret from an account that has accepted the Gemma "
147
+ f"license.\n\n{_EMBEDDER_ERROR}"
148
+ )
149
+
150
+ device = "cuda" if torch.cuda.is_available() else "cpu"
151
+ embedder = _EMBEDDER.to(device)
152
+ discipline_model = DISCIPLINE_MODEL.to(device)
153
+ concept_model = CONCEPT_MODEL.to(device)
154
+ remap = _REMAP.to(device)
155
+
156
+ with torch.inference_mode():
157
+ vector = embedder.encode(
158
+ prompt_template.format(text),
159
+ prompt="", # stop ST applying the model's own default prefix on top
160
+ convert_to_numpy=True,
161
+ )
162
+ embedding = torch.as_tensor(vector, dtype=torch.float32, device=device).unsqueeze(0)
163
+
164
+ discipline_probs = torch.sigmoid(discipline_model(embedding))[0]
165
+ conditioned = torch.cat([embedding, discipline_probs[remap].unsqueeze(0)], dim=1)
166
+ concept_probs = torch.sigmoid(concept_model(conditioned))[0]
167
+
168
+ return discipline_probs.float().cpu().tolist(), concept_probs.float().cpu().tolist()
169
+
170
+
171
  def classify(text: str, threshold: float, prompt_choice: str, top_k: int):
172
  text = (text or "").strip()
173
  if not text:
174
  return {}, {}, "Paste some text — a title and abstract work best."
175
 
176
+ template = PROMPT_TEMPLATES.get(prompt_choice, PROMPT_TEMPLATES[DEFAULT_PROMPT])
177
+ discipline_scores, concept_scores = infer(text, template)
 
 
 
 
 
 
 
178
 
179
+ disciplines = dict(zip(DISCIPLINE_LABELS, discipline_scores))
180
+ concepts = dict(zip(CONCEPT_LABELS, concept_scores))
181
 
 
182
  return (
183
  dict(sorted(disciplines.items(), key=lambda kv: -kv[1])[:top_k]),
184
  dict(sorted(concepts.items(), key=lambda kv: -kv[1])[:top_k]),
185
+ _summarize(disciplines, concepts, threshold),
186
  )
187
 
188
 
requirements.txt CHANGED
@@ -1,13 +1,12 @@
1
  gradio==6.28.0
2
 
 
 
 
3
  # EmbeddingGemma needs sentence-transformers >= 5.0 / transformers >= 4.56,
4
- # and does not support float16 activations (float32 on CPU is the default here).
5
  sentence-transformers>=5.0
6
  transformers>=4.56
7
  huggingface_hub>=0.34
8
 
9
- # PyPI's torch wheel bundles CUDA (~3 GB) and is wasted on a CPU Space. If build
10
- # times or disk become a problem, replace the line below with these two:
11
- # --extra-index-url https://download.pytorch.org/whl/cpu
12
- # torch>=2.4
13
  torch>=2.4
 
1
  gradio==6.28.0
2
 
3
+ # ZeroGPU: provides the @spaces.GPU decorator the Space is scanned for at startup.
4
+ spaces
5
+
6
  # EmbeddingGemma needs sentence-transformers >= 5.0 / transformers >= 4.56,
7
+ # and does not support float16 activations — this app keeps it in float32.
8
  sentence-transformers>=5.0
9
  transformers>=4.56
10
  huggingface_hub>=0.34
11
 
 
 
 
 
12
  torch>=2.4
test_local.py CHANGED
@@ -1,42 +1,71 @@
1
  """Smoke test: loads the real checkpoints, stubs out EmbeddingGemma, and runs the
2
- full chain. Run with PHYSH_WEIGHTS_DIR pointing at the cloned model repo."""
3
- import os, sys, numpy as np, torch
 
 
 
 
4
 
5
  os.environ.setdefault(
6
  "PHYSH_WEIGHTS_DIR",
7
  os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "physh_topic_supervised_classifier"),
8
  )
 
 
 
 
9
  import app
10
 
 
11
  class FakeEmbedder:
 
 
 
 
 
12
  def encode(self, text, prompt=None, convert_to_numpy=True):
13
  rng = np.random.default_rng(abs(hash(text)) % (2**32))
14
  v = rng.normal(size=768).astype("float32")
15
  return v / np.linalg.norm(v) # EmbeddingGemma returns L2-normalised vectors
16
 
17
- app.get_embedder.cache_clear()
18
- app.get_embedder = lambda: FakeEmbedder()
19
 
20
  print("disciplines:", len(app.DISCIPLINE_LABELS), "concepts:", len(app.CONCEPT_LABELS))
21
  print("remap is identity:", torch.equal(app._REMAP, torch.arange(18)))
22
- print("discipline net:", app.DISCIPLINE_MODEL.network)
23
- print("concept in_features:", app.CONCEPT_MODEL.network[0].in_features)
 
24
 
25
  d, c, summary = app.classify(app.EXAMPLES[0], 0.5, app.DEFAULT_PROMPT, 8)
26
  assert len(d) == 8 and len(c) == 8, (len(d), len(c))
27
  assert all(0.0 <= v <= 1.0 for v in {**d, **c}.values())
28
- print("\ntop disciplines:", [f"{k} {v:.3f}" for k, v in d.items()][:4])
29
- print("top concepts: ", [f"{k} {v:.3f}" for k, v in c.items()][:4])
30
- print("\n--- summary ---\n" + summary)
31
 
32
- # determinism (dropout must be off) and empty input
33
  d2, _, _ = app.classify(app.EXAMPLES[0], 0.5, app.DEFAULT_PROMPT, 8)
34
  assert d == d2, "eval() mode not applied — outputs are not deterministic"
35
- empty = app.classify(" ", 0.5, app.DEFAULT_PROMPT, 8)
36
- assert empty[0] == {} and "Paste" in empty[2]
 
37
  for p in app.PROMPT_TEMPLATES:
38
  app.classify("test abstract", 0.5, p, 5)
39
- print("\nOK: deterministic, empty input handled, all 3 prompt formats run.")
40
 
41
- app.demo # Blocks built at import; confirm the UI graph is constructed
42
- print("gradio Blocks built OK")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """Smoke test: loads the real checkpoints, stubs out EmbeddingGemma, and runs the
2
+ full chain on CPU. Needs no HF token and downloads nothing.
3
+
4
+ PHYSH_WEIGHTS_DIR=../physh_topic_supervised_classifier python test_local.py
5
+ """
6
+
7
+ import os
8
 
9
  os.environ.setdefault(
10
  "PHYSH_WEIGHTS_DIR",
11
  os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "physh_topic_supervised_classifier"),
12
  )
13
+
14
+ import numpy as np
15
+ import torch
16
+
17
  import app
18
 
19
+
20
  class FakeEmbedder:
21
+ """Stands in for SentenceTransformer: deterministic unit-norm vectors."""
22
+
23
+ def to(self, device):
24
+ return self
25
+
26
  def encode(self, text, prompt=None, convert_to_numpy=True):
27
  rng = np.random.default_rng(abs(hash(text)) % (2**32))
28
  v = rng.normal(size=768).astype("float32")
29
  return v / np.linalg.norm(v) # EmbeddingGemma returns L2-normalised vectors
30
 
31
+
32
+ app._EMBEDDER = FakeEmbedder()
33
 
34
  print("disciplines:", len(app.DISCIPLINE_LABELS), "concepts:", len(app.CONCEPT_LABELS))
35
  print("remap is identity:", torch.equal(app._REMAP, torch.arange(18)))
36
+ print("concept head in_features:", app.CONCEPT_MODEL.network[0].in_features)
37
+ import sys
38
+ print("real `spaces` package in use:", "spaces" in sys.modules)
39
 
40
  d, c, summary = app.classify(app.EXAMPLES[0], 0.5, app.DEFAULT_PROMPT, 8)
41
  assert len(d) == 8 and len(c) == 8, (len(d), len(c))
42
  assert all(0.0 <= v <= 1.0 for v in {**d, **c}.values())
43
+ print("\ntop disciplines:", [f"{k} {v:.3f}" for k, v in d.items()][:3])
44
+ print("top concepts: ", [f"{k} {v:.3f}" for k, v in c.items()][:3])
 
45
 
46
+ # dropout must be off, so repeat calls agree
47
  d2, _, _ = app.classify(app.EXAMPLES[0], 0.5, app.DEFAULT_PROMPT, 8)
48
  assert d == d2, "eval() mode not applied — outputs are not deterministic"
49
+
50
+ # empty input, and every prompt format
51
+ assert app.classify(" ", 0.5, app.DEFAULT_PROMPT, 8)[0] == {}
52
  for p in app.PROMPT_TEMPLATES:
53
  app.classify("test abstract", 0.5, p, 5)
 
54
 
55
+ # the GPU-decorated entry point returns plain lists (ZeroGPU pickles them)
56
+ dl, cl = app.infer("test", app.PROMPT_TEMPLATES[app.DEFAULT_PROMPT])
57
+ assert isinstance(dl, list) and isinstance(cl, list) and len(dl) == 18 and len(cl) == 186
58
+ assert all(isinstance(x, float) for x in dl)
59
+
60
+ # a missing embedder must surface as a readable error, not an AttributeError
61
+ app._EMBEDDER, app._EMBEDDER_ERROR = None, "401 gated repo"
62
+ try:
63
+ app.infer("test", "{}")
64
+ raise SystemExit("FAIL: expected a gr.Error when the embedder is missing")
65
+ except Exception as exc:
66
+ assert "HF_TOKEN" in str(exc), exc
67
+ app._EMBEDDER = FakeEmbedder()
68
+
69
+ print("\nOK: deterministic, empty input handled, all prompt formats run,")
70
+ print(" infer() returns picklable lists, missing-token error is readable.")
71
+ print("gradio Blocks built OK:", app.demo is not None)