Skip to content

Writing your first application definition

This tutorial will guide you through writing your first valid NeXus application definition.

What should you should know before this tutorial?

Note

For this tutorial, we will use the nyaml tool (developed by FAIRmat) that lets you write the NXDL definition in YAML and convert it to NXDL XML. You can learn more in the nyaml documentation.

What you will know at the end of this tutorial?

You will know

  • how to write a basic application definition
  • how to validate the result with pynxtools
  • how to add your new definition to pynxtools

You will understand

  • the structure of NXDL files
  • the role of base classes and application definitions
  • NeXus naming rules
  • optionality and dimensions in NXDL

Goals

We want to build an application definition NXdouble_slit from scratch — a minimal but complete NeXus application definition for a classic optics experiment.


The experiment

In a double-slit experiment a coherent light source illuminates a barrier with two narrow slits. The diffracted waves interfere and produce a characteristic fringe pattern on a detector screen. Standard analysis extracts fringe spacing (→ wavelength) and envelope width (→ coherence length).

The data we need to record:

Quantity Why
Source wavelength Determines fringe spacing
Slit width, slit separation Determines diffraction envelope
Detector distance and pixel layout Maps pixels to angles
2D intensity array The measurement itself

Steps

1. Start with the skeleton

Every application definition is an XML file following the NXDL schema. Create NXdouble_slit.nxdl.xml with the following minimal content:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="nxdlformat.xsl"?>
<definition xmlns="http://definition.nexusformat.org/nxdl/3.1"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            category="application"
            type="group"
            name="NXdouble_slit"
            extends="NXobject"
            xsi:schemaLocation="http://definition.nexusformat.org/nxdl/3.1 ../nxdl.xsd">

    <doc>Application definition for a double-slit interference experiment.</doc>

    <group type="NXentry">

        <field name="definition">
            <enumeration><item value="NXdouble_slit"/></enumeration>
        </field>
        <field name="title"/>
        <field name="start_time" type="NX_DATE_TIME"/>

    </group>

</definition>

If you have pynxtools installed (see Installation Guide), add the NXDL XML file under src/pynxtools/definitions/contributed_definitions and validate that pynxtools can read it:

dataconverter generate-template --nxdl NXdouble_slit

You should see a JSON template listing paths like /ENTRY[entry]/title and /ENTRY[entry]/start_time.

Concept paths vs. instance paths

The template uses concept names (ENTRY, INSTRUMENT) and instance names in brackets ([entry]). The bracketed name is the literal group name written to the HDF5 file; the upper-case name is the name of the NeXus concept. Read more in Learn > ... > Rules for storing data in NeXus.

The definition field with an <enumeration> is a convention — it locks the field to the name of the application definition so that readers and validators can identify the file format unambiguously.

Working with nyaml

From now on, you will exclusively develop this application definition in YAML. As outlined above, there exists a tool nyaml (developed by FAIRmat) that lets you write the NXDL definition in YAML and convert it to NXDL XML.

Install the nyaml Python package:

uv pip install nyaml

Note that you will need to install the Python version manually beforehand.

pip install --upgrade pip
pip install nyaml

This installs a CLI command called nyaml2nxdl (or short n2n). Use this to convert your NXDL XML file to YAML:

nyaml2nxdl NXdouble_slit.nxdl.xml --output-file NXdouble_slit.yaml   # → 

Your basic application definition should look like this is the YAML format:

category: application
doc: |
  Application definition for a double-slit interference experiment.
type: group
NXdouble_slit(NXobject):
  (NXentry):
    definition:
      enumeration: [NXdouble_slit]
    title:
    start_time(NX_DATE_TIME):

Now we can start adding components to the experiment model. Conceptually, the experiment will be structured roughly as follows:

NXentry
 ├─ NXinstrument
 │   ├─ NXsource
 │   ├─ NXslit
 │   └─ NXdetector
 │      └─ NXdata   (measurement data)
 └─ NXdata          (default plot)

NXentry represents a single measurement, NXinstrument describes the experimental setup, and NXdata defines the default plottable dataset.


2. Add the instrument

Nest the physical components inside NXinstrument. Start with the light source:

    (NXinstrument):
      source(NXsource):
        wavelength(NX_FLOAT):
          unit: NX_WAVELENGTH
          doc: |
            Central wavelength of the light source.

Unit categories

unit: "NX_WAVELENGTH" is a unit category, not a unit. It declares that the field stores a wavelength-equivalent quantity (nm, Å, µm, …). The actual unit chosen by the writer is stored as a sibling HDF5 attribute wavelength/@units. You should always try to use one of the existing unit categories (see NeXus manual > Unit Categories). If none of these apply, you may use a raw string like "eV/mm" as an example. In this example, the instance units must match the dimension of energy over length.


3. Choose the right base class for the slit

Browse the base class catalogue in NeXus manual > ... > Base classes. You will find:

  • NXaperture — generic aperture
  • NXslit — specific to slit-type apertures, with x_gap and y_gap already defined

Always prefer the most specific class. Add it with an instance name that describes the physical object:

      double_slit(NXslit):
        x_gap(NX_FLOAT):
          unit: NX_LENGTH
          doc: |
            Width of each individual slit.
        slit_separation(NX_FLOAT):
          unit: NX_LENGTH
          doc: |
            Center-to-center distance between the two slits.
        height(NX_FLOAT):
          unit: NX_LENGTH
          exists: optional
        material(NX_CHAR):
          exists: optional

Instance name vs. concept name

double_slit is the concept name — the name of the concept as defined in the NeXus definitions. NXslit is the associated base class. By default (nameType="specified") this exact literal string is required in every conforming HDF5 file. You can relax this with nameType:"any" to accept any valid name. See Learn > ... > Rules for storing data in NeXus > Name resolution.


4. Add the detector with dimensions

The detector produces a 2D array. Use <symbols> to name the array dimensions and reference them in <dimensions>.

Add this block at the top, outside of the class NXslit:

symbols:
  doc: |
    Dimension symbols used in this definition.
  n_x: |
    Number of detector pixels along x.
  n_y: |
    Number of detector pixels along y.

Using symbolic names (n_x, n_y) instead of hardcoded integers makes the definition self-documenting and allows validation tools to verify dimensional consistency across fields.

Then add the detector group inside NXinstrument:

      detector(NXdetector):
        distance(NX_FLOAT):
          unit: NX_LENGTH
          doc: |
            Distance from the slit plane to the detector surface.
        data(NXdata):
          doc: |
            Raw 2D pixel data collected by the detector with no calibration
            applied. This group stores data at the lowest level of processing
            possible, indexed by integer pixel coordinates.
          \@signal:
            enumeration: [data]
          \@axes:
            enumeration: [['x', 'y']]
          data(NX_NUMBER):
            exists: recommended
            unit: NX_ANY
            doc: |
              Raw 2D intensity array indexed by pixel position.
            dimensions:
              rank: 2
              dim: (n_x, n_y)
          x(NX_INT):
            doc: |
              Pixel indices along the horizontal detector axis (0-based).
            dimensions:
              rank: 1
              dim: (n_x,)
          y(NX_INT):
            doc: |
              Pixel indices along the vertical detector axis (0-based).
            dimensions:
              rank: 1
              dim: (n_y,)

Raw data first, processed data separately

Always store the rawest data you have. Here the detector axes are integer pixel indices (x, y), i.e., exactly what the hardware records. The calibrated view with physical spatial offsets (mm, µm) belongs in a separate NXdata group at the NXentry level, wired as the default plot (step 6 below). An optional NXprocess group can document the conversion between the two (step 5 below).


5. Add an optional processing group

Three levels express how important a concept is for conformance:

Level NXDL nyaml What validators do
Required required="true" required: true Fail/warn if absent
Recommended recommended="true" recommended: true Warn if absent or accept
Optional optional="true" optional: true Silently accept if absent

Note that by default, every concept in a base class is optional (even if none of these keys is written), whereas in application definition, every concept is required, unless it is marked recommended or optional.

The pixel offsets above are recommended — essential for calibrated analysis but not always stored. Use optional for supplementary metadata like material.

Tip

When in doubt, lean towards recommended over required. A definition that is too strict discourages adoption; a definition that is too loose loses its interoperability value.

Flexible naming with nameType

Names in both NXDL files and the files that instantiate the schema follow a particular naming logic. There's the possibility to define concept names that are different to the names of the actual data instances. You can learn more about the naming rules in Learn > ... > Rules for storing data in NeXus. In summary, NeXus names are defined by a combination of the nameType attribute and whether (parts of) the names are lower- or uppercase.

By default every named group, field, or attribute in NXDL requires an exact name in the HDF5 file (nameType="specified"). NeXus provides two relaxed alternatives:

nameType nyaml example What it means
specified (default) double_slit(NXslit): Exactly the literal name double_slit
any (NXinstrument): Any valid HDF5 group name is accepted
partial processID(NXprocess): The ID suffix is a placeholder; any string can replace it

The (NXinstrument): syntax (parentheses only, no name) that you already wrote is the nameType="any" shorthand — any instrument group name is valid.

nameType="partial" is useful when you expect multiple concepts of the same type but want to give each a meaningful name. The uppercase sequence ID marks the variable part; everything before it is the fixed prefix. For example, processID would match processing_pixel_cal, processing01, etc.

Add an optional processing group at the (NXentry) level (sibling of (NXinstrument)) to document how raw pixel data were converted to calibrated spatial offsets:

    processID(NXprocess):
      nameType: partial
      exists: optional
      doc: |
        Describes one step in the processing chain that converts raw detector pixel data to the calibrated interference pattern stored in ``interference_pattern``. The 'ID' suffix in the group name is replaced by a short identifier chosen by the writer, e.g. 'pixel_calibration' or 'background_correction'. Multiple NXprocess groups may be present; their order is given by sequence_index.
      sequence_index(NX_INT):
        doc: |
          Sequence index of processing, for determining the order of multiple
          NXprocess steps. Starts with 1.
      description(NX_CHAR):
        doc: Free-text description of what this step does.
      program(NX_CHAR):
        exists: optional
        doc: Version string of the software.
      version(NX_CHAR):
        exists: optional
      date(NX_DATE_TIME):
        exists: optional

6. Wire up NXdata

NXdata marks the default plot. Tools like NOMAD use the @signal and @axes attributes to render data without requiring user configuration. Add it as a sibling of NXinstrument (and processID):

    interference_pattern(NXdata):
      doc: |
        Default plot: the calibrated 2D interference pattern with spatial axes. The signal and axes may be linked to the raw detector arrays or derived from it via one or more NXprocess steps.
      \@signal:
        enumeration: [data]
      \@axes:
        enumeration: [['x_offset', 'y_offset']]
      data(NX_NUMBER):
        unit: NX_ANY
        doc: |
          2D interference intensity after any processing steps.
        dimensions:
          rank: 2
          dim: (n_x, n_y)
      x_offset(NX_FLOAT):
        unit: NX_LENGTH
        doc: |
          Horizontal spatial offset from the detector centre, derived from
          pixel index and pixel pitch.
        dimensions:
          rank: 1
          dim: (n_x,)
      y_offset(NX_FLOAT):
        unit: NX_LENGTH
        doc: |
          Vertical spatial offset from the detector centre, derived from
          pixel index and pixel pitch.
        dimensions:
          rank: 1
          dim: (n_y,)

The axes here are physical lengths (NX_LENGTH), not pixel indices — they represent where each pixel falls in real space after calibration.

Note

In a real HDF5 file, data and the axis fields in NXdata could be HDF5 links pointing to the detector group — not duplicated data. The NXDL defines what must be accessible at that path; the writer decides whether to copy or link.


7. Validate

Now that we are done, we can convert back to NXDL XML:

nyaml2nxdl NXdouble_slit.yaml --output-file NXdouble_slit.nxdl.xml

Run generate-template one final time and check that all required paths are listed:

dataconverter generate-template --nxdl NXdouble_slit

Write a minimal HDF5 test file filling all required fields, then validate:

validate_nexus my_test_file.nxs

See How-tos > pynxtools > Validate NeXus files for details on interpreting the output.


The complete definition

You can find both NXdouble_slit.yaml and NXdouble_slit.nxdl.xml in the pynxtools examples:

NXdouble_slit.yaml (full)
category: application
doc: |
  Application definition for a double-slit interference experiment.
  Records the light source, aperture geometry, detector layout, and the
  measured 2D interference pattern needed to determine fringe spacing and
  source coherence length.

  See https://en.wikipedia.org/wiki/Double-slit_experiment.
symbols:
  doc: |
    Dimension symbols used in this definition.
  n_x: |
    Number of detector pixels along x.
  n_y: |
    Number of detector pixels along y.
type: group
NXdouble_slit(NXobject):
  (NXentry):
    definition:
      enumeration: [NXdouble_slit]
    title:
    start_time(NX_DATE_TIME):
      doc: |
        ISO 8601 datetime of the measurement start.
    end_time(NX_DATE_TIME):
      exists: recommended
    (NXinstrument):
      source(NXsource):
        wavelength(NX_FLOAT):
          unit: NX_WAVELENGTH
          doc: |
            Central wavelength of the light source.
        coherence_length(NX_FLOAT):
          unit: NX_LENGTH
          exists: recommended
          doc: |
            Temporal coherence length of the source.
        type(NX_CHAR):
          exists: optional
          enumeration: [Laser, Filtered lamp, LED]
      double_slit(NXslit):
        x_gap(NX_FLOAT):
          unit: NX_LENGTH
          doc: |
            Width of each individual slit.
        slit_separation(NX_FLOAT):
          unit: NX_LENGTH
          doc: |
            Center-to-center distance between the two slits.
        height(NX_FLOAT):
          unit: NX_LENGTH
          exists: optional
        material(NX_CHAR):
          exists: optional
      detector(NXdetector):
        distance(NX_FLOAT):
          unit: NX_LENGTH
          doc: |
            Distance from the slit plane to the detector surface.
        data(NXdata):
          doc: |
            Raw 2D pixel data collected by the detector with no calibration
            applied. This group stores data at the lowest level of processing
            possible, indexed by integer pixel coordinates.
          \@signal:
            enumeration: [data]
          \@axes:
            enumeration: [['x', 'y']]
          data(NX_NUMBER):
            exists: recommended
            unit: NX_ANY
            doc: |
              Raw 2D intensity array indexed by pixel position.
            dimensions:
              rank: 2
              dim: (n_x, n_y)
          x(NX_INT):
            doc: |
              Pixel indices along the horizontal detector axis (0-based).
            dimensions:
              rank: 1
              dim: (n_x,)
          y(NX_INT):
            doc: |
              Pixel indices along the vertical detector axis (0-based).
            dimensions:
              rank: 1
              dim: (n_y,)
    processID(NXprocess):
      nameType: partial
      exists: optional
      doc: |
        Describes one step in the processing chain that converts raw detector
        pixel data to the calibrated interference pattern stored in
        ``interference_pattern``. The 'ID' suffix in the group name is replaced
        by a short identifier chosen by the writer, e.g. 'pixel_calibration'
        or 'background_correction'. Multiple NXprocess groups may be present;
        their order is given by sequence_index.
      sequence_index(NX_POSINT):
        doc: |
          Sequence index of processing, for determining the order of multiple
          NXprocess steps. Starts with 1.
      description(NX_CHAR):
        doc: |
          Free-text description of what this processing step does.
      program(NX_CHAR):
        exists: optional
        doc: Version string of the software.
      version(NX_CHAR):
        exists: optional
      date(NX_DATE_TIME):
        exists: optional
    interference_pattern(NXdata):
      doc: |
        Default plot: the calibrated 2D interference pattern with spatial
        axes. The signal data may be identical to the raw detector array or
        derived from it via one or more NXprocess steps.
      \@signal:
        enumeration: [data]
      \@axes:
        enumeration: [['x_offset', 'y_offset']]
      data(NX_NUMBER):
        unit: NX_ANY
        doc: |
          2D interference intensity after any processing steps.
        dimensions:
          rank: 2
          dim: (n_x, n_y)
      x_offset(NX_FLOAT):
        unit: NX_LENGTH
        doc: |
          Horizontal spatial offset from the detector centre, derived from
          pixel index and pixel pitch.
        dimensions:
          rank: 1
          dim: (n_x,)
      y_offset(NX_FLOAT):
        unit: NX_LENGTH
        doc: |
          Vertical spatial offset from the detector centre, derived from
          pixel index and pixel pitch.
        dimensions:
          rank: 1
          dim: (n_y,)
NXdouble_slit.nxdl.xml (full)
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="nxdlformat.xsl"?>
<!--
# NeXus - Neutron and X-ray Common Data Format
#
# Copyright (C) 2026-2026 NeXus International Advisory Committee (NIAC)
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# For further information, see http://www.nexusformat.org
-->
<definition xmlns="http://definition.nexusformat.org/nxdl/3.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" category="application" type="group" name="NXdouble_slit" extends="NXobject" xsi:schemaLocation="http://definition.nexusformat.org/nxdl/3.1 ../nxdl.xsd">
    <symbols>
        <doc>
            Dimension symbols used in this definition.
        </doc>
        <symbol name="n_x">
            <doc>
                Number of detector pixels along x.
            </doc>
        </symbol>
        <symbol name="n_y">
            <doc>
                Number of detector pixels along y.
            </doc>
        </symbol>
    </symbols>
    <doc>
        Application definition for a double-slit interference experiment.
        Records the light source, aperture geometry, detector layout, and the
        measured 2D interference pattern needed to determine fringe spacing and
        source coherence length.

        See https://en.wikipedia.org/wiki/Double-slit_experiment.
    </doc>
    <group type="NXentry">
        <field name="definition">
            <enumeration>
                <item value="NXdouble_slit"/>
            </enumeration>
        </field>
        <field name="title"/>
        <field name="start_time" type="NX_DATE_TIME">
            <doc>
                ISO 8601 datetime of the measurement start.
            </doc>
        </field>
        <field name="end_time" type="NX_DATE_TIME" recommended="true"/>
        <group type="NXinstrument">
            <group name="source" type="NXsource">
                <field name="wavelength" type="NX_FLOAT" units="NX_WAVELENGTH">
                    <doc>
                        Central wavelength of the light source.
                    </doc>
                </field>
                <field name="coherence_length" type="NX_FLOAT" units="NX_LENGTH" recommended="true">
                    <doc>
                        Temporal coherence length of the source.
                    </doc>
                </field>
                <field name="type" type="NX_CHAR" optional="true">
                    <enumeration>
                        <item value="Laser"/>
                        <item value="Filtered lamp"/>
                        <item value="LED"/>
                    </enumeration>
                </field>
            </group>
            <group name="double_slit" type="NXslit">
                <field name="x_gap" type="NX_FLOAT" units="NX_LENGTH">
                    <doc>
                        Width of each individual slit.
                    </doc>
                </field>
                <field name="slit_separation" type="NX_FLOAT" units="NX_LENGTH">
                    <doc>
                        Center-to-center distance between the two slits.
                    </doc>
                </field>
                <field name="height" type="NX_FLOAT" units="NX_LENGTH" optional="true"/>
                <field name="material" type="NX_CHAR" optional="true"/>
            </group>
            <group name="detector" type="NXdetector">
                <field name="distance" type="NX_FLOAT" units="NX_LENGTH">
                    <doc>
                        Distance from the slit plane to the detector surface.
                    </doc>
                </field>
                <group name="data" type="NXdata">
                    <doc>
                        Raw 2D pixel data collected by the detector with no calibration
                        applied. This group stores data at the lowest level of processing
                        possible, indexed by integer pixel coordinates.
                    </doc>
                    <attribute name="signal">
                        <enumeration>
                            <item value="data"/>
                        </enumeration>
                    </attribute>
                    <attribute name="axes">
                        <enumeration>
                            <item value="['x', 'y']"/>
                        </enumeration>
                    </attribute>
                    <field name="data" type="NX_NUMBER" recommended="true" units="NX_ANY">
                        <doc>
                            Raw 2D intensity array indexed by pixel position.
                        </doc>
                        <dimensions rank="2">
                            <dim index="1" value="n_x"/>
                            <dim index="2" value="n_y"/>
                        </dimensions>
                    </field>
                    <field name="x" type="NX_INT">
                        <doc>
                            Pixel indices along the horizontal detector axis (0-based).
                        </doc>
                        <dimensions rank="1">
                            <dim index="1" value="n_x"/>
                        </dimensions>
                    </field>
                    <field name="y" type="NX_INT">
                        <doc>
                            Pixel indices along the vertical detector axis (0-based).
                        </doc>
                        <dimensions rank="1">
                            <dim index="1" value="n_y"/>
                        </dimensions>
                    </field>
                </group>
            </group>
        </group>
        <group name="processID" type="NXprocess" nameType="partial" optional="true">
            <doc>
                Describes one step in the processing chain that converts raw detector
                pixel data to the calibrated interference pattern stored in
                ``interference_pattern``. The 'ID' suffix in the group name is replaced
                by a short identifier chosen by the writer, e.g. 'pixel_calibration'
                or 'background_correction'. Multiple NXprocess groups may be present;
                their order is given by sequence_index.
            </doc>
            <field name="sequence_index" type="NX_POSINT">
                <doc>
                    Sequence index of processing, for determining the order of multiple
                    NXprocess steps. Starts with 1.
                </doc>
            </field>
            <field name="description" type="NX_CHAR">
                <doc>
                    Free-text description of what this processing step does.
                </doc>
            </field>
            <field name="program" type="NX_CHAR" optional="true">
                <doc>
                    Version string of the software.
                </doc>
            </field>
            <field name="version" type="NX_CHAR" optional="true"/>
            <field name="date" type="NX_DATE_TIME" optional="true"/>
        </group>
        <group name="interference_pattern" type="NXdata">
            <doc>
                Default plot: the calibrated 2D interference pattern with spatial
                axes. The signal data may be identical to the raw detector array or
                derived from it via one or more NXprocess steps.
            </doc>
            <attribute name="signal">
                <enumeration>
                    <item value="data"/>
                </enumeration>
            </attribute>
            <attribute name="axes">
                <enumeration>
                    <item value="['x_offset', 'y_offset']"/>
                </enumeration>
            </attribute>
            <field name="data" type="NX_NUMBER" units="NX_ANY">
                <doc>
                    2D interference intensity after any processing steps.
                </doc>
                <dimensions rank="2">
                    <dim index="1" value="n_x"/>
                    <dim index="2" value="n_y"/>
                </dimensions>
            </field>
            <field name="x_offset" type="NX_FLOAT" units="NX_LENGTH">
                <doc>
                    Horizontal spatial offset from the detector centre, derived from
                    pixel index and pixel pitch.
                </doc>
                <dimensions rank="1">
                    <dim index="1" value="n_x"/>
                </dimensions>
            </field>
            <field name="y_offset" type="NX_FLOAT" units="NX_LENGTH">
                <doc>
                    Vertical spatial offset from the detector centre, derived from
                    pixel index and pixel pitch.
                </doc>
                <dimensions rank="1">
                    <dim index="1" value="n_y"/>
                </dimensions>
            </field>
        </group>
    </group>
</definition>

Advanced: adding a new base class

In Nexus, you can not only compose existing base classes into application definitions, but also define your own base class. For this tutorial, we will create a new base class NXlaser that is a specialization of NXsource.

In our full application definition, we have an NXsource inside NXinstrument:

NXdouble_slit(NXobject):
  (NXentry):
    (NXinstrument):
      source(NXsource):
        wavelength(NX_FLOAT):
          unit: NX_WAVELENGTH
          doc: |
            Central wavelength of the light source.
        coherence_length(NX_FLOAT):
          unit: NX_LENGTH
          exists: recommended
          doc: |
            Temporal coherence length of the source.
        type(NX_CHAR):
          exists: optional
          enumeration: [Laser]

Note that: - the wavelength field is already defined in the NXsource base class. We are just redefining its documentation - we added here the coherence_length which is special for a laser, but not defined for all sources - we narrowed down the possible values for the type field from all available options in NXsource/type to just "Laser". This specialization constrains the source to lasers.

All of this is possible and valid NXDL syntax. However, many experiments in NeXus may use lasers. In that case, it makes sense to specialize the NXsource class and create a new NXlaser base class.

Create a new YAML file called NXlaser.yaml:

category: base
doc: |
  A specialisation of NXsource for coherent laser sources.

  Extends NXsource by adding a temporal coherence_length field and
  restricting the inherited type field to the single value "Laser".
  Use this base class in application definitions that require a laser
  as the illumination source.
type: group
NXlaser(NXsource):
  wavelength(NX_FLOAT):
    doc: |
      Central wavelength of the laser line.
  coherence_length(NX_FLOAT):
    unit: NX_LENGTH
    exists: recommended
    doc: |
      Temporal coherence length of the source.
      Determines the maximum path-length difference for which
      interference fringes are visible.
  type(NX_CHAR):
    enumeration: [Laser]

Note

You do not need to redefine the units of the wavelength field here. In general, only those concepts that are either not defined in the inherited class or are overwritten shall be defined in the child class.

The complete NXlaser.yaml and NXlaser.nxdl.xml are in the pynxtools examples:

You can then use this new base class NXlaser in NXdouble_slit:

NXdouble_slit(NXobject):
  (NXentry):
    (NXinstrument):
      source(NXlaser):
        wavelength(NX_FLOAT):
        coherence_length(NX_FLOAT):
          exists: recommended

Here, you only need to define whether any of the fields in NXlaser are required or recommended. All fields in base classes are optional; in the application definition, all of them are inherited. If we want to require or recommend any of them, we have to explicitly say so.

Convert both NXlaser and NXdouble_slit back to NXDL XML:

nyaml2nxdl NXlaser.yaml --output-file NXlaser.nxdl.xml
nyaml2nxdl NXdouble_slit.yaml --output-file NXdouble_slit.nxdl.xml

Because you now use the class NXlaser, you should see paths like /ENTRY[entry]/LASER/wavelength and /ENTRY[entry]/LASER/coherence_length in the generated JSON template.


Add your definition to pynxtools

There are two paths depending on whether you want a local prototype or a community contribution.

Option A — Local development (fastest)

Copy both NXDL files directly into the contributed_definitions/ folder of your local pynxtools install or working tree:

cp NXlaser.nxdl.xml        src/pynxtools/definitions/contributed_definitions/
cp NXdouble_slit.nxdl.xml  src/pynxtools/definitions/contributed_definitions/

Verify that pynxtools picks them up:

dataconverter generate-template --nxdl NXdouble_slit

This change lives only in your local checkout. It is useful for iterating quickly before submitting upstream.

Option B — Community contribution (permanent)

When your definition is stable, contribute it to the shared NeXus definitions repository so that all pynxtools users can benefit:

  1. Fork FAIRmat-NFDI/nexus_definitions on GitHub.
  2. Add your NXDL files under contributed_definitions/ (new definitions) or applications/ (promoted application definitions).
  3. Open a pull request. The FAIRmat team will review the definition.
  4. Once merged, update the pynxtools definitions submodule to bring the new definition in:

    # From the pynxtools repository root
    ./scripts/definitions.sh update
    

    This runs git submodule update --remote on the definitions submodule and records the new commit hash in pynxtools.

Note

For very early-stage or instrument-specific definitions that are not yet ready for the community repository, Option A is the right choice. The contributed_definitions/ path is scanned automatically by pynxtools — no code changes are needed.

Option C — Standardization with the NIAC

When you use option B, your new definition will only be part of the FAIRmat NeXus definitions. Once your application definition or base class has gained sufficient approval from the community, it is possible to submit it to the NeXus International Advisory Committee (NIAC) for standardization. If approved, the new application definitions and base classes get eventually promoted to applications/ or base classes, respectively.

Next steps