diff --git a/.gitignore b/.gitignore index c82bdae..e6d8f9a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ venv/ **.so __pycache__/ rayx/_version.py +docs/_build \ No newline at end of file diff --git a/README.md b/README.md index 94aaa7a..26ac579 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ The package can be built in two ways: The 1. option supports build cashing and the build result can be used immediatly without any installation steps. See [example](./examples/metrix.ipynb) The 2. option builds the package as a wheel. The resulting wheel can be installed in your python environment. +### Building the docs + +Use `bash tools/build_docs.sh --html --open` to build and open the documentation. The script creates a dedicated docs virtual environment, installs the docs dependencies, and writes the HTML output to `docs/_build/html/`. + ### Running tests To run the tests you need to build the package with cmake. diff --git a/docs/_static/.gitkeep b/docs/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/api/beamline.md b/docs/api/beamline.md new file mode 100644 index 0000000..a007ca8 --- /dev/null +++ b/docs/api/beamline.md @@ -0,0 +1,203 @@ +# `Beamline` and `Rays` + +This page covers the main tracing workflow: loading a beamline, inspecting it, running a trace, and working with the resulting event data. + +## `rayx.import_beamline(path)` + +Load a beamline definition from an `.rml` file and return a `Beamline` object. + +### Parameters + +- `path`: path to a beamline file in RML format + +### Returns + +- `Beamline` + +### Typical use + +```python +import rayx + +beamline = rayx.import_beamline("path/to/beamline.rml") +``` + +### Common failure modes + +- the file does not exist +- the file is not a valid RML beamline +- referenced resources cannot be resolved + +:::{note} +On the C++ side, `rayx.import_beamline(...)` is exposed from `rayx/main.cpp` and delegates to the RML import/parsing layer in `extern/rayx/Intern/rayx-core/src/Rml/*`. +::: + +`Beamline` is the main object you work with after loading an `.rml` file. It contains the sources and optical elements that define a tracing setup. + +:::{note} +The Python `Beamline` object is backed by the beamline/domain model in `extern/rayx/Intern/rayx-core/src/Beamline/*`. Name lookup and tracing entrypoints are exposed through the binding layer in `rayx/main.cpp`. +::: + +## `beamline.sources` + +Returns the beamline sources as a sequence of source objects. + +Typical use: + +```python +for source in beamline.sources: + print(source.name, source.type, source.energy) +``` + +## `beamline.elements` + +Returns the optical elements as a sequence of element objects. + +Typical use: + +```python +for element in beamline.elements: + print(element.name, element.type) +``` + +## `beamline["name"]` + +Look up a source or element by its `name` property. + +```python +grating = beamline["Spherical Grating"] +print(grating.lineDensity) +``` + +If no source or element with that name exists, the lookup raises a runtime error. + +## `beamline.trace(sequential=False, seed=None, max_events=None, device_index=None, device_type=rayx.DeviceType.All)` + +Run a ray trace and return a `Rays` object. + +### Parameters + +- `sequential`: if `True`, rays interact with elements in beamline order; otherwise tracing is non-sequential +- `seed`: optional integer seed for deterministic traces +- `max_events`: optional cap on the number of events recorded per ray in non-sequential mode +- `device_index`: optional device index from `rayx.list_devices()` +- `device_type`: restrict tracing to a class of device such as CPU or GPU + +### Returns + +- `Rays` + +### Typical use + +```python +rays = beamline.trace() +``` + +```python +rays = beamline.trace( + sequential=False, + seed=1234, + max_events=20, + device_type=rayx.DeviceType.Cpu, +) +``` + +The returned `Rays` object stores event data column-wise and can be inspected directly or converted with `rayx.rays_to_df(...)`. + +:::{note} +`Beamline.trace(...)` is exposed from `rayx/main.cpp`. The tracing pipeline itself lives in `extern/rayx/Intern/rayx-core/src/Tracer/*`, where device selection, ray generation, propagation, and event recording are implemented. +::: + +## `Rays` + +`Beamline.trace(...)` returns a `Rays` object. It stores the recorded event data column-wise, with one NumPy array per attribute. + +:::{note} +`Rays` is backed by the C++ result container in `extern/rayx/Intern/rayx-core/src/Rays.h` and related tracing code. The bindings expose its stored vectors to Python as NumPy array views. +::: + +### What `Rays` contains + +The object exposes one-dimensional arrays such as: + +- `path_id` +- `path_event_id` +- `position_x`, `position_y`, `position_z` +- `direction_x`, `direction_y`, `direction_z` +- `electric_field_x`, `electric_field_y`, `electric_field_z` +- `optical_path_length` +- `energy` +- `order` +- `object_id` +- `source_id` +- `event_type` + +Typical use: + +```python +rays = beamline.trace() + +print(rays.position_x.shape) +print(rays.energy[:10]) +``` + +### When to use direct arrays + +Use direct `Rays` properties when you want: + +- NumPy-native workflows +- low-overhead access to event columns +- custom analysis without creating a DataFrame + +## `rayx.rays_to_df(rays, columns=None)` + +Convert a `Rays` object into a {class}`pandas.DataFrame`. + +This is the easiest way to inspect trace output when you want tabular analysis, filtering, or plotting with the pandas ecosystem. + +### Parameters + +- `rays`: a `Rays` instance returned by `Beamline.trace(...)` +- `columns`: optional list of attribute names to include; if omitted, a default set of event columns is used + +### Default columns + +- `path_id` +- `path_event_id` +- `position_x`, `position_y`, `position_z` +- `direction_x`, `direction_y`, `direction_z` +- `electric_field_x`, `electric_field_y`, `electric_field_z` +- `optical_path_length` +- `energy` +- `order` +- `object_id` +- `source_id` +- `event_type` + +### Returns + +- `pandas.DataFrame` + +### Typical use + +```python +df = rayx.rays_to_df(rays) +absorbed = df[df["event_type"] == rayx.EventType.ABSORBED.value] +``` + +Use `rayx.rays_to_df(...)` when you want: + +- easy filtering by event type or object id +- summary statistics with pandas +- integration with plotting or reporting tools built around DataFrames + +### Notes on event data + +- each row-like event is represented across multiple arrays +- `event_type` is stored numerically, so comparisons are usually done against enum `.value` +- image-plane hits are typically identified by combining `object_id` and `event_type` + +See also: + +- [Utility functions](functions.md) +- [Sources and elements](sources-and-elements.md) diff --git a/docs/api/functions.md b/docs/api/functions.md new file mode 100644 index 0000000..9d3dc19 --- /dev/null +++ b/docs/api/functions.md @@ -0,0 +1,15 @@ +# Utility functions + +These helpers are useful, but they are secondary to the main tracing workflow documented on the [Beamline and Rays](beamline.md) page. + +## Small utility functions + +These helpers exist, but they are secondary to the core tracing workflow: + +- `rayx.get_info()`: return version and installation information +- `rayx.list_devices(...)`: list available tracing devices +- `rayx.fix_seed(...)` / `rayx.random_seed()`: control the global random seed used for tracing + +:::{note} +These utility functions are exposed from the nanobind layer in `rayx/main.cpp`. They are Python-facing helpers rather than direct wrappers around one large C++ subsystem in `rayx-core`. +::: diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..00c4fb8 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,18 @@ +# API reference + +This section documents the main Python API exposed by `rayx-python`. + +The reference is intentionally curated. It focuses on the tracing workflow most users need: + +- loading a beamline from an `.rml` file +- inspecting sources and elements +- running a trace +- working with the resulting event data + +```{toctree} +:maxdepth: 1 + +functions +beamline +sources-and-elements +``` diff --git a/docs/api/sources-and-elements.md b/docs/api/sources-and-elements.md new file mode 100644 index 0000000..eb2c035 --- /dev/null +++ b/docs/api/sources-and-elements.md @@ -0,0 +1,252 @@ +# Sources and elements + +`beamline.sources` and `beamline.elements` expose C++ objects through the Python bindings. These objects have many reflected properties, but most users only need a small subset. + +This page documents the most useful properties for inspection and tracing workflows. It is not an exhaustive list of every reflected field. + +:::{note} +These Python objects come from reflected C++ data models. The binding-side reflection metadata is defined in `rayx/main.cpp`, while the underlying source and element structures live in `extern/rayx/Intern/rayx-core/src/Design/*` and `extern/rayx/Intern/rayx-core/src/Element/*`. +::: + +## Sources + +Source objects describe how rays are emitted into the beamline. + +Commonly used properties: + +- `name`: source name +- `type`: source type +- `energy`: photon energy +- `numberOfRays`: number of rays emitted by the source +- `position`: source position +- `orientation`: source orientation +- `photonFlux`: flux metadata when available + +Representative example: + +```python +for source in beamline.sources: + print( + source.name, + source.type, + source.energy, + source.numberOfRays, + ) +``` + +### Source kinds + +The currently exposed source kinds are: + +- Point source (`POINT_SOURCE`) +- Matrix source (`MATRIX_SOURCE`) +- Dipole source (`DIPOLE_SOURCE`) +- Pixel source (`PIXEL_SOURCE`) +- Circle source (`CIRCLE_SOURCE`) +- Simple undulator source (`SIMPLE_UNDULATOR_SOURCE`) +- Ray list source (`RAY_LIST_SOURCE`) + +Other source properties exist for specific source models, such as divergence, opening angles, energy spread, and undulator parameters. + +## Elements + +Element objects describe the optical system through which rays propagate. + +Commonly used properties: + +- `name`: element name +- `type`: element type +- `position`: element position +- `orientation`: element orientation +- `material`: substrate material +- `grazingIncAngle`: grazing incidence angle for mirror- and grating-like elements +- `lineDensity`: line density for gratings +- `radius`, `longRadius`, `shortRadius`: representative curvature parameters +- `orderOfDiffraction`: diffraction order for grating elements + +Representative example: + +```python +for element in beamline.elements: + print(element.name, element.type) +``` + +The element kind names below come from the exposed `ElementType` enum in the bindings, and `element.type` returns one of those values. + +### Element kinds + +- Cone mirror (`CONE_MIRROR`) +- Crystal (`CRYSTAL`) +- Cylindrical mirror (`CYLINDRICAL_MIRROR`) +- Ellipsoid mirror (`ELLIPSOID_MIRROR`) +- Experts mirror (`EXPERTS_MIRROR`) +- Foil (`FOIL`) +- Image plane (`IMAGE_PLANE`) +- Paraboloid mirror (`PARABOLOID_MIRROR`) +- Plane grating (`PLANE_GRATING`) +- Plane mirror (`PLANE_MIRROR`) +- Reflection zoneplate (`REFLECTION_ZONEPLATE`) +- Slit (`SLIT`) +- Sphere (`SPHERE`) +- Sphere grating (`SPHERE_GRATING`) +- Sphere mirror (`SPHERE_MIRROR`) +- Toroid grating (`TOROID_GRATING`) +- Toroid mirror (`TOROID_MIRROR`) + +#### Mirrors + +Mirror elements cover the reflective optics in the beamline, including plane, spherical, cylindrical, ellipsoidal, paraboloidal, toroidal, cone, and expert-defined variants. + +Commonly used properties: + +- `name`, `type`, `position`, `orientation`: identity and placement +- `material`: substrate material +- `surfaceCoatingType`, `materialCoating`, `thicknessCoating`, `roughnessCoating`: coating configuration when present +- `grazingIncAngle`: grazing incidence angle for mirror geometries that use it +- `radius`, `shortRadius`, `longRadius`: curvature parameters for spherical, cylindrical, and toroidal shapes +- `entranceArmLength`, `exitArmLength`: arm lengths used by several curved mirror models +- `longHalfAxisA`, `shortHalfAxisB`: ellipsoid parameters +- `armLength`, `parameterP`, `parameterPType`: paraboloid parameters +- `expertsOptics`, `expertsCubic`: reflected expert-mode geometry definitions + +Representative kinds: + +- `PLANE_MIRROR` +- `SPHERE_MIRROR` +- `CYLINDRICAL_MIRROR` +- `ELLIPSOID_MIRROR` +- `PARABOLOID_MIRROR` +- `TOROID_MIRROR` +- `CONE_MIRROR` +- `EXPERTS_MIRROR` +- `SPHERE` + +:::{note} +Mirror parameters are imported through functions such as `getPlaneMirror`, `getSphereMirror`, `getCylinder`, `getEllipsoid`, `getParaboloid`, `getToroidMirror`, `getCone`, `getExpertsOptics`, and `getExpertsCubic` in `extern/rayx/Intern/rayx-core/src/Rml/DesignElementWriter.h`. +::: + +#### Gratings + +Grating elements add diffraction-specific parameters on top of their geometry. + +Commonly used properties: + +- `name`, `type`, `position`, `orientation`: identity and placement +- `lineDensity`: groove density +- `orderOfDiffraction`: active diffraction order +- `radius`, `shortRadius`, `longRadius`: curvature parameters for spherical and toroidal gratings +- `deviationAngle`, `entranceArmLength`, `exitArmLength`: geometry parameters used by spherical gratings +- `surfaceCoatingType`, `materialCoating`, `thicknessCoating`, `roughnessCoating`: coating metadata when present + +Representative kinds: + +- `PLANE_GRATING` +- `SPHERE_GRATING` +- `TOROID_GRATING` + +:::{note} +The common grating setup is assembled in `getGrating`, with shape-specific import in `getPlaneGrating`, `getSphereGrating`, and `getToroidalGrating` inside `extern/rayx/Intern/rayx-core/src/Rml/DesignElementWriter.h`. Runtime grating behavior is created by `makeGrating` in `extern/rayx/Intern/rayx-core/src/Element/Behaviour.cpp`. +::: + +#### Slit + +`Slit` elements model an opening, optionally with a central beamstop, in the `XY` design plane. + +Commonly used properties: + +- `name`, `type`, `position`, `orientation`: identity and placement +- `cutout`: overall outer shape imported from the beamline description +- `openingShape`: opening geometry, such as rectangular or elliptical +- `openingWidth`, `openingHeight`: size of the transmitting opening +- `centralBeamstop`: whether a central stop is present +- `stopWidth`, `stopHeight`: size of the beamstop when used +- `totalWidth`, `totalHeight`: full slit body dimensions +- `distancePreceding`: spacing metadata used in imported beamlines + +:::{note} +In the C++ core, slit import and parameter loading live in `extern/rayx/Intern/rayx-core/src/Rml/DesignElementWriter.h` (`getSlit`), and the tracing behavior is built in `extern/rayx/Intern/rayx-core/src/Element/Behaviour.cpp` (`makeSlit`). +::: + +#### Image plane + +`Image plane` elements are detector-like planes used to collect traced rays. + +Commonly used properties: + +- `name`, `type`, `position`, `orientation`: identity and placement +- `cutout`: active area geometry +- `material`: substrate or material metadata when present + +Unlike reflective or diffractive optics, image planes do not add many element-specific reflected parameters in the current Python surface. + +:::{note} +The C++ import path for image planes is `getImageplane` in `extern/rayx/Intern/rayx-core/src/Rml/DesignElementWriter.h`. Their tracing behavior maps to the `ImagePlane` branch in `extern/rayx/Intern/rayx-core/src/Element/Behaviour.h`. +::: + +#### Foil + +`Foil` elements represent thin transmissive interaction elements. + +Commonly used properties: + +- `name`, `type`, `position`, `orientation`: identity and placement +- `cutout`: active area geometry +- `material`: substrate material metadata +- `thicknessSubstrate`: foil or substrate thickness +- `roughnessSubstrate`: substrate roughness + +:::{note} +Foil import is handled by `getFoil` in `extern/rayx/Intern/rayx-core/src/Rml/DesignElementWriter.h`, and the traced foil behavior is assembled by `makeFoil` in `extern/rayx/Intern/rayx-core/src/Element/Behaviour.cpp`. +::: + +#### Crystal + +`Crystal` elements expose the parameters needed for crystal diffraction behavior. + +Commonly used properties: + +- `name`, `type`, `position`, `orientation`: identity and placement +- `cutout`: active area geometry +- `material`: general material metadata +- `crystalType`: crystal model or configuration kind +- `offsetAngle`: crystal offset angle +- `structureFactorReF0`, `structureFactorImF0`: forward structure-factor components +- `structureFactorReFH`, `structureFactorImFH`: diffracted structure-factor components +- `structureFactorReFHC`, `structureFactorImFHC`: conjugate structure-factor components +- `unitCellVolume`: crystal unit cell volume +- `dSpacing2`: lattice-spacing parameter used by the crystal model + +:::{note} +The import-side crystal setup is in `getCrystal` inside `extern/rayx/Intern/rayx-core/src/Rml/DesignElementWriter.h`, and the runtime crystal behavior is built by `makeCrystal` in `extern/rayx/Intern/rayx-core/src/Element/Behaviour.cpp`. +::: + +#### Reflection zoneplate + +`Reflection zoneplate` elements expose design parameters for reflection zoneplate tracing. + +Commonly used properties: + +- `name`, `type`, `position`, `orientation`: identity and placement +- `cutout`: active area geometry +- `fresnelZOffset`: Fresnel zone offset +- `designSagittalEntranceArmLength`, `designSagittalExitArmLength`: sagittal design arm lengths +- `designMeridionalEntranceArmLength`, `designMeridionalExitArmLength`: meridional design arm lengths +- `designEnergy`: design energy used to derive the tracing wavelength +- `designOrderOfDiffraction`, `orderOfDiffraction`: design and active diffraction orders +- `designAlphaAngle`, `designBetaAngle`: design incidence and exit angles +- `imageType`: zoneplate imaging mode flag +- `additionalOrder`: additional diffraction-order handling +- `shortRadius`, `longRadius`: curvature parameters for non-planar zoneplate variants + +:::{note} +Reflection zoneplate import lives in `getRZP` in `extern/rayx/Intern/rayx-core/src/Rml/DesignElementWriter.h`, and the corresponding behavior object is created by `makeRZPBehaviour` in `extern/rayx/Intern/rayx-core/src/Element/Behaviour.cpp`. +::: + +```python +grating = beamline["Spherical Grating"] +print(grating.lineDensity) +print(grating.orderOfDiffraction) +print(grating.radius) +``` + +Many additional reflected properties are available for specialized optics, coatings, cutouts, crystals, and expert-mode configurations. Advanced users should consult the binding definitions in `rayx/main.cpp` and the underlying C++ model if they need the full surface. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..84f54de --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,41 @@ +import importlib.metadata + +project = "rayx-python" +author = "RAYX team" +try: + release = importlib.metadata.version("rayx") +except importlib.metadata.PackageNotFoundError: + release = "unknown" +version = release + +extensions = [ + "myst_parser", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", +] + +source_suffix = {".rst": "restructuredtext", ".md": "markdown"} +master_doc = "index" + +autodoc_mock_imports = ["rayx.core"] +autodoc_typehints = "signature" +autodoc_member_order = "bysource" +autosummary_generate = True + +napoleon_google_docstring = True +napoleon_numpy_docstring = False + +myst_enable_extensions = ["colon_fence", "deflist"] +myst_substitutions = {"release": release} + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "numpy": ("https://numpy.org/doc/stable", None), + "pandas": ("https://pandas.pydata.org/docs", None), +} + +html_theme = "furo" +html_static_path = ["_static"] diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..ea0ae1d --- /dev/null +++ b/docs/index.md @@ -0,0 +1,16 @@ +# rayx-python + +**rayx-python** is a Python API for [RAYX](https://pypi.org/project/rayx/), the ray-tracing engine for synchrotron optics developed at Helmholtz-Zentrum Berlin. It lets you load beamline definitions from `.rml` files, manipulate optical elements and sources, run ray traces, and work with the resulting ray data as numpy arrays or pandas DataFrames. + +:::{note} +For a higher-level interface that integrates rayx-python into a full simulation and optimization workflow, see [raypyng](https://raypyng.readthedocs.io/en/latest/). +::: + +```{toctree} +:maxdepth: 2 +:caption: User guide + +installation/index +tutorials/index +api/index +``` diff --git a/docs/installation/index.md b/docs/installation/index.md new file mode 100644 index 0000000..512c63d --- /dev/null +++ b/docs/installation/index.md @@ -0,0 +1,13 @@ +# Installation + +## Requirements + +- **Platform**: Linux only. Pre-built wheels are provided for `manylinux x86_64`. macOS and Windows are not currently supported. +- **Python**: 3.9 or later +- **Compute**: CPU only. Unlike the original [RAYX](https://pypi.org/project/rayx/) C++ program, this Python package does not expose GPU acceleration at this time. + +## Install + +```bash +pip install rayx +``` diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md new file mode 100644 index 0000000..ca6b3fe --- /dev/null +++ b/docs/tutorials/index.md @@ -0,0 +1,7 @@ +# Tutorials + +```{toctree} +:maxdepth: 1 + +tracing-a-beamline +``` diff --git a/docs/tutorials/tracing-a-beamline.md b/docs/tutorials/tracing-a-beamline.md new file mode 100644 index 0000000..f878c43 --- /dev/null +++ b/docs/tutorials/tracing-a-beamline.md @@ -0,0 +1,137 @@ +# Tracing a beamline from an RML file + +This tutorial walks through the core workflow: loading a beamline, running a ray trace, and analysing the results. + +## 1. Import and verify + +```python +import rayx + +info = rayx.get_info() +print(info) +# {'version': '...', 'python_wrapper': '...', 'cpp_module': '...', 'module_path': '...'} +``` + +## 2. Load a beamline + +Beamlines are defined in `.rml` files (RAY-X Markup Language). Pass the file path to {func}`rayx.import_beamline`: + +```python +beamline = rayx.import_beamline("path/to/your/beamline.rml") +``` + +## 3. Inspect sources and elements + +```python +# List all sources +for source in beamline.sources: + print(source.name, source.type) + +# List all optical elements +for element in beamline.elements: + print(element.name, element.type) + +# Look up by name +mirror = beamline["M1"] +print(mirror.grazingIncAngle) +``` + +## 4. Run a trace + +Call {meth}`Beamline.trace` to perform the ray trace. It returns a {class}`rayx.Rays` object. + +```python +rays = beamline.trace() +``` + +Key keyword arguments: + +| Argument | Default | Description | +| --- | --- | --- | +| `sequential` | `False` | If `True`, rays interact with elements in the order they appear in the file | +| `max_events` | `None` | Maximum number of scattering events per ray | + +## 5. Convert rays to a DataFrame + +{func}`rayx.rays_to_df` converts the result to a {class}`pandas.DataFrame` with one row per ray event: + +```python +df = rayx.rays_to_df(rays) +print(df.columns.tolist()) +# ['path_id', 'path_event_id', 'position_x', 'position_y', 'position_z', +# 'direction_x', 'direction_y', 'direction_z', 'electric_field_x', ... +# 'energy', 'order', 'object_id', 'source_id', 'event_type'] +print(df.shape) +``` + +## 6. Filter and analyse + +The `event_type` column is stored as an unsigned integer — compare against the enum's `.value` attribute: + +```python +from rayx import EventType + +# Rays blocked by apertures/slits along the beamline +absorbed = df[df["event_type"] == EventType.ABSORBED.value] +print(f"{len(absorbed)} rays blocked by apertures") +``` + +Rays that reached the image plane (the last element in the list) appear with `event_type == EventType.HIT_ELEMENT`: + +```python +image_plane_id = len(beamline.elements) - 1 +on_image = df[ + (df["object_id"] == image_plane_id) + & (df["event_type"] == EventType.HIT_ELEMENT.value) +] +print(f"{len(on_image)} rays on the image plane") +print(on_image[["position_x", "position_y", "position_z"]].describe()) +``` + +## Complete example + +```python +import rayx +from rayx import EventType + +# Load the beamline +beamline = rayx.import_beamline("path/to/your/beamline.rml") + +# Inspect what's in it +for source in beamline.sources: + print(source.name, source.type, source.energy, "eV") +for i, element in enumerate(beamline.elements): + print(i, element.name, element.type) + +# Run the trace +rays = beamline.trace() + +# Convert to a DataFrame +df = rayx.rays_to_df(rays) +print(df.shape) + +# Event type breakdown (compare using .value — event_type column is uint32) +for et in EventType: + count = (df["event_type"] == et.value).sum() + if count > 0: + print(f"{et.name}: {count}") + +# Rays blocked by apertures/slits +absorbed = df[df["event_type"] == EventType.ABSORBED.value] +print(f"{len(absorbed)} rays blocked by apertures") + +# Rays that reached the image plane (last element) +image_plane_id = len(beamline.elements) - 1 +on_image = df[ + (df["object_id"] == image_plane_id) + & (df["event_type"] == EventType.HIT_ELEMENT.value) +] +print(f"{len(on_image)} rays on the image plane") +print(on_image[["position_x", "position_y", "position_z"]].describe()) +``` + +--- + +:::{seealso} +For a higher-level interface that integrates rayx-python into a full simulation and optimisation workflow see [raypyng](https://raypyng.readthedocs.io/en/latest/). +::: diff --git a/examples/tutorial_tracing.py b/examples/tutorial_tracing.py new file mode 100644 index 0000000..be5b3f1 --- /dev/null +++ b/examples/tutorial_tracing.py @@ -0,0 +1,66 @@ +""" +Example accompanying the "Tracing a beamline from an RML file" tutorial. +Uses the bundled test beamline (tests/res/test.rml). +""" + +from pathlib import Path + +import rayx +from rayx import EventType + +RML_FILE = "METRIX_U41_G1_H1_318eV_PS_MLearn_v114.rml" + +# --- 1. Import and verify --- +info = rayx.get_info() +print("rayx version:", info["version"]) + +# --- 2. Load the beamline --- +beamline = rayx.import_beamline(str(RML_FILE)) + +# --- 3. Inspect sources and elements --- +print("\nSources:") +for source in beamline.sources: + print(f" {source.name!r} type={source.type} energy={source.energy} eV rays={source.numberOfRays}") + +print("\nElements:") +for i, element in enumerate(beamline.elements): + print(f" [{i}] {element.name!r} type={element.type}") + +# Look up a specific element by name +grating = beamline["Spherical Grating"] +print(f"\nSpherical Grating:") +print(f" line density : {grating.lineDensity:.3f} lines/mm") +print(f" order : {grating.orderOfDiffraction}") +print(f" radius : {grating.radius:.1f} mm") + +# --- 4. Run a trace --- +rays = beamline.trace() +print(f"\nTrace complete.") + +# --- 5. Convert to DataFrame --- +df = rayx.rays_to_df(rays) +print(f"DataFrame shape: {df.shape}") + +# --- 6. Filter and analyse --- +# event_type is stored as uint32 — compare against the enum's integer value +print("\nEvent type breakdown:") +for et in EventType: + count = (df["event_type"] == et.value).sum() + if count > 0: + print(f" {et.name}: {count}") + +# Rays absorbed by apertures/slits along the beamline +absorbed = df[df["event_type"] == EventType.ABSORBED.value] +print(f"\nRays blocked by apertures: {len(absorbed)}") + +# Rays that reached the image plane (last element) +image_plane_id = len(beamline.elements) - 1 +on_image = df[ + (df["object_id"] == image_plane_id) + & (df["event_type"] == EventType.HIT_ELEMENT.value) +] +print(f"Rays on ImagePlane (object_id={image_plane_id}): {len(on_image)}") + +if len(on_image) > 0: + print("\nImagePlane spot statistics (mm):") + print(on_image[["position_x", "position_y", "position_z"]].describe().to_string()) diff --git a/pyproject.toml b/pyproject.toml index 454184c..df67032 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,11 @@ maintainers = [ [project.optional-dependencies] dev = ["pytest>=7.0", "matplotlib>=3.5", "ipython"] test = ["pytest>=7.0", "pytest-cov"] +docs = [ + "sphinx>=7.0", + "myst-parser>=2.0", + "furo>=2024.8.6", +] [build-system] requires = ["scikit-build-core", "nanobind"] diff --git a/tools/build_docs.sh b/tools/build_docs.sh new file mode 100755 index 0000000..52c9b1b --- /dev/null +++ b/tools/build_docs.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +set -euo pipefail + +LAUNCH_HTML=false +BUILD_HTML=false +EXPLICIT_BUILD_SELECTION=false + +for arg in "$@"; do + case "$arg" in + --html) + BUILD_HTML=true + EXPLICIT_BUILD_SELECTION=true + ;; + --open) + LAUNCH_HTML=true + ;; + *) + echo "Unknown option: $arg" + echo "Usage: $0 [--html] [--open]" + exit 1 + ;; + esac +done + +if [[ "${EXPLICIT_BUILD_SELECTION}" == false ]]; then + BUILD_HTML=true +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +DOCS_DIR="${PROJECT_ROOT}/docs" +DOCS_VENV_DIR="${PROJECT_ROOT}/venv_docs" +UV_BIN="${UV_BIN:-uv}" + +if ! command -v "${UV_BIN}" >/dev/null 2>&1; then + echo "Error: 'uv' is required to bootstrap docs environment." + echo "Install uv first: https://docs.astral.sh/uv/getting-started/installation/" + exit 1 +fi + +if [[ ! -x "${DOCS_VENV_DIR}/bin/python" ]]; then + echo "============================================================" + echo "Creating docs virtual environment: ${DOCS_VENV_DIR}" + echo "============================================================" + "${UV_BIN}" venv --python 3.12 "${DOCS_VENV_DIR}" +fi + +echo "============================================================" +echo "Installing docs dependencies into ${DOCS_VENV_DIR}" +echo "============================================================" +DOCS_DEPS="$("${DOCS_VENV_DIR}/bin/python" -c "import tomllib; d=tomllib.load(open('${PROJECT_ROOT}/pyproject.toml','rb')); print(' '.join(d['project']['optional-dependencies']['docs']))")" +"${UV_BIN}" pip install --python "${DOCS_VENV_DIR}/bin/python" ${DOCS_DEPS} + +PYTHON_BIN="${DOCS_VENV_DIR}/bin/python" +HTML_BUILD_DIR="${DOCS_DIR}/_build/html" + +rm -rf "${DOCS_DIR}/_build" + +if [[ "${BUILD_HTML}" == true ]]; then + echo "============================================================" + echo "Building HTML documentation" + echo "============================================================" + "${PYTHON_BIN}" -m sphinx -b html "${DOCS_DIR}" "${HTML_BUILD_DIR}" +fi + +echo +echo "============================================================" +echo "Build completed" +echo "============================================================" +if [[ "${BUILD_HTML}" == true ]]; then + echo "HTML:" + echo " ${HTML_BUILD_DIR}/index.html" +fi + +if [[ "${LAUNCH_HTML}" == true ]]; then + echo + echo "Opening HTML documentation..." + HTML_INDEX="${HTML_BUILD_DIR}/index.html" + DOCS_BROWSER="${DOCS_BROWSER:-google-chrome}" + if command -v "${DOCS_BROWSER}" >/dev/null 2>&1; then + if "${DOCS_BROWSER}" "file://${HTML_INDEX}" >/dev/null 2>&1; then + : + elif "${DOCS_BROWSER}" "${HTML_INDEX}" >/dev/null 2>&1; then + : + elif command -v xdg-open >/dev/null 2>&1 && xdg-open "${HTML_INDEX}" >/dev/null 2>&1; then + : + elif command -v gio >/dev/null 2>&1 && gio open "${HTML_INDEX}" >/dev/null 2>&1; then + : + elif command -v python3 >/dev/null 2>&1 && python3 -m webbrowser "file://${HTML_INDEX}" >/dev/null 2>&1; then + : + else + echo "Could not auto-open browser." + echo "Open manually: ${HTML_INDEX}" + fi + elif command -v xdg-open >/dev/null 2>&1; then + if xdg-open "${HTML_INDEX}" >/dev/null 2>&1; then + : + elif command -v gio >/dev/null 2>&1 && gio open "${HTML_INDEX}" >/dev/null 2>&1; then + : + elif command -v python3 >/dev/null 2>&1 && python3 -m webbrowser "file://${HTML_INDEX}" >/dev/null 2>&1; then + : + else + echo "Could not auto-open browser." + echo "Open manually: ${HTML_INDEX}" + fi + elif command -v gio >/dev/null 2>&1; then + if gio open "${HTML_INDEX}" >/dev/null 2>&1; then + : + elif command -v python3 >/dev/null 2>&1 && python3 -m webbrowser "file://${HTML_INDEX}" >/dev/null 2>&1; then + : + else + echo "Could not auto-open browser." + echo "Open manually: ${HTML_INDEX}" + fi + elif command -v python3 >/dev/null 2>&1; then + if ! python3 -m webbrowser "file://${HTML_INDEX}" >/dev/null 2>&1; then + echo "Could not auto-open browser." + echo "Open manually: ${HTML_INDEX}" + fi + else + echo "Could not auto-open browser." + echo "Open manually: ${HTML_INDEX}" + fi +fi