From 95ff1bd320764ed02f250ab3151f4a18710712ea Mon Sep 17 00:00:00 2001
From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com>
Date: Mon, 19 Oct 2026 20:45:49 +0200
Subject: [PATCH 1/2] test: domain images in physical coordinates, legacy
creation parameters (#400)
Sigima now displays the Gaussian, 2D ramp, sinusoidal grating, ring,
Siemens star and 2D sinc images over their [xmin, xmax] x [ymin, ymax]
domain. Tests relying on pixel coordinates (geometry transforms, analysis
recompute) now request them explicitly, and a new test checks that
creation parameters stored by earlier versions (object metadata and
history) keep pixel coordinates when the image is recreated.
Assisted-by: Claude Opus 5.5
---
.../auto_analysis_recompute_unit_test.py | 13 ++++--
.../common/creation_metadata_unit_test.py | 43 +++++++++++++++++++
.../image/geometry_transform_unit_test.py | 2 +
3 files changed, 54 insertions(+), 4 deletions(-)
diff --git a/datalab/tests/features/common/auto_analysis_recompute_unit_test.py b/datalab/tests/features/common/auto_analysis_recompute_unit_test.py
index df5251e47..dc910db42 100644
--- a/datalab/tests/features/common/auto_analysis_recompute_unit_test.py
+++ b/datalab/tests/features/common/auto_analysis_recompute_unit_test.py
@@ -54,9 +54,12 @@ def test_analysis_recompute_after_roi_change():
with datalab_test_app_context(console=False) as win:
panel = win.imagepanel
- # Create a 2D Gaussian image in a 200x200 image
+ # Create a 2D Gaussian image in a 200x200 image (pixel coordinates, as
+ # the ROIs and expected centroids below)
size = 200
- param = Gauss2DParam.create(height=size, width=size, sigma=20)
+ param = Gauss2DParam.create(
+ height=size, width=size, sigma=20, physical_coords=False
+ )
img = create_image_from_param(param)
panel.add_object(img)
@@ -202,10 +205,12 @@ def test_analysis_recompute_after_recompute_1_to_1():
size = 200
# In Gauss2DParam, x0 and y0 are the center coordinates with Xmin=-10.0,
# Ymin=-10.0, Xmax=10.0, Ymax=10.0 by default.
- # The centroid position should be at (49.75, 99.5).
+ # The centroid position should be at (49.75, 99.5) in pixel coordinates.
# After a 45° rotation, it should move closer to (64.32, 134.68).
# Or after a 90° rotation, it should move to (99.5, 149.2).
- param = Gauss2DParam.create(height=size, width=size, x0=-5.0)
+ param = Gauss2DParam.create(
+ height=size, width=size, x0=-5.0, physical_coords=False
+ )
img = create_image_from_param(param)
panel.add_object(img)
diff --git a/datalab/tests/features/common/creation_metadata_unit_test.py b/datalab/tests/features/common/creation_metadata_unit_test.py
index ad9a16059..98c5e4b9b 100644
--- a/datalab/tests/features/common/creation_metadata_unit_test.py
+++ b/datalab/tests/features/common/creation_metadata_unit_test.py
@@ -14,10 +14,13 @@
PEAK_PARAMETERIZATION,
GaussParam,
SineParam,
+ SinusoidalGrating2DParam,
+ create_image,
create_signal,
)
from sigima.tools.signal.pulse import GaussianModel, LegacyPeakParameterizationError
+from datalab.gui.creation import create_image_from_param
from datalab.gui.newobject import (
CREATION_PARAMETERS_FORMAT_VERSION,
CREATION_PARAMETERS_OPTION,
@@ -26,6 +29,7 @@
extract_creation_parameters,
insert_creation_parameters,
)
+from datalab.history.core import decode_kwargs
def _legacy_gaussian_json(
@@ -117,3 +121,42 @@ def test_creation_parameter_formats_never_fall_back_implicitly() -> None:
obj.set_metadata_option(LEGACY_CREATION_PARAMETERS_OPTION, _legacy_gaussian_json())
with pytest.raises(ValueError, match="Conflicting creation parameter formats"):
extract_creation_parameters(obj)
+
+
+def test_domain_image_creation_parameters_keep_their_coordinates() -> None:
+ """Images created before the "Physical coordinates" option stay in pixels.
+
+ Recreating an image from creation parameters stored by DataLab 1.3 (without
+ the option) must give back the original pixel coordinates, while parameters
+ stored by this version are restored as they were.
+ """
+ param = SinusoidalGrating2DParam.create(height=16, width=32)
+ image = create_image_from_param(param)
+ assert (image.x0, image.dx) == (param.xmin, (param.xmax - param.xmin) / 31)
+ restored = extract_creation_parameters(image)
+ assert isinstance(restored, SinusoidalGrating2DParam)
+ assert restored.physical_coords is True
+
+ payload = json.loads(dataset_to_json(param))
+ del payload["physical_coords"]
+ legacy = create_image("Legacy grating", np.zeros((16, 32)))
+ legacy.set_metadata_option(
+ CREATION_PARAMETERS_OPTION,
+ {
+ "format_version": CREATION_PARAMETERS_FORMAT_VERSION,
+ "dataset_json": json.dumps(payload),
+ },
+ )
+ legacy_param = extract_creation_parameters(legacy)
+ assert isinstance(legacy_param, SinusoidalGrating2DParam)
+ assert legacy_param.physical_coords is False
+ recreated = create_image_from_param(legacy_param)
+ assert (recreated.x0, recreated.y0, recreated.dx, recreated.dy) == (
+ 0.0,
+ 0.0,
+ 1.0,
+ 1.0,
+ )
+
+ history_kwargs = decode_kwargs({"param": {"__dataset_json__": json.dumps(payload)}})
+ assert history_kwargs["param"].physical_coords is False
diff --git a/datalab/tests/features/image/geometry_transform_unit_test.py b/datalab/tests/features/image/geometry_transform_unit_test.py
index c1b026789..fb329bf39 100644
--- a/datalab/tests/features/image/geometry_transform_unit_test.py
+++ b/datalab/tests/features/image/geometry_transform_unit_test.py
@@ -20,6 +20,7 @@
def __create_test_image() -> sio.ImageObj:
"""Create a test image with geometry results for testing."""
+ # Geometry results and ROI below are defined in pixel coordinates
param = sio.Gauss2DParam.create(
height=600,
width=600,
@@ -27,6 +28,7 @@ def __create_test_image() -> sio.ImageObj:
y0=7.5,
title="Test image (with geometry results)",
dtype=sio.ImageDatatypes.UINT16,
+ physical_coords=False,
)
obj = sio.create_image_from_param(param)
for geometry in test_data.generate_geometry_results():
From 29567498907ac2e98ecfbd4596b5210057e15e5e Mon Sep 17 00:00:00 2001
From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com>
Date: Mon, 19 Oct 2026 20:45:54 +0200
Subject: [PATCH 2/2] docs: fit model formulas, Planck relations, image
physical coordinates (#400)
Document the formula shown in interactive fit dialogs and the name under
which each model parameter is stored, how to derive the temperature and
emissivity from a Planckian fit, the default axes of the blackbody signal
and the "Physical coordinates" option of domain-based images. Release
notes and French translations.
Assisted-by: Claude Opus 5.5
---
doc/features/image/menu_create.rst | 16 ++
doc/features/signal/menu_create.rst | 5 +
doc/features/signal/menu_processing.rst | 54 ++++--
.../LC_MESSAGES/features/image/menu_create.po | 7 +
.../features/signal/menu_create.po | 13 ++
.../features/signal/menu_processing.po | 164 +++++++++++-------
.../LC_MESSAGES/release_notes/release_1.04.po | 35 ++++
doc/release_notes/release_1.04.md | 20 +++
8 files changed, 244 insertions(+), 70 deletions(-)
diff --git a/doc/features/image/menu_create.rst b/doc/features/image/menu_create.rst
index 363d62eb9..0d2699a40 100644
--- a/doc/features/image/menu_create.rst
+++ b/doc/features/image/menu_create.rst
@@ -72,3 +72,19 @@ Create a new image from various models
:width: 30px
- 2D sinc
- :math:`z = A \cdot \mathrm{sinc}\left(\dfrac{\sqrt{(x-x_0)^2+(y-y_0)^2}}{\sigma}\right) + C`
+
+The 2D Gaussian, 2D ramp, sinusoidal grating, ring pattern, Siemens star and
+2D sinc models are computed over a domain
+:math:`[x_{min}, x_{max}] \times [y_{min}, y_{max}]`. When the
+**Physical coordinates** option is enabled (default), this domain defines the
+image coordinates: the axes show :math:`x` and :math:`y` as used in the
+equation, so that centers, widths and spatial frequencies can be read directly
+on the image (e.g. a sinusoidal grating of frequency :math:`f_x = 0.1` shows
+one period every 10 units along the X axis). When the option is disabled, the
+image is displayed in pixel coordinates.
+
+.. note::
+
+ Images created with DataLab 1.3 or earlier were displayed in pixel
+ coordinates: they keep these coordinates when their creation parameters
+ are edited or recomputed.
diff --git a/doc/features/signal/menu_create.rst b/doc/features/signal/menu_create.rst
index 8def78f44..3b503f253 100644
--- a/doc/features/signal/menu_create.rst
+++ b/doc/features/signal/menu_create.rst
@@ -142,3 +142,8 @@ Create a new signal from various models:
For Gaussian, Lorentzian and Voigt models, :math:`A` is the signed peak height
above the baseline :math:`y_0`, expressed in the signal's Y unit.
+
+The blackbody signal is the spectral radiance per unit wavelength of a blackbody
+at temperature :math:`T`: wavelengths :math:`\lambda` are expressed in meters and
+the spectral radiance in W·sr⁻¹·m⁻³. These axis titles and units are set by
+default, and may be changed in the creation dialog.
diff --git a/doc/features/signal/menu_processing.rst b/doc/features/signal/menu_processing.rst
index 84e74528f..65efa1d59 100644
--- a/doc/features/signal/menu_processing.rst
+++ b/doc/features/signal/menu_processing.rst
@@ -374,44 +374,64 @@ Fitting
^^^^^^^
Fit a model to each selected signal.
-This can be done automatically or through an interactive curve fitting dialog.
+This can be done automatically or through an interactive curve fitting dialog,
+which shows the formula of the model below the fit parameters.
+
+The fitted signal stores the model parameters in its metadata
+(``metadata["fit_params"]``), under the names given in the last column of the
+table below:
.. list-table::
:header-rows: 1
- :widths: 20, 80
+ :widths: 15, 55, 30
* - Model
- Equation
+ - Stored parameters
* - Linear
- - :math:`y = c_{0} + c_{1} \cdot x`
+ - :math:`y = a \cdot x + b`
+ - ``a``, ``b``
* - Polynomial
- - :math:`y = c_{0} + c_{1} \cdot x + c_{2} \cdot x^2 + ... + c_{n} \cdot x^n`
+ - :math:`y = a \cdot x^n + b \cdot x^{n-1} + c \cdot x^{n-2} + \ldots`
+ - ``a``, ``b``, ``c``, ...
* - Gaussian
- :math:`y = y_{0} + A \exp\left(-\dfrac{1}{2} \left(\dfrac{x - x_{0}}{\sigma}\right)^2\right)`
+ - :math:`A`: ``amplitude``, :math:`\sigma`: ``sigma``, :math:`x_0`: ``x0``, :math:`y_0`: ``y0``
* - Lorentzian
- :math:`y = y_{0} + \dfrac{A}{1+\left(\dfrac{x-x_{0}}{\sigma}\right)^2}`
+ - :math:`A`: ``amplitude``, :math:`\sigma`: ``sigma``, :math:`x_0`: ``x0``, :math:`y_0`: ``y0``
* - Voigt
- :math:`y = y_{0} + A \cdot \dfrac{\Re\left(w(z)\right)}{\Re\left(w(j/\sqrt{2})\right)}` with :math:`w(z) = \exp(-z^2) \operatorname{erfc}(-jz)` and :math:`z = \dfrac{x - x_{0} + j \sigma}{\sqrt{2} \sigma}`
+ - :math:`A`: ``amplitude``, :math:`\sigma`: ``sigma``, :math:`x_0`: ``x0``, :math:`y_0`: ``y0``
* - Multi-Gaussian
- :math:`y = y_{0} + \sum_{i=1}^{N} A_{i} \exp\left(-\dfrac{1}{2} \left(\dfrac{x - x_{0,i}}{\sigma_{i}}\right)^2\right)`
+ - :math:`A_i`: ``amplitude_i``, :math:`\sigma_i`: ``sigma_i``, :math:`x_{0,i}`: ``x0_i``, :math:`y_0`: ``y0``
* - Multi-Lorentzian
- :math:`y = y_{0} + \sum_{i=1}^{N} \dfrac{A_{i}}{1 + \left(\dfrac{x - x_{0,i}}{\sigma_{i}}\right)^2}`
+ - :math:`A_i`: ``amplitude_i``, :math:`\sigma_i`: ``sigma_i``, :math:`x_{0,i}`: ``x0_i``, :math:`y_0`: ``y0``
* - Planck
- - :math:`y = y_{0} + A \cdot \left(\dfrac{x}{x_{0}}\right)^{-5} \cdot \left(\exp\left(\dfrac{5 x_{0}}{\sigma \cdot x}\right)-1\right)^{-1}` where :math:`x_{0}` and :math:`\sigma` are scale and width factors (see the note below)
+ - :math:`y = y_{0} + A \cdot \left(\dfrac{x}{x_{0}}\right)^{-5} \cdot \left(\exp\left(\dfrac{5 x_{0}}{\sigma \cdot x}\right)-1\right)^{-1}` where :math:`x_{0}` and :math:`\sigma` are scale and temperature factors (see the note below)
+ - :math:`A`: ``amp``, :math:`x_0`: ``x0``, :math:`\sigma`: ``sigma``, :math:`y_0`: ``y0``
* - Two half Gaussians
- - | :math:`y = y_{L} + A_{L} \cdot \exp\left(-\dfrac{1}{2}\left(\dfrac{x - x_{0}}{\sigma_{L}}\right)^2\right)` if :math:`x < x_{0}`
- | :math:`y = y_{R} + A_{R} \cdot \exp\left(-\dfrac{1}{2}\left(\dfrac{x - x_{0}}{\sigma_{R}}\right)^2\right)` otherwise
+ - | :math:`y = y_{0,L} + A_{L} \cdot \exp\left(-\dfrac{1}{2}\left(\dfrac{x - x_{0}}{\sigma_{L}}\right)^2\right)` if :math:`x < x_{0}`
+ | :math:`y = y_{0,R} + A_{R} \cdot \exp\left(-\dfrac{1}{2}\left(\dfrac{x - x_{0}}{\sigma_{R}}\right)^2\right)` otherwise
+ - :math:`A_L`: ``amp_left``, :math:`A_R`: ``amp_right``, :math:`\sigma_L`: ``sigma_left``, :math:`\sigma_R`: ``sigma_right``, :math:`x_0`: ``x0``, :math:`y_{0,L}`: ``y0_left``, :math:`y_{0,R}`: ``y0_right``
* - Two back-to-back exponentials
- - | :math:`y = y_{0} + A_{L} \cdot \exp\left(B_{L} \cdot x\right)` if :math:`x < x_{c}`
- | :math:`y = y_{0} + A_{R} \cdot \exp\left(B_{R} \cdot x\right)` otherwise
+ - | :math:`y = y_{0} + a_{L} \cdot \exp\left(b_{L} \cdot x\right)` if :math:`x < x_{c}`
+ | :math:`y = y_{0} + a_{R} \cdot \exp\left(b_{R} \cdot x\right)` otherwise
+ - :math:`x_c`: ``x_center``, :math:`a_L`: ``a_left``, :math:`b_L`: ``b_left``, :math:`a_R`: ``a_right``, :math:`b_R`: ``b_right``, :math:`y_0`: ``y0``
* - Exponential
- - :math:`y = y_{0} + A \exp\left(B \cdot x\right)`
+ - :math:`y = y_{0} + a \exp\left(b \cdot x\right)`
+ - ``a``, ``b``, :math:`y_0`: ``y0``
* - Sinusoidal
- :math:`y = y_{0} + A \sin\left(2 \pi f \cdot x + \phi\right)`
+ - :math:`A`: ``amplitude``, :math:`f`: ``frequency``, :math:`\phi`: ``phase`` (in radians), :math:`y_0`: ``offset``
* - Cumulative Distribution Function (CDF)
- - :math:`y = y_{0} + A \erf\left(\dfrac{x - x_{0}}{\sqrt{2} \sigma}\right)`
+ - :math:`y = y_{0} + A \erf\left(\dfrac{x - \mu}{\sqrt{2} \sigma}\right)`
+ - :math:`A`: ``amplitude``, :math:`\mu`: ``mu``, :math:`\sigma`: ``sigma``, :math:`y_0`: ``baseline``
* - Sigmoid
- :math:`y = y_{0} + \dfrac{A}{1 + \exp\left(-k \left(x - x_{0}\right)\right)}`
+ - :math:`A`: ``amplitude``, ``k``, :math:`x_0`: ``x0``, :math:`y_0`: ``offset``
For Gaussian, Lorentzian and Voigt models, :math:`A` is the signed peak height
above the baseline :math:`y_0`, expressed in the signal's Y unit.
@@ -426,6 +446,18 @@ above the baseline :math:`y_0`, expressed in the signal's Y unit.
particular the emission peak is located at approximately
:math:`1.007 \, x_{0}/\sigma`, not at :math:`x_{0}`.
+ The physical quantities are derived from these combinations. With
+ wavelengths in meters, the model matches Planck's law (spectral radiance
+ per unit wavelength) for:
+
+ * a temperature :math:`T = \dfrac{h c \, \sigma}{5 k_B \, x_{0}} \approx 2.878 \times 10^{-3} \, \dfrac{\sigma}{x_{0}}` K,
+ * an emissivity (or any other scale factor of the measurement)
+ :math:`\varepsilon = \dfrac{A \, x_{0}^5}{2 h c^2}`, when the signal is a
+ spectral radiance in W·sr⁻¹·m⁻³.
+
+ For example, fitting the blackbody signal created at 293 K gives back
+ :math:`T = 293` K and :math:`\varepsilon = 1`.
+
Multi-peak fit parameters
~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/doc/locale/fr/LC_MESSAGES/features/image/menu_create.po b/doc/locale/fr/LC_MESSAGES/features/image/menu_create.po
index 1b125d319..a1b041e16 100644
--- a/doc/locale/fr/LC_MESSAGES/features/image/menu_create.po
+++ b/doc/locale/fr/LC_MESSAGES/features/image/menu_create.po
@@ -105,3 +105,10 @@ msgstr "Sinc 2D"
msgid ":math:`z = A \\cdot \\mathrm{sinc}\\left(\\dfrac{\\sqrt{(x-x_0)^2+(y-y_0)^2}}{\\sigma}\\right) + C`"
msgstr ":math:`z = A \\cdot \\mathrm{sinc}\\left(\\dfrac{\\sqrt{(x-x_0)^2+(y-y_0)^2}}{\\sigma}\\right) + C`"
+
+#, python-brace-format
+msgid "The 2D Gaussian, 2D ramp, sinusoidal grating, ring pattern, Siemens star and 2D sinc models are computed over a domain :math:`[x_{min}, x_{max}] \\times [y_{min}, y_{max}]`. When the **Physical coordinates** option is enabled (default), this domain defines the image coordinates: the axes show :math:`x` and :math:`y` as used in the equation, so that centers, widths and spatial frequencies can be read directly on the image (e.g. a sinusoidal grating of frequency :math:`f_x = 0.1` shows one period every 10 units along the X axis). When the option is disabled, the image is displayed in pixel coordinates."
+msgstr "Les modèles gaussienne 2D, rampe 2D, réseau sinusoïdal, anneaux, mire de Siemens et sinus cardinal 2D sont calculés sur un domaine :math:`[x_{min}, x_{max}] \\times [y_{min}, y_{max}]`. Lorsque l'option **Coordonnées physiques** est activée (par défaut), ce domaine définit les coordonnées de l'image : les axes représentent :math:`x` et :math:`y` tels qu'utilisés dans l'équation, de sorte que les centres, largeurs et fréquences spatiales se lisent directement sur l'image (par exemple, un réseau sinusoïdal de fréquence :math:`f_x = 0.1` présente une période toutes les 10 unités le long de l'axe X). Lorsque l'option est désactivée, l'image est affichée en coordonnées pixel."
+
+msgid "Images created with DataLab 1.3 or earlier were displayed in pixel coordinates: they keep these coordinates when their creation parameters are edited or recomputed."
+msgstr "Les images créées avec DataLab 1.3 ou une version antérieure étaient affichées en coordonnées pixel : elles conservent ces coordonnées lorsque leurs paramètres de création sont modifiés ou recalculés."
diff --git a/doc/locale/fr/LC_MESSAGES/features/signal/menu_create.po b/doc/locale/fr/LC_MESSAGES/features/signal/menu_create.po
index 04721894c..75f93a4cb 100644
--- a/doc/locale/fr/LC_MESSAGES/features/signal/menu_create.po
+++ b/doc/locale/fr/LC_MESSAGES/features/signal/menu_create.po
@@ -311,3 +311,16 @@ msgstr ""
"Pour les modèles gaussien, lorentzien et de Voigt, :math:`A` est la hauteur "
"signée du pic au-dessus de la ligne de base :math:`y_0`, exprimée dans "
"l'unité Y du signal."
+
+msgid ""
+"The blackbody signal is the spectral radiance per unit wavelength of a "
+"blackbody at temperature :math:`T`: wavelengths :math:`\\lambda` are "
+"expressed in meters and the spectral radiance in W·sr⁻¹·m⁻³. These axis "
+"titles and units are set by default, and may be changed in the creation "
+"dialog."
+msgstr ""
+"Le signal de corps noir est la luminance énergétique spectrale (par unité de "
+"longueur d'onde) d'un corps noir à la température :math:`T` : les longueurs "
+"d'onde :math:`\\lambda` sont exprimées en mètres et la luminance énergétique "
+"spectrale en W·sr⁻¹·m⁻³. Ces titres et unités d'axes sont définis par défaut "
+"et peuvent être modifiés dans la boîte de dialogue de création."
diff --git a/doc/locale/fr/LC_MESSAGES/features/signal/menu_processing.po b/doc/locale/fr/LC_MESSAGES/features/signal/menu_processing.po
index 2b462ead1..261581e12 100644
--- a/doc/locale/fr/LC_MESSAGES/features/signal/menu_processing.po
+++ b/doc/locale/fr/LC_MESSAGES/features/signal/menu_processing.po
@@ -702,11 +702,15 @@ msgstr "Ajustement"
msgid ""
"Fit a model to each selected signal. This can be done automatically or "
-"through an interactive curve fitting dialog."
-msgstr ""
-"Ajuste un modèle à chaque signal sélectionné. Cela peut être fait "
-"automatiquement ou via une boîte de dialogue d'ajustement de courbe "
-"interactive."
+"through an interactive curve fitting dialog, which shows the formula of "
+"the model below the fit parameters."
+msgstr "Ajuste un modèle à chaque signal sélectionné. Cela peut être fait automatiquement ou via une boîte de dialogue d'ajustement de courbe interactive, qui affiche la formule du modèle sous les paramètres d'ajustement."
+
+msgid ""
+"The fitted signal stores the model parameters in its metadata "
+"(``metadata[\"fit_params\"]``), under the names given in the last column "
+"of the table below:"
+msgstr "Le signal ajusté enregistre les paramètres du modèle dans ses métadonnées (``metadata[\"fit_params\"]``), sous les noms indiqués dans la dernière colonne du tableau ci-dessous :"
msgid "Model"
msgstr "Modèle"
@@ -714,23 +718,28 @@ msgstr "Modèle"
msgid "Equation"
msgstr "Equation"
+msgid "Stored parameters"
+msgstr "Paramètres enregistrés"
+
msgid "Linear"
msgstr "Linéaire"
#, python-brace-format
-msgid ":math:`y = c_{0} + c_{1} \\cdot x`"
-msgstr ":math:`y = c_{0} + c_{1} \\cdot x`"
+msgid ":math:`y = a \\cdot x + b`"
+msgstr ":math:`y = a \\cdot x + b`"
+
+msgid "``a``, ``b``"
+msgstr "``a``, ``b``"
msgid "Polynomial"
msgstr "Polynomial"
#, python-brace-format
-msgid ""
-":math:`y = c_{0} + c_{1} \\cdot x + c_{2} \\cdot x^2 + ... + c_{n} \\cdot"
-" x^n`"
-msgstr ""
-":math:`y = c_{0} + c_{1} \\cdot x + c_{2} \\cdot x^2 + ... + c_{n} \\cdot"
-" x^n`"
+msgid ":math:`y = a \\cdot x^n + b \\cdot x^{n-1} + c \\cdot x^{n-2} + \\ldots`"
+msgstr ":math:`y = a \\cdot x^n + b \\cdot x^{n-1} + c \\cdot x^{n-2} + \\ldots`"
+
+msgid "``a``, ``b``, ``c``, ..."
+msgstr "``a``, ``b``, ``c``, ..."
msgid ""
":math:`y = y_{0} + A \\exp\\left(-\\dfrac{1}{2} \\left(\\dfrac{x - "
@@ -739,6 +748,11 @@ msgstr ""
":math:`y = y_{0} + A \\exp\\left(-\\dfrac{1}{2} \\left(\\dfrac{x - "
"x_{0}}{\\sigma}\\right)^2\\right)`"
+msgid ""
+":math:`A`: ``amplitude``, :math:`\\sigma`: ``sigma``, :math:`x_0`: "
+"``x0``, :math:`y_0`: ``y0``"
+msgstr ":math:`A`: ``amplitude``, :math:`\\sigma`: ``sigma``, :math:`x_0`: ``x0``, :math:`y_0`: ``y0``"
+
msgid "Lorentzian"
msgstr "Lorentzien"
@@ -773,6 +787,12 @@ msgstr ""
":math:`y = y_{0} + \\sum_{i=1}^{N} A_{i} \\exp\\left(-\\dfrac{1}{2} "
"\\left(\\dfrac{x - x_{0,i}}{\\sigma_{i}}\\right)^2\\right)`"
+#, python-brace-format
+msgid ""
+":math:`A_i`: ``amplitude_i``, :math:`\\sigma_i`: ``sigma_i``, "
+":math:`x_{0,i}`: ``x0_i``, :math:`y_0`: ``y0``"
+msgstr ":math:`A_i`: ``amplitude_i``, :math:`\\sigma_i`: ``sigma_i``, :math:`x_{0,i}`: ``x0_i``, :math:`y_0`: ``y0``"
+
msgid "Multi-Lorentzian"
msgstr "Multi-lorentzien"
@@ -790,57 +810,66 @@ msgid ""
":math:`y = y_{0} + A \\cdot \\left(\\dfrac{x}{x_{0}}\\right)^{-5} \\cdot "
"\\left(\\exp\\left(\\dfrac{5 x_{0}}{\\sigma \\cdot "
"x}\\right)-1\\right)^{-1}` where :math:`x_{0}` and :math:`\\sigma` are "
-"scale and width factors (see the note below)"
-msgstr ""
-":math:`y = y_{0} + A \\cdot \\left(\\dfrac{x}{x_{0}}\\right)^{-5} \\cdot "
-"\\left(\\exp\\left(\\dfrac{5 x_{0}}{\\sigma \\cdot "
-"x}\\right)-1\\right)^{-1}` où :math:`x_{0}` et :math:`\\sigma` sont des "
-"facteurs d'échelle et de largeur (voir la note ci-dessous)"
+"scale and temperature factors (see the note below)"
+msgstr ":math:`y = y_{0} + A \\cdot \\left(\\dfrac{x}{x_{0}}\\right)^{-5} \\cdot \\left(\\exp\\left(\\dfrac{5 x_{0}}{\\sigma \\cdot x}\\right)-1\\right)^{-1}` où :math:`x_{0}` et :math:`\\sigma` sont des facteurs d'échelle et de température (voir la note ci-dessous)"
+
+msgid ""
+":math:`A`: ``amp``, :math:`x_0`: ``x0``, :math:`\\sigma`: ``sigma``, "
+":math:`y_0`: ``y0``"
+msgstr ":math:`A`: ``amp``, :math:`x_0`: ``x0``, :math:`\\sigma`: ``sigma``, :math:`y_0`: ``y0``"
msgid "Two half Gaussians"
msgstr "Deux demi-gaussiennes"
msgid ""
-":math:`y = y_{L} + A_{L} \\cdot "
+":math:`y = y_{0,L} + A_{L} \\cdot "
"\\exp\\left(-\\dfrac{1}{2}\\left(\\dfrac{x - "
"x_{0}}{\\sigma_{L}}\\right)^2\\right)` if :math:`x < x_{0}`"
-msgstr ""
-":math:`y = y_{L} + A_{L} \\cdot "
-"\\exp\\left(-\\dfrac{1}{2}\\left(\\dfrac{x - "
-"x_{0}}{\\sigma_{L}}\\right)^2\\right)` si :math:`x < x_{0}`"
+msgstr ":math:`y = y_{0,L} + A_{L} \\cdot \\exp\\left(-\\dfrac{1}{2}\\left(\\dfrac{x - x_{0}}{\\sigma_{L}}\\right)^2\\right)` si :math:`x < x_{0}`"
msgid ""
-":math:`y = y_{R} + A_{R} \\cdot "
+":math:`y = y_{0,R} + A_{R} \\cdot "
"\\exp\\left(-\\dfrac{1}{2}\\left(\\dfrac{x - "
"x_{0}}{\\sigma_{R}}\\right)^2\\right)` otherwise"
-msgstr ""
-":math:`y = y_{R} + A_{R} \\cdot "
-"\\exp\\left(-\\dfrac{1}{2}\\left(\\dfrac{x - "
-"x_{0}}{\\sigma_{R}}\\right)^2\\right)` sinon"
+msgstr ":math:`y = y_{0,R} + A_{R} \\cdot \\exp\\left(-\\dfrac{1}{2}\\left(\\dfrac{x - x_{0}}{\\sigma_{R}}\\right)^2\\right)` sinon"
+
+#, python-brace-format
+msgid ""
+":math:`A_L`: ``amp_left``, :math:`A_R`: ``amp_right``, :math:`\\sigma_L`:"
+" ``sigma_left``, :math:`\\sigma_R`: ``sigma_right``, :math:`x_0`: ``x0``,"
+" :math:`y_{0,L}`: ``y0_left``, :math:`y_{0,R}`: ``y0_right``"
+msgstr ":math:`A_L`: ``amp_left``, :math:`A_R`: ``amp_right``, :math:`\\sigma_L`: ``sigma_left``, :math:`\\sigma_R`: ``sigma_right``, :math:`x_0`: ``x0``, :math:`y_{0,L}`: ``y0_left``, :math:`y_{0,R}`: ``y0_right``"
msgid "Two back-to-back exponentials"
msgstr "Deux exponentielles dos à dos"
#, python-brace-format
msgid ""
-":math:`y = y_{0} + A_{L} \\cdot \\exp\\left(B_{L} \\cdot x\\right)` if "
-":math:`x < x_{c}`"
-msgstr ""
-":math:`y = y_{0} + A_{L} \\cdot \\exp\\left(B_{L} \\cdot x\\right)` si "
+":math:`y = y_{0} + a_{L} \\cdot \\exp\\left(b_{L} \\cdot x\\right)` if "
":math:`x < x_{c}`"
+msgstr ":math:`y = y_{0} + a_{L} \\cdot \\exp\\left(b_{L} \\cdot x\\right)` si :math:`x < x_{c}`"
#, python-brace-format
msgid ""
-":math:`y = y_{0} + A_{R} \\cdot \\exp\\left(B_{R} \\cdot x\\right)` "
+":math:`y = y_{0} + a_{R} \\cdot \\exp\\left(b_{R} \\cdot x\\right)` "
"otherwise"
-msgstr ":math:`y = y_{0} + A_{R} \\cdot \\exp\\left(B_{R} \\cdot x\\right)` sinon"
+msgstr ":math:`y = y_{0} + a_{R} \\cdot \\exp\\left(b_{R} \\cdot x\\right)` sinon"
+
+msgid ""
+":math:`x_c`: ``x_center``, :math:`a_L`: ``a_left``, :math:`b_L`: "
+"``b_left``, :math:`a_R`: ``a_right``, :math:`b_R`: ``b_right``, "
+":math:`y_0`: ``y0``"
+msgstr ":math:`x_c`: ``x_center``, :math:`a_L`: ``a_left``, :math:`b_L`: ``b_left``, :math:`a_R`: ``a_right``, :math:`b_R`: ``b_right``, :math:`y_0`: ``y0``"
msgid "Exponential"
msgstr "Exponentielle"
#, python-brace-format
-msgid ":math:`y = y_{0} + A \\exp\\left(B \\cdot x\\right)`"
-msgstr ":math:`y = y_{0} + A \\exp\\left(B \\cdot x\\right)`"
+msgid ":math:`y = y_{0} + a \\exp\\left(b \\cdot x\\right)`"
+msgstr ":math:`y = y_{0} + a \\exp\\left(b \\cdot x\\right)`"
+
+msgid "``a``, ``b``, :math:`y_0`: ``y0``"
+msgstr "``a``, ``b``, :math:`y_0`: ``y0``"
msgid "Sinusoidal"
msgstr "Sinusoïdal"
@@ -849,15 +878,23 @@ msgstr "Sinusoïdal"
msgid ":math:`y = y_{0} + A \\sin\\left(2 \\pi f \\cdot x + \\phi\\right)`"
msgstr ":math:`y = y_{0} + A \\sin\\left(2 \\pi f \\cdot x + \\phi\\right)`"
+msgid ""
+":math:`A`: ``amplitude``, :math:`f`: ``frequency``, :math:`\\phi`: "
+"``phase`` (in radians), :math:`y_0`: ``offset``"
+msgstr ":math:`A`: ``amplitude``, :math:`f`: ``frequency``, :math:`\\phi`: ``phase`` (en radians), :math:`y_0`: ``offset``"
+
msgid "Cumulative Distribution Function (CDF)"
msgstr "Fonction de distribution cumulative (CDF)"
msgid ""
-":math:`y = y_{0} + A \\erf\\left(\\dfrac{x - x_{0}}{\\sqrt{2} "
-"\\sigma}\\right)`"
-msgstr ""
-":math:`y = y_{0} + A \\erf\\left(\\dfrac{x - x_{0}}{\\sqrt{2} "
+":math:`y = y_{0} + A \\erf\\left(\\dfrac{x - \\mu}{\\sqrt{2} "
"\\sigma}\\right)`"
+msgstr ":math:`y = y_{0} + A \\erf\\left(\\dfrac{x - \\mu}{\\sqrt{2} \\sigma}\\right)`"
+
+msgid ""
+":math:`A`: ``amplitude``, :math:`\\mu`: ``mu``, :math:`\\sigma`: "
+"``sigma``, :math:`y_0`: ``baseline``"
+msgstr ":math:`A`: ``amplitude``, :math:`\\mu`: ``mu``, :math:`\\sigma`: ``sigma``, :math:`y_0`: ``baseline``"
msgid "Sigmoid"
msgstr "Sigmoïde"
@@ -869,6 +906,11 @@ msgstr ""
":math:`y = y_{0} + \\dfrac{A}{1 + \\exp\\left(-k \\left(x - "
"x_{0}\\right)\\right)}`"
+msgid ""
+":math:`A`: ``amplitude``, ``k``, :math:`x_0`: ``x0``, :math:`y_0`: "
+"``offset``"
+msgstr ":math:`A`: ``amplitude``, ``k``, :math:`x_0`: ``x0``, :math:`y_0`: ``offset``"
+
msgid ""
"For Gaussian, Lorentzian and Voigt models, :math:`A` is the signed peak "
"height above the baseline :math:`y_0`, expressed in the signal's Y unit."
@@ -896,6 +938,28 @@ msgstr ""
"situe approximativement en :math:`1.007 \\, x_{0}/\\sigma`, et non en "
":math:`x_{0}`."
+msgid ""
+"The physical quantities are derived from these combinations. With "
+"wavelengths in meters, the model matches Planck's law (spectral radiance "
+"per unit wavelength) for:"
+msgstr "Les grandeurs physiques se déduisent de ces combinaisons. Avec des longueurs d'onde en mètres, le modèle correspond à la loi de Planck (luminance énergétique spectrale par unité de longueur d'onde) pour :"
+
+msgid ""
+"a temperature :math:`T = \\dfrac{h c \\, \\sigma}{5 k_B \\, x_{0}} "
+"\\approx 2.878 \\times 10^{-3} \\, \\dfrac{\\sigma}{x_{0}}` K,"
+msgstr "une température :math:`T = \\dfrac{h c \\, \\sigma}{5 k_B \\, x_{0}} \\approx 2.878 \\times 10^{-3} \\, \\dfrac{\\sigma}{x_{0}}` K,"
+
+msgid ""
+"an emissivity (or any other scale factor of the measurement) "
+":math:`\\varepsilon = \\dfrac{A \\, x_{0}^5}{2 h c^2}`, when the signal "
+"is a spectral radiance in W·sr⁻¹·m⁻³."
+msgstr "une émissivité (ou tout autre facteur d'échelle de la mesure) :math:`\\varepsilon = \\dfrac{A \\, x_{0}^5}{2 h c^2}`, lorsque le signal est une luminance énergétique spectrale en W·sr⁻¹·m⁻³."
+
+msgid ""
+"For example, fitting the blackbody signal created at 293 K gives back "
+":math:`T = 293` K and :math:`\\varepsilon = 1`."
+msgstr "Par exemple, l'ajustement du signal de corps noir créé à 293 K redonne :math:`T = 293` K et :math:`\\varepsilon = 1`."
+
msgid "Multi-peak fit parameters"
msgstr "Paramètres d'ajustement multi-pic"
@@ -1335,21 +1399,3 @@ msgid ""
msgstr ""
"L'option \"Toutes les analyses de stabilité\" permet de calculer toutes "
"les méthodes d'analyse de stabilité en une seule fois."
-
-#~ msgid ""
-#~ ":math:`y = y_{0} + A\\cdot \\dfrac{2h"
-#~ " \\cdot c^2}{\\lambda^5} \\cdot "
-#~ "\\left(\\exp\\left(\\dfrac{h \\cdot c}{\\lambda "
-#~ "\\cdot k \\cdot T}\\right)-1\\right)^{-1}`"
-#~ msgstr ""
-
-#~ msgid ""
-#~ ":math:`y = y_{0} + A \\cdot "
-#~ "\\dfrac{1}{\\sqrt{2 \\pi} \\sigma_{1}} \\cdot "
-#~ "\\exp\\left(-\\dfrac{1}{2}\\left(\\dfrac{x - "
-#~ "x_{1}}{\\sigma_{1}}\\right)^2\\right)` otherwise"
-#~ msgstr ""
-#~ ":math:`y = y_{0} + A \\cdot "
-#~ "\\dfrac{1}{\\sqrt{2 \\pi} \\sigma_{1}} \\cdot "
-#~ "\\exp\\left(-\\dfrac{1}{2}\\left(\\dfrac{x - "
-#~ "x_{1}}{\\sigma_{1}}\\right)^2\\right)` sinon"
diff --git a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po
index f4c8efeef..d4550f362 100644
--- a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po
+++ b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.04.po
@@ -246,9 +246,30 @@ msgstr "Lorsqu'un objet source est supprimé, sa référence est affichée sous
msgid "Titles of workspaces saved by previous versions keep their former `s001`-style references as plain text"
msgstr "Les titres des espaces de travail enregistrés par les versions précédentes conservent leurs anciennes références de type `s001` sous forme de texte simple"
+msgid "**Curve fitting formulas:**"
+msgstr "**Formules des modèles d'ajustement :**"
+
+msgid "Interactive fitting dialogs now show the formula of the fitted model below the fit parameters, and each parameter label shows its symbol in the formula (e.g. \"Scale factor (x₀)\"): the parameters stored in the fitted signal metadata can thus be related to the model without reading the source code. The *Fitting* documentation also gives, for each model, the name under which each parameter is stored (fixes [Issue #400](https://github.com/DataLab-Platform/DataLab/issues/400))"
+msgstr "Les fenêtres d'ajustement interactif affichent désormais la formule du modèle ajusté sous les paramètres d'ajustement, et le libellé de chaque paramètre indique son symbole dans la formule (par exemple \"Facteur d'échelle (x₀)\") : les paramètres enregistrés dans les métadonnées du signal ajusté peuvent ainsi être reliés au modèle sans lire le code source. La documentation *Ajustement* indique également, pour chaque modèle, le nom sous lequel chaque paramètre est enregistré (corrige [l'issue #400](https://github.com/DataLab-Platform/DataLab/issues/400))"
+
+msgid "Polynomial and linear fit coefficients are labelled `a`, `b`, `c`... as in the formula and in the metadata, instead of `c2`, `c1`, `c0`"
+msgstr "Les coefficients des ajustements polynomial et linéaire sont nommés `a`, `b`, `c`... comme dans la formule et dans les métadonnées, au lieu de `c2`, `c1`, `c0`"
+
+msgid "The *Fitting* documentation explains how to derive the peak wavelength, the temperature and the emissivity from the parameters of a Planckian fit"
+msgstr "La documentation *Ajustement* explique comment déduire la longueur d'onde du pic, la température et l'émissivité des paramètres d'un ajustement planckien"
+
msgid "🔄 Changes"
msgstr "🔄 Changements"
+msgid "**Synthetic images in physical coordinates:**"
+msgstr "**Images synthétiques en coordonnées physiques :**"
+
+msgid "The 2D Gaussian, 2D ramp, sinusoidal grating, ring pattern, Siemens star and 2D sinc images were computed over the [xmin, xmax] × [ymin, ymax] domain of their creation parameters, but were displayed in pixel coordinates: their centers, widths and spatial frequencies did not match the axes (a sinusoidal grating of frequency 0.1 over [0, 100] showed 10 periods along an axis graduated from 0 to 1023). These images are now displayed over their domain. The new **Physical coordinates** option of their creation parameters restores pixel coordinates when disabled (fixes [Issue #400](https://github.com/DataLab-Platform/DataLab/issues/400))"
+msgstr "Les images gaussienne 2D, rampe 2D, réseau sinusoïdal, anneaux, mire de Siemens et sinus cardinal 2D étaient calculées sur le domaine [xmin, xmax] × [ymin, ymax] de leurs paramètres de création, mais étaient affichées en coordonnées pixel : leurs centres, largeurs et fréquences spatiales ne correspondaient pas aux axes (un réseau sinusoïdal de fréquence 0,1 sur [0, 100] présentait 10 périodes le long d'un axe gradué de 0 à 1023). Ces images sont désormais affichées sur leur domaine. La nouvelle option **Coordonnées physiques** de leurs paramètres de création rétablit les coordonnées pixel lorsqu'elle est désactivée (corrige [l'issue #400](https://github.com/DataLab-Platform/DataLab/issues/400))"
+
+msgid "Images created with earlier versions keep their pixel coordinates when their creation parameters are edited or recomputed"
+msgstr "Les images créées avec des versions antérieures conservent leurs coordonnées pixel lorsque leurs paramètres de création sont modifiés ou recalculés"
+
msgid "**Guided tour:**"
msgstr "**Visite guidée :**"
@@ -336,3 +357,17 @@ msgstr "Correction des références aux sources illisibles dans les titres lorsq
msgid "Fixed result titles pointing to the wrong source objects after appending an HDF5 workspace to a non-empty session; appending the same workspace twice now also keeps each imported processing chain independent, so **Recompute** and **Select source objects** use the sources of the same import (fixes [Issue #357](https://github.com/DataLab-Platform/DataLab/issues/357))"
msgstr "Correction des titres de résultats qui désignaient de mauvais objets sources après l'ajout d'un espace de travail HDF5 à une session non vide ; ajouter deux fois le même espace de travail conserve désormais aussi des chaînes de traitement importées indépendantes, de sorte que **Retraiter** et **Sélectionner les objets source** utilisent les sources issues du même import (corrige [Issue #357](https://github.com/DataLab-Platform/DataLab/issues/357))"
+msgid "**Curve fitting:**"
+msgstr "**Ajustement de courbes :**"
+
+msgid "Fixed the automatic Planckian fit (**Processing > Fitting > Planckian fit**) returning a curve about 150 times lower than the signal on data in physical units, such as a blackbody signal with wavelengths in meters; the interactive Planckian fit dialog also starts from a curve matching the signal peak (fixes [Issue #400](https://github.com/DataLab-Platform/DataLab/issues/400))"
+msgstr "Correction de l'ajustement planckien automatique (**Traitement > Ajustement > Ajustement planckien**), qui renvoyait une courbe environ 150 fois plus basse que le signal sur des données en unités physiques, comme un signal de corps noir avec des longueurs d'onde en mètres ; la fenêtre d'ajustement planckien interactif part également d'une courbe correspondant au pic du signal (corrige [l'issue #400](https://github.com/DataLab-Platform/DataLab/issues/400))"
+
+msgid "Fixed exponential fitting failing on short time constants depending on the X unit, e.g. a 1 ms time constant with X in seconds (automatic and interactive fits)"
+msgstr "Correction de l'ajustement exponentiel, qui échouait sur des constantes de temps courtes selon l'unité de X, par exemple une constante de temps de 1 ms avec X en secondes (ajustements automatique et interactif)"
+
+msgid "**Signal creation:**"
+msgstr "**Création de signaux :**"
+
+msgid "The blackbody signal (**Create > Blackbody (Planck's law)**) now has axis titles and units (wavelength in m, spectral radiance in W·sr⁻¹·m⁻³), which were missing (fixes [Issue #400](https://github.com/DataLab-Platform/DataLab/issues/400))"
+msgstr "Le signal de corps noir (**Création > Corps noir (loi de Planck)**) a désormais des titres et des unités d'axes (longueur d'onde en m, luminance énergétique spectrale en W·sr⁻¹·m⁻³), qui étaient absents (corrige [l'issue #400](https://github.com/DataLab-Platform/DataLab/issues/400))"
diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md
index 6546eee18..718d4b8e4 100644
--- a/doc/release_notes/release_1.04.md
+++ b/doc/release_notes/release_1.04.md
@@ -98,8 +98,19 @@ DataLab now uses stable UUID-based object references across object trees, result
* When a source object is deleted, its reference falls back to its 8-character UUID instead of becoming an anonymous placeholder
* Titles of workspaces saved by previous versions keep their former `s001`-style references as plain text
+**Curve fitting formulas:**
+
+* Interactive fitting dialogs now show the formula of the fitted model below the fit parameters, and each parameter label shows its symbol in the formula (e.g. "Scale factor (x₀)"): the parameters stored in the fitted signal metadata can thus be related to the model without reading the source code. The *Fitting* documentation also gives, for each model, the name under which each parameter is stored (fixes [Issue #400](https://github.com/DataLab-Platform/DataLab/issues/400))
+* Polynomial and linear fit coefficients are labelled `a`, `b`, `c`... as in the formula and in the metadata, instead of `c2`, `c1`, `c0`
+* The *Fitting* documentation explains how to derive the peak wavelength, the temperature and the emissivity from the parameters of a Planckian fit
+
### 🔄 Changes ###
+**Synthetic images in physical coordinates:**
+
+* The 2D Gaussian, 2D ramp, sinusoidal grating, ring pattern, Siemens star and 2D sinc images were computed over the [xmin, xmax] × [ymin, ymax] domain of their creation parameters, but were displayed in pixel coordinates: their centers, widths and spatial frequencies did not match the axes (a sinusoidal grating of frequency 0.1 over [0, 100] showed 10 periods along an axis graduated from 0 to 1023). These images are now displayed over their domain. The new **Physical coordinates** option of their creation parameters restores pixel coordinates when disabled (fixes [Issue #400](https://github.com/DataLab-Platform/DataLab/issues/400))
+* Images created with earlier versions keep their pixel coordinates when their creation parameters are edited or recomputed
+
**Guided tour:**
* The guided tour no longer starts automatically at first launch: it is now offered from the welcome page, and remains available from the **?** menu
@@ -147,3 +158,12 @@ DataLab now uses stable UUID-based object references across object trees, result
* Fixed plot legends still showing the original generated title after renaming an object, e.g. a profile extracted from an image (fixes [Issue #323](https://github.com/DataLab-Platform/DataLab/issues/323))
* Fixed source references in titles being unreadable when the object is selected in the signal or image tree, notably on Linux (fixes [Issue #358](https://github.com/DataLab-Platform/DataLab/issues/358))
* Fixed result titles pointing to the wrong source objects after appending an HDF5 workspace to a non-empty session; appending the same workspace twice now also keeps each imported processing chain independent, so **Recompute** and **Select source objects** use the sources of the same import (fixes [Issue #357](https://github.com/DataLab-Platform/DataLab/issues/357))
+
+**Curve fitting:**
+
+* Fixed the automatic Planckian fit (**Processing > Fitting > Planckian fit**) returning a curve about 150 times lower than the signal on data in physical units, such as a blackbody signal with wavelengths in meters; the interactive Planckian fit dialog also starts from a curve matching the signal peak (fixes [Issue #400](https://github.com/DataLab-Platform/DataLab/issues/400))
+* Fixed exponential fitting failing on short time constants depending on the X unit, e.g. a 1 ms time constant with X in seconds (automatic and interactive fits)
+
+**Signal creation:**
+
+* The blackbody signal (**Create > Blackbody (Planck's law)**) now has axis titles and units (wavelength in m, spectral radiance in W·sr⁻¹·m⁻³), which were missing (fixes [Issue #400](https://github.com/DataLab-Platform/DataLab/issues/400))