diff --git a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.01.po b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.01.po index c33fbdb..430291b 100644 --- a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.01.po +++ b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.01.po @@ -12,15 +12,15 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +msgid "Version 1.1.0" +msgstr "Version 1.1.0" + msgid "Unreleased" msgstr "À paraître" msgid "Python 3.10 or later is now required. Python 3.9 users must keep an earlier compatible release." msgstr "Python 3.10 ou ultérieur est désormais requis. Les utilisateurs de Python 3.9 doivent conserver une version antérieure compatible." -msgid "Version 1.1.0" -msgstr "Version 1.1.0" - msgid "SigimaX Version 1.1.0" msgstr "SigimaX Version 1.1.0" @@ -81,8 +81,8 @@ msgstr "L'explorateur HDF5 lit les aperçus dans un fil d'exécution de travail msgid "Numeric datasets with three dimensions or more (image stacks, spectral cubes...) are now listed in the HDF5 browser, with their shape and type. They are not imported as a whole, and there is no graphical slice selector: developers can read their 1D or 2D views with `sigimax.h5.selection.read_selection`, which reads only the selected elements (`NDSelection`: displayed axes, possibly transposed, and an index along each other axis)." msgstr "Les jeux de données numériques à trois dimensions ou plus (piles d'images, cubes spectraux...) sont désormais listés dans l'explorateur HDF5 avec leur forme et leur type. Ils ne sont pas importés en entier, et il n'existe pas de sélecteur graphique de coupes : les développeurs peuvent lire leurs vues 1D ou 2D avec `sigimax.h5.selection.read_selection`, qui ne lit que les éléments sélectionnés (`NDSelection` : axes affichés, éventuellement transposés, et un indice sur chaque autre axe)." -msgid "NeXus `NXdata` groups are shown as single nodes, imported as a signal (1D) or an image (2D) with their axes, units, labels and, for signals, uncertainties: `@signal`/`@axes` attributes with `@_indices`, legacy `signal`/`axes` dataset attributes, bin edges converted to bin centers, non-uniform image axes. Uniform axes are detected within the precision of their data type (e.g. `float32` axes), and decreasing image axes are reversed with the image. Invalid axes (axis count differing from the signal rank, axes designating a missing or already described dimension, axes not holding real numbers, image axes not strictly monotonic) are reported instead of being guessed. Imported objects record their origin in the `experimental_source` metadata entry (file, group, signal and axes, `\".\"` for a dimension without axis, conversions). `.nxs` and `.nx5` files are proposed in HDF5 file dialogs." -msgstr "Les groupes NeXus `NXdata` sont présentés comme des nœuds uniques, importés comme signal (1D) ou image (2D) avec leurs axes, unités, libellés et, pour les signaux, incertitudes : attributs `@signal`/`@axes` avec `@_indices`, anciens attributs de jeux de données `signal`/`axes`, conversion des bords de classes en centres, axes d'image non uniformes. Les axes uniformes sont détectés à la précision de leur type de données (par exemple des axes `float32`), et les axes d'image décroissants sont inversés avec l'image. Les axes invalides (nombre d'axes différent du rang du signal, axes désignant une dimension absente ou déjà décrite, axes ne contenant pas de nombres réels, axes d'image non strictement monotones) sont signalés au lieu d'être devinés. Les objets importés enregistrent leur origine dans la métadonnée `experimental_source` (fichier, groupe, signal et axes, `\".\"` pour une dimension sans axe, conversions). Les fichiers `.nxs` et `.nx5` sont proposés dans les dialogues de fichiers HDF5." +msgid "NeXus `NXdata` groups are shown as single nodes, imported as a signal (1D) or an image (2D) with their axes, units, labels and, for signals, uncertainties: `@signal`/`@axes` attributes with `@_indices`, legacy `signal`/`axes` dataset attributes, bin edges converted to bin centers, non-uniform image axes. Image axes deviating from even spacing by at most 2.5 units in the last place of their data type (`UNIFORM_AXIS_TOLERANCE`) are imported as uniform coordinates, so that evenly spaced `float32` axes are recognized, and their maximum deviation is recorded among the conversions; other image axes keep their stored coordinates. Decreasing image axes are reversed with the image. Invalid axes (axis count differing from the signal rank, axes designating a missing or already described dimension, axes not holding real numbers, image axes not strictly monotonic) are reported instead of being guessed. Imported objects record their origin in the `experimental_source` metadata entry (file, group, signal and axes, `\".\"` for a dimension without axis, conversions). `.nxs` and `.nx5` files are proposed in HDF5 file dialogs." +msgstr "Les groupes NeXus `NXdata` sont présentés comme des nœuds uniques, importés comme signal (1D) ou image (2D) avec leurs axes, unités, libellés et, pour les signaux, incertitudes : attributs `@signal`/`@axes` avec `@_indices`, anciens attributs de jeux de données `signal`/`axes`, conversion des bords de classes en centres, axes d'image non uniformes. Les axes d'image qui s'écartent d'un espacement régulier d'au plus 2,5 unités de dernière position (ULP) de leur type de données (`UNIFORM_AXIS_TOLERANCE`) sont importés en coordonnées uniformes, de sorte que les axes `float32` régulièrement espacés sont reconnus, et leur écart maximal est enregistré parmi les conversions ; les autres axes d'image conservent leurs coordonnées stockées. Les axes d'image décroissants sont inversés avec l'image. Les axes invalides (nombre d'axes différent du rang du signal, axes désignant une dimension absente ou déjà décrite, axes ne contenant pas de nombres réels, axes d'image non strictement monotones) sont signalés au lieu d'être devinés. Les objets importés enregistrent leur origine dans la métadonnée `experimental_source` (fichier, groupe, signal et axes, `\".\"` pour une dimension sans axe, conversions). Les fichiers `.nxs` et `.nx5` sont proposés dans les dialogues de fichiers HDF5." msgid "Files that do not designate their data with suitable `NXdata` groups (no axes, several counters...) can be described by declarative data mappings (`sigimax.h5.mapping`): JSON-compatible descriptions of the signals and images of the groups having given attributes and paths, with their axes, uncertainties, labels and units, and a name pattern to repeat an object for each counter of a scan. Mapped objects are shown as extra nodes of the HDF5 browser and record the mapping in their `experimental_source`. Applications and plugins register mappings with `register_mapping` (and `unregister_mapping`); `NodeFactory.add_children_provider` lets them add such extra nodes. Blank units and labels are now ignored." msgstr "Les fichiers qui ne désignent pas leurs données par des groupes `NXdata` adaptés (sans axes, plusieurs compteurs...) peuvent être décrits par des profils de correspondance déclaratifs (`sigimax.h5.mapping`) : descriptions compatibles JSON des signaux et images des groupes ayant les attributs et chemins donnés, avec leurs axes, incertitudes, libellés et unités, et un motif de nom pour répéter un objet pour chaque compteur d'un scan. Les objets décrits apparaissent comme des nœuds supplémentaires de l'explorateur HDF5 et enregistrent le profil dans leur `experimental_source`. Les applications et plugins enregistrent les profils avec `register_mapping` (et `unregister_mapping`) ; `NodeFactory.add_children_provider` leur permet d'ajouter ces nœuds supplémentaires. Les unités et libellés vides sont désormais ignorés." diff --git a/doc/locale/fr/LC_MESSAGES/user_guide/hdf5_workspace.po b/doc/locale/fr/LC_MESSAGES/user_guide/hdf5_workspace.po index c65ecf2..a75d6ee 100644 --- a/doc/locale/fr/LC_MESSAGES/user_guide/hdf5_workspace.po +++ b/doc/locale/fr/LC_MESSAGES/user_guide/hdf5_workspace.po @@ -93,8 +93,8 @@ msgstr "Objets scientifiques NeXus" msgid "With Sigima 1.4 or later, an ``NXdata`` group is importable as a 1D signal or 2D image with its physical axes, labels and units. Modern group attributes (``signal``, ``axes`` and ``_indices``) and legacy dataset attributes (``signal=1`` and ``axes``) are supported. Selecting a group imports the scientific object; selecting a child dataset still gives access to raw values." msgstr "Avec Sigima 1.4 ou ultérieur, un groupe ``NXdata`` est importable comme signal 1D ou image 2D avec ses axes physiques, libellés et unités. Les attributs modernes du groupe (``signal``, ``axes`` et ``_indices``) et les anciens attributs des jeux de données (``signal=1`` et ``axes``) sont pris en charge. Sélectionner un groupe importe l'objet scientifique ; sélectionner un jeu de données enfant donne toujours accès aux valeurs brutes." -msgid "Axis datasets must be 1D, hold real numbers and contain either one coordinate per point or one extra value for bin edges. Edges are converted to centers. Image coordinates may be non-uniform; a decreasing image axis is reversed, with the image, and this conversion is recorded, while an image axis that is not strictly monotonic prevents import. The ``axes`` attribute must give one entry per signal dimension, and ``_indices`` attributes must designate distinct existing dimensions. Invalid declared axes are reported and prevent group import; undeclared axes use indices. Signal Y uncertainties are read from ``_errors`` or ``errors`` when they hold real numbers and their shape matches; image uncertainties are not imported." -msgstr "Les jeux de données des axes doivent être 1D, contenir des nombres réels et contenir une coordonnée par point, ou une valeur supplémentaire pour des bords de classes. Les bords sont convertis en centres. Les coordonnées d'image peuvent être non uniformes ; un axe d'image décroissant est inversé, avec l'image, et cette conversion est enregistrée, tandis qu'un axe d'image qui n'est pas strictement monotone empêche l'import. L'attribut ``axes`` doit donner une entrée par dimension du signal, et les attributs ``_indices`` doivent désigner des dimensions existantes et distinctes. Les axes déclarés invalides sont signalés et empêchent l'import du groupe ; les axes non déclarés utilisent les indices. Les incertitudes Y des signaux sont lues dans ``_errors`` ou ``errors`` lorsqu'elles contiennent des nombres réels et que leur forme correspond ; les incertitudes des images ne sont pas importées." +msgid "Axis datasets must be 1D, hold real numbers and contain either one coordinate per point or one extra value for bin edges. Edges are converted to centers. Image coordinates may be non-uniform; a decreasing image axis is reversed, with the image, and this conversion is recorded, while an image axis that is not strictly monotonic prevents import. An image gets uniform coordinates (origin and pixel size) only if the values of both axes deviate from even spacing by at most 2.5 units in the last place of their stored data type (``sigimax.h5.nexus.UNIFORM_AXIS_TOLERANCE``), so that evenly spaced ``float32`` axes are recognized; the maximum deviation of each converted axis is recorded. Otherwise, the stored coordinates are kept unchanged. The ``axes`` attribute must give one entry per signal dimension, and ``_indices`` attributes must designate distinct existing dimensions. Invalid declared axes are reported and prevent group import; undeclared axes use indices. Signal Y uncertainties are read from ``_errors`` or ``errors`` when they hold real numbers and their shape matches; image uncertainties are not imported." +msgstr "Les jeux de données des axes doivent être 1D, contenir des nombres réels et contenir une coordonnée par point, ou une valeur supplémentaire pour des bords de classes. Les bords sont convertis en centres. Les coordonnées d'image peuvent être non uniformes ; un axe d'image décroissant est inversé, avec l'image, et cette conversion est enregistrée, tandis qu'un axe d'image qui n'est pas strictement monotone empêche l'import. Une image reçoit des coordonnées uniformes (origine et taille de pixel) seulement si les valeurs de ses deux axes s'écartent d'un espacement régulier d'au plus 2,5 unités de dernière position (ULP) de leur type de données stocké (``sigimax.h5.nexus.UNIFORM_AXIS_TOLERANCE``), de sorte que les axes ``float32`` régulièrement espacés sont reconnus ; l'écart maximal de chaque axe converti est enregistré. Sinon, les coordonnées stockées sont conservées telles quelles. L'attribut ``axes`` doit donner une entrée par dimension du signal, et les attributs ``_indices`` doivent désigner des dimensions existantes et distinctes. Les axes déclarés invalides sont signalés et empêchent l'import du groupe ; les axes non déclarés utilisent les indices. Les incertitudes Y des signaux sont lues dans ``_errors`` ou ``errors`` lorsqu'elles contiennent des nombres réels et que leur forme correspond ; les incertitudes des images ne sont pas importées." msgid "Imported objects record their file, group, signal, axes and conversions in ``experimental_source`` metadata; a dimension without axis is recorded as ``\".\"``, as in NeXus ``axes`` attributes. This describes the measurement origin, not a reproducible acquisition or a processing history. The application must preserve object metadata in its own workspace serialization." msgstr "Les objets importés enregistrent leur fichier, groupe, signal, axes et conversions dans les métadonnées ``experimental_source`` ; une dimension sans axe est enregistrée sous la forme ``\".\"``, comme dans les attributs ``axes`` NeXus. Cela décrit l'origine de la mesure, pas une acquisition reproductible ni un historique de traitement. L'application doit préserver les métadonnées des objets dans sa propre sérialisation d'espace de travail." diff --git a/doc/release_notes/release_1.01.md b/doc/release_notes/release_1.01.md index 552229b..c048076 100644 --- a/doc/release_notes/release_1.01.md +++ b/doc/release_notes/release_1.01.md @@ -41,7 +41,7 @@ * Browsing an HDF5 file no longer reads its numeric datasets entirely just to classify them: generic nodes are classified and described from the dataset shape and data type, and value previews read only the first elements (`TEXT_PREVIEW_MAX_ITEMS`). Compound datasets mixing numeric and string fields are checked on their first records only. Full data is read when creating an imported object or an undecimated graphical preview. * The HDF5 browser reads previews in a worker thread, so that it stays responsive while a large dataset is read: only the last selected node is shown, the previews of nodes selected meanwhile are not read, selecting a node that cannot be previewed (a group, for example) clears the preview, and closing a file or the browser dialog waits for the preview being read (imported objects are then read without concurrent access to the file). Ordinary 1D arrays and 2D images, NeXus and mapped objects with more than `PlotPreview.MAX_ITEMS` (2²⁴) elements are previewed with a step along each axis. Compound datasets and signals stored in 2D tables still use complete objects for graphical previews; this is not a universal memory bound. * Numeric datasets with three dimensions or more (image stacks, spectral cubes...) are now listed in the HDF5 browser, with their shape and type. They are not imported as a whole, and there is no graphical slice selector: developers can read their 1D or 2D views with `sigimax.h5.selection.read_selection`, which reads only the selected elements (`NDSelection`: displayed axes, possibly transposed, and an index along each other axis). -* NeXus `NXdata` groups are shown as single nodes, imported as a signal (1D) or an image (2D) with their axes, units, labels and, for signals, uncertainties: `@signal`/`@axes` attributes with `@_indices`, legacy `signal`/`axes` dataset attributes, bin edges converted to bin centers, non-uniform image axes. Uniform axes are detected within the precision of their data type (e.g. `float32` axes), and decreasing image axes are reversed with the image. Invalid axes (axis count differing from the signal rank, axes designating a missing or already described dimension, axes not holding real numbers, image axes not strictly monotonic) are reported instead of being guessed. Imported objects record their origin in the `experimental_source` metadata entry (file, group, signal and axes, `"."` for a dimension without axis, conversions). `.nxs` and `.nx5` files are proposed in HDF5 file dialogs. +* NeXus `NXdata` groups are shown as single nodes, imported as a signal (1D) or an image (2D) with their axes, units, labels and, for signals, uncertainties: `@signal`/`@axes` attributes with `@_indices`, legacy `signal`/`axes` dataset attributes, bin edges converted to bin centers, non-uniform image axes. Image axes deviating from even spacing by at most 2.5 units in the last place of their data type (`UNIFORM_AXIS_TOLERANCE`) are imported as uniform coordinates, so that evenly spaced `float32` axes are recognized, and their maximum deviation is recorded among the conversions; other image axes keep their stored coordinates. Decreasing image axes are reversed with the image. Invalid axes (axis count differing from the signal rank, axes designating a missing or already described dimension, axes not holding real numbers, image axes not strictly monotonic) are reported instead of being guessed. Imported objects record their origin in the `experimental_source` metadata entry (file, group, signal and axes, `"."` for a dimension without axis, conversions). `.nxs` and `.nx5` files are proposed in HDF5 file dialogs. * Files that do not designate their data with suitable `NXdata` groups (no axes, several counters...) can be described by declarative data mappings (`sigimax.h5.mapping`): JSON-compatible descriptions of the signals and images of the groups having given attributes and paths, with their axes, uncertainties, labels and units, and a name pattern to repeat an object for each counter of a scan. Mapped objects are shown as extra nodes of the HDF5 browser and record the mapping in their `experimental_source`. Applications and plugins register mappings with `register_mapping` (and `unregister_mapping`); `NodeFactory.add_children_provider` lets them add such extra nodes. Blank units and labels are now ignored. * Dataset attributes with more than `MAX_ATTRIBUTE_ITEMS` (10 000) values are no longer copied to object metadata. * Images with unsigned 32-bit values above 2 147 483 647 are no longer clipped to `int32`: they are converted with Sigima's converter, which keeps every value exactly with Sigima ≥ 1.4.0. `BaseNode.uint32_wng` is kept for compatibility and is always `False`. diff --git a/doc/user_guide/hdf5_workspace.rst b/doc/user_guide/hdf5_workspace.rst index b695e5a..83e02b5 100644 --- a/doc/user_guide/hdf5_workspace.rst +++ b/doc/user_guide/hdf5_workspace.rst @@ -94,12 +94,17 @@ Axis datasets must be 1D, hold real numbers and contain either one coordinate per point or one extra value for bin edges. Edges are converted to centers. Image coordinates may be non-uniform; a decreasing image axis is reversed, with the image, and this conversion is recorded, while an image axis that is -not strictly monotonic prevents import. The ``axes`` attribute must give one -entry per signal dimension, and ``_indices`` attributes must designate -distinct existing dimensions. Invalid declared axes are reported and prevent -group import; undeclared axes use indices. Signal Y uncertainties are read from -``_errors`` or ``errors`` when they hold real numbers and their shape -matches; image uncertainties are not imported. +not strictly monotonic prevents import. An image gets uniform coordinates +(origin and pixel size) only if the values of both axes deviate from even +spacing by at most 2.5 units in the last place of their stored data type +(``sigimax.h5.nexus.UNIFORM_AXIS_TOLERANCE``), so that evenly spaced +``float32`` axes are recognized; the maximum deviation of each converted axis +is recorded. Otherwise, the stored coordinates are kept unchanged. The ``axes`` +attribute must give one entry per signal dimension, and ``_indices`` +attributes must designate distinct existing dimensions. Invalid declared axes +are reported and prevent group import; undeclared axes use indices. Signal Y +uncertainties are read from ``_errors`` or ``errors`` when they hold +real numbers and their shape matches; image uncertainties are not imported. Imported objects record their file, group, signal, axes and conversions in ``experimental_source`` metadata; a dimension without axis is recorded as diff --git a/sigimax/h5/__init__.py b/sigimax/h5/__init__.py index 5ed7e45..618cf53 100644 --- a/sigimax/h5/__init__.py +++ b/sigimax/h5/__init__.py @@ -31,6 +31,7 @@ .. autoclass:: sigimax.h5.nexus.NXdataNode .. autofunction:: sigimax.h5.nexus.describe_nxdata .. autofunction:: sigimax.h5.nexus.describe_signal +.. autodata:: sigimax.h5.nexus.UNIFORM_AXIS_TOLERANCE Files without suitable ``NXdata`` groups are described by declarative data mappings (:mod:`sigimax.h5.mapping`), registered by applications or plugins. diff --git a/sigimax/h5/nexus.py b/sigimax/h5/nexus.py index a4a0596..45f7bfa 100644 --- a/sigimax/h5/nexus.py +++ b/sigimax/h5/nexus.py @@ -18,6 +18,9 @@ - axis datasets holding one real value per point, or bin edges (one more value), which are converted to bin centers; decreasing image axes are reversed, with the image, as images need increasing coordinates +- image axes evenly spaced within :data:`UNIFORM_AXIS_TOLERANCE` are imported as + uniform coordinates, their maximum deviation being recorded as a conversion; + other image axes keep their stored coordinates - ``_errors`` (or legacy ``errors``) uncertainties of a 1D signal - ``units`` and ``long_name`` dataset attributes """ @@ -45,6 +48,7 @@ from sigimax.h5.selection import preview_step __all__ = [ + "UNIFORM_AXIS_TOLERANCE", "NXAxis", "NXdataDescription", "NXdataNode", @@ -52,6 +56,12 @@ "describe_signal", ] +#: Maximum deviation of image axis values from even spacing for the axis to be +#: imported as uniform coordinates, in units in the last place (ULP) of the data +#: type they were stored with, at the largest axis magnitude: evenly spaced values +#: computed in their storage precision deviate by up to about 2 ULP +UNIFORM_AXIS_TOLERANCE = 2.5 + def _text(value) -> str: """Return attribute value as text""" @@ -292,15 +302,22 @@ def _read_axis(group: h5py.Group, axis: NXAxis) -> tuple[np.ndarray, np.dtype]: return values, dset.dtype -def _is_uniform(values: np.ndarray, dtype: np.dtype) -> bool: - """Return True if monotonic values are evenly spaced, within the precision of - the data type they were stored with""" +def _precision_type(dtype: np.dtype) -> type[np.floating]: + """Return the floating type giving the precision of axis values stored with + ``dtype`` (axis values are read as float64)""" + return dtype.type if dtype.kind == "f" and dtype.itemsize < 8 else np.float64 + + +def _uniform_deviation(values: np.ndarray, dtype: np.dtype) -> tuple[float, float]: + """Return the maximum deviation of monotonic values from even spacing, and + this deviation in units in the last place (ULP) of the data type they were + stored with, at the largest axis magnitude""" if values.size < 3: - return True - eps = np.finfo(dtype if dtype.kind == "f" else float).eps - tolerance = 4 * float(eps) * max(abs(values[0]), abs(values[-1])) + return 0.0, 0.0 expected = np.linspace(values[0], values[-1], values.size) - return bool(np.all(np.abs(values - expected) <= tolerance)) + deviation = float(np.max(np.abs(values - expected))) + bound = max(abs(values[0]), abs(values[-1])) + return deviation, deviation / float(np.spacing(_precision_type(dtype)(bound))) def _is_decreasing(values: np.ndarray, name: str) -> bool: @@ -343,23 +360,41 @@ def _image_axis( def _set_image_coords( obj: ImageObj, - xaxis: tuple[np.ndarray, np.dtype], - yaxis: tuple[np.ndarray, np.dtype], + xaxis: tuple[str | None, np.ndarray, np.dtype], + yaxis: tuple[str | None, np.ndarray, np.dtype], + conversions: list[str], ) -> None: - """Set image coordinates: uniform if axes are evenly spaced, else non-uniform + """Set image coordinates: uniform if both axes are evenly spaced within + :data:`UNIFORM_AXIS_TOLERANCE`, else the stored (non-uniform) coordinates Args: obj: image object - xaxis: X axis values in increasing order, and their stored data type - yaxis: Y axis values in increasing order, and their stored data type + xaxis: X axis name (None for a dimension without axis), values in + increasing order, and their stored data type + yaxis: Y axis name (None for a dimension without axis), values in + increasing order, and their stored data type + conversions: conversions applied to the source values, completed with the + deviation of the axes converted to uniform coordinates """ - (xc, xdtype), (yc, ydtype) = xaxis, yaxis - if _is_uniform(xc, xdtype) and _is_uniform(yc, ydtype): - dx = float(xc[-1] - xc[0]) / (xc.size - 1) if xc.size > 1 else 1.0 - dy = float(yc[-1] - yc[0]) / (yc.size - 1) if yc.size > 1 else 1.0 - obj.set_uniform_coords(dx, dy, float(xc[0]), float(yc[0])) - else: + (xname, xc, xdtype), (yname, yc, ydtype) = xaxis, yaxis + xdev, ydev = _uniform_deviation(xc, xdtype), _uniform_deviation(yc, ydtype) + # Written so that a NaN deviation (non-finite axis values) is not uniform + if not (xdev[1] <= UNIFORM_AXIS_TOLERANCE and ydev[1] <= UNIFORM_AXIS_TOLERANCE): obj.set_coords(xc, yc) + return + dx = float(xc[-1] - xc[0]) / (xc.size - 1) if xc.size > 1 else 1.0 + dy = float(yc[-1] - yc[0]) / (yc.size - 1) if yc.size > 1 else 1.0 + obj.set_uniform_coords(dx, dy, float(xc[0]), float(yc[0])) + for name, dtype, (deviation, ulps) in ( + (xname, xdtype, xdev), + (yname, ydtype, ydev), + ): + if name is not None and deviation > 0: + ptype = np.dtype(_precision_type(dtype)).name + conversions.append( + f"axis '{name}': uniform coordinates " + f"(max deviation {deviation:.3g}, {ulps:.2g} ULP of {ptype})" + ) class NXdataNode(common.GroupNode): @@ -546,7 +581,12 @@ def _build_image( ), ) obj.zlabel, obj.zunit = desc.label, desc.units - _set_image_coords(obj, (xc, xdtype), (yc, ydtype)) + _set_image_coords( + obj, + (xaxis.name if xaxis else None, xc, xdtype), + (yaxis.name if yaxis else None, yc, ydtype), + conversions, + ) if xaxis is not None: obj.xlabel, obj.xunit = xaxis.label, xaxis.units if yaxis is not None: diff --git a/sigimax/tests/hdf5/test_h5_nexus.py b/sigimax/tests/hdf5/test_h5_nexus.py index e693e76..97651f7 100644 --- a/sigimax/tests/hdf5/test_h5_nexus.py +++ b/sigimax/tests/hdf5/test_h5_nexus.py @@ -314,6 +314,48 @@ def build(h5file: h5py.File) -> None: nodes[f"/{name}"].get_native_object() +def test_nxdata_image_axis_precision(tmp_path) -> None: + """Image axes are imported as uniform coordinates only if they deviate from + even spacing by at most UNIFORM_AXIS_TOLERANCE ULP of their data type, and the + deviation is recorded; other axes keep their stored coordinates exactly.""" + f32 = np.float32 + # float32 axes with a large origin: the third step deviates by 4 ULP (0.25) + uneven = np.array([1e6, 1e6 + 1, 1e6 + 2.25, 1e6 + 3], dtype=f32) + + def build(h5file: h5py.File) -> None: + _image_group(h5file, "uneven", uneven) + _image_group(h5file, "large_origin", np.linspace(8e3, 8.1e3, 1001, dtype=f32)) + # Evenly spaced values computed in float32 arithmetic deviate by 1.87 ULP + arithmetic = f32(-50.0) + np.arange(300, dtype=f32) * f32(0.37) + _image_group(h5file, "arithmetic", arithmetic) + + def uniform_conversions(image: ImageObj) -> list[str]: + conversions = get_experimental_source(image).get("conversions", []) + return [text for text in conversions if "uniform coordinates" in text] + + with _nodes_of(tmp_path, build) as nodes: + image = nodes["/uneven"].get_native_object() + assert not image.is_uniform_coords + assert np.array_equal(image.xcoords, uneven.astype(float)) + assert np.array_equal(image.ycoords, [0.0, 1.0]) + assert not uniform_conversions(image) + + image = nodes["/large_origin"].get_native_object() + assert image.is_uniform_coords + assert image.x0 == 8000.0 and np.isclose(image.dx, 0.1, rtol=1e-12) + assert (image.y0, image.dy) == (0.0, 1.0) + assert uniform_conversions(image) == [ + "axis 'x': uniform coordinates (max deviation 0.000195, 0.4 ULP of float32)" + ] + + image = nodes["/arithmetic"].get_native_object() + assert image.is_uniform_coords + assert image.x0 == -50.0 and np.isclose(image.dx, 0.37, rtol=1e-6) + assert uniform_conversions(image) == [ + "axis 'x': uniform coordinates (max deviation 7.14e-06, 1.9 ULP of float32)" + ] + + def test_nxdata_decimated_preview(tmp_path) -> None: """Previews of large NXdata groups read one element out of a step, with the matching axis values; small groups are previewed with the native object."""