harishaseebat92 commited on
Commit
c90c95d
·
1 Parent(s): b8c9ea0

command now spins up the main UI plus the EM (:8701) and QLBM (:8702) workers without binding conflicts

Browse files
Dockerfile CHANGED
@@ -22,7 +22,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
22
  # *** UPDATED this section to use current package names (e.g., libgl1, libegl1) ***
23
  #
24
  RUN apt-get update && apt-get install -y --no-install-recommends \
25
- build-essential cmake wget xvfb \
26
  libosmesa6 libosmesa6-dev \
27
  libgl1 libgl1-mesa-dev \
28
  libegl1 libegl1-mesa-dev \
@@ -47,10 +47,22 @@ RUN python3 -m pip install --upgrade pip setuptools wheel \
47
  # This copies app.py, delta_impulse_generator.py, etc.
48
  # We set the owner to our new 'user'.
49
  COPY --chown=user:user . .
 
50
 
51
  # 7. Switch to the non-root user
52
  USER user
53
 
 
 
 
 
 
 
 
 
 
 
 
54
  # 8. Expose the port the app will run on
55
  EXPOSE 7860
56
 
@@ -64,6 +76,6 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
64
  # b) 'exec' runs your app. Using 'exec' is important as it makes the Python
65
  # process the main one, which properly handles signals (like stopping the container).
66
  # '--host 0.0.0.0' is ESSENTIAL to make the server accessible from outside the container.
67
- CMD ["sh", "-c", "Xvfb :99 -screen 0 1024x768x24 >/dev/null 2>&1 & exec python3 app.py --server --host 0.0.0.0 --port ${PORT:-7860}"]
68
 
69
 
 
22
  # *** UPDATED this section to use current package names (e.g., libgl1, libegl1) ***
23
  #
24
  RUN apt-get update && apt-get install -y --no-install-recommends \
25
+ build-essential cmake wget xvfb nginx \
26
  libosmesa6 libosmesa6-dev \
27
  libgl1 libgl1-mesa-dev \
28
  libegl1 libegl1-mesa-dev \
 
47
  # This copies app.py, delta_impulse_generator.py, etc.
48
  # We set the owner to our new 'user'.
49
  COPY --chown=user:user . .
50
+ COPY docker/nginx.conf /etc/nginx/nginx.conf
51
 
52
  # 7. Switch to the non-root user
53
  USER user
54
 
55
+ # Default runtime configuration for multiprocess layout
56
+ ENV OMP_NUM_THREADS=1 \
57
+ APP_HOST=127.0.0.1 \
58
+ APP_PORT=8700 \
59
+ EM_APP_PORT=8701 \
60
+ QLBM_APP_PORT=8702 \
61
+ EM_HOST=127.0.0.1 \
62
+ QLBM_HOST=127.0.0.1 \
63
+ EM_IFRAME_SRC=/em/ \
64
+ QLBM_IFRAME_SRC=/qlbm/
65
+
66
  # 8. Expose the port the app will run on
67
  EXPOSE 7860
68
 
 
76
  # b) 'exec' runs your app. Using 'exec' is important as it makes the Python
77
  # process the main one, which properly handles signals (like stopping the container).
78
  # '--host 0.0.0.0' is ESSENTIAL to make the server accessible from outside the container.
79
+ CMD ["sh", "-c", "Xvfb :99 -screen 0 1024x768x24 >/dev/null 2>&1 & nginx && exec python3 app.py --server --host ${APP_HOST:-127.0.0.1} --port ${APP_PORT:-8700}"]
80
 
81
 
README.md CHANGED
@@ -8,4 +8,27 @@ hf_oauth: false
8
  pinned: false
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  pinned: false
9
  ---
10
 
11
+ Check out the configuration reference at <https://huggingface.co/docs/hub/spaces-config-reference>
12
+
13
+ ## Running locally
14
+
15
+ ```powershell
16
+ python app.py
17
+ ```
18
+
19
+ By default the UI listens on `http://127.0.0.1:7860`. The EM and QLBM pages start in the
20
+ background and expose themselves on `http://127.0.0.1:8701` and `http://127.0.0.1:8702`.
21
+
22
+ ## Single-port / Hugging Face deployment
23
+
24
+ Spaces (and other single-port hosts) now work by running everything behind an internal
25
+ reverse proxy. Set the following environment variables (the Dockerfile already does this):
26
+
27
+ | Variable | Purpose |
28
+ | --- | --- |
29
+ | `APP_HOST` / `APP_PORT` | Internal address that `app.py` binds to (defaults: `127.0.0.1:8700`). |
30
+ | `EM_APP_PORT` / `QLBM_APP_PORT` | Ports for the EM and QLBM subprocesses (defaults: `8701` and `8702`). |
31
+ | `EM_IFRAME_SRC` / `QLBM_IFRAME_SRC` | Public paths served by the proxy (e.g., `/em/` and `/qlbm/`). |
32
+
33
+ The bundled `docker/nginx.conf` proxies incoming traffic on `${PORT:-7860}` to the
34
+ individual services so the browser never tries to contact `127.0.0.1` directly.
app.py CHANGED
@@ -88,8 +88,9 @@ with SinglePageLayout(server) as layout:
88
  trame_html.Div(f"QLBM embed failed: {e}", style="padding:8px;color:#b00020;")
89
 
90
  if __name__ == "__main__":
91
- # For Hugging Face, use the PORT env var and listen on 0.0.0.0
92
- port = int(os.environ.get("PORT", 7860))
93
- server.start(host="0.0.0.0", port=port, open_browser=False)
 
94
 
95
 
 
88
  trame_html.Div(f"QLBM embed failed: {e}", style="padding:8px;color:#b00020;")
89
 
90
  if __name__ == "__main__":
91
+ # Allow reverse-proxy setups to pin the internal host/port independently from the public PORT
92
+ port = int(os.environ.get("APP_PORT") or os.environ.get("PORT", 7860))
93
+ host = os.environ.get("APP_HOST", "0.0.0.0")
94
+ server.start(host=host, port=port, open_browser=False)
95
 
96
 
docker/nginx.conf ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ user user;
2
+ worker_processes auto;
3
+ pid /tmp/nginx.pid;
4
+ error_log /tmp/nginx.error.log warn;
5
+ access_log /tmp/nginx.access.log;
6
+
7
+ events {
8
+ worker_connections 1024;
9
+ }
10
+
11
+ http {
12
+ include /etc/nginx/mime.types;
13
+ default_type application/octet-stream;
14
+ sendfile on;
15
+ keepalive_timeout 65;
16
+
17
+ map $http_upgrade $connection_upgrade {
18
+ default upgrade;
19
+ '' close;
20
+ }
21
+
22
+ upstream app_root {
23
+ server 127.0.0.1:8700;
24
+ }
25
+
26
+ upstream app_em {
27
+ server 127.0.0.1:8701;
28
+ }
29
+
30
+ upstream app_qlbm {
31
+ server 127.0.0.1:8702;
32
+ }
33
+
34
+ server {
35
+ listen 7860;
36
+ server_name _;
37
+
38
+ location /em/ {
39
+ proxy_pass http://app_em/;
40
+ proxy_http_version 1.1;
41
+ proxy_set_header Host $host;
42
+ proxy_set_header X-Real-IP $remote_addr;
43
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
44
+ proxy_set_header X-Forwarded-Proto $scheme;
45
+ proxy_set_header Upgrade $http_upgrade;
46
+ proxy_set_header Connection $connection_upgrade;
47
+ }
48
+
49
+ location /qlbm/ {
50
+ proxy_pass http://app_qlbm/;
51
+ proxy_http_version 1.1;
52
+ proxy_set_header Host $host;
53
+ proxy_set_header X-Real-IP $remote_addr;
54
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
55
+ proxy_set_header X-Forwarded-Proto $scheme;
56
+ proxy_set_header Upgrade $http_upgrade;
57
+ proxy_set_header Connection $connection_upgrade;
58
+ }
59
+
60
+ location / {
61
+ proxy_pass http://app_root/;
62
+ proxy_http_version 1.1;
63
+ proxy_set_header Host $host;
64
+ proxy_set_header X-Real-IP $remote_addr;
65
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
66
+ proxy_set_header X-Forwarded-Proto $scheme;
67
+ proxy_set_header Upgrade $http_upgrade;
68
+ proxy_set_header Connection $connection_upgrade;
69
+ }
70
+ }
71
+ }
em.ipynb DELETED
@@ -1,254 +0,0 @@
1
- {
2
- "cells": [
3
- {
4
- "cell_type": "code",
5
- "execution_count": 17,
6
- "id": "fe68f8a9",
7
- "metadata": {},
8
- "outputs": [
9
- {
10
- "name": "stdout",
11
- "output_type": "stream",
12
- "text": [
13
- "Available backends: [<qiskit_ionq.ionq_backend.IonQSimulatorBackend object at 0x7f91854ff520>, <qiskit_ionq.ionq_backend.IonQQPUBackend object at 0x7f91851f8d90>]\n"
14
- ]
15
- },
16
- {
17
- "name": "stderr",
18
- "output_type": "stream",
19
- "text": [
20
- "/home/cudaq/.local/lib/python3.10/site-packages/qiskit_ionq/ionq_backend.py:127: IonQTranspileLevelWarning: Transpiler default optimization_level=2. IonQ (QIS) recommends 0-1 to avoid aggressive re-synthesis; use transpile(..., optimization_level=1).\n",
21
- " warn_bad_transpile_level()\n"
22
- ]
23
- }
24
- ],
25
- "source": [
26
- "from qiskit import QuantumCircuit\n",
27
- "from qiskit_ionq import IonQProvider\n",
28
- "import os\n",
29
- "my_api_key = os.getenv(\"IONQ_API_KEY\")\n",
30
- "# # provider = IonQProvider()\n",
31
- "\n",
32
- "api_token = \"SgUkiDq1r2bVEadyiUfvtuxQ03Qci6UW\"\n",
33
- "provider = IonQProvider(api_token)\n",
34
- "\n",
35
- "\n",
36
- "backends = provider.backends()\n",
37
- "print(\"Available backends:\", backends)"
38
- ]
39
- },
40
- {
41
- "cell_type": "code",
42
- "execution_count": 15,
43
- "id": "6b2b90ef",
44
- "metadata": {},
45
- "outputs": [
46
- {
47
- "name": "stderr",
48
- "output_type": "stream",
49
- "text": [
50
- "/home/cudaq/.local/lib/python3.10/site-packages/qiskit_ionq/ionq_backend.py:127: IonQTranspileLevelWarning: Transpiler default optimization_level=2. IonQ (QIS) recommends 0-1 to avoid aggressive re-synthesis; use transpile(..., optimization_level=1).\n",
51
- " warn_bad_transpile_level()\n"
52
- ]
53
- },
54
- {
55
- "name": "stdout",
56
- "output_type": "stream",
57
- "text": [
58
- "Expectation values: [1.55664062]\n"
59
- ]
60
- }
61
- ],
62
- "source": [
63
- "from qiskit import QuantumCircuit\n",
64
- "from qiskit_ionq import IonQProvider\n",
65
- "\n",
66
- "backend = provider.get_backend(\"simulator\")\n",
67
- "\n",
68
- "from qiskit.circuit.library import real_amplitudes\n",
69
- "from qiskit.quantum_info import SparsePauliOp\n",
70
- "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
71
- "from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2 as Estimator\n",
72
- "from qiskit.primitives import StatevectorEstimator\n",
73
- "\n",
74
- "# service = QiskitRuntimeService()\n",
75
- "# backend = service.least_busy(operational=True, simulator=False)\n",
76
- " \n",
77
- "psi = real_amplitudes(num_qubits=2, reps=2)\n",
78
- "hamiltonian = SparsePauliOp.from_list([(\"II\", 1), (\"IZ\", 2), (\"XI\", 3)])\n",
79
- "theta = [0, 1, 1, 2, 3, 5]\n",
80
- " \n",
81
- "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n",
82
- "isa_psi = pm.run(psi)\n",
83
- "isa_observables = hamiltonian.apply_layout(isa_psi.layout)\n",
84
- " \n",
85
- "estimator = Estimator(mode=backend)\n",
86
- " \n",
87
- "# calculate [ <psi(theta)|hamiltonian|psi(theta)> ]\n",
88
- "job = estimator.run([(isa_psi, isa_observables, [theta])])\n",
89
- "pub_result = job.result()[0]\n",
90
- "print(f\"Expectation values: {pub_result.data.evs}\")"
91
- ]
92
- },
93
- {
94
- "cell_type": "code",
95
- "execution_count": 16,
96
- "id": "4147b18a",
97
- "metadata": {},
98
- "outputs": [
99
- {
100
- "name": "stdout",
101
- "output_type": "stream",
102
- "text": [
103
- "Expectation values: [1.55555728]\n"
104
- ]
105
- }
106
- ],
107
- "source": [
108
- "qiskit_estimator = StatevectorEstimator()\n",
109
- "job2 = qiskit_estimator.run([(psi, hamiltonian, [theta])])\n",
110
- "pub_result = job2.result()[0]\n",
111
- "print(f\"Expectation values: {pub_result.data.evs}\") "
112
- ]
113
- },
114
- {
115
- "cell_type": "code",
116
- "execution_count": null,
117
- "id": "109a7251",
118
- "metadata": {},
119
- "outputs": [],
120
- "source": [
121
- "def run_sim_ionq(nx, na, R, initial_state, T, snapshot_dt=None, stop_check=None, progress_callback=None):\n",
122
- " \"\"\"\n",
123
- " Runs the quantum simulation for electromagnetic scattering with fixed dt=0.1.\n",
124
- " Captures frames only at user-defined snapshot times: [0, Δt, 2Δt, ..., ≤ T_eff],\n",
125
- " always including t=0 and the final solver-aligned T (T_eff = floor(T/dt)*dt).\n",
126
- "\n",
127
- " Returns:\n",
128
- " frames (np.ndarray), snapshot_times (np.ndarray)\n",
129
- " \"\"\"\n",
130
- " dt = 0.1\n",
131
- " # Validate total time and compute solver-aligned end time\n",
132
- " try:\n",
133
- " T_val = float(T)\n",
134
- " except Exception:\n",
135
- " return np.array([]), np.array([])\n",
136
- " if T_val <= 0:\n",
137
- " return np.array([]), np.array([])\n",
138
- "\n",
139
- " steps = int(np.floor(T_val / dt))\n",
140
- " if steps <= 0:\n",
141
- " return np.array([]), np.array([])\n",
142
- " T_eff = steps * dt\n",
143
- "\n",
144
- " # Determine snapshot Δt on solver grid\n",
145
- " tol = 1e-12\n",
146
- " if snapshot_dt is None:\n",
147
- " snapshot_dt_val = dt\n",
148
- " else:\n",
149
- " try:\n",
150
- " snapshot_dt_val = float(snapshot_dt)\n",
151
- " except Exception:\n",
152
- " snapshot_dt_val = dt\n",
153
- " if snapshot_dt_val < dt - tol:\n",
154
- " snapshot_dt_val = dt\n",
155
- " k = max(1, int(round(snapshot_dt_val / dt)))\n",
156
- " snapshot_dt_eff = k * dt\n",
157
- "\n",
158
- " # Build requested snapshot times on solver grid\n",
159
- " target_times = [0.0]\n",
160
- " t = 0.0\n",
161
- " while t + snapshot_dt_eff <= T_eff + tol:\n",
162
- " t = round(t + snapshot_dt_eff, 12)\n",
163
- " if t <= T_eff + tol:\n",
164
- " target_times.append(min(t, T_eff))\n",
165
- " if abs(target_times[-1] - T_eff) > tol:\n",
166
- " target_times.append(T_eff)\n",
167
- "\n",
168
- " # Setup circuit\n",
169
- " nq = int(np.ceil(np.log2(nx)))\n",
170
- " dp = 2 * R * np.pi / 2 ** na\n",
171
- " p = np.arange(-R * np.pi, R * np.pi, step=dp)\n",
172
- " fp = np.exp(-np.abs(p))\n",
173
- " system, ancilla = QuantumRegister(2 * nq + 2), QuantumRegister(na)\n",
174
- " qc = QuantumCircuit(system, ancilla)\n",
175
- " qc.append(StatePreparation(initial_state), system)\n",
176
- " qc.append(StatePreparation(fp / np.linalg.norm(fp)), ancilla)\n",
177
- " expA1 = V1(nx, dt).to_gate()\n",
178
- " expA2 = V2(nx, dt)\n",
179
- "\n",
180
- " frames = []\n",
181
- " \n",
182
- " # Run the circuit on IonQ's platform:\n",
183
- " job = simulator_backend.run(qc, shots=10000)\n",
184
- "\n",
185
- " # Print the counts\n",
186
- " # print(job.get_counts())\n",
187
- " # # Capture initial frame at t=0\n",
188
- " # sv0 = np.real(Statevector(qc)).reshape(2 ** na, 2 ** (2 * nq + 2))\n",
189
- " # frames.append(sv0[2 ** (na - 1)])\n",
190
- " # next_idx = 1 # next target_times index to capture\n",
191
- "\n",
192
- " # for i in range(steps):\n",
193
- " # if stop_check and stop_check():\n",
194
- " # print(f\"Simulation interrupted at step {i}/{steps}\")\n",
195
- " # break\n",
196
- " # # One solver step\n",
197
- " # qc.append(QFTGate(na), ancilla)\n",
198
- " # qc.x(ancilla[-1])\n",
199
- " # for j in range(na - 1):\n",
200
- " # qc.append(expA1.control().repeat(2 ** j), [ancilla[j]] + system[:])\n",
201
- " # qc.append(expA1.inverse().control(ctrl_state=\"0\").repeat(2 ** (na - 1)), [ancilla[na - 1]] + system[:])\n",
202
- " # qc.append(expA2, system[:])\n",
203
- " # qc.x(ancilla[-1])\n",
204
- " # qc.append(QFTGate(na).inverse(), ancilla)\n",
205
- "\n",
206
- " # current_time = (i + 1) * dt\n",
207
- " # if next_idx < len(target_times) and abs(current_time - target_times[next_idx]) <= tol:\n",
208
- " # u = np.real(Statevector(qc)).reshape(2 ** na, 2 ** (2 * nq + 2))\n",
209
- " # frames.append(u[2 ** (na - 1)])\n",
210
- " # next_idx += 1\n",
211
- "\n",
212
- " # if progress_callback:\n",
213
- " # try:\n",
214
- " # progress = ((i + 1) / steps) * 100\n",
215
- " # progress_callback(progress)\n",
216
- " # except Exception:\n",
217
- " # pass\n",
218
- "\n",
219
- " # if progress_callback:\n",
220
- " # try:\n",
221
- " # progress_callback(100.0)\n",
222
- " # except Exception:\n",
223
- " # pass\n",
224
- "\n",
225
- " # # Ensure snapshot_times align with number of captured frames (covers early stop)\n",
226
- " # frames_arr = np.asarray(frames)\n",
227
- " # times_arr = np.asarray(target_times[: len(frames_arr)])\n",
228
- " # return frames_arr, times_arr\n",
229
- "\n"
230
- ]
231
- }
232
- ],
233
- "metadata": {
234
- "kernelspec": {
235
- "display_name": "Python 3",
236
- "language": "python",
237
- "name": "python3"
238
- },
239
- "language_info": {
240
- "codemirror_mode": {
241
- "name": "ipython",
242
- "version": 3
243
- },
244
- "file_extension": ".py",
245
- "mimetype": "text/x-python",
246
- "name": "python",
247
- "nbconvert_exporter": "python",
248
- "pygments_lexer": "ipython3",
249
- "version": "3.10.12"
250
- }
251
- },
252
- "nbformat": 4,
253
- "nbformat_minor": 5
254
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
em_trame.py CHANGED
@@ -2557,6 +2557,7 @@ def on_qpu_plot_filter_change(qpu_plot_filter, **kwargs):
2557
 
2558
  # --- Inline embedding support for single-script startup ---
2559
  _em_started = False
 
2560
 
2561
  def _ensure_em_server_started():
2562
  global _em_started
@@ -2599,11 +2600,12 @@ def build(host_server):
2599
  _ensure_em_server_started()
2600
  h_state, _ = host_server.state, host_server.controller
2601
  default_port = os.environ.get("EM_APP_PORT", os.environ.get("PORT_EM", "8701"))
 
2602
  if not getattr(h_state, "em_iframe_src", None):
2603
- h_state.em_iframe_src = f"http://localhost:{default_port}/"
2604
  with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
2605
  trame_html.Iframe(
2606
- src=("em_iframe_src", f"http://localhost:{default_port}/"),
2607
  style="border:0; width:100%; height: calc(100vh - 64px);",
2608
  )
2609
  # Optional debug note if background start has issues
 
2557
 
2558
  # --- Inline embedding support for single-script startup ---
2559
  _em_started = False
2560
+ _em_iframe_src = os.environ.get("EM_IFRAME_SRC", "").strip()
2561
 
2562
  def _ensure_em_server_started():
2563
  global _em_started
 
2600
  _ensure_em_server_started()
2601
  h_state, _ = host_server.state, host_server.controller
2602
  default_port = os.environ.get("EM_APP_PORT", os.environ.get("PORT_EM", "8701"))
2603
+ iframe_src = _em_iframe_src or f"http://localhost:{default_port}/"
2604
  if not getattr(h_state, "em_iframe_src", None):
2605
+ h_state.em_iframe_src = iframe_src
2606
  with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
2607
  trame_html.Iframe(
2608
+ src=("em_iframe_src", iframe_src),
2609
  style="border:0; width:100%; height: calc(100vh - 64px);",
2610
  )
2611
  # Optional debug note if background start has issues
pages/__pycache__/em_page.cpython-311.pyc CHANGED
Binary files a/pages/__pycache__/em_page.cpython-311.pyc and b/pages/__pycache__/em_page.cpython-311.pyc differ
 
pages/__pycache__/qlbm_page.cpython-311.pyc CHANGED
Binary files a/pages/__pycache__/qlbm_page.cpython-311.pyc and b/pages/__pycache__/qlbm_page.cpython-311.pyc differ
 
pages/em_page.py CHANGED
@@ -8,6 +8,7 @@ import atexit
8
  # Keep a single child process for the EM app
9
  _em_proc = None
10
  _EM_HOST = os.environ.get("EM_HOST", "127.0.0.1")
 
11
 
12
 
13
  def _kill_em_process():
@@ -37,6 +38,9 @@ def _ensure_em_process_started():
37
  base_dir = os.path.dirname(os.path.dirname(__file__))
38
  em_path = os.path.join(base_dir, "em_trame.py")
39
  env = os.environ.copy()
 
 
 
40
  # Port used by iframe
41
  env.setdefault("EM_APP_PORT", env.get("PORT_EM", "8701"))
42
  env.setdefault("EM_HOST", _EM_HOST)
@@ -54,9 +58,10 @@ def build(server):
54
  _ensure_em_process_started()
55
  port = os.environ.get("EM_APP_PORT", os.environ.get("PORT_EM", "8701"))
56
  host = os.environ.get("EM_HOST", _EM_HOST)
 
57
  with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
58
  trame_html.Iframe(
59
- src=("em_iframe_src", f"http://{host}:{port}/"),
60
  style="border:0; width:100%; height: calc(100vh - 64px);",
61
  )
62
  trame_html.Div(
 
8
  # Keep a single child process for the EM app
9
  _em_proc = None
10
  _EM_HOST = os.environ.get("EM_HOST", "127.0.0.1")
11
+ _EM_IFRAME_SRC = os.environ.get("EM_IFRAME_SRC", "").strip()
12
 
13
 
14
  def _kill_em_process():
 
38
  base_dir = os.path.dirname(os.path.dirname(__file__))
39
  em_path = os.path.join(base_dir, "em_trame.py")
40
  env = os.environ.copy()
41
+ # Prevent hosted platforms from forcing the subprocess to bind to $PORT
42
+ env.pop("PORT", None)
43
+ env.pop("HF_PORT", None)
44
  # Port used by iframe
45
  env.setdefault("EM_APP_PORT", env.get("PORT_EM", "8701"))
46
  env.setdefault("EM_HOST", _EM_HOST)
 
58
  _ensure_em_process_started()
59
  port = os.environ.get("EM_APP_PORT", os.environ.get("PORT_EM", "8701"))
60
  host = os.environ.get("EM_HOST", _EM_HOST)
61
+ iframe_src = _EM_IFRAME_SRC or f"http://{host}:{port}/"
62
  with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
63
  trame_html.Iframe(
64
+ src=("em_iframe_src", iframe_src),
65
  style="border:0; width:100%; height: calc(100vh - 64px);",
66
  )
67
  trame_html.Div(
pages/qlbm_page.py CHANGED
@@ -13,6 +13,7 @@ from trame.widgets import html as trame_html
13
 
14
  _qlbm_proc = None
15
  _QLBM_HOST = os.environ.get("QLBM_HOST", "127.0.0.1")
 
16
 
17
 
18
  def _kill_qlbm_process():
@@ -37,6 +38,8 @@ def _ensure_qlbm_process_started():
37
  base_dir = os.path.dirname(os.path.dirname(__file__))
38
  qlbm_path = os.path.join(base_dir, "qlbm.py")
39
  env = os.environ.copy()
 
 
40
  env.setdefault("QLBM_APP_PORT", env.get("PORT_QLBM", "8702"))
41
  env.setdefault("QLBM_HOST", _QLBM_HOST)
42
  py = sys.executable or "python"
@@ -57,9 +60,10 @@ def build(server): # signature matches app.py expectation
57
  _ensure_qlbm_process_started()
58
  port = os.environ.get("QLBM_APP_PORT", os.environ.get("PORT_QLBM", "8702"))
59
  host = os.environ.get("QLBM_HOST", _QLBM_HOST)
 
60
  with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
61
  trame_html.Iframe(
62
- src=("qlbm_iframe_src", f"http://{host}:{port}/"),
63
  style="border:0;width:100%;height:100%;min-height:0;",
64
  )
65
  trame_html.Div(
 
13
 
14
  _qlbm_proc = None
15
  _QLBM_HOST = os.environ.get("QLBM_HOST", "127.0.0.1")
16
+ _QLBM_IFRAME_SRC = os.environ.get("QLBM_IFRAME_SRC", "").strip()
17
 
18
 
19
  def _kill_qlbm_process():
 
38
  base_dir = os.path.dirname(os.path.dirname(__file__))
39
  qlbm_path = os.path.join(base_dir, "qlbm.py")
40
  env = os.environ.copy()
41
+ env.pop("PORT", None)
42
+ env.pop("HF_PORT", None)
43
  env.setdefault("QLBM_APP_PORT", env.get("PORT_QLBM", "8702"))
44
  env.setdefault("QLBM_HOST", _QLBM_HOST)
45
  py = sys.executable or "python"
 
60
  _ensure_qlbm_process_started()
61
  port = os.environ.get("QLBM_APP_PORT", os.environ.get("PORT_QLBM", "8702"))
62
  host = os.environ.get("QLBM_HOST", _QLBM_HOST)
63
+ iframe_src = _QLBM_IFRAME_SRC or f"http://{host}:{port}/"
64
  with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
65
  trame_html.Iframe(
66
+ src=("qlbm_iframe_src", iframe_src),
67
  style="border:0;width:100%;height:100%;min-height:0;",
68
  )
69
  trame_html.Div(
qlbm.py CHANGED
@@ -613,6 +613,7 @@ ctrl.sim_plot_update = sim_fig.update
613
  # Embedding helper
614
  # --------------------------------------------------------------------------------------
615
  _started = False
 
616
 
617
  def build(host_server):
618
  """Embed this app into another Trame server via an iframe, auto-starting its own server."""
@@ -629,40 +630,11 @@ def build(host_server):
629
  _started = True
630
  h_state = host_server.state
631
  default_port = os.environ.get("TRAME3D_PORT", "8811")
 
632
  if not getattr(h_state, "trame3d_iframe_src", None):
633
- h_state.trame3d_iframe_src = f"http://localhost:{default_port}/"
634
  with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
635
- trame_html.Iframe(src=("trame3d_iframe_src", f"http://localhost:{default_port}/"), style="border:0; width:100%; height: calc(100vh - 64px);")
636
- trame_html.Div("If the 3D QLBM view is blank, wait a few seconds or verify TRAME3D_PORT.", style="color: rgba(0,0,0,.6); padding: 6px;")
637
-
638
- # Controller hooks for plot updates
639
- ctrl.qubit_plot_update = qubit_fig.update
640
- ctrl.sim_plot_update = sim_fig.update
641
-
642
- # --------------------------------------------------------------------------------------
643
- # Embedding helper
644
- # --------------------------------------------------------------------------------------
645
- _started = False
646
-
647
- def build(host_server):
648
- """Embed this app into another Trame server via an iframe, auto-starting its own server."""
649
- global _started
650
- if not _started:
651
- import threading
652
- def _run():
653
- try:
654
- port = int(os.environ.get("TRAME3D_PORT", "8811"))
655
- server.start(host="0.0.0.0", port=port, open_browser=False)
656
- except Exception as e:
657
- print(f"trame3d server failed to start: {e}")
658
- threading.Thread(target=_run, daemon=True).start()
659
- _started = True
660
- h_state = host_server.state
661
- default_port = os.environ.get("TRAME3D_PORT", "8811")
662
- if not getattr(h_state, "trame3d_iframe_src", None):
663
- h_state.trame3d_iframe_src = f"http://localhost:{default_port}/"
664
- with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
665
- trame_html.Iframe(src=("trame3d_iframe_src", f"http://localhost:{default_port}/"), style="border:0; width:100%; height: calc(100vh - 64px);")
666
  trame_html.Div("If the 3D QLBM view is blank, wait a few seconds or verify TRAME3D_PORT.", style="color: rgba(0,0,0,.6); padding: 6px;")
667
 
668
  # --- Entry point ---
 
613
  # Embedding helper
614
  # --------------------------------------------------------------------------------------
615
  _started = False
616
+ _iframe_src = os.environ.get("QLBM_IFRAME_SRC", "").strip()
617
 
618
  def build(host_server):
619
  """Embed this app into another Trame server via an iframe, auto-starting its own server."""
 
630
  _started = True
631
  h_state = host_server.state
632
  default_port = os.environ.get("TRAME3D_PORT", "8811")
633
+ iframe_src = _iframe_src or f"http://localhost:{default_port}/"
634
  if not getattr(h_state, "trame3d_iframe_src", None):
635
+ h_state.trame3d_iframe_src = iframe_src
636
  with vuetify3.VContainer(fluid=True, classes="pa-0 fill-height"):
637
+ trame_html.Iframe(src=("trame3d_iframe_src", iframe_src), style="border:0; width:100%; height: calc(100vh - 64px);")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
638
  trame_html.Div("If the 3D QLBM view is blank, wait a few seconds or verify TRAME3D_PORT.", style="color: rgba(0,0,0,.6); padding: 6px;")
639
 
640
  # --- Entry point ---