diff --git a/src/ess/estia/normalization.py b/src/ess/estia/normalization.py index 82c99def..5140d44b 100644 --- a/src/ess/estia/normalization.py +++ b/src/ess/estia/normalization.py @@ -23,6 +23,7 @@ angular_resolution, q_resolution, sample_size_resolution, + wavelength_resolution, ) @@ -101,9 +102,8 @@ def evaluate_reference( ), { **graph, - # The wavelength resolution of Estia is not implemented yet - # when it is this will be replaced by the correct value. - "wavelength_resolution": lambda: sc.scalar(0.05), + "source_pulse_length": lambda: sc.scalar(3.0, unit='ms').to(unit='ns'), + "wavelength_resolution": wavelength_resolution, "sample_size_resolution": sample_size_resolution, "angular_resolution": angular_resolution, "Q_resolution": q_resolution, diff --git a/src/ess/estia/resolution.py b/src/ess/estia/resolution.py index 75205945..63f5f48b 100644 --- a/src/ess/estia/resolution.py +++ b/src/ess/estia/resolution.py @@ -1,12 +1,15 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2025 Scipp contributors (https://github.com/scipp) import scipp as sc +from scipp import constants from ..reflectometry.tools import fwhm_to_std def wavelength_resolution( - # What parameters are needed? + source_pulse_length: sc.Variable, + wavelength: sc.Variable, + Ltotal: sc.Variable, ): """ Find the wavelength resolution contribution of the ESTIA instrument. @@ -14,18 +17,27 @@ def wavelength_resolution( Parameters ---------- - L1: - Distance from midpoint between choppers to sample. - L2: - Distance from sample to detector. + source_pulse_length: + The length of the ESS source pulse. + wavelength: + The wavelength of the neutron. + Ltotal: + The distance from source to detector. Returns ------- : - The wavelength resolution variable, as standard deviation. + The wavelength resolution, + ratio of the standard deviation of wavelength and the wavelength. """ - # Don't yet know how to do this - raise NotImplementedError() + # The exact factor depends on the shape of the source pulse. + # The factor here assumes a rectangular source pulse: + # import sympy as sp + # x, D = sp.symbols('x, D', positive=True) + # sp.sqrt(sp.integrate(1/D * (x-D/2)**2, (x, 0, D))) + standard_deviation_of_time_at_source = (1 / 12**0.5) * source_pulse_length + tof = wavelength * Ltotal * constants.m_n / constants.h + return (standard_deviation_of_time_at_source / tof).to(unit='dimensionless') def sample_size_resolution( @@ -72,7 +84,7 @@ def angular_resolution( Returns ------- : - Angular resolution standard deviation + Angular resolution standard deviation over the reflection angle. """ return ( fwhm_to_std( diff --git a/src/ess/reflectometry/tools.py b/src/ess/reflectometry/tools.py index f77f823b..b7077cae 100644 --- a/src/ess/reflectometry/tools.py +++ b/src/ess/reflectometry/tools.py @@ -440,22 +440,42 @@ def combine_curves( if len({c.coords['Q'].unit for c in curves}) != 1: raise ValueError('The Q-coordinates must have the same unit for each curve') - r = _interpolate_on_qgrid(map(sc.values, curves), q_bin_edges).values - v = _interpolate_on_qgrid(map(sc.variances, curves), q_bin_edges).values + r = _interpolate_on_qgrid(map(sc.values, curves), q_bin_edges) + v = _interpolate_on_qgrid(map(sc.variances, curves), q_bin_edges) - v[v == 0] = np.nan + v = sc.where(v == sc.scalar(0.0, unit=v.unit), sc.scalar(np.nan, unit=v.unit), v) inv_v = 1.0 / v - r_avg = np.nansum(r * inv_v, axis=0) / np.nansum(inv_v, axis=0) - v_avg = 1 / np.nansum(inv_v, axis=0) - return sc.DataArray( + r_avg = sc.nansum(r * inv_v, dim='curves') / sc.nansum(inv_v, dim='curves') + v_avg = 1 / sc.nansum(inv_v, dim='curves') + + out = sc.DataArray( data=sc.array( dims='Q', - values=r_avg, - variances=v_avg, + values=r_avg.values, + variances=v_avg.values, unit=next(iter(curves)).data.unit, ), coords={'Q': q_bin_edges}, ) + if any('Q_resolution' in c.coords for c in curves): + # This might need to be revisited. The question about how to combine curves + # with different Q-resolution is not completely resolved. + # However, in practice the difference in Q-resolution between different curves + # is small so it's not likely to make a big difference. + q_res = ( + sc.DataArray( + data=c.coords.get( + 'Q_resolution', sc.full_like(c.coords['Q'], value=np.nan) + ), + coords={'Q': c.coords['Q']}, + ) + for c in curves + ) + qs = _interpolate_on_qgrid(q_res, q_bin_edges) + out.coords['Q_resolution'] = sc.nansum(qs * inv_v, dim='curves') / sc.nansum( + sc.where(sc.isnan(qs), sc.scalar(0.0, unit=inv_v.unit), inv_v), dim='curves' + ) + return out def batch_processor( diff --git a/tests/reflectometry/tools_test.py b/tests/reflectometry/tools_test.py index 3d4e52be..24a395af 100644 --- a/tests/reflectometry/tools_test.py +++ b/tests/reflectometry/tools_test.py @@ -213,6 +213,35 @@ def test_combined_curves(): ) +@pytest.mark.filterwarnings("ignore:invalid value encountered in divide") +def test_combined_curves_resolution(): + qgrid = sc.linspace('Q', 0, 1, 26) + data = sc.concat( + ( + sc.ones(dims=['Q'], shape=[10], with_variances=True), + 0.5 * sc.ones(dims=['Q'], shape=[15], with_variances=True), + ), + dim='Q', + ) + + def curve(d, qmin, qmax): + return sc.DataArray( + data=d, coords={'Q': sc.linspace('Q', qmin, qmax, len(d) + 1)} + ) + + data.variances[:] = 0.1 + curves = ( + curve(data, 0, 0.3), + curve(0.5 * data, 0.2, 0.7), + curve(0.25 * data, 0.6, 1.0), + ) + curves[0].coords['Q_resolution'] = sc.midpoints(curves[0].coords['Q']) / 5 + combined = combine_curves(curves, qgrid) + assert 'Q_resolution' in combined.coords + assert combined.coords['Q_resolution'][0] == curves[0].coords['Q_resolution'][1] + assert sc.isnan(combined.coords['Q_resolution'][-1]) + + def test_linlogspace_linear(): q_lin = linlogspace( dim='qz', edges=[0.008, 0.08], scale='linear', num=50, unit='1/angstrom'