diff --git a/docs/_ext/jfjoch_downloads.py b/docs/_ext/jfjoch_downloads.py index 0fe07e327..f93438361 100644 --- a/docs/_ext/jfjoch_downloads.py +++ b/docs/_ext/jfjoch_downloads.py @@ -12,11 +12,18 @@ script matches against the release's asset list, written loosely enough to survi bump. """ +import pathlib +import shutil + from docutils import nodes from docutils.parsers.rst import Directive RELEASES = "https://gitea.psi.ch/mx/jungfraujoch/releases" +# docs/landing/: the front page, rendered from its template into the output root as index.html at +# the end of the build (write_landing below), with the same download blocks as the Sphinx pages. +LANDING_FILES = ("landing.css", "landing.js") + # program -> [(platform, requirements, [(label, asset name with {v}, regex for the live lookup)])] # The first button of a platform is the recommended one and is drawn filled. PROGRAMS = { @@ -69,7 +76,8 @@ def render(program, version): ' · all releases

' % (version, RELEASES, version, RELEASES), '
'] for platform, requirements, archives in PROGRAMS[program]: - out.append('

%s

' % platform) + out.append('

%s

' + % (platform.lower(), platform)) for i, (label, pattern, regex) in enumerate(archives): name = pattern.format(v=version) cls = "jfj-dl__btn" if i == 0 else "jfj-dl__btn jfj-dl__btn--secondary" @@ -93,6 +101,23 @@ class JfjochDownloads(Directive): return [nodes.raw("", render(program, version), format="html")] +def write_landing(app, exception): + """Render docs/landing/index.html into the HTML output root, next to the Sphinx pages.""" + if exception is not None or app.builder.name != "html": + return + src = pathlib.Path(app.confdir) / "landing" + out = pathlib.Path(app.outdir) + version = app.config.release + html = (src / "index.html").read_text(encoding="utf-8") + html = (html.replace("{{release}}", version) + .replace("{{downloads:rugnux}}", render("rugnux", version)) + .replace("{{downloads:viewer}}", render("viewer", version))) + (out / "index.html").write_text(html, encoding="utf-8") + for name in LANDING_FILES: + shutil.copy(src / name, out / name) + + def setup(app): app.add_directive("jfjoch-downloads", JfjochDownloads) + app.connect("build-finished", write_landing) return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/conf.py b/docs/conf.py index 9284e684d..393d3a17b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,6 +26,10 @@ extensions = [ 'jfjoch_downloads', ] +# The site's front page is docs/landing/index.html, a standalone page the jfjoch_downloads +# extension renders into the output root as index.html; the Sphinx tree starts one page below it. +root_doc = 'documentation' + templates_path = ['_templates'] # docs/review/ is gitignored working material (review reports, work plans); Sphinx renders # whatever is on disk, so without this a local review file would become a published page. diff --git a/docs/index.rst b/docs/documentation.rst similarity index 92% rename from docs/index.rst rename to docs/documentation.rst index 9e177b386..0cdd8c704 100644 --- a/docs/index.rst +++ b/docs/documentation.rst @@ -1,11 +1,12 @@ -PSI Jungfraujoch -================ +Documentation +============= .. raw:: html -

Open-source software from the Paul Scherrer - Institute for X-ray diffraction data, from the detector to the merged reflections. Three - programs, each usable on its own.

+

The manuals and reference of Jungfraujoch and Rugnux, open-source software + from the Paul Scherrer Institute for X-ray diffraction data. + The front page has the overview and the downloads; this page is the + map of the documentation.

diff --git a/docs/landing/index.html b/docs/landing/index.html new file mode 100644 index 000000000..31e9016f4 --- /dev/null +++ b/docs/landing/index.html @@ -0,0 +1,149 @@ + + + + + + +Jungfraujoch and Rugnux: X-ray diffraction data, from the detector to the merged reflections + + + + + + + +
+ Jungfraujoch + + +
+ +
+ +
+ +
+

Paul Scherrer Institute · open source

+

From the detector to the merged reflections.

+

Software for X-ray diffraction data: Rugnux processes a stored + dataset with one command, the Viewer shows what is in it, and + Jungfraujoch acquires it from the detector at full speed.

+ +

rugnux <diffraction image file>is the whole command: cell, space group and resolution come out of the data.

+
+
+ +
+
+ +
+

Data processing

+

Rugnux

+

Takes a macromolecular diffraction dataset, a rotation sweep or serial stills, from the + images to merged reflections: spot finding, indexing, integration, scaling and merging, with + the space group, cell and resolution decided from the data and a report that says on what + evidence. Writes .mtz, .cif and .hkl for the programs + that come next.

+

Linux · macOS · Windows · command line · fast on an NVIDIA GPU, works without one

+ +
+ +
+

Desktop application

+

Jungfraujoch Viewer

+

Shows what is in a dataset: every image with its spots, predictions and resolution rings, + per-image statistics over the sweep, grid-scan maps, and the same processing pipeline as + Rugnux, run from a settings panel on the open file. Follows a live collection from a running + Jungfraujoch broker.

+

Linux · macOS · Windows · Qt 6

+ +
+ +
+

Data acquisition

+

Jungfraujoch

+

The acquisition and real-time analysis system for the PSI JUNGFRAU and EIGER detectors, + and for DECTRIS detectors through their control unit: an FPGA and GPU pipeline that takes + the detector's frames at full rate, analyses them on the fly, streams them to the NXmx HDF5 + writer and serves a web interface and REST API to the beamline.

+

Linux servers · AMD Alveo U55C FPGA · NVIDIA GPUs

+ +
+ +
+
+ +
+
+

Download

+

Pre-built, signed where the platform asks for it, nothing to install beside them. +

+
+ +
+

Rugnux

+

One self-contained executable. Unpack the archive into a directory of its own and run + bin/rugnux; no CUDA toolkit, no Qt, no service. On a machine with an NVIDIA GPU + take the GPU build, which also runs without one. Details, packages and building from source.

+ {{downloads:rugnux}} +
+ +
+

Jungfraujoch Viewer

+

A desktop application with the Qt runtime included: a portable archive on Linux, a + notarized disk image on macOS, an installer on Windows. Details and building from source.

+ {{downloads:viewer}} +
+ +
+

Jungfraujoch, the acquisition system

+

Installed on the beamline's Linux servers from the RPM and APT + repositories (jfjoch, jfjoch-writer, jfjoch-driver-dkms, + jfjoch-viewer, rugnux), with the FPGA firmware and the writer's + packages attached to every release. + Deployment is the step-by-step guide; Release contents says what each package needs on the machine.

+
+
+ +
+

Free to use, in academia and industry

+

Rugnux, the viewer and the Jungfraujoch services are free software under the + GNU General Public License v3; the FPGA design is released under the + CERN Open Hardware Licence v2, strongly reciprocal. Both let + anyone use, study and modify the software and the hardware design at no charge, commercial use + included; what they ask is that a modified version which is passed on is shared under the same + terms. The released binaries bundle third-party components under GPL-compatible licences, and the + CUDA builds the NVIDIA CUDA runtime under NVIDIA's EULA: see the + third-party notices.

+
+ +
+ + + + + + + diff --git a/docs/landing/landing.css b/docs/landing/landing.css new file mode 100644 index 000000000..f95f805fc --- /dev/null +++ b/docs/landing/landing.css @@ -0,0 +1,369 @@ +/* The front page (docs/landing/index.html). Self-contained: system fonts, no framework, one + palette per theme. The download blocks use the same jfj-dl markup as the Sphinx download pages + (docs/_ext/jfjoch_downloads.py) and are styled here for this page. */ + +:root { + --bg: #fbfbfd; + --bg-raised: #ffffff; + --bg-tint: #eef0fa; + --fg: #1b1d2a; + --fg-muted: #5b6072; + --line: rgba(27, 29, 42, 0.12); + --primary: #3949ab; + --primary-strong: #2c3a8f; + --primary-soft: rgba(57, 73, 171, 0.1); + --on-primary: #ffffff; + --accent: #e9a23b; + --accent-soft: rgba(233, 162, 59, 0.16); + --shadow: 0 1px 2px rgba(27, 29, 42, 0.06), 0 8px 24px rgba(27, 29, 42, 0.06); + --ring: rgba(57, 73, 171, 0.22); + --spot: rgba(57, 73, 171, 0.55); +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg: #11131c; + --bg-raised: #181b27; + --bg-tint: #1d2130; + --fg: #e8e9f1; + --fg-muted: #9ea3b8; + --line: rgba(232, 233, 241, 0.12); + --primary: #8e9dff; + --primary-strong: #aab5ff; + --primary-soft: rgba(142, 157, 255, 0.14); + --on-primary: #0d1030; + --accent: #f0b65b; + --accent-soft: rgba(240, 182, 91, 0.16); + --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35); + --ring: rgba(142, 157, 255, 0.24); + --spot: rgba(170, 181, 255, 0.7); + } +} + +:root[data-theme="dark"] { + --bg: #11131c; + --bg-raised: #181b27; + --bg-tint: #1d2130; + --fg: #e8e9f1; + --fg-muted: #9ea3b8; + --line: rgba(232, 233, 241, 0.12); + --primary: #8e9dff; + --primary-strong: #aab5ff; + --primary-soft: rgba(142, 157, 255, 0.14); + --on-primary: #0d1030; + --accent: #f0b65b; + --accent-soft: rgba(240, 182, 91, 0.16); + --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35); + --ring: rgba(142, 157, 255, 0.24); + --spot: rgba(170, 181, 255, 0.7); +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font: 16px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + -webkit-font-smoothing: antialiased; +} + +a { color: var(--primary); text-decoration: none; } +a:hover { color: var(--primary-strong); text-decoration: underline; } + +code { + font-family: "SFMono-Regular", Menlo, Consolas, "Roboto Mono", monospace; + font-size: 0.92em; + background: var(--primary-soft); + padding: 0.1em 0.35em; + border-radius: 4px; +} + +h1, h2, h3 { line-height: 1.2; letter-spacing: -0.01em; margin: 0; } + +main > section, .top, .foot { + max-width: 1120px; + margin: 0 auto; + padding: 0 24px; +} + +/* ---- header ---------------------------------------------------------------------------------- */ + +.top { + display: flex; + align-items: center; + gap: 24px; + padding-top: 18px; + padding-bottom: 18px; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 10px; + color: var(--fg); + font-weight: 600; + font-size: 1.05em; +} +.brand:hover { text-decoration: none; } +.brand img { display: block; } + +.top__nav { display: flex; gap: 22px; margin-left: auto; font-weight: 500; } +.top__nav a { color: var(--fg-muted); } +.top__nav a:hover { color: var(--primary); text-decoration: none; } + +.theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 38px; height: 38px; + border: 1px solid var(--line); + border-radius: 50%; + background: var(--bg-raised); + color: var(--fg-muted); + cursor: pointer; +} +.theme-toggle:hover { color: var(--primary); border-color: var(--primary); } +.theme-toggle__moon { display: none; } +:root[data-theme="dark"] .theme-toggle__sun { display: none; } +:root[data-theme="dark"] .theme-toggle__moon { display: block; } +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .theme-toggle__sun { display: none; } + :root:not([data-theme="light"]) .theme-toggle__moon { display: block; } +} + +/* ---- hero ------------------------------------------------------------------------------------ */ + +.hero { + position: relative; + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); + align-items: center; + gap: 32px; + padding-top: 56px; + padding-bottom: 64px; +} + +.hero__art { + grid-column: 2; + grid-row: 1; + width: 100%; + max-width: 460px; + justify-self: end; + aspect-ratio: 1; +} +.hero__art .ring { fill: none; stroke: var(--ring); stroke-width: 1; } +.hero__art .ring--strong { stroke-width: 1.6; } +.hero__art .spot { fill: var(--spot); } +.hero__art .beam { stroke: var(--accent); stroke-width: 2; } + +.hero__text { grid-column: 1; grid-row: 1; max-width: 600px; } + +.eyebrow { + margin: 0 0 14px; + font-size: 0.78em; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--primary); +} + +.hero h1 { + font-size: clamp(2.1rem, 4.6vw, 3.4rem); + font-weight: 700; + letter-spacing: -0.025em; + margin-bottom: 18px; +} + +.lede { font-size: 1.2em; color: var(--fg-muted); margin: 0 0 26px; } +.lede strong { color: var(--fg); font-weight: 600; } + +.hero__actions { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 26px; } + +.btn { + display: inline-block; + padding: 11px 20px; + border-radius: 8px; + font-weight: 600; + border: 1px solid transparent; + transition: transform 0.12s, box-shadow 0.12s, background 0.12s; +} +.btn:hover { text-decoration: none; transform: translateY(-1px); } +.btn--primary { background: var(--primary); color: var(--on-primary); box-shadow: var(--shadow); } +.btn--primary:hover { background: var(--primary-strong); color: var(--on-primary); } +.btn--ghost { border-color: var(--line); color: var(--fg); background: var(--bg-raised); } +.btn--ghost:hover { border-color: var(--primary); color: var(--primary); } + +.hero__run { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 10px 14px; + margin: 0; + color: var(--fg-muted); + font-size: 0.95em; +} +.hero__run code { font-size: 1em; padding: 0.35em 0.7em; } + +/* ---- programs -------------------------------------------------------------------------------- */ + +.programs { padding-bottom: 56px; } + +.programs__grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 20px; +} + +.program { + display: flex; + flex-direction: column; + padding: 24px 24px 20px; + border: 1px solid var(--line); + border-radius: 14px; + background: var(--bg-raised); + box-shadow: var(--shadow); +} +.program__kicker { + margin: 0 0 8px; + font-size: 0.74em; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--primary); +} +.program h2 { font-size: 1.45em; font-weight: 650; margin-bottom: 12px; } +.program > p:not([class]) { margin: 0 0 14px; color: var(--fg); flex: 1 1 auto; } +.program__meta { margin: 0 0 14px; font-size: 0.82em; color: var(--fg-muted); } +.program__links { + margin: 0; + padding-top: 14px; + border-top: 1px solid var(--line); + display: flex; + flex-wrap: wrap; + gap: 6px 18px; + font-weight: 500; +} +.program__links a:first-child { font-weight: 700; } + +/* ---- download -------------------------------------------------------------------------------- */ + +.download { + padding-top: 48px; + padding-bottom: 24px; + border-top: 1px solid var(--line); +} +.download__head { max-width: 640px; margin-bottom: 28px; } +.download__head h2 { font-size: 2em; font-weight: 700; margin-bottom: 10px; } +.download__head p { margin: 0; color: var(--fg-muted); font-size: 1.05em; } +.download__head [data-os-note]:not(:empty) { color: var(--fg); font-weight: 500; } + +.download__program { margin-bottom: 36px; } +.download__program h3 { font-size: 1.3em; font-weight: 650; margin-bottom: 8px; } +.download__program > p { margin: 0 0 16px; max-width: 720px; color: var(--fg-muted); } +.download__program--text > p { color: var(--fg); } + +.jfj-dl__release { margin: 0 0 14px; font-size: 0.95em; color: var(--fg-muted); } +.jfj-dl__release strong { color: var(--fg); } + +.jfj-dl__grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 16px; +} + +.jfj-dl__card { + position: relative; + padding: 18px 18px 8px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--bg-raised); +} +.jfj-dl__card.is-yours { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft), var(--shadow); +} +.jfj-dl__card.is-yours::after { + content: "Your system"; + position: absolute; + top: -11px; right: 14px; + padding: 2px 10px; + border-radius: 999px; + background: var(--accent); + color: #1b1d2a; + font-size: 0.72em; + font-weight: 700; + letter-spacing: 0.04em; +} + +.jfj-dl__platform { font-size: 1.1em; font-weight: 650; margin-bottom: 12px; } + +a.jfj-dl__btn { + display: block; + margin-bottom: 10px; + padding: 10px 14px; + border: 1px solid var(--primary); + border-radius: 8px; + background: var(--primary); + color: var(--on-primary); + font-weight: 600; + line-height: 1.3; + transition: background 0.12s, box-shadow 0.12s; +} +a.jfj-dl__btn:hover { text-decoration: none; background: var(--primary-strong); box-shadow: var(--shadow); color: var(--on-primary); } +a.jfj-dl__btn--secondary { background: transparent; color: var(--primary); } +a.jfj-dl__btn--secondary:hover { background: var(--primary-soft); color: var(--primary-strong); } +a.jfj-dl__btn--missing, a.jfj-dl__btn--missing:hover { + background: transparent; + border-style: dashed; + border-color: var(--line); + color: var(--fg-muted); + box-shadow: none; + cursor: default; +} +.jfj-dl__file { + display: block; + margin-top: 2px; + font-family: "SFMono-Regular", Menlo, Consolas, "Roboto Mono", monospace; + font-size: 0.74em; + font-weight: 400; + opacity: 0.85; + word-break: break-all; +} +.jfj-dl__req { margin: 6px 0 8px; font-size: 0.82em; line-height: 1.45; color: var(--fg-muted); } +.jfj-dl__note { margin: 10px 0 0; font-size: 0.82em; color: var(--fg-muted); } +.jfj-dl__note:empty { display: none; } + +/* ---- licence, footer ------------------------------------------------------------------------- */ + +.licence { + padding-top: 40px; + padding-bottom: 48px; + border-top: 1px solid var(--line); +} +.licence h2 { font-size: 1.5em; font-weight: 650; margin-bottom: 12px; } +.licence p { margin: 0; max-width: 760px; color: var(--fg-muted); } + +.foot { + padding-top: 24px; + padding-bottom: 40px; + border-top: 1px solid var(--line); + font-size: 0.88em; + color: var(--fg-muted); +} +.foot p { margin: 0 0 6px; } + +/* ---- small screens --------------------------------------------------------------------------- */ + +@media (max-width: 760px) { + body { font-size: 15px; } + .top { gap: 14px; flex-wrap: wrap; } + .top__nav { gap: 14px; margin-left: 0; order: 3; width: 100%; font-size: 0.95em; } + .theme-toggle { margin-left: auto; } + .hero { grid-template-columns: 1fr; padding-top: 28px; padding-bottom: 40px; } + .hero__art { grid-column: 1; grid-row: 1; max-width: 220px; justify-self: center; opacity: 0.9; } + .hero__text { grid-row: 2; } + .programs__grid { grid-template-columns: 1fr; } +} diff --git a/docs/landing/landing.js b/docs/landing/landing.js new file mode 100644 index 000000000..c4fdd47f0 --- /dev/null +++ b/docs/landing/landing.js @@ -0,0 +1,60 @@ +// The front page: theme toggle, the hero's diffraction-pattern drawing, and the download section's +// "your system" highlight. The download buttons themselves are repointed at the newest release by +// _static/jfjoch_downloads.js, shared with the Sphinx download pages. +(function () { + // ---- theme: follows the system until the toggle is used, then remembered ----------------- + var root = document.documentElement; + var toggle = document.querySelector(".theme-toggle"); + function isDark() { + var set = root.getAttribute("data-theme"); + if (set) return set === "dark"; + return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; + } + if (toggle) toggle.addEventListener("click", function () { + var next = isDark() ? "light" : "dark"; + root.setAttribute("data-theme", next); + try { localStorage.setItem("jfj-theme", next); } catch (e) {} + }); + + // ---- hero art: resolution rings and a scatter of spots, the same every time -------------- + var art = document.querySelector(".hero__art"); + if (art) { + var seed = 7; + function rand() { seed = (seed * 16807) % 2147483647; return seed / 2147483647; } + var ns = "http://www.w3.org/2000/svg", out = []; + function el(name, attrs) { + var e = document.createElementNS(ns, name); + for (var k in attrs) e.setAttribute(k, attrs[k]); + return e; + } + var cx = 300, cy = 300; + [60, 110, 160, 210, 260].forEach(function (r, i) { + art.appendChild(el("circle", { cx: cx, cy: cy, r: r, "class": "ring" + (i === 2 ? " ring--strong" : "") })); + }); + // beam centre + art.appendChild(el("path", { d: "M" + (cx - 9) + " " + cy + "h18M" + cx + " " + (cy - 9) + "v18", "class": "beam" })); + // spots: denser near the middle, fainter and smaller towards the edge, a few bright ones + for (var i = 0; i < 230; i++) { + var a = rand() * Math.PI * 2; + var r = Math.sqrt(rand()) * 285; + if (r < 25) continue; + var x = cx + r * Math.cos(a), y = cy + r * Math.sin(a); + var fade = 1 - r / 320; + var size = 1.2 + fade * 2.2 * rand() + (rand() > 0.94 ? 2.5 : 0); + var c = el("circle", { cx: x.toFixed(1), cy: y.toFixed(1), r: size.toFixed(1), "class": "spot" }); + c.style.opacity = (0.25 + 0.75 * fade * rand()).toFixed(2); + art.appendChild(c); + } + } + + // ---- "your system": highlight the platform the browser runs on ---------------------------- + var ua = navigator.userAgent || "", platform = navigator.platform || ""; + var os = /Windows/.test(ua) ? "windows" : /Mac/.test(platform) || /Macintosh/.test(ua) ? "macos" : + /Linux|X11/.test(platform + ua) && !/Android/.test(ua) ? "linux" : null; + if (os) { + document.querySelectorAll('.jfj-dl__card[data-platform="' + os + '"]').forEach(function (c) { c.classList.add("is-yours"); }); + var note = document.querySelector("[data-os-note]"); + var names = { windows: "Windows", macos: "macOS", linux: "Linux" }; + if (note) note.textContent = "The " + names[os] + " builds are marked for you."; + } +})(); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0ab6a4cdc..3b6287ac6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -254,7 +254,7 @@ function App() { )}, { id: 'documentation', label: 'Documentation', icon: , render: () => ( - + )}, { id: 'changelog', label: 'Changelog', icon: , render: () => (