1 line
104 KiB
HTML
1 line
104 KiB
HTML
<!DOCTYPE html> <html lang=en data-content_root="./"> <meta charset=utf-8 /> <meta name=viewport content="width=device-width, initial-scale=1.0" /><meta name=viewport content="width=device-width, initial-scale=1" /> <meta name=viewport content="width=device-width,initial-scale=1"> <meta http-equiv=x-ua-compatible content="ie=edge"> <meta name="lang:clipboard.copy" content="Copy to clipboard"> <meta name="lang:clipboard.copied" content="Copied to clipboard"> <meta name="lang:search.language" content=en > <meta name="lang:search.pipeline.stopwords" content=True > <meta name="lang:search.pipeline.trimmer" content=True > <meta name="lang:search.result.none" content="No matching documents"> <meta name="lang:search.result.one" content="1 matching document"> <meta name="lang:search.result.other" content="# matching documents"> <meta name="lang:search.tokenizer" content="[\s\-]+"> <link href="https://fonts.gstatic.com/" rel=preconnect crossorigin> <link href="https://fonts.googleapis.com/css?family=Roboto+Mono:400,500,700|Roboto:300,400,400i,700&display=fallback" rel=stylesheet > <style> body, input { font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif } code, kbd, pre { font-family: "Roboto Mono", "Courier New", Courier, monospace } </style> <link rel=stylesheet href="_static/stylesheets/application.css"/> <link rel=stylesheet href="_static/stylesheets/application-palette.css"/> <link rel=stylesheet href="_static/stylesheets/application-fixes.css"/> <link rel=stylesheet href="_static/fonts/material-icons.css"/> <meta name=theme-color content="#3f51b5"> <script src="_static/javascripts/modernizr.js"></script> <title>Data analysis: from images to spots (§0–§3) — Jungfraujoch 1.0.0-rc.174 documentation</title> <link rel=stylesheet type="text/css" href="_static/pygments.css?v=83e35b93" /> <link rel=stylesheet type="text/css" href="_static/material.css?v=79c92029" /> <script src="_static/documentation_options.js?v=3dccfb20"></script> <script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script>window.MathJax = {"tex": {"tags": "ams", "inlineMath": [["\\(", "\\)"], ["$", "$"]], "displayMath": [["\\[", "\\]"], ["$$", "$$"]], "processEscapes": true}, "options": {"processHtmlClass": "tex2jax_process|mathjax_process|math|output_area"}}</script> <script defer=defer src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script> <link rel=icon href="_static/jfjoch.png"/> <link rel=index title=Index href=genindex.html /> <link rel=search title=Search href=search.html /> <link rel=next title="Data analysis: indexing and geometry (§4–§7)" href=CPU_DATA_ANALYSIS_INDEXING.html /> <link rel=prev title="CPU-side crystallographic data analysis (Jungfraujoch)" href=CPU_DATA_ANALYSIS.html /> <body dir=ltr data-md-color-primary=indigo data-md-color-accent=lime> <svg class=md-svg > <defs data-children-count=0 > <svg xmlns="http://www.w3.org/2000/svg" width=500 height=500 viewBox="0 0 500 500" id=__gitlab ><path fill=currentColor d="M93.667 473.347l90.684-279.097H2.983l90.684 279.097z" transform="translate(156.198 1.16)"/><path fill=currentColor d="M221.333 473.345L130.649 194.25H3.557l217.776 279.095z" transform="translate(28.531 1.16)" opacity=.7 /><path fill=currentColor d="M32 195.155L4.441 279.97a18.773 18.773 0 0 0 6.821 20.99l238.514 173.29L32 195.155z" transform="translate(.089 .256)" opacity=.5 /><path fill=currentColor d="M2.667-84.844h127.092L75.14-252.942c-2.811-8.649-15.047-8.649-17.856 0L2.667-84.844z" transform="translate(29.422 280.256)"/><path fill=currentColor d="M2.667 473.345L93.351 194.25h127.092L2.667 473.345z" transform="translate(247.198 1.16)" opacity=.7 /><path fill=currentColor d="M221.334 195.155l27.559 84.815a18.772 18.772 0 0 1-6.821 20.99L3.557 474.25l217.777-279.095z" transform="translate(246.307 .256)" opacity=.5 /><path fill=currentColor d="M130.667-84.844H3.575l54.618-168.098c2.811-8.649 15.047-8.649 17.856 0l54.618 168.098z" transform="translate(336.974 280.256)"/></svg> </defs> </svg> <input class=md-toggle data-md-toggle=drawer type=checkbox id=__drawer > <input class=md-toggle data-md-toggle=search type=checkbox id=__search > <label class=md-overlay data-md-component=overlay for=__drawer ></label> <a href="#CPU_DATA_ANALYSIS_IMAGE" tabindex=1 class=md-skip > Skip to content </a> <header class=md-header data-md-component=header > <nav class="md-header-nav md-grid"> <div class="md-flex navheader"> <div class="md-flex__cell md-flex__cell--shrink"> <a href=index.html title="Jungfraujoch 1.0.0-rc.174 documentation" class="md-header-nav__button md-logo"> <i class=md-icon ></i> </a> </div> <div class="md-flex__cell md-flex__cell--shrink"> <label class="md-icon md-icon--menu md-header-nav__button" for=__drawer ></label> </div> <div class="md-flex__cell md-flex__cell--stretch"> <div class="md-flex__ellipsis md-header-nav__title" data-md-component=title > <span class=md-header-nav__topic >PSI Jungfraujoch</span> <span class=md-header-nav__topic > Data analysis: from images to spots (§0–§3) </span> </div> </div> <div class="md-flex__cell md-flex__cell--shrink"> <label class="md-icon md-icon--search md-header-nav__button" for=__search ></label> <div class=md-search data-md-component=search role=dialog > <label class=md-search__overlay for=__search ></label> <div class=md-search__inner role=search > <form class=md-search__form action=search.html method=get name=search > <input type=text class=md-search__input name=q placeholder=""Search"" autocapitalize=off autocomplete=off spellcheck=false data-md-component=query data-md-state=active > <label class="md-icon md-search__icon" for=__search ></label> <button type=reset class="md-icon md-search__icon" data-md-component=reset tabindex=-1 >  </button> </form> <div class=md-search__output > <div class=md-search__scrollwrap data-md-scrollfix> <div class=md-search-result data-md-component=result > <div class=md-search-result__meta > Type to start searching </div> <ol class=md-search-result__list ></ol> </div> </div> </div> </div> </div> </div> <div class="md-flex__cell md-flex__cell--shrink"> <div class=md-header-nav__source > <a href="https://gitea.psi.ch/mx/jungfraujoch" title="Go to repository" class=md-source data-md-source=github > <div class=md-source__icon > <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" width=28 height=28 > <use xlink:href="#__gitlab" width=24 height=24 ></use> </svg> </div> <div class=md-source__repository > Jungfraujoch </div> </a> </div> </div> <script src="_static/javascripts/version_dropdown.js"></script> <script> var json_loc = ""versions.json"", target_loc = "../", text = "Versions"; $( document ).ready( add_version_dropdown(json_loc, target_loc, text)); </script> </div> </nav> </header> <div class=md-container > <nav class=md-tabs data-md-component=tabs > <div class="md-tabs__inner md-grid"> <ul class=md-tabs__list > <li class=md-tabs__item ><a href=index.html class=md-tabs__link >Jungfraujoch 1.0.0-rc.174 documentation</a> </ul> </div> </nav> <main class=md-main > <div class="md-main__inner md-grid" data-md-component=container > <div class="md-sidebar md-sidebar--primary" data-md-component=navigation > <div class=md-sidebar__scrollwrap > <div class=md-sidebar__inner > <nav class="md-nav md-nav--primary" data-md-level=0 > <label class="md-nav__title md-nav__title--site" for=__drawer > <a href=index.html title="Jungfraujoch 1.0.0-rc.174 documentation" class="md-nav__button md-logo"> <i class=md-icon ></i> </a> <a href=index.html title="Jungfraujoch 1.0.0-rc.174 documentation">PSI Jungfraujoch</a> </label> <div class=md-nav__source > <a href="https://gitea.psi.ch/mx/jungfraujoch" title="Go to repository" class=md-source data-md-source=github > <div class=md-source__icon > <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" width=28 height=28 > <use xlink:href="#__gitlab" width=24 height=24 ></use> </svg> </div> <div class=md-source__repository > Jungfraujoch </div> </a> </div> <ul class=md-nav__list > <li class=md-nav__item > <span class="md-nav__link caption"><span class=caption-text >rugnux — data processing</span></span> <li class=md-nav__item > <a href=RUGNUX.html class=md-nav__link >Rugnux</a> <li class=md-nav__item > <a href=RUGNUX_OVERVIEW.html class=md-nav__link >What Rugnux does</a> <li class=md-nav__item > <a href=RUGNUX_INSTALL.html class=md-nav__link >Installing Rugnux</a> <li class=md-nav__item > <a href=RUGNUX_FORMATS.html class=md-nav__link >What Rugnux reads</a> <li class=md-nav__item > <a href=RUGNUX_TUTORIAL.html class=md-nav__link >Running Rugnux</a> <li class=md-nav__item > <a href=RUGNUX_INTEGRATION.html class=md-nav__link >Rugnux with other programs</a> <li class=md-nav__item > <a href=RUGNUX_REPORT.html class=md-nav__link >The results report</a> <li class=md-nav__item > <a href=RUGNUX_ADVANCED.html class=md-nav__link >Advanced Rugnux</a> <li class=md-nav__item > <a href=RUGNUX_CALIBRATION.html class=md-nav__link >Detector calibration from powder rings (<code class="docutils literal notranslate"><span class=pre >rugnux</span> <span class=pre >--mode</span> <span class=pre >calibration</span></code>)</a> <li class=md-nav__item > <a href=CPU_DATA_ANALYSIS.html class=md-nav__link >CPU-side crystallographic data analysis (Jungfraujoch)</a> <li class=md-nav__item > <input class="md-toggle md-nav__toggle" data-md-toggle=toc type=checkbox id=__toc > <label class="md-nav__link md-nav__link--active" for=__toc > Data analysis: from images to spots (§0–§3) </label> <a href="#" class="md-nav__link md-nav__link--active">Data analysis: from images to spots (§0–§3)</a> <nav class="md-nav md-nav--secondary"> <ul class=md-nav__list data-md-scrollfix=""> </ul> </nav> <ul class=md-nav__list > <li class=md-nav__item > <a href="#getting-the-image-onto-the-gpu-device-side-bitshuffle-lz4-decoding" class=md-nav__link >0. Getting the image onto the GPU: device-side bitshuffle+LZ4 decoding</a> <li class=md-nav__item > <a href="#geometry-reciprocal-space-mapping-and-basic-quantities" class=md-nav__link >1. Geometry, reciprocal-space mapping, and basic quantities</a> <li class=md-nav__item > <a href="#azimuthal-integration-radial-profiles" class=md-nav__link >2. Azimuthal integration (radial profiles)</a> <li class=md-nav__item > <a href="#spot-finding-strong-pixels-bragg-spots" class=md-nav__link >3. Spot finding (strong pixels → Bragg spots)</a> </ul> <li class=md-nav__item > <a href=CPU_DATA_ANALYSIS_INDEXING.html class=md-nav__link >Data analysis: indexing and geometry (§4–§7)</a> <li class=md-nav__item > <a href=CPU_DATA_ANALYSIS_INTEGRATION.html class=md-nav__link >Data analysis: integration, scaling and merging (§8–§12)</a> <li class=md-nav__item > <a href=CPU_DATA_ANALYSIS_DECISIONS.html class=md-nav__link >Data analysis: space group and validation (§13–§14)</a> <li class=md-nav__item > <span class="md-nav__link caption"><span class=caption-text >Jungfraujoch — acquisition</span></span> <li class=md-nav__item > <a href=JFJOCH_BROKER.html class=md-nav__link >jfjoch_broker</a> <li class=md-nav__item > <a href=JFJOCH_WRITER.html class=md-nav__link >jfjoch_writer</a> <li class=md-nav__item > <a href=JFJOCH_VIEWER.html class=md-nav__link >jfjoch_viewer</a> <li class=md-nav__item > <a href=SOFTWARE_INTEGRATION.html class=md-nav__link >Integration with MX data processing software</a> <li class=md-nav__item > <a href=TOOLS.html class=md-nav__link >Tools</a> <li class=md-nav__item > <a href=DEPLOYMENT.html class=md-nav__link >Deployment</a> <li class=md-nav__item > <a href=DETECTORS.html class=md-nav__link >Supported detectors</a> <li class=md-nav__item > <a href=HARDWARE.html class=md-nav__link >Hardware requirements</a> <li class=md-nav__item > <a href=SOFTWARE.html class=md-nav__link >Software requirements</a> <li class=md-nav__item > <span class="md-nav__link caption"><span class=caption-text >FPGA</span></span> <li class=md-nav__item > <a href=FPGA.html class=md-nav__link >FPGA smartNIC</a> <li class=md-nav__item > <a href=FPGA_LICENSE.html class=md-nav__link >FPGA license</a> <li class=md-nav__item > <a href=FPGA_DESIGN.html class=md-nav__link >FPGA data flow</a> <li class=md-nav__item > <a href=FPGA_NETWORK.html class=md-nav__link >FPGA network</a> <li class=md-nav__item > <a href=FPGA_PCIE_DRIVER.html class=md-nav__link >FPGA PCIe driver</a> <li class=md-nav__item > <a href=FPGA_SETTINGS.html class=md-nav__link >FPGA advanced reference</a> <li class=md-nav__item > <a href=FPGA_DATA_ANALYSIS.html class=md-nav__link >FPGA data analysis</a> <li class=md-nav__item > <span class="md-nav__link caption"><span class=caption-text >Reference</span></span> <li class=md-nav__item > <a href=DETECTOR_GEOMETRY.html class=md-nav__link >Detector geometry</a> <li class=md-nav__item > <a href=OPENAPI.html class=md-nav__link >OpenAPI</a> <li class=md-nav__item > <a href=OPENAPI_SPECS.html class=md-nav__link >OpenAPI specification</a> <li class=md-nav__item > <a href=PYTHON_CLIENT.html class=md-nav__link >OpenAPI Python client</a> <li class=md-nav__item > <a href=CBOR.html class=md-nav__link >CBOR messages</a> <li class=md-nav__item > <a href=HDF5.html class=md-nav__link >HDF5 / NeXus data format</a> <li class=md-nav__item > <a href=IMAGE_STREAM.html class=md-nav__link >Data streams</a> <li class=md-nav__item > <a href=PIXEL_MASK.html class=md-nav__link >Pixel mask</a> <li class=md-nav__item > <a href=WEB_FRONTEND.html class=md-nav__link >Web frontend</a> <li class=md-nav__item > <a href=TESTS.html class=md-nav__link >Tests</a> <li class=md-nav__item > <span class="md-nav__link caption"><span class=caption-text >Project</span></span> <li class=md-nav__item > <a href=ACKNOWLEDGEMENT.html class=md-nav__link >Acknowledgements</a> <li class=md-nav__item > <a href=EXTERNAL_TEST_DATA.html class=md-nav__link >External test data</a> <li class=md-nav__item > <a href=LICENSE.html class=md-nav__link >License</a> <li class=md-nav__item > <a href=THIRD_PARTY_NOTICES.html class=md-nav__link >Third-party software notices</a> <li class=md-nav__item > <a href=VERSIONING.html class=md-nav__link >Semantic versioning</a> <li class=md-nav__item > <a href=SECURITY.html class=md-nav__link >Security</a> <li class=md-nav__item > <a href=RELEASE_CONTENTS.html class=md-nav__link >Release contents</a> <li class=md-nav__item > <a href=REPOSITORIES.html class=md-nav__link >Linux package repositories</a> <li class=md-nav__item > <a href=NAMING.html class=md-nav__link >Naming</a> <li class=md-nav__item > <a href=CHANGELOG.html class=md-nav__link >Changelog</a> </ul> </nav> </div> </div> </div> <div class="md-sidebar md-sidebar--secondary" data-md-component=toc > <div class=md-sidebar__scrollwrap > <div class=md-sidebar__inner > <nav class="md-nav md-nav--secondary"> <ul class=md-nav__list data-md-scrollfix=""> </ul> </nav> </div> </div> </div> <div class=md-content > <article class="md-content__inner md-typeset" role=main > <section class="tex2jax_ignore mathjax_ignore" id=data-analysis-from-images-to-spots-03 > <h1 id=cpu-data-analysis-image--page-root >Data analysis: from images to spots (§0–§3)<a class=headerlink href="#cpu-data-analysis-image--page-root" title="Link to this heading">¶</a></h1> <p>Part of the <a class="reference internal" href=CPU_DATA_ANALYSIS.html ><span class="std std-doc">CPU/GPU data-analysis reference</span></a>; the section numbers are continuous across its four parts.</p> <nav class="contents local" id=on-this-page > <p class=topic-title >On this page</p> <ul class=simple > <li><p><a class="reference internal" href="#getting-the-image-onto-the-gpu-device-side-bitshuffle-lz4-decoding" id=id1 >0. Getting the image onto the GPU: device-side bitshuffle+LZ4 decoding</a></p> <li><p><a class="reference internal" href="#geometry-reciprocal-space-mapping-and-basic-quantities" id=id2 >1. Geometry, reciprocal-space mapping, and basic quantities</a></p> <ul> <li><p><a class="reference internal" href="#coordinate-conventions" id=id3 >1.1 Coordinate conventions</a></p> <li><p><a class="reference internal" href="#two-theta-azimuth-resolution-and-q" id=id4 >1.2 Two-theta, azimuth, resolution and <span class="math notranslate nohighlight">\(q\)</span></a></p> <li><p><a class="reference internal" href="#distance-from-the-ewald-sphere" id=id5 >1.3 Distance from the Ewald sphere</a></p> <li><p><a class="reference internal" href="#the-beam-centre" id=id6 >1.4 The beam centre</a></p> <li><p><a class="reference internal" href="#finding-the-beam-stop" id=id7 >1.5 Finding the beam stop</a></p> <li><p><a class="reference internal" href="#defective-pixels" id=id8 >1.6 Defective pixels</a></p> </ul> <li><p><a class="reference internal" href="#azimuthal-integration-radial-profiles" id=id9 >2. Azimuthal integration (radial profiles)</a></p> <ul> <li><p><a class="reference internal" href="#histogram-estimator" id=id10 >2.1 Histogram estimator</a></p> <li><p><a class="reference internal" href="#corrections-applied" id=id11 >2.2 Corrections applied</a></p> <li><p><a class="reference internal" href="#background-estimate-for-profiles" id=id12 >2.3 Background estimate for profiles</a></p> </ul> <li><p><a class="reference internal" href="#spot-finding-strong-pixels-bragg-spots" id=id13 >3. Spot finding (strong pixels → Bragg spots)</a></p> <ul> <li><p><a class="reference internal" href="#strong-pixel-detection-by-local-statistics" id=id14 >3.1 Strong-pixel detection by local statistics</a></p> <li><p><a class="reference internal" href="#adaptive-self-calibrating-detection" id=id15 >3.2 Adaptive (self-calibrating) detection</a></p> <li><p><a class="reference internal" href="#resolution-and-ice-ring-handling" id=id16 >3.3 Resolution and ice-ring handling</a></p> <li><p><a class="reference internal" href="#connected-component-labeling-ccl" id=id17 >3.4 Connected-component labeling (CCL)</a></p> <li><p><a class="reference internal" href="#adaptive-per-image-minimum-spot-size" id=id18 >3.5 Adaptive per-image minimum spot size</a></p> <li><p><a class="reference internal" href="#predicting-the-resolution-the-merged-data-will-reach" id=id19 >3.6 Predicting the resolution the merged data will reach</a></p> </ul> </ul> </nav> <section id=getting-the-image-onto-the-gpu-device-side-bitshuffle-lz4-decoding > <h2 id=getting-the-image-onto-the-gpu-device-side-bitshuffle-lz4-decoding ><a class=toc-backref href="#id1" role=doc-backlink >0. Getting the image onto the GPU: device-side bitshuffle+LZ4 decoding</a><a class=headerlink href="#getting-the-image-onto-the-gpu-device-side-bitshuffle-lz4-decoding" title="Link to this heading">¶</a></h2> <p>Images arrive bitshuffle+LZ4 compressed (HDF5 filter 32008), and everything from §1 onwards runs on the GPU when one is present. Instead of decompressing on the host and uploading the image, the compressed chunk is uploaded — a few MB rather than tens of MB — and decoded on the device. The approach follows Jon Wright (ESRF); the kernels are Jungfraujoch’s own.</p> <p>One kernel does the work: one CUDA block owns one bitshuffle block, from the compressed payload through to finished pixels.</p> <ol class="arabic simple"> <li><p><strong>LZ4 into shared memory, one warp per bitshuffle block.</strong> Blocks are independent, so the parallelism is across them; within the warp every lane runs the same sequence parser over the same bytes, and the literal and match copies are split across the 32 lanes so the stores coalesce. An overlapping match is treated as a pattern of period <code class="docutils literal notranslate"><span class=pre >offset</span></code> sourced from bytes that already precede the write position, which keeps it parallel rather than a serial byte loop; <code class="docutils literal notranslate"><span class=pre >offset</span> <span class=pre >==</span> <span class=pre >1</span></code> (a run of one repeated byte, the common case in sparse detector data) and power-of-two offsets avoid the modulo altogether. Because the lanes cooperate on the copies, each one is followed by <code class="docutils literal notranslate"><span class=pre >__syncwarp()</span></code> — a later match can read bytes another lane wrote, and since Volta that ordering is not implicit.</p> <li><p><strong>The bitshuffle inverse fused with preprocessing.</strong> The whole CUDA block then reads that shared buffer back: one thread owns one group of 8 elements across every byte-plane, so once it has transposed its 8 bytes out of each plane it holds 8 complete elements — and it applies the pixel mask, the error marker and the saturation cap and emits 8 finished <code class="docutils literal notranslate"><span class=pre >int32</span></code> pixels directly. Nothing of the block reaches device memory but the pixels — neither the bitshuffled bytes nor the decompressed image is ever materialised. For 8-bit images there is a single plane and the assembly degenerates to a copy.</p> </ol> <p>Decoding into shared memory is worth more than the bandwidth it saves: an LZ4 match reads back bytes written a few sequences earlier, so every copy step is a dependent round trip — tens of cycles in shared memory against hundreds in device memory. It is paid for in residency, because the buffer holds a whole bitshuffle block, and a block larger than 16 kB (larger than either writer this pipeline reads produces) falls back to a pair of kernels instead, the first writing the shuffled image to device memory and the second un-transposing and preprocessing out of it.</p> <p>The block offsets inside the container can only be discovered by reading the block lengths in order, so that scan stays on the host.</p> <p>Only <code class="docutils literal notranslate"><span class=pre >BSHUF_LZ4</span></code> is decoded on the device. For the zstd variants (<code class="docutils literal notranslate"><span class=pre >BSHUF_ZSTD</span></code>, <code class="docutils literal notranslate"><span class=pre >BSHUF_ZSTD_RLE</span></code>, <code class="docutils literal notranslate"><span class=pre >BSHUF_ZSTD_RLE_HUFF</span></code>), and for uncompressed or float images, <code class="docutils literal notranslate"><span class=pre >BSLZ4DecoderGPU::Supports()</span></code> returns false and the pipeline decompresses on the host and uploads as before.</p> <p>The container arrives off the network or off disk and is not trusted. Everything checkable on the host — declared sizes, the block scan, a block size that is not a multiple of 8 elements, a block count the chunk could not hold, trailing bytes — is rejected before any work is queued; the kernel additionally flags a block that did not decode to exactly its declared length, which becomes an exception once the caller has synchronised. That last check matters because the decode buffers are reused frame to frame: a block that stopped early would leave the <em>previous</em> image’s most significant byte-plane in place, which reads not as a missing corner but as real pixels several powers of two too bright.</p> </section> <section id=geometry-reciprocal-space-mapping-and-basic-quantities > <h2 id=geometry-reciprocal-space-mapping-and-basic-quantities ><a class=toc-backref href="#id2" role=doc-backlink >1. Geometry, reciprocal-space mapping, and basic quantities</a><a class=headerlink href="#geometry-reciprocal-space-mapping-and-basic-quantities" title="Link to this heading">¶</a></h2> <section id=coordinate-conventions > <h3 id=coordinate-conventions ><a class=toc-backref href="#id3" role=doc-backlink >1.1 Coordinate conventions</a><a class=headerlink href="#coordinate-conventions" title="Link to this heading">¶</a></h3> <p>For a pixel coordinate <span class="math notranslate nohighlight">\((x,y)\)</span> (in pixels), Jungfraujoch converts to a laboratory direction vector via:</p> <ol class="arabic simple"> <li><p>shift by the beam-centre pixel <span class="math notranslate nohighlight">\((x_\mathrm{beam}, y_\mathrm{beam})\)</span> — the <strong>PONI</strong>, pyFAI’s point of normal incidence, which coincides with the direct-beam impact point only for an untilted detector (see <a class="reference internal" href=DETECTOR_GEOMETRY.html ><span class="std std-doc">Detector geometry</span></a>),</p> <li><p>scale by pixel size <span class="math notranslate nohighlight">\(p\)</span> (mm),</p> <li><p>set detector distance <span class="math notranslate nohighlight">\(D\)</span> (mm),</p> <li><p>apply detector orientation rotation <span class="math notranslate nohighlight">\(R_\mathrm{det}\)</span> (PyFAI-like parameterization).</p> </ol> <p>The unnormalized detector coordinate (mm) is: <span class="math notranslate nohighlight">\( \mathbf{r}_\mathrm{det}(x,y) = \begin{pmatrix} (x-x_\mathrm{beam})p\\ (y-y_\mathrm{beam})p\\ D \end{pmatrix}. \)</span></p> <p>The lab-frame vector is: <span class="math notranslate nohighlight">\( \mathbf{r}_\mathrm{lab} = R_\mathrm{det}\,\mathbf{r}_\mathrm{det}. \)</span></p> <p>By this construction <span class="math notranslate nohighlight">\((x_\mathrm{beam}, y_\mathrm{beam})\)</span> maps to <span class="math notranslate nohighlight">\((0,0,D)\)</span> <em>before</em> the rotation — the point where the detector normal through the sample meets the detector — which is what makes it the PONI rather than the direct beam; the two differ by <span class="math notranslate nohighlight">\(D\tan(\mathrm{tilt})\)</span> on a tilted detector. The laboratory frame is fixed the same way everywhere in the system: <span class="math notranslate nohighlight">\(+z\)</span> along the beam propagation, <span class="math notranslate nohighlight">\(+x\)</span> along increasing pixel <strong>column</strong> (the fast axis) and <span class="math notranslate nohighlight">\(+y\)</span> along increasing pixel <strong>row</strong> (the slow axis) — a right-handed triple that coincides with XDS’s laboratory frame, which is what makes the geometry echo of <a class="reference internal" href="RUGNUX_INTEGRATION.html#comparing-the-geometry-with-xds"><span class="std std-ref">Rugnux</span></a> directly comparable. The absolute hand of an indexing — and with it the Bijvoet hands of §14.6–§14.7 — follows from this convention.</p> <p>Let the incident wavevector magnitude be <span class="math notranslate nohighlight">\(k = 1/\lambda\)</span> in Å<span class="math notranslate nohighlight">\(^{-1}\)</span>, and define: <span class="math notranslate nohighlight">\( \mathbf{S}_0 = (0,0,k). \)</span></p> <p>The <strong>reciprocal-space scattering vector</strong> associated with pixel <span class="math notranslate nohighlight">\((x,y)\)</span> is: <span class="math notranslate nohighlight">\( \mathbf{s}(x,y) = k\,\frac{\mathbf{r}_\mathrm{lab}}{\lVert \mathbf{r}_\mathrm{lab}\rVert} - \mathbf{S}_0. \)</span></p> <p>This <span class="math notranslate nohighlight">\(\mathbf{s}\)</span> is the fundamental quantity used for spot finding (resolution filters), indexing, and refinement.</p> </section> <section id=two-theta-azimuth-resolution-and-q > <h3 id=two-theta-azimuth-resolution-and-q ><a class=toc-backref href="#id4" role=doc-backlink >1.2 Two-theta, azimuth, resolution and <span class="math notranslate nohighlight">\(q\)</span></a><a class=headerlink href="#two-theta-azimuth-resolution-and-q" title="Link to this heading">¶</a></h3> <p>The scattering angle <span class="math notranslate nohighlight">\(2\theta\)</span> is computed from <span class="math notranslate nohighlight">\(\mathbf{r}_\mathrm{lab}\)</span> via: <span class="math notranslate nohighlight">\( 2\theta = \mathrm{atan2}\!\left(\sqrt{x_\mathrm{lab}^2 + y_\mathrm{lab}^2},\; z_\mathrm{lab}\right), \)</span></p> <p>evaluated as a two-argument arctangent, so the mapping stays correct where a strongly tilted detector’s far corner reaches past <span class="math notranslate nohighlight">\(2\theta = 90°\)</span>.</p> <p>Resolution (Å) at a pixel is: <span class="math notranslate nohighlight">\( d = \frac{\lambda}{2\sin\theta}. \)</span></p> <p>The magnitude <span class="math notranslate nohighlight">\(q = 2\pi/d\)</span> is used for radial binning and ice-ring handling.</p> </section> <section id=distance-from-the-ewald-sphere > <h3 id=distance-from-the-ewald-sphere ><a class=toc-backref href="#id5" role=doc-backlink >1.3 Distance from the Ewald sphere</a><a class=headerlink href="#distance-from-the-ewald-sphere" title="Link to this heading">¶</a></h3> <p>For a reciprocal lattice point <span class="math notranslate nohighlight">\(\mathbf{p}\)</span> (Å<span class="math notranslate nohighlight">\(^{-1}\)</span>), define: <span class="math notranslate nohighlight">\( \Delta_\mathrm{Ewald}(\mathbf{p}) = \lVert \mathbf{p} + \mathbf{S}_0\rVert - k. \)</span> Jungfraujoch uses <span class="math notranslate nohighlight">\(|\Delta_\mathrm{Ewald}|\)</span> as an operational proxy for excitation error. This appears in:</p> <ul class=simple > <li><p>still prediction (accept if <span class="math notranslate nohighlight">\(|\Delta_\mathrm{Ewald}|\le \Delta_\mathrm{cut}\)</span>),</p> <li><p>profile radius estimation (see §11.1),</p> <li><p>still partiality option in scaling/merging (§10.2).</p> </ul> </section> <section id=the-beam-centre > <h3 id=the-beam-centre ><a class=toc-backref href="#id6" role=doc-backlink >1.4 The beam centre</a><a class=headerlink href="#the-beam-centre" title="Link to this heading">¶</a></h3> <p>A run starts from the beam centre in the file, or from <code class="docutils literal notranslate"><span class=pre >--beam-x</span></code>/<code class="docutils literal notranslate"><span class=pre >--beam-y</span></code> where they are given. Header centres are often typed rather than measured, and on rotation data a wrong one is not repaired by anything downstream: the error is fixed in the laboratory frame, so accumulating a sweep smears every reciprocal-lattice point around a circle, and a displacement <span class="math notranslate nohighlight">\(\delta p\)</span> on the detector multiplies the FFT amplitude at an axis of length <span class="math notranslate nohighlight">\(a\)</span> by <span class="math notranslate nohighlight">\(J_0(2\pi\,\delta p\,a/(D\lambda))\)</span>. Past the first zero the true axis is gone and its harmonic wins. Rugnux therefore measures the centre itself on every run and treats the measurement as a second hypothesis to be tested against the file’s, not as a replacement for it. The steps, in the order they run:</p> <table> <thead> <tr class=row-odd ><th class=head ><p>Step</p> <th class=head ><p>Runs</p> <th class=head ><p>Effect on the centre</p> <tr class=row-even ><td><p>Header check</p> <td><p>every run, before a frame is read</p> <td><p>none; reported</p> <tr class=row-odd ><td><p>Background measurement (<code class="docutils literal notranslate"><span class=pre >--beam-center-check</span></code>)</p> <td><p>every run with the beam-stop pre-scan</p> <td><p>none at this point; reported</p> <tr class=row-even ><td><p><code class="docutils literal notranslate"><span class=pre >--estimate-beam-center</span></code></p> <td><p>only when asked</p> <td><p>replaces the file’s before indexing, where measured precisely enough</p> <tr class=row-odd ><td><p>Second first pass at the measured centre</p> <td><p>rotation</p> <td><p>adopts the measured centre in the cases listed below</p> <tr class=row-even ><td><p>Both centres judged on the merge</p> <td><p>rotation, two-pass</p> <td><p>adopts the measured centre where its first pass merges better</p> <tr class=row-odd ><td><p><code class="docutils literal notranslate"><span class=pre >--beam-center-search</span></code></p> <td><p>rotation, after a first pass that indexes under half the validation frames</p> <td><p>steps the centre a pixel at a time</p> <tr class=row-even ><td><p>Post-refinement (§7.5)</p> <td><p>rotation, two-pass</p> <td><p>refines the centre from the integrated reflections, within 15 px</p> </table> <p>On most runs the measured centre is reported, the second first pass finds the same lattice at both centres, and the run continues at the file’s centre until post-refinement moves it.</p> <p><strong>What the file says.</strong> Before any frame is read, a centre within one pixel of the geometric centre of the detector, or a whole number of pixels in both coordinates, is reported as <em>a value written rather than measured</em>, and a centre that lands on a masked pixel is a warning — a real beam does not sit on a dead pixel or in a module gap. These lines change nothing.</p> <p><strong>Measured on every run (<code class="docutils literal notranslate"><span class=pre >--beam-center-check</span></code>, on by default).</strong> The isotropy of the scattered background places the point the beam lands on; the leverage is the curvature of the solvent ring. The fit runs over the mean image the beam-stop pre-scan already builds (§1.5), with the opaque part of the shadow masked, so it costs no frames of its own. For the same reason <code class="docutils literal notranslate"><span class=pre >--detect-beam-stop=off</span></code> leaves nothing to measure it on, and the check then does nothing.</p> <p>The fit starts with a <strong>whole-detector capture</strong>. The centrosymmetry score of the projection about a candidate centre <span class="math notranslate nohighlight">\(\mathbf{c}\)</span>, <span class="math notranslate nohighlight">\(\sum_\mathbf{x} I(\mathbf{x})\,I(2\mathbf{c}-\mathbf{x})\)</span>, is the self-convolution <span class="math notranslate nohighlight">\((I*I)(2\mathbf{c})\)</span>, so <strong>one FFT pair scores every candidate centre on the detector</strong> at half-pixel spacing: the cost is <span class="math notranslate nohighlight">\(O(N\log N)\)</span> and independent of how far the header is from the truth. Three surfaces are scored — the 2D point inversion, which measures the same isotropic background the walk below fits, and a 1D line mirror per detector axis, which along the spindle is exact Friedel physics (a reflection’s mate half a turn later lands mirrored in the line through the beam across the spindle). The beam-stop shadow is blanked out of the scored image, a one-sided opaque obstruction being a centrosymmetry defect in its own right. What comes back is a shortlist and a margin per surface, not a centre: the surface can be locally flat over tens of pixels, and the peak-to-runner-up margin says so. A <strong>local walk</strong> is then seeded at the capture and refines it; where it declines there it is started again from the file’s centre, and only where neither start gives it something to fit does the capture stand alone. The transforms run on the GPU where one is present and on FFTW otherwise, with everything that decides anything shared between the two.</p> <p>The run log then carries:</p> <ul class=simple > <li><p><code class="docutils literal notranslate"><span class=pre >Beam</span> <span class=pre >centre</span> <span class=pre >capture:</span></code> the strongest point-symmetry centre, the two line-mirror coordinates and the three margins (under about 3 % the surface is flat).</p> <li><p><code class="docutils literal notranslate"><span class=pre >Beam</span> <span class=pre >centre</span> <span class=pre >check:</span> <span class=pre >the</span> <span class=pre >file</span> <span class=pre >puts</span> <span class=pre >the</span> <span class=pre >direct</span> <span class=pre >beam</span> <span class=pre >at</span> <span class=pre >(x,y),</span> <span class=pre >the</span> <span class=pre >scattered</span> <span class=pre >background</span> <span class=pre >puts</span> <span class=pre >it</span> <span class=pre >at</span> <span class=pre >(x,y)</span> <span class=pre >+-</span> <span class=pre >σ</span> <span class=pre >px</span> <span class=pre >-</span> <span class=pre >a</span> <span class=pre >difference</span> <span class=pre >of</span> <span class=pre >d</span> <span class=pre >px,</span> <span class=pre >against</span> <span class=pre >the</span> <span class=pre >t</span> <span class=pre >px</span> <span class=pre >this</span> <span class=pre >geometry</span> <span class=pre >asks</span> <span class=pre >the</span> <span class=pre >centre</span> <span class=pre >to</span> <span class=pre >be</span> <span class=pre >right</span> <span class=pre >to.</span></code> The comparison is against the <strong>direct beam</strong>, not the PONI: <code class="docutils literal notranslate"><span class=pre >beam_x_pxl</span></code> is the point of normal incidence, and the two part by <span class="math notranslate nohighlight">\(D\tan(\mathrm{rot})/p\)</span> as soon as the detector is tilted, while the background is centrosymmetric about where the beam lands. <span class="math notranslate nohighlight">\(t = 0.183\,D\lambda/(p\,a_\mathrm{max})\)</span> is the displacement at which the <span class="math notranslate nohighlight">\(J_0\)</span> factor above has fallen to 0.70 for the longest axis of the cell given with <code class="docutils literal notranslate"><span class=pre >-C</span></code> (200 Å where none is given); it runs from well under a pixel to several. It is reported, not used as a gate: a centre change far smaller than <span class="math notranslate nohighlight">\(t\)</span> can still decide between a cell and its harmonic.</p> <li><p>One of three follow-ups: the difference is under three times the fit’s σ (the file’s centre is as good as this measurement can tell); more than <span class="math notranslate nohighlight">\(t\)</span> of it lies <strong>across</strong> the spindle, where it can cost peaks or an axis; or it is a real difference, mostly <strong>along</strong> the spindle, where a wrong centre does not announce itself.</p> </ul> <p>None of these lines moves the centre. What consumes the measurement is the second first pass below (rotation runs), <code class="docutils literal notranslate"><span class=pre >--estimate-beam-center</span></code>’s fall-through, and the bound on the post-refinement.</p> <p><strong>The second first pass (rotation).</strong> After the first pass — and after the rotation-axis sign rescue below — the first pass is indexed <strong>again at the measured centre</strong>, however small the difference. A trial centre gets its own azimuthal mapping, spot engines and spot cache: raw centroids do not move with the centre, but <em>which spots are in the list</em> does (the resolution limit, the ice-ring flag, the beam-stop mask and the strongest-<span class="math notranslate nohighlight">\(N\)</span> ranking are all radial), and the starting centre’s spots are parked and restored so a trial that adopts nothing leaves the run as it was.</p> <p>The indexed frame count is <strong>not</strong> used to choose between two centres that both index: acceptance is a fractional-Miller test, so a cell twice as long must place every spot twice as accurately to score the same, and a halved axis can index <em>more</em> frames than the true cell. “Indexes” below means more than half of the validation frames; “same lattice” means the same Bravais class with primitive volumes within 2 %. The outcomes:</p> <ol class="arabic simple"> <li><p><strong>The file’s centre indexes and the measured one does not.</strong> The file’s centre stands.</p> <li><p><strong>Both index the same lattice.</strong> The file’s centre is kept, and the log says the difference does not decide this crystal’s cell. A centre off by about one reflection spacing still indexes the same cell, with part of the sweep given indices one out, so where the two centres are further apart than both <span class="math notranslate nohighlight">\(t\)</span> and three σ of the fit, the measured centre is kept as a hypothesis for the merge (next paragraph).</p> <li><p><strong>Both index the same cell in a different Bravais class.</strong> Whether the centre could have decided the class is arithmetic: a shift of <span class="math notranslate nohighlight">\(n\)</span> pixels of size <span class="math notranslate nohighlight">\(p\)</span> moves an axis of length <span class="math notranslate nohighlight">\(a\)</span> by <span class="math notranslate nohighlight">\(n\,p\,a/(D\lambda)\)</span> relative, and the lattice search holds an axis equality to 3 %. Where the shift is worth that much on the pair of reduced-cell axes the equality compares, both centres are carried to the merge; otherwise the file’s centre and its class are kept.</p> <li><p><strong>Both index, on different lattices.</strong> Both cells are reported in a warning. Where the primitive volumes differ by an integer factor of 2 to 4 — an axis harmonic, which a centre error along the spindle produces — the choice is made on the <strong>pooled validation spots</strong>: the measured centre is adopted where its lattice, larger or smaller, beats chance and puts a share of the spots on itself over its own wrong-spindle null that exceeds the file’s by more than the binomial noise of the two (z = 3.29). Otherwise the file’s centre is kept and the warning suggests <code class="docutils literal notranslate"><span class=pre >--estimate-beam-center</span></code>.</p> <li><p><strong>The file’s centre does not index.</strong> Nothing defends it, so:</p> <ul class=simple > <li><p>where the measured centre indexes, it is <strong>adopted</strong>;</p> <li><p>where it does not, the rotation-axis sign is flipped <strong>at the measured centre</strong>, since each of the two errors can hide the other; where that indexes, both are adopted;</p> <li><p>where it still does not, the first-pass depth ladder of §6 (how much of each frame the pass reads) is run at <strong>both</strong> centres, so the centre moves only when it is the centre that pays. The measured centre is adopted where its lean rung indexes and beats the file’s; where both index the same lattice once less of each frame is read, both centres are carried to the merge; where the file’s centre wins or ties, the depth was the problem and the file’s centre stays;</p> <li><p>where nothing indexes at either centre, the pooled spots decide as in case 4; failing that, the file’s centre is put back, so a run that fails for some other reason fails at the geometry it was given.</p> </ul> </ol> <p><strong>Both centres judged on the merge (rotation, two-pass).</strong> Where case 3 or 5 carried both centres, the whole first pass is run again at the measured centre — with its own short-axis pass and post-refinement — and the two arms are compared on their search merges (<span class="math notranslate nohighlight">\(P1\)</span>, whole range, before the correction surfaces). Where case 2 held a hypothesis, this is done only if the file’s arm merges inconsistently: <span class="math notranslate nohighlight">\(\mathrm{CC}_{1/2}\)</span> below 0.9 in the lowest-resolution shell of its search merge, where every reflection is strong and a consistent merge reads close to 1. The measured centre wins where:</p> <ul class=simple > <li><p>both arms ended on the same lattice, and it merges <strong>more reflections at <span class="math notranslate nohighlight">\(I/\sigma \ge 2\)</span></strong>, or its lowest-shell <span class="math notranslate nohighlight">\(\mathrm{CC}_{1/2}\)</span> is higher by more than 0.05;</p> <li><p>the arms ended on different lattices, and its search-merge <span class="math notranslate nohighlight">\(\mathrm{CC}_{1/2}\)</span> is higher by more than 0.05.</p> </ul> <p>Otherwise, or where the measured arm does not complete, the run keeps the file’s centre. The log lines are <code class="docutils literal notranslate"><span class=pre >Beam</span> <span class=pre >centre</span> <span class=pre >check:</span> <span class=pre >running</span> <span class=pre >the</span> <span class=pre >first</span> <span class=pre >pass</span> <span class=pre >again</span> <span class=pre >at</span> <span class=pre >the</span> <span class=pre >measured</span> <span class=pre >centre</span> <span class=pre >…</span></code> followed by <code class="docutils literal notranslate"><span class=pre >…</span> <span class=pre >the</span> <span class=pre >run</span> <span class=pre >adopts</span> <span class=pre >the</span> <span class=pre >measured</span> <span class=pre >centre</span> <span class=pre >and</span> <span class=pre >the</span> <span class=pre >lattice</span> <span class=pre >it</span> <span class=pre >finds</span></code> or <code class="docutils literal notranslate"><span class=pre >the</span> <span class=pre >measured</span> <span class=pre >centre</span> <span class=pre >does</span> <span class=pre >not</span> <span class=pre >win</span></code>. Over the validation battery about one rotation run in ten runs this second arm, and about one in ten ends on the measured centre by one of the routes above.</p> <p><strong>After a first pass that failed.</strong> A rotation sweep leaves two further things the input often cannot settle, both decided on the same count the rest of the first pass uses — the right answer indexes and the wrong one does not.</p> <ul class=simple > <li><p><strong>The rotation-axis sign.</strong> A miniCBF header names the axis but gives no direction, and an NXmx vector is only meaningful together with the detector mounting. So after a poor first pass the opposite sign is tried and whichever indexes more validation frames is kept, the flipped one only where its lattice also beats chance on the pooled spots (§4.1), so that one frame against none on a sparse pattern cannot flip it. The decision holds for the rest of the run, and it flips the <strong>axis</strong>, not the angles, because prediction reads the axis too. It runs before the second first pass above and before the long-axis rescue, since with the sign wrong every candidate lattice is wrong.</p> <li><p><strong>The beam-centre search (<code class="docutils literal notranslate"><span class=pre >--beam-center-search[=N|off]</span></code>, on by default, N = 12 px).</strong> Where the first pass, after the second first pass above, still indexes fewer than half the validation frames, the centre is stepped a pixel at a time out to N px along <strong>both</strong> detector axes, each rung with its own spots, and the first rung that indexes a majority and beats the starting count is adopted (<code class="docutils literal notranslate"><span class=pre >Beam</span> <span class=pre >centre</span> <span class=pre >from</span> <span class=pre >indexing:</span> <span class=pre >…</span></code>). Both directions are searched deliberately: an error <em>across</em> the spindle collapses the indexed fraction and announces itself, while an error <em>along</em> it leaves the transform’s peaks sharp, holds nearly every frame indexed and lets the lattice fit commit to an axis harmonic. A rung whose primitive volume is an integer or <span class="math notranslate nohighlight">\(\sqrt{3}\)</span> multiple of the starting cell’s is refused for that reason. The search is skipped where the background measurement places the centre further away than both N px and three σ, since no rung could reach it, and it stops after two rings where no rung has reached a third of the majority. The step is a flat pixel: derived from the <span class="math notranslate nohighlight">\(J_0\)</span> law it would have to use the cell the <em>failed</em> pass returned, which can be a small spurious sub-cell and asks for a step that jumps the lobe being looked for.</p> </ul> <p><strong>Post-refinement.</strong> On a two-pass rotation run, whatever centre the first pass ends on is refined together with the distance, cell, orientation and axis by the post-refinement of §7.5, and the second pass is integrated there. The background measurement does not move that fit; it only widens where the fit may go: the refined beam is committed only within 15 px of the <strong>nearer</strong> of the pass-1 centre and the measured one (the measured one counting where its σ is within <span class="math notranslate nohighlight">\(\max(1\ \mathrm{px}, t)\)</span>), so a header that is far out can still be corrected. Where the refined pass is judged worse, the run goes back to the pass-1 geometry.</p> <p><strong>Committed before indexing (<code class="docutils literal notranslate"><span class=pre >--estimate-beam-center</span></code>, off by default).</strong> Here the centre is measured before anything is indexed and used in place of the file’s. On a sweep that reaches half a turn it comes from the spot positions alone; two exact facts about a rotation sweep supply the two coordinates.</p> <p><strong>Friedel mates half a turn apart.</strong> The Laue condition fixes the component of <span class="math notranslate nohighlight">\(\mathbf{q}\)</span> along the beam, <span class="math notranslate nohighlight">\(q_\parallel = -\lVert\mathbf{q}\rVert^2\lambda/2\)</span>. Rotating 180° about the spindle <span class="math notranslate nohighlight">\(\mathbf{m}\)</span> negates the two components perpendicular to <span class="math notranslate nohighlight">\(\mathbf{m}\)</span> and taking <span class="math notranslate nohighlight">\(-h\)</span> negates all three, so together they negate <strong>only</strong> the component along <span class="math notranslate nohighlight">\(\mathbf{m}\)</span> and leave <span class="math notranslate nohighlight">\(q_\parallel\)</span> untouched. With the spindle perpendicular to the beam, <span class="math notranslate nohighlight">\(-h\)</span> therefore diffracts at <span class="math notranslate nohighlight">\(\varphi+180°\)</span> exactly where <span class="math notranslate nohighlight">\(h\)</span> diffracts at <span class="math notranslate nohighlight">\(\varphi\)</span>, and its spot sits at the mirror image of <span class="math notranslate nohighlight">\(h\)</span>’s along the spindle. This gives the beam coordinate <strong>along</strong> the spindle. Only the reciprocal lattice’s centrosymmetry is needed for the geometry; Friedel’s law <span class="math notranslate nohighlight">\(|F(h)|=|F(-h)|\)</span> is used separately, to tell a true pairing from an accidental one.</p> <p><strong>The second crossing.</strong> The same reflection meets the Ewald sphere twice, at two angles that are generally <em>not</em> 180° apart, differing only in the sign of the lab component perpendicular to both <span class="math notranslate nohighlight">\(\mathbf{m}\)</span> and the beam. This gives the remaining coordinate. The two crossings are separated by a sweep angle fixed by the reflection’s own position, which is what identifies genuine pairs.</p> <p>Neither observable requires a cell or an orientation matrix: each candidate pairing votes for a beam coordinate, and the true value accumulates while wrong pairings scatter. A Friedel pair needs both <span class="math notranslate nohighlight">\(\varphi\)</span> and <span class="math notranslate nohighlight">\(\varphi+180°\)</span> recorded, so a sweep of <span class="math notranslate nohighlight">\(S°\)</span> yields only <span class="math notranslate nohighlight">\(S-180\)</span> degrees’ worth of pairs.</p> <p>The mirror is exact in the <strong>laboratory</strong> frame, so it is sensitive to the spindle direction. A skew of the spindle about the beam <em>spreads</em> the vote rather than shifting it, and is fitted alongside the centre (<code class="docutils literal notranslate"><span class=pre >--no-fit-spindle</span></code> keeps the axis from the file); a tilt of the spindle towards the beam is measured and reported but not applied, being confounded with the detector tilt. Nothing inside the fit can tell that the vote settled on the wrong periodic maximum — every frame pair agrees with every other — so its σ is how far the answer moves when the search is started from a different position. The frames are read in pairs half a turn apart, twice as many as the beam-stop projection uses and away from both ends of the sweep, where shutter synchronisation can spoil an image; where the answer does not come out on them, up to 400 more are read.</p> <p>Where the spot symmetry does not come out — in practice on sweeps shorter than about 220° — the centre is taken from the background measurement above. The estimate replaces the file’s centre only where its σ is within <span class="math notranslate nohighlight">\(\max(1\ \mathrm{px}, t)\)</span> and it moves the centre by more than three σ; otherwise, or where neither method measures anything, the file’s centre is kept. The log line is <code class="docutils literal notranslate"><span class=pre >Beam</span> <span class=pre >centre</span> <span class=pre >from</span> <span class=pre >spot</span> <span class=pre >symmetry|background:</span> <span class=pre >(x,y)</span> <span class=pre >-></span> <span class=pre >(x,y),</span> <span class=pre >moved</span> <span class=pre >d</span> <span class=pre >px,</span> <span class=pre >sigma</span> <span class=pre >σ</span> <span class=pre >px</span> <span class=pre >against</span> <span class=pre >a</span> <span class=pre >c</span> <span class=pre >px</span> <span class=pre >ceiling</span> <span class=pre >=></span> <span class=pre >COMMIT</span></code> (or <code class="docutils literal notranslate"><span class=pre >reject</span></code>, with the reason). <code class="docutils literal notranslate"><span class=pre >--estimate-beam-center</span></code> is ignored where <code class="docutils literal notranslate"><span class=pre >--beam-x</span></code>/<code class="docutils literal notranslate"><span class=pre >--beam-y</span></code> are given, and on stills where <code class="docutils literal notranslate"><span class=pre >--refine-geometry</span></code> has already placed the centre from indexed spots.</p> <p><strong>Stills.</strong> There is no second first pass, no search and no post-refinement on stills; the background measurement is reported only. The centre moves through <code class="docutils literal notranslate"><span class=pre >--estimate-beam-center</span></code> (background estimator) or through the stills geometry refinement, <code class="docutils literal notranslate"><span class=pre >--refine-geometry</span></code>, which bundle-adjusts the beam centre, distance and cell over strongly indexed frames and is on by default where a reference cell is given.</p> <p><strong>What ends up in the output.</strong> The report’s <code class="docutils literal notranslate"><span class=pre >BEAM_CENTRE</span></code> (the PONI) and <code class="docutils literal notranslate"><span class=pre >DIRECT_BEAM</span></code> are the geometry the result was integrated at, and so is <code class="docutils literal notranslate"><span class=pre >JFJOCH_DATASET_SETTINGS</span></code>; <code class="docutils literal notranslate"><span class=pre >POSTREFINE_BEAM_CENTRE</span></code> gives the post-refinement’s move as <code class="docutils literal notranslate"><span class=pre >before</span> <span class=pre >-></span> <span class=pre >after</span></code>. Which of the steps above changed the centre, and why, is in the run log only.</p> <p><strong>Options.</strong></p> <ul class=simple > <li><p><code class="docutils literal notranslate"><span class=pre >--beam-x</span></code>/<code class="docutils literal notranslate"><span class=pre >--beam-y</span></code> set the starting centre and switch <code class="docutils literal notranslate"><span class=pre >--estimate-beam-center</span></code> off. The background check still runs and can still adopt the measured centre in the cases above; add <code class="docutils literal notranslate"><span class=pre >--beam-center-check=off</span></code> to hold a typed centre until post-refinement.</p> <li><p><code class="docutils literal notranslate"><span class=pre >--beam-center-check=off</span></code> skips the background measurement and the second first pass, and removes the measured centre from the post-refinement bound.</p> <li><p><code class="docutils literal notranslate"><span class=pre >--beam-center-search=off</span></code> or <code class="docutils literal notranslate"><span class=pre >=N</span></code> turns off or resizes the search after a failed pass.</p> <li><p><code class="docutils literal notranslate"><span class=pre >--estimate-beam-center</span></code> measures and commits the centre before indexing; <code class="docutils literal notranslate"><span class=pre >--no-fit-spindle</span></code> keeps the spindle direction from the file in that estimate.</p> <li><p><code class="docutils literal notranslate"><span class=pre >--detect-beam-stop=off</span></code> removes the projection the background measurement is read from.</p> <li><p><code class="docutils literal notranslate"><span class=pre >--rotation-no-postrefine</span></code> removes the post-refinement, and with it the merge-judged arms.</p> </ul> </section> <section id=finding-the-beam-stop > <h3 id=finding-the-beam-stop ><a class=toc-backref href="#id7" role=doc-backlink >1.5 Finding the beam stop</a><a class=headerlink href="#finding-the-beam-stop" title="Link to this heading">¶</a></h3> <p>The beam stop and its holder arm shadow part of the detector. A reflection behind them is attenuated but otherwise ordinary — it integrates low, with a plausible <span class="math notranslate nohighlight">\(\sigma\)</span>, and no outlier test catches it — so the shadow is found and masked instead. <code class="docutils literal notranslate"><span class=pre >--detect-beam-stop[=N|off]</span></code> (on by default, <span class="math notranslate nohighlight">\(N=60\)</span> frames) projects those frames to a per-pixel mean and maximum, and writes the result into the pixel mask as bit 9, from where it excludes those pixels from every later stage.</p> <p>The shadow is a place where the background is <em>missing</em>, so it is found by comparing each pixel’s background against the background at the same radius. The mean is pooled over a small box first — a single pixel of a sparse background carries too few counts to tell a shadow from a Poisson hole, and the stop is much wider than the box — and the comparison is against the median of the pixel’s own radius ring, taken over the pixels not already excluded and iterated a few times so the shadow stays out of the baseline it is measured against. A pixel is shadow when the ratio falls below 0.50 <strong>and</strong> the deficit is significant against its own Poisson scatter, <span class="math notranslate nohighlight">\(\sqrt{2\,(E-N+N\ln(N/E))}\)</span> over the pooled counts. A ratio says nothing when the background behind it is a handful of photons, and it is that significance that makes the comparison scale-free rather than tuned to one exposure: rebuilt from six frames of a low-background sweep, the same ratio without it masks three quarters of the detector. The index of dispersion does <em>not</em> do this job — it is 1.0 inside the shadow and 1.0 outside it, a shadowed pixel being Poisson at a low rate and a lit one Poisson at a high rate — only the rate relative to the ring separates them.</p> <p>Nothing assumes the stop and the beam are concentric, because only the per-ring comparison is used; a ring lying wholly inside the stop has no unshadowed pixel for its median to find, which is exactly the case where the comparison must fail. Such a ring is judged against what this detector’s background typically is — the median over the rings the walk is willing to judge — and is shadow in its entirety when it falls far below that. Comparing it instead against the <em>largest</em> background further out reads an ordinary background inside a strong ring as blocked, and the walk then runs out to that ring and returns a filled disk of good detector with diffraction rings plainly visible inside it.</p> <p><strong>The rings are drawn about the centre the data measure, not the one the file claims.</strong> Displacing the centre costs nothing for tens of pixels and a great deal beyond: at 150 px out the comparison describes the background’s own radial fall-off rather than the hardware, and returns a tenth of the detector as shadow with nothing blocking it — and header centres are wrong by that much. The centre is therefore fitted from the same per-pixel mean the mask is computed from, before the mask is read, and named to the finder; it costs no frame of its own. That first fit is itself biased by the shadow it has not yet masked (on a sweep with a third of the detector behind a pin it landed 5 px out and called itself 0.40 px), and it does not have to be unbiased: the ring comparison does not notice tens of pixels, and the centre the run <em>reports and consumes</em> is the second fit of §1.4, the one that runs after the mask is loaded with the shadow out of the way. The order is fit, mask, fit, and only the second answer leaves. Two rounds are enough, measured rather than assumed — on clean sweeps the two fits agree to 0.03 px, so a third would draw the same rings.</p> <p><strong>A ring is only flat once the polarization is divided out.</strong> The comparison assumes the background is flat around a ring with nothing in the beam, and it is not: a polarized source suppresses the background in its own plane by the azimuthal factor of §2.2, a factor of three at <span class="math notranslate nohighlight">\(2\theta=55°\)</span> and four at <span class="math notranslate nohighlight">\(70°\)</span> — several times the dip the test is looking for, so on a short-distance geometry the two in-plane lobes of every outer ring read as shadow (measured: 5 % and 11 % of two detectors, with nothing visible under either mask). The mean projection is therefore divided by that factor before the ring comparison, and the Poisson deficit multiplies it back in so the significance is still the significance of the counts that were recorded. The factor is the geometry’s own, evaluated about the centre just fitted rather than the one in the file: polarization is the only correction a ring carries that varies <em>along</em> it — solid angle, detector response and air absorption are functions of <span class="math notranslate nohighlight">\(2\theta\)</span> and the ring median absorbs them — and the geometry is what knows where the polarization plane lies once the detector is tilted, the stored image quarter-turned or the detector rotated in its own plane.</p> <p>What survives is then shaped into a region, and what makes it specific is <strong>size</strong>, not connection to the beam. A shadow is cast by something physical and is correspondingly large, while the background wanders a pixel or two at a time, so the connected components holding at least 2000 core pixels are kept and the rest dropped; measured on clean sweeps spanning 0.05 to 9.5 counts/px/frame of background, every one returns exactly one such component — the beam stop — and the largest spurious candidate anywhere is 74 pixels. Requiring the region to touch the direct beam instead is written for the stop and its holder arm; hardware standing in the beam further out — a pin, a loop — casts a shadow that begins some way out in radius, with lit detector between it and the stop and no bridge across the gap, and on one sweep that discarded 950 k correctly-found pixels and sent them into integration as measured-and-near-zero. The kept components are bridged across the module gaps the holder arm crosses, grown outward through the partially shadowed penumbra — much wider for a pin than for a stop edge — closed, and with the interior of the disk filled; the rings found lying wholly inside the stop join the region after the size filter rather than being asked to be large themselves.</p> <p>Last, any pixel that ever recorded a real reflection — a maximum over the frames well above background, in a cluster, so that a single-frame zinger does not count — is given back, because a beam stop cannot have blocked a reflection that was measured.</p> </section> <section id=defective-pixels > <h3 id=defective-pixels ><a class=toc-backref href="#id8" role=doc-backlink >1.6 Defective pixels</a><a class=headerlink href="#defective-pixels" title="Link to this heading">¶</a></h3> <p>A pixel that reads high frame after frame, whatever the crystal does, is integrated into whichever reflection’s box it falls in; under rotation that is one pixel collecting a different reflection on every frame that reaches it, and a merged intensity hundreds or thousands of times its shell mean, carried by one or two observations the merge’s outlier test cannot judge. The file’s mask misses such pixels on many detectors, so on rotation data from a counting sensor Rugnux measures them on the pre-scan’s own sample of frames (<code class="docutils literal notranslate"><span class=pre >HotPixelFinder</span></code>, <code class="docutils literal notranslate"><span class=pre >rugnux/HotPixels.h</span></code>) and masks them as bit 10. The frames are read a second time for it, once the beam-stop projection has measured where the scattered background puts the beam: the rings have to be drawn about the true beam, and a file’s centre can be far enough out that a ring crosses the background’s radial fall-off.</p> <p>On each frame a pixel is <strong>lit</strong> when it exceeds <span class="math notranslate nohighlight">\(b + 3.3\,\max(\sqrt{b}, 1.4826\,\mathrm{MAD}) + 2\)</span>, with <span class="math notranslate nohighlight">\(b\)</span> the larger of the median of its 2 px iso-<span class="math notranslate nohighlight">\(2\theta\)</span> ring and of that ring’s sixteenth in azimuth, so ice and powder rings, the polarization dip and partial shadows set their own level. It is <strong>persistent</strong> when it is lit on at least <span class="math notranslate nohighlight">\(\max(k_1, k_B)\)</span> of the sampled frames: one reflection stays on a pixel for <span class="math notranslate nohighlight">\((\Delta\phi + w)/|\zeta|\)</span> of rotation (<span class="math notranslate nohighlight">\(w = 5°\)</span>, a generous rocking width), which covers fewer than <span class="math notranslate nohighlight">\(k_1 = 1 + \lceil (\Delta\phi + w)/(|\zeta|\,\delta) \rceil\)</span> frames sampled <span class="math notranslate nohighlight">\(\delta\)</span> apart; and <span class="math notranslate nohighlight">\(k_B\)</span> is the binomial bound, from the lit rate of the ring’s other pixels, that fewer than 0.01 pixels of the whole detector reach by chance. A persistent pixel is masked if it reads on average at least ten times its ring and its mean excess is above the Poisson bound; a weaker one cannot make an outlier. This holds for a patch of such pixels as for a lone one: a patch lit through the whole sweep is stationary in the lab, so it is no reflection of the rotating crystal, and a reflection crossing it would read the patch. Pixels holding the detector’s error value on most frames (already invalid on every frame, but not in the static mask) are masked with them. A CCD (no sensor depth: read with an offset, not Poisson) is left alone. One log line says how many pixels were masked and why.</p> </section> </section> <hr class=docutils /> <section id=azimuthal-integration-radial-profiles > <h2 id=azimuthal-integration-radial-profiles ><a class=toc-backref href="#id9" role=doc-backlink >2. Azimuthal integration (radial profiles)</a><a class=headerlink href="#azimuthal-integration-radial-profiles" title="Link to this heading">¶</a></h2> <p>Azimuthal integration produces a radial profile <span class="math notranslate nohighlight">\(I(q)\)</span> or <span class="math notranslate nohighlight">\(I(d)\)</span> by histogramming pixels into radial bins. Pixels are <strong>not split</strong> across bins; each pixel contributes wholly to a single bin. By default the profile is purely radial (a single azimuthal bin), but the azimuth can optionally be split into up to 512 <span class="math notranslate nohighlight">\(\phi\)</span> sectors (<code class="docutils literal notranslate"><span class=pre >azim_bins</span></code>, <code class="docutils literal notranslate"><span class=pre >--azim-phi-bins</span></code>), giving a <strong>2D <span class="math notranslate nohighlight">\(q\times\phi\)</span> profile</strong> that exposes azimuthal anisotropy such as detector shadowing or sample texture.</p> <section id=histogram-estimator > <h3 id=histogram-estimator ><a class=toc-backref href="#id10" role=doc-backlink >2.1 Histogram estimator</a><a class=headerlink href="#histogram-estimator" title="Link to this heading">¶</a></h3> <p>Let bin index <span class="math notranslate nohighlight">\(b(x,y)\)</span> be precomputed from <span class="math notranslate nohighlight">\(q(x,y)\)</span> (or equivalently from <span class="math notranslate nohighlight">\(d(x,y)\)</span>) and, when <span class="math notranslate nohighlight">\(\phi\)</span> sectors are enabled, the azimuth <span class="math notranslate nohighlight">\(\phi(x,y)\)</span> — so <span class="math notranslate nohighlight">\(b = b_q + b_\phi B_q\)</span>. For each bin <span class="math notranslate nohighlight">\(b\)</span>:</p> <ul class=simple > <li><p>accumulate corrected intensity and its square: <span class="math notranslate nohighlight">\( S_b = \sum_{(x,y):\,b(x,y)=b} I(x,y)\,C(x,y),\qquad S^{(2)}_b = \sum I(x,y)^2\,C(x,y)^2, \)</span></p> <li><p>and count: <span class="math notranslate nohighlight">\( N_b = \#\{(x,y):\,b(x,y)=b \text{ and pixel is valid}\}. \)</span></p> </ul> <p>The profile reports both the mean <span class="math notranslate nohighlight">\(\bar{I}_b = S_b / N_b\)</span> (when <span class="math notranslate nohighlight">\(N_b>0\)</span>) and a per-bin sample standard deviation <span class="math notranslate nohighlight">\(\sigma_b = \sqrt{(S^{(2)}_b - S_b^2/N_b)/(N_b-1)}\)</span> (a spread/error estimate for each radial point). Invalid pixels (masked, saturated, detector error codes) are excluded.</p> </section> <section id=corrections-applied > <h3 id=corrections-applied ><a class=toc-backref href="#id11" role=doc-backlink >2.2 Corrections applied</a><a class=headerlink href="#corrections-applied" title="Link to this heading">¶</a></h3> <p>Two standard corrections are available:</p> <p><strong>(i) Solid angle / geometric correction.</strong> A flat pixel’s solid angle falls off with the <strong>incidence angle <span class="math notranslate nohighlight">\(\alpha\)</span> between the scattered ray and the detector normal</strong>. With the in-plane detector offsets <span class="math notranslate nohighlight">\(u=(x-x_\mathrm{beam})p\)</span> and <span class="math notranslate nohighlight">\(v=(y-y_\mathrm{beam})p\)</span> — measured from the PONI (§1.1), which is what the tilt-invariance below rests on — and detector distance <span class="math notranslate nohighlight">\(D\)</span>, <span class="math notranslate nohighlight">\( \cos\alpha = \frac{D}{\sqrt{u^2+v^2+D^2}},\qquad C_\Omega = \cos^3\alpha, \)</span> applied — like the polarization term below — as a <strong>divisor</strong> (intensities are scaled by <span class="math notranslate nohighlight">\(1/\cos^3\alpha\)</span>), so pixels at oblique incidence, which subtend a smaller solid angle, are boosted. Because <span class="math notranslate nohighlight">\(\alpha\)</span> is evaluated in the detector’s own frame it is <strong>invariant under detector tilt</strong> (<span class="math notranslate nohighlight">\(\mathrm{rot1}/\mathrm{rot2}/\mathrm{rot3}\)</span>), matching PyFAI’s <code class="docutils literal notranslate"><span class=pre >solidAngleArray</span></code> and MAX IV azint. It reduces to the commonly quoted <span class="math notranslate nohighlight">\(\cos^3(2\theta)\)</span> form only for an untilted detector, where the incidence angle coincides with the scattering angle.</p> <p><strong>(ii) Polarization correction.</strong> With polarization coefficient <span class="math notranslate nohighlight">\(P\)</span> (beamline dependent) and azimuth <span class="math notranslate nohighlight">\(\phi\)</span>: <span class="math notranslate nohighlight">\( C_\mathrm{pol}(2\theta,\phi) = \frac{1}{2}\left(1+\cos^2(2\theta) - P\cos(2\phi)\left(1-\cos^2(2\theta)\right)\right), \)</span> applied as a divisor to intensities (i.e. scale by <span class="math notranslate nohighlight">\(1/C_\mathrm{pol}\)</span>) when enabled. This is the factor of Kahn <em>et al.</em> (1982); <span class="math notranslate nohighlight">\(\phi\)</span> is the azimuth in the <strong>lab</strong> frame, so the correction follows detector tilt and a swung-out <span class="math notranslate nohighlight">\(2\theta\)</span> arm without further work.</p> <p>The <strong>polarization plane</strong> is taken to contain the lab <span class="math notranslate nohighlight">\(x\)</span> axis — a horizontally polarized source. A vertically polarized one is expressed by a <strong>negative</strong> <span class="math notranslate nohighlight">\(P\)</span>: flipping the sign of the <span class="math notranslate nohighlight">\(\cos 2\phi\)</span> term is exactly a <span class="math notranslate nohighlight">\(90^\circ\)</span> rotation of the plane. The plane is <strong>not</strong> autodetected and is not read from the file — no format Rugnux reads declares one — so a vertical beamline must say so with <code class="docutils literal notranslate"><span class=pre >--polarization</span></code>. <span class="math notranslate nohighlight">\(P\)</span> is the polarization <em>degree</em> (XDS’s <code class="docutils literal notranslate"><span class=pre >FRACTION_OF_POLARIZATION</span></code> is <span class="math notranslate nohighlight">\((1+P)/2\)</span>). The default <span class="math notranslate nohighlight">\(P = 0.99\)</span>, an undulator value, is applied to every Rugnux run; it is not fitted, because a single dataset does not determine it. Bending-magnet and wiggler beamlines are typically lower (0.8–0.95), and a known value for one should be passed with <code class="docutils literal notranslate"><span class=pre >--polarization</span></code>.</p> </section> <section id=background-estimate-for-profiles > <h3 id=background-estimate-for-profiles ><a class=toc-backref href="#id12" role=doc-backlink >2.3 Background estimate for profiles</a><a class=headerlink href="#background-estimate-for-profiles" title="Link to this heading">¶</a></h3> <p>A background estimate is derived from the profile as its mean intensity over a fixed low-to-mid <span class="math notranslate nohighlight">\(Q\)</span> window (default <span class="math notranslate nohighlight">\(2\pi/5\)</span> to <span class="math notranslate nohighlight">\(2\pi/3\)</span> Å<span class="math notranslate nohighlight">\(^{-1}\)</span>). This background is used for monitoring and diagnostics; it is <strong>not</strong> the same as the local Bragg-spot background used in summation integration (§9.2).</p> </section> </section> <hr class=docutils /> <section id=spot-finding-strong-pixels-bragg-spots > <h2 id=spot-finding-strong-pixels-bragg-spots ><a class=toc-backref href="#id13" role=doc-backlink >3. Spot finding (strong pixels → Bragg spots)</a><a class=headerlink href="#spot-finding-strong-pixels-bragg-spots" title="Link to this heading">¶</a></h2> <p>Spot finding is a two-stage process:</p> <ol class="arabic simple"> <li><p><strong>Strong-pixel selection</strong> using intensity and/or local signal-to-noise criteria.</p> <li><p><strong>Connected-component labeling (CCL)</strong> to group strong pixels into candidate spots, followed by spot-level filtering and feature extraction.</p> </ol> <section id=strong-pixel-detection-by-local-statistics > <h3 id=strong-pixel-detection-by-local-statistics ><a class=toc-backref href="#id14" role=doc-backlink >3.1 Strong-pixel detection by local statistics</a><a class=headerlink href="#strong-pixel-detection-by-local-statistics" title="Link to this heading">¶</a></h3> <p>For each pixel <span class="math notranslate nohighlight">\(i\)</span> with value <span class="math notranslate nohighlight">\(v_i\)</span>, consider a square window (nominally <span class="math notranslate nohighlight">\(31\times 31\)</span> pixels) around it. Let the window contain <span class="math notranslate nohighlight">\(n\)</span> valid pixels (excluding masked/bad/saturated), and define: <span class="math notranslate nohighlight">\( \Sigma = \sum v,\qquad \Sigma_2 = \sum v^2. \)</span></p> <p>To avoid biasing the local statistics by the test pixel itself, Jungfraujoch evaluates the pixel against the window with the pixel removed: <span class="math notranslate nohighlight">\( \Sigma' = \Sigma - v_i,\quad \Sigma_2' = \Sigma_2 - v_i^2,\quad n' = n-1. \)</span></p> <p>A variance-like quantity proportional to <span class="math notranslate nohighlight">\(n'^2\)</span> is formed: <span class="math notranslate nohighlight">\( V = n'\Sigma_2' - (\Sigma')^2, \)</span> and the deviation-from-mean quantity: <span class="math notranslate nohighlight">\( \Delta = v_i n' - \Sigma'. \)</span></p> <p>A pixel is considered strong if:</p> <ul class=simple > <li><p>it is above a photon/count threshold, and</p> <li><p>its window contains enough valid neighbours (more than 100), so the local statistics are meaningful, and</p> <li><p><span class="math notranslate nohighlight">\(\Delta>0\)</span>, and</p> <li><p>the squared deviation exceeds a scaled variance: <span class="math notranslate nohighlight">\( \Delta^2 > V\cdot T^2, \)</span> where <span class="math notranslate nohighlight">\(T\)</span> is the configured signal-to-noise threshold.</p> </ul> <p>This is equivalent to a local z-score criterion but implemented in integer arithmetic to be robust and fast.</p> <p>The test is applied in <strong>two passes</strong> over the image. The first is as described above. The second repeats it with every pixel found strong by the first excluded from the local background — it is treated exactly like a saturated pixel, so it contributes to no window it falls into and stays strong itself. This matters for any spot wide enough to reach into its own background box: on a single pass such a spot inflates the mean and variance it is then tested against, and its outer pixels fail the criterion. Excluding the core recovers them, so the spot is reported with its true extent rather than its brightest few pixels. Both the CPU and GPU implementations run these two passes and return the same spot list for the same frame.</p> <p>Special cases:</p> <ul class=simple > <li><p>saturated pixels can be forced to “strong” (useful for detecting overloaded Bragg spots),</p> <li><p>invalid pixels are never strong.</p> </ul> </section> <section id=adaptive-self-calibrating-detection > <h3 id=adaptive-self-calibrating-detection ><a class=toc-backref href="#id15" role=doc-backlink >3.2 Adaptive (self-calibrating) detection</a><a class=headerlink href="#adaptive-self-calibrating-detection" title="Link to this heading">¶</a></h3> <p>The local-statistics test above needs a fixed photon/count threshold whose correct value depends on the background level, which varies between datasets. The <strong>adaptive</strong> mode (<code class="docutils literal notranslate"><span class=pre >--adaptive-spots</span></code>; the default in <code class="docutils literal notranslate"><span class=pre >rugnux</span></code> and in the viewer for both stills and rotation data, <code class="docutils literal notranslate"><span class=pre >--no-adaptive-spots</span></code> reverts) derives that threshold from each image’s own noise, per resolution ring, so no per-dataset value is needed. It admits more spots than the fixed threshold, including genuine reflections that belong to no indexed lattice; these are down-weighted rather than filtered in the per-image geometry fit (§7.4).</p> <p>Pixels are binned into the same resolution rings as the azimuthal integrator (§2). For each ring a robust background is estimated in three passes: one plain pass over all valid pixels, then two <span class="math notranslate nohighlight">\(\sigma\)</span>-clipping passes that keep only pixels within <span class="math notranslate nohighlight">\(\pm 3\sigma\)</span> of the current ring mean (removing the Bragg peaks from the background estimate). This yields a per-ring background mean <span class="math notranslate nohighlight">\(\mu_b\)</span> and scatter <span class="math notranslate nohighlight">\(\sigma_b\)</span>.</p> <p>The ring’s detection threshold is the larger of two arms, <span class="math notranslate nohighlight">\( t_b = \max\!\big(\;\mu_b + z\,\sqrt{\sigma_b^2 + \sigma_\mathrm{read}^2}\;,\;\; k_\mathrm{Poisson}(\mu_b, p)\;\big), \)</span> where <span class="math notranslate nohighlight">\(k_\mathrm{Poisson}(\mu_b,p)\)</span> is the smallest count whose Poisson<span class="math notranslate nohighlight">\((\mu_b)\)</span> upper tail is <span class="math notranslate nohighlight">\(\le p\)</span>. The Poisson arm is correct where the background is countable (a bright low-resolution ring gets a high threshold); the Gaussian arm — floored by a detector-level excess-noise constant <span class="math notranslate nohighlight">\(\sigma_\mathrm{read}\)</span> — takes over on near-empty high-resolution rings, where the Poisson arm degenerates to “one photon is significant” and would flood. The operating point <span class="math notranslate nohighlight">\(p = E/N\)</span> is set from a single portable knob <span class="math notranslate nohighlight">\(E\)</span>, the expected number of false pixels tolerated per frame (<code class="docutils literal notranslate"><span class=pre >--spot-false-pixels</span></code>, default 100), with <span class="math notranslate nohighlight">\(N\)</span> the number of valid pixels. Because <span class="math notranslate nohighlight">\(p\)</span> and every <span class="math notranslate nohighlight">\(\mu_b,\sigma_b\)</span> come from the image itself, the same <span class="math notranslate nohighlight">\(E\)</span> lands a sensible photon threshold on strong and weak datasets alike, with no per-dataset tuning. Rings too sparse to characterise (fewer than ~40 pixels) fall back to a whole-frame background. A pixel is strong when <span class="math notranslate nohighlight">\(v_i \ge t_b\)</span> for its ring (saturated pixels are still forced strong); the strong pixels then feed the same CCL stage (§3.4). The signal-to-noise and photon-count criteria of §3.1 are not used in this mode.</p> <p>Because detection reads the pixel’s ring, a pixel that falls outside the azimuthal-integration <span class="math notranslate nohighlight">\(q\)</span> range has no ring and can never be strong: the integration range bounds what adaptive detection can see. Both upper limits are therefore optional and default to the detector itself — the azimuthal integration runs to the highest <span class="math notranslate nohighlight">\(q\)</span> any pixel of the detector reaches (<code class="docutils literal notranslate"><span class=pre >--azim-max-q</span></code> unset), and spot finding is not clipped in resolution (<code class="docutils literal notranslate"><span class=pre >--spot-high-resolution</span></code> unset), for rotation data as well as stills. Setting either one narrows detection accordingly — appropriate for weak, high-background data, where the spots admitted at the detector edge are dominated by noise.</p> <p><strong>Fused GPU engine.</strong> The per-ring reduction the adaptive threshold needs is the <em>same</em> reduction the azimuthal integrator performs. On the GPU path the two are fused into a single image pass (<code class="docutils literal notranslate"><span class=pre >AdaptiveSpotFinderGPU</span></code>): one reduction accumulates the corrected per-ring sums for the azimuthal profile (§2) <em>and</em> the raw per-ring statistics for the threshold, after which a light kernel flags the strong pixels. One GPU pass therefore replaces both the separate azimuthal-integration pass and the host-side adaptive spot-finding pass, at a small fraction of the CPU finder’s cost per frame and producing the same spot list and azimuthal profile. It is enabled by default in the offline <code class="docutils literal notranslate"><span class=pre >rugnux</span></code> path, the interactive viewer and the online receiver.</p> <p><strong>Online.</strong> <code class="docutils literal notranslate"><span class=pre >spot_finding_settings</span></code> in the REST API carries <code class="docutils literal notranslate"><span class=pre >adaptive_threshold</span></code> and <code class="docutils literal notranslate"><span class=pre >false_pixels_per_frame</span></code>, so the mode is reachable from the broker and from the web frontend as well as from <code class="docutils literal notranslate"><span class=pre >rugnux</span></code> and the viewer. It defaults to <em>off</em> online, unlike <code class="docutils literal notranslate"><span class=pre >rugnux</span></code> and the viewer, because the broker serves both workflows and only one of them can run it: spots are found in software only on the DECTRIS/SIMPLON path, while the JUNGFRAU and EIGER workflows find them on the FPGA at its own fixed threshold. Setting <code class="docutils literal notranslate"><span class=pre >adaptive_threshold</span></code> on those is refused with an error rather than accepted and ignored, so a detection setting that had no effect cannot be mistaken for one that did.</p> </section> <section id=resolution-and-ice-ring-handling > <h3 id=resolution-and-ice-ring-handling ><a class=toc-backref href="#id16" role=doc-backlink >3.3 Resolution and ice-ring handling</a><a class=headerlink href="#resolution-and-ice-ring-handling" title="Link to this heading">¶</a></h3> <p>Spot finding can be restricted to a resolution range <span class="math notranslate nohighlight">\([d_\mathrm{high}, d_\mathrm{low}]\)</span> by masking pixels outside the range. Optionally, spots in identified ice-ring regions can be tagged so that subsequent indexing/refinement may include or exclude them (see §4 and §6).</p> <p>A single per-image <strong>ice-ring score</strong> is derived from a radial profile: for each hexagonal-ice powder ring (see <em>Where the ring positions come from</em> below), the profile intensity at the ring is divided by a smooth background estimated from the <em>whole</em> profile — a running median of the non-ice bins, interpolated under each ring — and the strongest ring’s ratio is reported (1 = no ice, <span class="math notranslate nohighlight">\(>1\)</span> = ice above background). A whole-profile background is used rather than a couple of adjacent shoulder bins so the estimate is robust to the radial binning: at a coarse Q-spacing a local shoulder can be only ~1 bin and would double-count the ring’s own edge (offline processing defaults to a fine 0.01 Å⁻¹ spacing — in <span class="math notranslate nohighlight">\(q = 2\pi/d\)</span>, like every <span class="math notranslate nohighlight">\(q\)</span> in this document (§1.2) — <code class="docutils literal notranslate"><span class=pre >--azim-q-spacing</span></code>, so the rings are well resolved). The reported quantity is the ice <em>magnitude</em> rather than a significance: with many photons any real ice ring is statistically significant, so significance does not discriminate.</p> <p>The profile the score is read off is the <strong>peak-excluded</strong> one, not the plain azimuthal integration: where adaptive spot finding runs (§3.2 — the offline and viewer default), the score uses the sigma-clipped per-resolution-ring background that finder already computes for its threshold. This matters more than it sounds. A plain azimuthal profile is a per-ring <em>mean</em>, so a few strong low-resolution reflections landing in a ring’s bin raise it exactly as ice would; measured over a rotation battery, that alone ranked ice-free crystals above crystals that really are iced. An ice ring is azimuthally smooth and survives the sigma clip, while Bragg peaks do not, so on the clipped profile ice-free crystals sit near 1 and crystals with confirmed ice above 2. Only where no adaptive finder ran (the FPGA workflow) does the score fall back to the plain profile.</p> <p>The radial profile sees ice only as a <strong>smooth powder ring</strong>. Ice in large crystallites diffracts as discrete spots, leaves the profile flat, and is invisible to the score above, so a second channel is read from the spot list itself: the spots found on the ice bands are counted against the spots found in the ice-free flanks <span class="math notranslate nohighlight">\([w,2w)\)</span> either side of each band, rescaled to the bands’ own <span class="math notranslate nohighlight">\(q\)</span> width (a flank landing on another ring is dropped with its width). The indicator is the ratio pooled over the run — per image the control is a handful of spots and the ratio means nothing — and it is taken before the spot-count filter, which orders ice spots last and would discard them first. The two channels barely overlap: smooth ice reads high on the profile and ~1 on the spots, textured ice the reverse, and a clean crystal ~1 on both. Both counts are stored per image (<code class="docutils literal notranslate"><span class=pre >spot_count_ice_rings</span></code>, <code class="docutils literal notranslate"><span class=pre >spot_count_ice_control</span></code>; HDF5 <code class="docutils literal notranslate"><span class=pre >/entry/MX/peakCountIceRingRes</span></code>, <code class="docutils literal notranslate"><span class=pre >/entry/MX/peakCountIceRingControl</span></code>).</p> <p>Both channels are used offline as a <strong>gate</strong> on ice handling: unless the run reaches <code class="docutils literal notranslate"><span class=pre >--ice-min-score</span></code> (default 1.5) on the profile or <code class="docutils literal notranslate"><span class=pre >--ice-min-spot-ratio</span></code> (default 2.0) on the spots — 0 disables a channel — ice-ring flagging and the exclusion from the scale fit (§10.10) are skipped. The fixed bands cover 16–26 % of the unique reflections at typical resolutions whether or not the crystal has ice, and more than that on a detector reaching past 1.5 Å, so handling ice on a clean crystal is a pure loss.</p> <p><strong>Where the ring positions come from.</strong> The eleven bands from 3.895 to 1.522 Å are the <em>measured</em> positions of Moreau, Atakisi & Thorne (Acta Cryst D77, 2021, 540–554). That list ends at 1.522 Å by its own scope — the paper states that hexagonal ice “has 11 diffraction rings between 4 and 1.5 Å resolution”, and its subject was detecting ice in deposited data rather than masking it — not because ice stops there. On a detector that reaches further, the rings it does not list are the ones left in the data.</p> <p>The eight bands below it are <strong>calculated</strong>, since past that paper there is nothing measured to copy. Enumerating <span class="math notranslate nohighlight">\(hkl\)</span> from the ice Ih cell is not enough: ice Ih is <span class="math notranslate nohighlight">\(P6_3/mmc\)</span> with oxygen on <span class="math notranslate nohighlight">\(4f\)</span>, and most of what enumeration emits is extinguished by the <em>oxygen sublattice</em> rather than by the space group — which is why (004) at 1.830 Å and (104) at 1.657 Å are missing from the measured list even though they lie inside its range and its reflection conditions allow them (for even <span class="math notranslate nohighlight">\(l\)</span> the <span class="math notranslate nohighlight">\(4f\)</span> structure factor carries a factor <span class="math notranslate nohighlight">\(\cos 2\pi l z\)</span> at every <span class="math notranslate nohighlight">\(hk\)</span>, and <span class="math notranslate nohighlight">\(z \approx 1/16\)</span> kills <span class="math notranslate nohighlight">\(l=4\)</span> — <span class="math notranslate nohighlight">\((104)\)</span> along with <span class="math notranslate nohighlight">\((004)\)</span>). Structure factors are computed instead — oxygen only, the hydrogens being half-occupancy disordered and weak to X-rays — and the lines kept are those reaching 3 % of the strongest. That rule <strong>reproduces the measured eleven exactly</strong>, and every line it drops inside their range computes to zero, which is what makes it trustworthy below 1.522 Å. The cell is that of Röttger <em>et al.</em> (Acta Cryst B50, 1994, 644–648). The list stops at 1.170 Å because below it the calculated real lines fall to 2–3 % while the extinct ones rise to about 1 %, and an oxygen-only calculation cannot separate them any further.</p> <p>One consequence is worth stating: the profile score is the <strong>strongest</strong> ring’s ratio, a maximum over the bands, so a longer list can only raise it. The gate at 1.5 is therefore read against a list of this length, and lengthening it again would need the gate re-checked.</p> <p><strong>Rings this sample actually shows.</strong> Hexagonal ice is the only phase whose rings can be named in advance, and it is not the only thing that powders: a shower of microcrystals around the crystal, or a salt out of the cryoprotectant, leaves the same textured rings at <span class="math notranslate nohighlight">\(d\)</span>-spacings no fixed list carries, and their spots are otherwise handed to the indexer as if they were this crystal’s. The rings are therefore also <strong>measured from the run’s own pre-scan spots</strong> — read off as what stands above the smooth fall-off of spot density with <span class="math notranslate nohighlight">\(q\)</span> — on every run, and reported whether or not anything acted on them (<code class="docutils literal notranslate"><span class=pre >POWDER_*</span></code> in the results report: the ring count, the fraction of the pre-scan’s spots they hold, and their <span class="math notranslate nohighlight">\(d\)</span>-spacings). The two lists are not the same list: on one such crystal 16 of the 24 measured rings were ice and the rest were not. A measured ring is set aside from indexing only where a first pass that found no lattice needs it set aside (§6); the fixed ice bands above are what the ice score, the scale fit and the resolution cut read.</p> <p>A further optional safeguard removes isolated high-resolution “spur” spots by detecting large gaps in <span class="math notranslate nohighlight">\(1/d\)</span> (or <span class="math notranslate nohighlight">\(q\)</span>) space and discarding spots beyond the gap. This is intended for macromolecular diffraction where edge-of-detector backgrounds can be extremely low.</p> </section> <section id=connected-component-labeling-ccl > <h3 id=connected-component-labeling-ccl ><a class=toc-backref href="#id17" role=doc-backlink >3.4 Connected-component labeling (CCL)</a><a class=headerlink href="#connected-component-labeling-ccl" title="Link to this heading">¶</a></h3> <p>Strong pixels are grouped into connected components (adjacent strong pixels) using a CCL algorithm. Each component yields a candidate spot with:</p> <ul class=simple > <li><p>centroid <span class="math notranslate nohighlight">\((x,y)\)</span> (often intensity-weighted),</p> <li><p>pixel count (spot size),</p> <li><p>integrated spot intensity proxy (sum of pixel values),</p> <li><p>resolution <span class="math notranslate nohighlight">\(d\)</span> at the centroid (or mean over pixels),</p> <li><p>and quality flags (e.g. ice-ring classification).</p> </ul> <p>Spot-level filters include minimum/maximum pixel count and resolution limits.</p> <p><strong>The upper bound is on how large a <em>round</em> spot may be, not on how bright one may be.</strong> Under the self-calibrating threshold (§3.2) a component’s area above the contour grows as <span class="math notranslate nohighlight">\(\sigma^2\ln(A/T)\)</span> — without bound in the peak amplitude <span class="math notranslate nohighlight">\(A\)</span> — so an upper bound on pixel count alone becomes an <em>intensity ceiling</em>: measured on a strongly diffracting crystal, footprints run 3 px at 30–100 counts to 50 px above 10 000, four times the slope the fixed local-box test gives, and the old bound of 50 discarded the brightest reflections on every image. The bound is therefore 200 (the same as CrystFEL <code class="docutils literal notranslate"><span class=pre >peakfinder8</span></code>’s <code class="docutils literal notranslate"><span class=pre >--max-pix-count</span></code>; XDS has no such parameter at all and guards on shape instead, with <code class="docutils literal notranslate"><span class=pre >SPOT_MAXIMUM-CENTROID</span></code>), and a component above 50 pixels must in addition <strong>fill at least a fifth of the square its bounding box fits inside</strong>. A Bragg reflection is round and fills about half of that square however bright it is; an ice arc, a cosmic-ray track or a lit detector row fills a fifth or less, which is what an upper bound was ever protecting against. Below 50 pixels no shape is asked for, so everything accepted before still is. The shape test is inert on every dataset it has been measured on — it exists to bound the <em>shape</em> of what the larger size bound now admits, on data carrying arcs or tracks, not because the crystals measured here needed it. The test is integer arithmetic, so the host and the GPU extractor agree by construction.</p> <p>The host implementation (<code class="docutils literal notranslate"><span class=pre >StrongPixelSet::sparseccl</span></code>) is the SparseCCL of the ACTS/traccc project: it runs over the strong pixels sorted row-major, uses a sliding window over the previous line and a union-find whose root is each component’s lowest index. On the GPU the same labelling runs <strong>on the device</strong> (<code class="docutils literal notranslate"><span class=pre >SpotExtractorGPU</span></code>): the packed strong-pixel bitmask is compacted into that same sorted list without leaving the card, each pixel finds its at most four earlier 8-neighbours by binary search, and a lock-free union-find with path halving labels them. Only the finished spot list — a few hundred entries — comes back to the host, instead of the whole bitmask (2.26 MB per frame at 18 MP). The two implementations produce the same components, in the same order, with the same pixel counts and intensities; <code class="docutils literal notranslate"><span class=pre >tests/SpotExtractorGPUParityTest.cpp</span></code> holds them to it. The device version is also insensitive to frame content: the host sliding window becomes quadratic when many pixels light up in one detector line — a hot module, or a diffraction ring where it runs tangent to a row — which costs tens to hundreds of milliseconds on such a frame, while the device version stays under a millisecond.</p> </section> <section id=adaptive-per-image-minimum-spot-size > <h3 id=adaptive-per-image-minimum-spot-size ><a class=toc-backref href="#id18" role=doc-backlink >3.5 Adaptive per-image minimum spot size</a><a class=headerlink href="#adaptive-per-image-minimum-spot-size" title="Link to this heading">¶</a></h3> <p>The minimum-pixels-per-spot filter (§3.4) trades sensitivity against noise: a small value keeps faint one- or two-pixel spots — real signal on strong data, but detector noise on high-background frames — while a larger value keeps only well-formed spots. The best value is dataset-dependent, so for serial-stills indexing it can be chosen <strong>per image</strong> rather than fixed. The frame is indexed three times, at min-pix 3, 2 and 1, and the setting that maximises</p> <div class="math notranslate nohighlight"> \[ \frac{n_\mathrm{indexed}^2}{n_\mathrm{total}} \quad\text{(indexed-spot count weighted by indexed fraction)} \]</div> <p>is kept; the frame is then integrated once at that min-pix. The fraction factor discounts the extra spots a smaller min-pix admits <em>unless the lattice actually explains them</em>, so strong frames keep their real weak spots (extending resolution) while noise-flooded frames stay strict. Because min-pix filters the connected components <em>after</em> detection, strong-pixel detection AND the connected-component labelling both run <strong>once</strong> per frame, and the three attempts only repeat the spot-level filter; the azimuthal profile is the one that single detection pass computed. The winning attempt’s spot list is kept rather than re-extracted, so the frame that is integrated is exactly the frame that was scored. This is a <strong>stills-only, indexing-path</strong> option — rotation indexing builds one global lattice from all frames and keeps a fixed min-pix. In <code class="docutils literal notranslate"><span class=pre >rugnux</span></code> it is the default; giving an explicit <code class="docutils literal notranslate"><span class=pre >--min-pix-per-spot</span></code> pins a fixed value instead.</p> </section> <section id=predicting-the-resolution-the-merged-data-will-reach > <h3 id=predicting-the-resolution-the-merged-data-will-reach ><a class=toc-backref href="#id19" role=doc-backlink >3.6 Predicting the resolution the merged data will reach</a><a class=headerlink href="#predicting-the-resolution-the-merged-data-will-reach" title="Link to this heading">¶</a></h3> <p>A per-image <strong>resolution estimate</strong> is read off the finished spot list. It predicts how far the <em>merged</em> data will reach, not how far the furthest spot on this image lies. Each non-ice spot is weighted by <span class="math notranslate nohighlight">\(\sqrt{I}\)</span> — the intensity is a summed photon count, so <span class="math notranslate nohighlight">\(\sqrt{I}\)</span> is its Poisson significance — the <span class="math notranslate nohighlight">\(1/d^2\)</span> is found beyond which a fraction <span class="math notranslate nohighlight">\(f=0.30\)</span> of that weight lies, and the estimate is that resolution taken <span class="math notranslate nohighlight">\(2.25\times\)</span> further in <span class="math notranslate nohighlight">\(1/d\)</span>. It is deliberately <strong>not</strong> limited to what the detector records: the quantile sits in the middle of the fall-off, well inside the recorded range, so it goes on measuring the crystal where the detector stops before the diffraction does, and on such a run it reads finer than the detector corner. The dataset value is the median over images.</p> <p>Both constants carry a mechanism. A quantile from the middle of the distribution measures the <em>shape</em> of the fall-off, which is the crystal’s own <span class="math notranslate nohighlight">\(\exp(-B/2d^{2})\)</span>, where the extreme end of it measures where detection stops — a threshold that moves with the exposure and with how many reflections the unit cell puts on a frame. And merging averages many observations of each reflection, so intensities go on being measurable a fixed factor in <span class="math notranslate nohighlight">\(1/d\)</span> past the point at which one image’s spot finder still detects them; that factor is the <span class="math notranslate nohighlight">\(2.25\)</span>. Both are calibrated on rotation data against the resolution at which per-shell CC1/2 falls through 0.30, and the estimate is good to about 0.2 Å there. It is a prediction and not a measurement of what a run achieved: nothing downstream is cut on it, and it is reported alone (Rugnux <code class="docutils literal notranslate"><span class=pre >SPOT_RESOLUTION_ESTIMATE</span></code>, and per image in the stream, the plots and HDF5).</p> </section> </section> </section> </article> </div> </div> </main> </div> <footer class=md-footer > <div class=md-footer-nav > <nav class="md-footer-nav__inner md-grid"> <a href=CPU_DATA_ANALYSIS.html title="CPU-side crystallographic data analysis (Jungfraujoch)" class="md-flex md-footer-nav__link md-footer-nav__link--prev" rel=prev > <div class="md-flex__cell md-flex__cell--shrink"> <i class="md-icon md-icon--arrow-back md-footer-nav__button"></i> </div> <div class="md-flex__cell md-flex__cell--stretch md-footer-nav__title"> <span class=md-flex__ellipsis > <span class=md-footer-nav__direction > "Previous" </span> CPU-side crystallographic data analysis (Jungfraujoch) </span> </div> </a> <a href=CPU_DATA_ANALYSIS_INDEXING.html title="Data analysis: indexing and geometry (§4–§7)" class="md-flex md-footer-nav__link md-footer-nav__link--next" rel=next > <div class="md-flex__cell md-flex__cell--stretch md-footer-nav__title"><span class=md-flex__ellipsis > <span class=md-footer-nav__direction > "Next" </span> Data analysis: indexing and geometry (§4–§7) </span> </div> <div class="md-flex__cell md-flex__cell--shrink"><i class="md-icon md-icon--arrow-forward md-footer-nav__button"></i> </div> </a> </nav> </div> <div class="md-footer-meta md-typeset"> <div class="md-footer-meta__inner md-grid"> <div class=md-footer-copyright > <div class=md-footer-copyright__highlight > © Copyright 2024, Paul Scherrer Institute. </div> Created using <a href="http://www.sphinx-doc.org/">Sphinx</a> 8.1.3. and <a href="https://github.com/bashtage/sphinx-material/">Material for Sphinx</a> </div> </div> </div> </footer> <script src="_static/javascripts/application.js"></script> <script>app.initialize({version: "1.0.4", url: {base: ".."}})</script> |