-
Notifications
You must be signed in to change notification settings - Fork 0
Local Setup
- Bun (frontend)
- uv (backend)
-
Redis (required for the job queue +
/api/jobs/*) - Optional (production stack): MongoDB, Hugging Face, Modal
cd src/backend
uv python pin 3.12
uv sync
# Optional: start from the example env file
cp .env.example .env
# For local dev (unlimited limits), set: PRODUCTION=false
# Start Redis (required for /api/jobs/* and /api/queue)
# macOS (Homebrew): brew install redis && redis-server
# Docker: docker run -p 6379:6379 redis:7
uv run uvicorn main:app --reload --port 8000Backend runs at http://localhost:8000
cd src/interface
bun install
# Required for GitHub sign-in
cp .env.example .env
# Set NEXTAUTH_URL=http://localhost:3000 and fill GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET
bun devFrontend runs at http://localhost:3000
For GPU-accelerated training (serverless, pay-per-use), deploy the Modal worker.
Important:
- Your backend must enqueue to a Redis instance reachable from Modal (
REDIS_URL). - Disable the backend's local worker so jobs aren't consumed on CPU (
START_LOCAL_RQ_WORKER=false). -
REDIS_URL=redis://localhost:6379will NOT work with Modal (localhost is inside the Modal container). -
REDIS_URLmust include a scheme likeredis://(e.g.redis://:PASSWORD@HOST:6379). - For instant spawn (no waiting for a periodic schedule), the backend process must have the
modalpackage installed and be authenticated to Modal. If that fails, you can configure an HTTP fallback (MODAL_POLL_URL).
Redis URL examples:
- Password-only Redis:
redis://:<PASSWORD>@HOST:6379/0 - ACL Redis (common user is
default):redis://default:<PASSWORD>@HOST:6379/0
cd src/backend
# Install backend + dev deps (includes Modal CLI)
uv sync
# Auth (pick one)
# Option A: browser/OAuth flow
uv run modal setup
#
# Option B: token flow (Modal dashboard -> Settings -> Tokens)
uv run modal token set --token-id <token-id> --token-secret <token-secret>
# Add your Redis secret (must match backend REDIS_URL).
# Modal containers can't read your local `.env`, and you shouldn't bake secrets into the image.
uv run modal secret create --force pyreflect-redis \
REDIS_URL="redis://:PASSWORD@YOUR_PUBLIC_REDIS_HOST:6379" \
HF_TOKEN="hf_..." \
HF_REPO_ID="your-username/pyreflect-models" \
HF_CHECKPOINT_REPO_ID="your-username/checkpoints" \
MODEL_STORAGE="hf" \
MODAL_TRIGGER_TOKEN="change-me" \
MONGODB_URI="mongodb+srv://..." # Optional: enables history persistence from Modal
# Deploy (backend triggers an instant spawn; optional HTTP trigger is a fallback)
uv run modal deploy modal_worker.pyThe worker automatically:
- Spins up a T4 GPU when jobs are queued
- Runs the same
service.jobs.run_training_jobcode as local workers (progress, results, model uploads) - Uploads the model directly to Hugging Face when
MODEL_STORAGE=hf(no backend model storage) - Scales down when idle (no cost)
Verify end-to-end:
- Backend:
GET /api/queueshould showlocal_worker_enabled: falseandremote_workers_compatible: true. - When you enqueue a training job,
queued_jobsshould become> 0briefly. - Modal logs should show
pending=<N>and thenStarting RQ SimpleWorker ... (burst mode):
cd src/backend
uv run modal app logs pyreflect-worker --timestampsStop/Undeploy:
cd src/backend
uv run modal app stop pyreflect-workerIf your Redis runs on your own machine, Modal can only reach it if it’s reachable from the public internet. That usually means your machine has a public IP (or you set up port-forwarding), and Redis is configured to accept remote connections securely.
Minimum checklist (Redis host):
- Configure Redis to listen on a reachable interface (
bind 0.0.0.0or your public NIC) and require auth (requirepassor ACLs). - Open firewall / router port-forward for TCP
6379to the Redis host. - Confirm connectivity from outside your network:
redis-cli -h <public-host> -a <password> ping(should returnPONG).
If you can’t safely expose Redis publicly, use a managed Redis (Upstash / Redis Cloud) and point both the backend and Modal at it.
No. uv run modal deploy ... deploys the Modal app to Modal’s infra and runs independently. Starting uvicorn only starts the API server.
modal deploy registers your functions. By default, the backend will try to trigger a GPU worker immediately
after enqueuing a job (MODAL_INSTANT_SPAWN=true). The Modal worker also exposes an optional HTTP trigger (poll_queue_http)
so you can trigger spawns without relying on the backend being authenticated to Modal.
To debug instant spawn from the backend, call:
curl -s -X POST http://localhost:8000/api/queue/spawn | jqThe backend also opportunistically triggers a spawn from GET /api/queue when it sees queued jobs and no workers.
If you see reason: modal_spawn_failed, the backend is not authenticated to Modal. Run uv run modal setup (or set
MODAL_TOKEN_ID + MODAL_TOKEN_SECRET in the backend environment).
If you see reason: modal_spawn_failed and want an auth-free backend, set these backend env vars:
-
MODAL_POLL_URL: the deployedpoll_queue_httpendpoint URL (from Modal deploy output) -
MODAL_TRIGGER_TOKEN: must match theMODAL_TRIGGER_TOKENstored in the Modal secret (sent as?token=...)
Note on cost: the on-demand poll_queue does not “keep a container warm” 24/7. Modal bills for compute time used by each
invocation; the poller is intentionally lightweight (1 vCPU, minimal deps) and exits quickly when the queue is empty.
To start immediately (for testing), run the poller once:
cd src/backend
uv run modal run modal_worker.py::poll_queueIf you see a Modal DNS error with %0a or spaces in the hostname, your MODAL_POLL_URL value contains whitespace/newlines.
Set MODAL_POLL_URL as a single line URL (no line wrapping) and restart the backend.
# Kill process on port 8000
lsof -ti:8000 | xargs kill -9
# Kill process on port 3000
lsof -ti:3000 | xargs kill -9To deploy with resource limits (prevents abuse):
Option 1: Environment variable
PRODUCTION=true uv run uvicorn main:app --port 8000Option 2: Create .env file in src/backend/
# .env
PRODUCTION=true
# Comma-separated GitHub usernames that can use local/unlimited limits in production.
# Example: LIMITS_WHITELIST_USER_IDS=undeemed,alice,bob
LIMITS_WHITELIST_USER_IDS=
# CORS (comma-separated origins)
CORS_ORIGINS=http://localhost:3000,https://your-app.vercel.app
# Or use a regex allowlist (useful for multiple subdomains):
#CORS_ALLOW_ORIGIN_REGEX=https://(pyreflect\.shlawg\.com|localhost:3000)$
# Redis queue (required for background jobs in the UI)
REDIS_URL=redis://localhost:6379
RQ_JOB_TIMEOUT=2h
# Instant Modal worker spawn (on-demand, no schedule)
MODAL_INSTANT_SPAWN=true
MODAL_POLL_URL=https://<your-modal-endpoint>/poll_queue_http
MODAL_TRIGGER_TOKEN=change-me # must match Modal secret MODAL_TRIGGER_TOKEN (sent as ?token=...)
# Disable local worker if using Modal/remote GPU workers
START_LOCAL_RQ_WORKER=false
# History + model storage (used in the hosted deployment)
MONGODB_URI=mongodb+srv://...
HF_TOKEN=hf_...
HF_REPO_ID=your-username/pyreflect-models
HF_CHECKPOINT_REPO_ID=your-username/checkpoints
MODEL_STORAGE=hf
# Checkpointing (for pause/resume)
CHECKPOINT_EVERY_N_EPOCHS=5
# Optional: override individual limits
MAX_CURVES=5000
MAX_EPOCHS=50
MAX_BATCH_SIZE=64
MAX_CNN_LAYERS=12
MAX_DROPOUT=0.5
MAX_LATENT_DIM=32
MAX_AE_EPOCHS=100
MAX_MLP_EPOCHS=100Security note: the backend allowlist is only as trustworthy as the X-User-ID header. In production, do not expose the backend directly to untrusted clients unless you add a real auth layer.
Then run normally:
uv run uvicorn main:app --port 8000If you want the backend + Redis on your own machine (and Modal only for GPU), the minimum flow is:
- On the bare-metal host, run Redis and make it reachable from Modal (see “Bare-metal Redis” above).
- Point the backend to that same
REDIS_URLand disable the local worker:
cd src/backend
cp .env.example .env
# Edit:
# REDIS_URL=redis://:PASSWORD@<your-public-host>:6379
# START_LOCAL_RQ_WORKER=false
uv sync
uv run uvicorn main:app --host 0.0.0.0 --port 8000- Run the frontend either on the same host or locally, pointing it at your backend:
cd src/interface
NEXT_PUBLIC_API_URL=http://<baremetal-host>:8000 bun devNote: Modal workers do not share your bare-metal filesystem. For fastest end-to-end, store models on Hugging Face (MODEL_STORAGE=hf + HF_TOKEN + HF_REPO_ID) so the backend can redirect downloads to HF without any model upload back to the backend.
cd src/interface
vercel| Variable | Value |
|---|---|
NEXT_PUBLIC_API_URL |
https://your-backend.railway.app (or wherever backend is hosted) |
In your backend .env, add your Vercel URL:
CORS_ORIGINS=http://localhost:3000,https://your-app.vercel.app-
Adjust parameters in the left sidebar:
- Film Layers: Add/remove layers, adjust SLD, thickness, roughness
- Generator: Set number of curves and layers
- Training: Configure batch size, epochs, dropout, etc.
-
Click GENERATE to compute and visualize:
- NR Chart: Ground truth (solid) vs Computed (dashed)
- SLD Profile: Ground truth (solid black) vs Predicted (dashed red)
- Training Loss: Training and validation loss curves
- Chi Parameters: Scatter plot of actual vs predicted SLD values
-
Tips:
- Click any numeric value to type a custom number (e.g., 50000 curves)
- Watch the console for real-time training progress, warnings, and timing
- Use RESET to restore the example defaults
- Use COLLAPSE/EXPAND to manage long film layer lists
- Export individual graphs as CSV or all data as JSON
- Charts show model predictions compared to ground truth after training
-
Layer Bounds (for synthetic data):
Each layer parameter (SLD, thickness, roughness, iSLD) supports optional min/max bounds that control the variation range during synthetic curve generation. This matches the
layer_boundfunctionality in pyreflect notebooks.Setting bounds:
- Drag handles: Use the ◀ ▶ arrow handles on either side of the slider
-
Type values: Edit the
[min]and[max]input fields directly (shown as[min] value [max]above each slider)
Behavior:
- When any bounds are set,
numFilmLayersis locked to the current layer count - The center value is constrained within the bounds
- Clear individual bounds by setting them back to the center value, or clear all bounds with the
×button in the Film Layers header
Example (matching notebook usage):
# pyreflect notebook equivalent: layer_bound = [ dict(i=0, par='roughness', bounds=[1.177, 1.5215]), # substrate dict(i=1, par='sld', bounds=[3.47, 3.47]), # fixed SLD dict(i=1, par='thickness', bounds=[9.72, 14.62]), dict(i=2, par='sld', bounds=[3.72, 4.20]), dict(i=2, par='thickness', bounds=[8.72, 98.87]), ]
In the UI, expand a layer and adjust the bounds for each parameter. The backend receives the same
layerBoundstructure for theReflectivityDataGenerator.
For pretrained models or existing datasets, use the Data & Models section:
Supported files:
├── *.npy → Saved to src/backend/data/curves/
├── *.pth, *.pt → Saved to src/backend/data/
└── settings.yml → Saved to src/backend/
Files from pyreflect/datasets/ can be uploaded:
-
normalization_stat.npy- Normalization statistics -
trained_nr_sld_model_no_dropout.pth- Pretrained CNN model -
X_train_5_layers.npy,y_train_5_layers.npy- Training data
FOOT
SIDE