Skip to content

Latest commit

 

History

63 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

surge

Generate every constitutional isomer of a molecular formula, and practise finding them yourself.

The page is a front end for Surge, which it runs itself: surge is carried as WebAssembly and enumerates in a worker of the browser, so there is no service to call and nothing to install.

What it holds

Address What it is
/ The generator: a formula in, every isomer out, with the restrictions surge understands.
/exercises The exercises: draw every isomer of a formula yourself, with hints and a correction.
/fragments The motifs a hint is built from, and how often each appears in a formula.
/news What the tool has learnt to do, newest first.

Surge is the only thing that decides what an isomer is: the number to find in an exercise is enumerated at run time, never hard-coded, so an exercise and its correction can never disagree.

Taking a result away

Under the formula, Export the structures opens a dialog that writes what was generated as SMILES, as openchemlib idCodes, or as an SDF — a molfile per structure with invented coordinates, carrying SMILES, ID_CODE, MF and MW as fields, assembled by sdf-creator. Each one can be copied or downloaded, under a name of one's choosing.

Exercises

An exercise is a molecular formula. The student draws a structure and there is nothing to press: as soon as the drawing holds the atoms of the formula it is checked, and it is kept when it is one of the isomers. Two structures that differ only by stereochemistry, by explicit hydrogens, or by which Kekulé form was drawn are the same answer. What was found is kept in the browser, per formula, and survives a reload.

Hints that look at what you drew

The first hints read the formula — the degree of unsaturation, the families each heteroatom opens up, the reminder that only connectivity counts.

The ones after them compare the answers with the structures already found. Both sides are searched for a library of motifs — a three-membered ring, an ether, a terminal double bond, a quaternary carbon, a spiro atom — and what the answers hold but the student never drew becomes the hint:

8 answers hold an ether, and none of yours does. The oxygen can sit inside the skeleton rather than at its edge, joining two carbons.

A motif whose answers are only half found is counted out instead ("you have 3 of the 8 answers that hold an ether"), and the detail of a motif waits until the motif itself has been drawn: the three-membered ring is named before the nitrogen sitting in it.

Every motif is an openchemlib query fragment stored as an idCode and matched by substructure search, so nothing about an answer is written down: /fragments draws the whole library, the sentence each motif produces, and — given a formula — how many of its isomers hold it.

Handing out a selection

Everything is in the address, so there is nothing to log in to and nothing to save:

Address What the student gets
/exercises The 23 exercises of the cheminfo course.
/exercises?formulas=C4H10O,C5H12,C3H8 Exactly those formulas, in that order.
/exercises?formulas=C4H10O&exercise=C4H10O That set, opened on that exercise.
/exercises?set=https://example.org/isomers.json A set the teacher hosts themselves.
/exercises?formulas=C4H10O&embed=1 The same without the header, to be framed.

A hosted set is a JSON document. Only exercises is required; the page counts the isomers itself.

{
  "title": "Series 3 — constitutional isomers",
  "description": "Draw every isomer. Stereochemistry is not taken into account.",
  "exercises": [{ "mf": "C4H10O" }, { "mf": "C5H10" }, { "mf": "C4H8O" }]
}

It is fetched by the browser, so it has to be served with a permissive Access-Control-Allow-Origin.

The colour of an exercise is its difficulty, and it is never written down either: it is read off the number of isomers — up to 5 green, up to 15 orange, more than that red — so a set named in a link is coloured like the one shipped with the tool.

Sharing and framing

Every page carries a Share button, top right. It builds the address of the page as it currently stands — the formula, the restrictions, the exercises chosen out of the set — and the iframe that frames it in another site. Two parameters configure the page rather than feed it:

Parameter What it does
embed Drops the header and the navigation, so only the activity shows through a frame.
hide Switches parts of the page off, comma separated. An unknown name is ignored.

What hide understands, per page:

Page Names
/ options, substructure, lists, about
/exercises list, hints, answers, clear

lists is the export of the results, kept under its old name so a link written before the dialog existed still switches it off.

Hidden is not disabled: what a link carries still applies. hide=options runs the search under the restrictions of the link without letting the visitor change them, and a framed generator never shows the limit and the timeout at all — it runs on the ones the link names.

The generator writes its search in the address, so a link reproduces it and runs it on arrival:

https://surge.cheminfo.org/?mf=C4H6&disallowTripleBonds=1&limit=200

In a course

learn.cheminfo.org opens a tile either framed in the page or in a new tab. For a framed tile, add embed=1 so the header of this application does not repeat the one of the course:

https://surge.cheminfo.org/exercises?formulas=C5H12,C6H14,C4H10O&embed=1
<iframe
  src="https://surge.cheminfo.org/exercises?formulas=C4H10O&embed=1&hide=list,answers"
  width="100%"
  height="800"
  style="border: 1px solid #d3d8de; border-radius: 8px"
  title="Surge — Exercises"
></iframe>

Where the enumeration happens

Everything runs in the page. The restrictions surge understands — triple bonds, planarity, ring counts, the nine substructure filters — are the options of the generator's fold, and they reach surge's own command line unchanged; aromaticity filtering (surge's -R) is on by default, so the Kekulé structures of one aromatic ring count once. The enumeration itself is done in a worker, so a formula that takes seconds never freezes what is being drawn, and nothing a student types leaves their browser.

Local development

git clone https://github.com/cheminfo/surge
cd surge
npm install
npm run dev
npm test           # unit tests, type-check, eslint, prettier
npm run test-e2e   # Playwright, against the dev server

Deployment

cp .env.example .env
# uncomment one COMPOSE_FILE line, set TUNNEL_TOKEN for the Cloudflare mode
docker compose up -d

Three modes, selected by COMPOSE_FILE in .env:

File Exposure
compose.yaml publishes PORT on the host (the default)
compose.traefik.yaml behind Traefik on surge.cheminfo.org
compose.cloudflared.yaml behind a Cloudflare Tunnel, no published port

One image serves the built pages, surge included: there is no service behind it to reach. The container runs read-only with every capability dropped; the pages it hands out are laid onto a tmpfs at startup, which is the only writable place in it.

Never deploy by hand with git pull && docker compose up -d --build: the build overwrites the running tag in place while git pull moves the source under it, so there is nothing left to roll back to. Deployment is the global deploy script installed on the server, which is why every compose file resolves ${IMAGE_NAME}:${IMAGE_TAG} and why .deploy is gitignored.

Audience

TRACKING_SCRIPT holds the snippet the analytics provider hands out, whole. The build writes one file per address so each page is titled and described as itself, and the container puts the snippet at the end of the <head> of every one of them at startup — so a visitor is counted wherever they arrived. A npm run dev run — where Vite serves the page — loads nothing, so development never reaches the counter.

Where the site is served

The site does not assume it owns the root of a host, and the build does not decide where it is mounted. Every asset is written relative, so one image — one build, one tag — serves both of these at once:

Address BASE_PATH
https://surge.cheminfo.org/ unset, or /
https://www.cheminfo.org/surge/ /surge/

BASE_PATH is read when the container starts: the entrypoint stamps it into the <base> of every page it serves, and the page reads its own mount back off document.baseURI. So mounting the tool somewhere else is a line in .env and a restart — never a rebuild, and never a second image:

BASE_PATH=/surge/ docker compose up -d

A shared host strips the prefix before forwarding (Traefik's StripPrefix), so the container behind it never knows it is mounted.

SITE_URL is a different thing and is read at build time: it is the address the site names as its own, for the canonical link, the social card and the sitemap. Every deployment claims that one address, so a mirror points a crawler back at the original instead of competing with it for one search result. Pass it only when publishing the tool as a site of its own elsewhere:

SITE_URL=https://example.org/surge/ npm run build
docker build --build-arg SITE_URL=https://example.org/surge/ .

Note that a crawler only reads robots.txt from the root of a host, so a deployment mounted under a path is covered by whatever answers that root, not by the file the build writes.

Environment

Every variable is documented in .env.example; only PORT has to be set.

Variable Default What it does
PORT 31228 Port the page is served on; the Vite dev server takes PORT + 1.
BASE_PATH / Where this deployment is mounted, read at container start. /surge/ serves the same image under a path.
SITE_URL https://surge.cheminfo.org/ Build time only: the address the site names as its own, for the canonical link, the card and the sitemap.
TRACKING_SCRIPT unset The analytics <script> tag, put at the end of the <head> of every page served. Unset, nothing is loaded.
COMPOSE_FILE unset Which deployment mode docker compose uses.
IMAGE_NAME ghcr.io/cheminfo/surge.cheminfo.org Image every compose file runs.
IMAGE_TAG latest Rewritten by the server's deploy script on each deploy and rollback — never edit by hand.
TUNNEL_TOKEN unset Cloudflare Tunnel token, for that mode only.

Released versions and what changed are in CHANGELOG.md.

How to cite

Two works, and the Cite button in the header hands both over in the style a journal asks for — as a citation, as BibTeX or RIS, or as one file a reference manager imports.

  • The isomer generator — every structure this site hands out comes out of surge. McKay, B.D., Yirik, M.A., Steinbeck, C. Surge: a fast open-source chemical graph generator. J Cheminform 14, 24 (2022). https://doi.org/10.1186/s13321-022-00604-9
  • Data processing in the browser — the site itself, which runs the generator in the browser. Patiny, L. Unlocking the Potential of Browser-Based Scientific Data Analysis: A 20-Year Journey of Expertise. Chimia 79, 66–69 (2025). https://doi.org/10.2533/chimia.2025.66

License

MIT

Surge is under its own license: https://github.com/StructureGenerator/surge.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages