Small Flask app for exploring the drop-impact scaling theory described in the paper linked below. The browser collects Weber number We and Ohnesorge number Oh, then calls JSON endpoints to compute Reynolds number, classify the impact regime, and predict BetaMax. The homepage also supports batch CSV uploads for beta predictions.
- Paper: https://arxiv.org/abs/2408.12714
- Live site: https://sl25.comphy-lab.org/
SLtheoryWebsite/
app.py
batchProcess.py
calculateReynoldsNumber.py
deploy.sh
regimeDecide.py
phase_diagram_svg.py
SLtheory_prediction.py
SLtheory_model.json
static/
tokens.css
site.css
site.js
comphy-lab-mark.png
favicon-96x96.png
templates/index.html
requirements.txt
runtime.txt
vercel.json
app.py creates the Flask app, registers the blueprints, caps request bodies at 1 MB, and runs the server locally. calculateReynoldsNumber.py serves the homepage and computes Reynolds number. regimeDecide.py classifies the regime from We and Oh and returns the model-based predBeta prediction using SLtheory_prediction.py and SLtheory_model.json. batchProcess.py accepts CSV uploads with We and Oh columns, reuses the same theory-range validation as /regime, and returns a CSV with beta filled in or error for invalid rows. The phase diagram is rendered in Python by phase_diagram_svg.py. The frontend is split between templates/index.html, static/site.css, and static/site.js.
The page follows the CoMPhy Lab design system.
static/tokens.css is a verbatim copy of the upstream tokens.css, with a
provenance header naming the upstream commit; do not edit it here. Change a
core colour in the design-system repository, then re-vendor the file.
static/site.css composes the token primitives (.panel, .card, .eyebrow,
.btn, .field, .input, .chip, .hero-title) into the calculator layout
and never redefines a token. Brand webfonts are self-hosted under
static/fonts/ (Cormorant Garamond for the hero wordmark, Fraunces for
headings, IBM Plex Sans and Mono for body and code), loaded via
static/fonts/fonts.css before tokens.css. No Google Fonts CDN.
phase_diagram_svg.py draws the server-side SVG with the same paper, ink and
brand hues, so the figure matches the page in both themes. The theme choice is
stored under the shared comphy-theme key and falls back to the OS preference.
pip install -r requirements.txt
python app.pyOr use the helper script to create a virtualenv, install dependencies, and start the local server in one step:
chmod +x deploy.sh
./deploy.shBy default the script serves the app at http://127.0.0.1:5000 with Flask debug mode disabled. You can override the port from the CLI with ./deploy.sh 8000 or ./deploy.sh --port 8000. You can still override the bind address or default port with environment variables such as HOST=0.0.0.0 PORT=8000 ./deploy.sh.
If you need the Werkzeug debugger for local development, enable it explicitly on a loopback bind:
FLASK_DEBUG=1 ./deploy.shFLASK_DEBUG=1 is intentionally rejected for non-loopback HOST values to avoid exposing the debugger or debug tracebacks over the network.
The app is configured for Python 3.9 in runtime.txt.
GET /renders the single-page UI fromtemplates/index.html.POST /addexpects JSON like{"weberNumber": 10, "ohnesorgeNumber": 0.1}with1 <= We <= 10^3and10^-3 <= Oh <= 10^2, and returns{"result": <Re>}rounded to two decimals.POST /regimeexpects the same JSON and range bounds, and returns{"regime": "I" | "II" | "III" | "IV", "predBeta": <BetaMax>}withpredBetarounded to two decimals.POST /batchexpectsmultipart/form-datawith a.csvfile field namedfile. The CSV must containWeandOhheaders, uses the same1 <= We <= 10^3and10^-3 <= Oh <= 10^2limits as/regime, and returns a CSV download with abetacolumn. Invalid rows are marked aserrorand summarized inX-Row-Errors. Uploads larger than 1 MB are rejected with HTTP 413.GET /regime-diagram.svgreturns a server-rendered SVG of the Weber-Ohnesorge phase diagram. OptionalweberNumber,ohnesorgeNumber, andthemequery parameters add the current input marker and match the active light or dark theme.
The frontend calls /add and /regime separately after the user submits the input form, then renders Re, Regime, and predBeta together. Numeric results are rounded in the Python endpoints and displayed to two decimal places in the UI. Inputs outside the theory range are rejected in both the browser and the Flask routes. The regime figure is generated by Python and returned as SVG.
vercel.json routes all origin traffic to app.py using @vercel/python.
Cloudflare's sl2-proxy Worker provides the canonical public hostname and
proxies accepted requests to that Vercel origin.
The canonical public site is sl25.comphy-lab.org. Legacy GET and HEAD requests
under comphy-lab.org/sl25 and comphy-lab.org/sl2 redirect permanently with
HTTP 308 while preserving the path suffix and query string. Existing root API
and asset routes on comphy-lab.org remain available for compatibility.
The Worker's two rate-limit bindings apply separate per-IP, per-route limits to
calculation POSTs: 120 requests per minute for each of /add and /regime, and
20 requests per minute for /batch. Canonical and legacy forms share counters.
Static assets, the phase-diagram GET, and other GET requests are not counted.
Limited requests receive HTTP 429 with Retry-After: 60. Wrangler disables both
workers.dev and preview URLs.
Run the Worker tests and validate its deployment bundle from cloudflare/:
node --test sl2-proxy.test.mjs
npx --yes wrangler@4.78.0 deploy --dry-runThe rate-limit binding requires Wrangler 4.36.0 or later.
The imported WSGI application requires X-SL25-Origin-Token on every request,
including static files and Socket.IO transport paths. The public Worker supplies
this header from its SL25_ORIGIN_TOKEN secret after checking the public route
and method. Client-supplied credentials are discarded. The hosted application
stores only the SHA-256 verifier in origin-auth.json; the random 256-bit bearer
must never be committed or exposed to the browser. Missing or malformed origin
configuration returns a non-cacheable 503; missing or incorrect credentials
return a non-cacheable 403 before application dispatch.
Vercel Authentication must use Standard Protection for this project. It protects older, generated and preview deployment URLs; the WSGI verifier protects the current production alias that Standard Protection leaves public. Verify both boundaries after deployment. Standard Protection is available on the existing Hobby plan; the paid All Deployments setting is not required.
For initial delivery, provision the Worker secret and deploy the reviewed proxy before deploying the guarded Python application. Verify ordinary canonical and legacy requests before moving to the second stage. Upstream redirects are rejected, and legacy HTML assets use the canonical public hostname, so the credential cannot escape through redirected or browser-issued origin requests. Treat token rotation as a coordinated release; zero-downtime rotation needs a staged verifier change that temporarily accepts both token digests.
Run the origin and compatibility tests with the repository's Python dependencies:
python -m unittest discover -s tests -p 'test_*.py'For anonymous local development, use python app.py or ./deploy.sh. Both use
the explicit local entry point and preserve HOST, PORT and loopback-only
debug behaviour. Importing app through a WSGI server enables the origin guard
even when Vercel's environment markers are absent.
This site has two production releases: the Vercel Python origin and the
Cloudflare sl2-proxy Worker. Record them as one compatible pair after a
change. The release receipt must contain:
- The Git
maincommit, passing Python origin tests and Worker tests, and the Vercel production deployment ID, source commit and production alias read back from Vercel. A successful build alone does not prove which origin the alias serves. - The active Cloudflare Worker version ID, the custom-domain and legacy route
assignments read back from Cloudflare, and confirmation that the
SL25_ORIGIN_TOKENsecret binding exists. Record presence only, never its value or the verifier digest. - Live checks through
https://sl25.comphy-lab.org/: the page, a local static asset, the phase diagram and representative/add,/regimeand/batchrequests. Check the legacy/sl25and/sl2GET/HEAD redirects and the retained root API paths. Confirm the rate-limit bindings and unrelated-path denial from the deployed Worker configuration and its automated tests; do not exhaust the production rate limits for a receipt. - Separate origin-isolation checks: an unauthenticated request to the Vercel
production alias is denied by the WSGI guard, and older/generated/preview
deployment URLs remain under Vercel Standard Protection. Confirm
workers.devand Worker preview URLs are still disabled.
For a failed release, restore the last verified pair. Promote the previous known-good Vercel deployment if the origin changed; roll back to the previous known-good Worker version if the proxy changed. Keep the Worker secret and Vercel verifier compatible throughout. Re-run the public route and origin isolation checks, and record the restored deployment and Worker version IDs. Credential rotation needs its staged verifier procedure above; rolling back only one side of a rotation can make the public site fail closed.
- The canonical public calculator remains callable within its generous limits; GET requests, including phase diagrams, remain outside the calculation counters.
- Existing root API routes on
comphy-lab.orgremain part of the compatibility surface; a later DNS-only transition for the main site needs separate route planning. - Cloudflare's browser-integrity control returns error 1010 for Python's default
user agent on both canonical and legacy hosts; browsers and
curlsucceed.
- The code validates that inputs are present, JSON object shaped, numeric, finite, and positive before doing the Reynolds, regime, or
predBetacalculations, and batch CSV rows reuse the same theory-range validation. Flask-SocketIOis used to run the app locally, but there are no socket event handlers in the current app. The helper keeps debug mode off by default and only allowsFLASK_DEBUG=1on loopback hosts.- The page loads MathJax from a CDN and embeds a YouTube iframe, so full rendering depends on external network access. The obsolete polyfill has been removed.