From fea635565983336783d523e96843edd7083b537b Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Tue, 4 Feb 2025 12:17:34 -0500 Subject: [PATCH 1/5] when using time sliced transmission run, skip time slices for which the transmission calculation fails --- docs/index.rst | 3 +- docs/release_notes.rst | 3 +- docs/user/corrections/index.rst | 9 ++ docs/user/corrections/transmission.rst | 109 ++++++++++++++++++ docs/user/reduction_output.rst | 4 +- src/drtsans/configuration/schema/common.json | 6 +- src/drtsans/mono/biosans/api.py | 7 +- src/drtsans/mono/transmission.py | 43 ++++++- src/drtsans/transmission.py | 52 ++++++++- .../drtsans/mono/biosans/test_api.py | 9 +- .../drtsans/tof/eqsans/test_transmission.py | 3 +- tests/unit/drtsans/test_transmission.py | 34 +++++- 12 files changed, 266 insertions(+), 16 deletions(-) create mode 100644 docs/user/corrections/index.rst create mode 100644 docs/user/corrections/transmission.rst diff --git a/docs/index.rst b/docs/index.rst index 961f81bc7..8631e3995 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,9 +13,10 @@ User Guide :maxdepth: 2 /user/calibration - /user/reduction_output + /user/corrections/index /user/slicing /user/binning + /user/reduction_output /drtsans/reduction_parameters /drtsans/reduction_scripts release_notes diff --git a/docs/release_notes.rst b/docs/release_notes.rst index e62afc5d8..b889eb3cf 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -23,6 +23,7 @@ Release Notes **Of interest to the User**: +- PR #993: Skip slices with too high transmission error when using time sliced sample transmission run - PR #325: Migrates repository from GitLab to GitHub - MR #1185: Rename output directory for coherent and incoherent-inelastic corrections as "info" - MR #1184: Document wedge binning @@ -37,7 +38,7 @@ Release Notes - MR #1167: Allow separate configurations for inelastic incoherence correction per frame in frame skipping mode - MR #1166: Added option to _fitSpectrum to auto-find one wedge and mirror it - MR #1162: When reducing `gpsans` data with `direct_beam` scaling, the `direct_beam_scaling` parameter is now logged during -the reduction process and stored in the output Nexus file at `reduction_information/special_parameters/direct_beam_scaling/value`. + the reduction process and stored in the output Nexus file at `reduction_information/special_parameters/direct_beam_scaling/value`. - MR #1161: Add a parameters removeAlgorithmHistory to write less data and speed up I/O during reduction - MR #1160: Expose pixel detector rescalings to the instrument API's - MR #1159: Separate configuration for elastic normalization and inelastic incoherence correction diff --git a/docs/user/corrections/index.rst b/docs/user/corrections/index.rst new file mode 100644 index 000000000..cafc05b22 --- /dev/null +++ b/docs/user/corrections/index.rst @@ -0,0 +1,9 @@ +.. _user.corrections.index: + +Corrections +=========== + +.. toctree:: + :maxdepth: 2 + + /user/corrections/transmission diff --git a/docs/user/corrections/transmission.rst b/docs/user/corrections/transmission.rst new file mode 100644 index 000000000..af61be548 --- /dev/null +++ b/docs/user/corrections/transmission.rst @@ -0,0 +1,109 @@ +.. _user.corrections.transmission: + +Transmission +============ + +The measured intensity must be corrected to account for sample transmission: + +.. math:: + + I'_{sample}(x,y,\lambda) = \frac{I_{sample}(x,y,\lambda)}{T(\lambda,x,y)} + +The transmission correction is calculated using a transmission run and an empty beam transmission +run (reference). +It is possible to specify the transmission value directly in the transmission parameter ``"value"``, +however, this is mostly used for diagnostic purposes. + +.. code-block:: json + + { + "sample": { + "runNumber": None, + "loadOptions": {}, + "thickness": 1.0, + "transmission": { + "runNumber": None, + "value": None, + "errorTolerance": None + } + }, + "background": { + "runNumber": None, + "transmission": { + "runNumber": None, + "value": None + } + }, + "emptyTransmission": { + "runNumber": None, + "value": None + }, + "mmRadiusForTransmission": None, + "useTimeSliceTransmission": False, + "useThetaDepTransCorrection": True, + } + +Time Slice Transmission (Bio-SANS only) +--------------------------------------- + +When using time slicing (``"useTimeSlice": True``), users can optionally calculate the transmission +correction using the time sliced sample run by setting the parameter +``"useTimeSliceTransmission": True``. The sample transmission run number is ignored when +``"useTimeSliceTransmission": True``. The time slice transmission option can be used when the sample +transmission is expected to change over time. + +Time slices for which the transmission calculation fails will be skipped. The transmission +calculation can fail due to all transmission values being NaN or if the transmission error is +higher than the allowed relative transmission error, which is configurable in the sample +transmission parameter ``"errorTolerance"`` (default: 0.01). + +.. code-block:: json + + { + "sample": { + "runNumber": 10010, + "loadOptions": {}, + "thickness": 1.0, + "transmission": { + "runNumber": None, + "value": None, + "errorTolerance": 0.05 + } + }, + "emptyTransmission": { + "runNumber": 10005, + "value": None + }, + "useTimeSlice": True, + "timeSliceInterval": 100.0, + "useTimeSliceTransmission": True, + } + +Parameters +---------- + +.. list-table:: + :widths: 25 65 10 + :header-rows: 1 + + * - Parameter + - Description + - Default + * - ``"mmRadiusForTransmission"`` + - Beam radius within which the transmission will be calculated. If ``None``, then the beam + radius is calculated from the sample logs. + - ``None`` + * - ``"useThetaDepTransCorrection"`` + - If ``True``, a theta dependent transmission correction will be applied, which takes into + account the effect of the scattering angle on the transmission. + - ``True`` + * - ``"useTimeSliceTransmission"`` + - (`Only for Bio-SANS and when` ``"useTimeSlice": True``.) If ``True``, the transmission + correction will be calculated using the time sliced sample run itself instead of a separate + sample transmission run. This is useful when the sample transmission is expected to change + over time. Slices with relative transmission error larger than + ``"transmissionErrorTolerance"`` will be skipped. + - ``False`` + * - ``"errorTolerance"`` + - (`Only for Bio-SANS and when` ``"useTimeSlice": True`` `and` ``"useTimeSliceTransmission": True``.) Maximum relative transmission error. + - 0.01 diff --git a/docs/user/reduction_output.rst b/docs/user/reduction_output.rst index 077462cda..bdadd335b 100644 --- a/docs/user/reduction_output.rst +++ b/docs/user/reduction_output.rst @@ -5,8 +5,8 @@ Reduction Output -CANSAS ------- +canSAS format +------------- Collective Action for Nomadic Small-Angle Scatterers (canSAS) provides standards and tools for the small-angle scattering user community. See more at https://www.cansas.org diff --git a/src/drtsans/configuration/schema/common.json b/src/drtsans/configuration/schema/common.json index c91a9cf12..87ae452a1 100644 --- a/src/drtsans/configuration/schema/common.json +++ b/src/drtsans/configuration/schema/common.json @@ -197,9 +197,13 @@ }, "value": { "$ref": "common.json#/definitions/transmissionValueTypes" + }, + "errorTolerance": { + "$ref": "common.json#/definitions/transmissionValueTypes", + "description": "Maximum allowed relative error in the calculated transmission" } }, - "maxProperties": 2, + "maxProperties": 3, "required": ["runNumber", "value"], "description": "The transmission for the sample, run number or value (0 < value <=1). Can be empty", "examples": ["0.9", 1.0] diff --git a/src/drtsans/mono/biosans/api.py b/src/drtsans/mono/biosans/api.py index d3b8a7c51..97d0ecaf5 100644 --- a/src/drtsans/mono/biosans/api.py +++ b/src/drtsans/mono/biosans/api.py @@ -31,6 +31,7 @@ from drtsans.stitch import stitch_binned_profiles from drtsans.reductionlog import savereductionlog from drtsans.thickness_normalization import normalize_by_thickness +from drtsans.transmission import TransmissionErrorToleranceError, TransmissionNanError # third party imports from mantid.dataobjects import Workspace2D @@ -1205,6 +1206,9 @@ def reduce_single_configuration( absolute_scale = reduction_config["StandardAbsoluteScale"] time_slice = reduction_config["useTimeSlice"] time_slice_transmission = reduction_config["useTimeSlice"] and reduction_config["useTimeSliceTransmission"] + sample_trans_error_tol = None + if time_slice_transmission: + sample_trans_error_tol = reduction_input["sample"]["transmission"]["errorTolerance"] output_dir = reduction_config["outputDir"] nxbins_main = reduction_config["numMainQxQyBins"] @@ -1369,6 +1373,7 @@ def _prepare_sample_transmission_ws(_sample_transmission): empty_trans_ws, radius=transmission_radius, radius_unit="mm", + transmission_error_tolerance=sample_trans_error_tol, ) sample_trans_ws = None @@ -1488,7 +1493,7 @@ def _prepare_sample_transmission_ws(_sample_transmission): "background": None, }, ) - except ZeroMonitorCountsError as e: + except (ZeroMonitorCountsError, TransmissionErrorToleranceError, TransmissionNanError) as e: if time_slice: logger.warning(f"Skipping slice {sample_name}: {e}.") continue diff --git a/src/drtsans/mono/transmission.py b/src/drtsans/mono/transmission.py index 09ded2951..cb45749ce 100644 --- a/src/drtsans/mono/transmission.py +++ b/src/drtsans/mono/transmission.py @@ -7,7 +7,47 @@ __all__ = ["calculate_transmission", "apply_transmission_correction"] -def calculate_transmission(input_sample, input_reference, radius=None, radius_unit="mm", output_workspace=None): +def calculate_transmission( + input_sample, + input_reference, + radius=None, + radius_unit="mm", + transmission_error_tolerance=None, + output_workspace=None, +): + """ + Calculate the raw transmission coefficients at zero scattering angle + from already prepared sample and reference data. + + Parameters + ---------- + input_sample: str, ~mantid.api.MatrixWorkspace, ~mantid.api.IEventWorkspace + Prepared sample workspace (possibly obtained with an attenuated beam) + input_reference: str, ~mantid.api.MatrixWorkspace, ~mantid.api.IEventWorkspace + Prepared direct beam workspace (possibly obtained with an attenuated beam) + radius: float + Radius around the beam center for pixel integration, in millimeters. + If None, radius will be obtained or calculated using `input_reference` workspace. + radius_unit: str + Either 'mm' or 'm', and only used in conjunction with option `radius`. + transmission_error_tolerance: float | None + Maximum relative error for transmission + output_workspace: str + Name of the output workspace containing the raw transmission values. + If None, a hidden random name will be provided. + + Returns + ------- + ~mantid.api.MatrixWorkspace + Workspace containing the raw transmission values + + Raises + ------ + TransmissionNanError + If all transmission values are NaN + TransmissionToleranceError + If there is insufficient statistics to calculate the transmission correction + """ if radius is None: logger.information("Calculating beam radius from sample logs") radius = beam_radius(input_reference, unit="mm") @@ -17,6 +57,7 @@ def calculate_transmission(input_sample, input_reference, radius=None, radius_un input_reference, radius, radius_unit=radius_unit, + transmission_error_tolerance=transmission_error_tolerance, output_workspace=output_workspace, ) diff --git a/src/drtsans/transmission.py b/src/drtsans/transmission.py index 6ab5ddc00..12a374e7a 100644 --- a/src/drtsans/transmission.py +++ b/src/drtsans/transmission.py @@ -25,10 +25,29 @@ from drtsans.mask_utils import circular_mask_from_beam_center, masked_detectors # Symbols to be exported -__all__ = ["apply_transmission_correction", "calculate_transmission"] +__all__ = [ + "apply_transmission_correction", + "calculate_transmission", + "TransmissionErrorToleranceError", + "TransmissionNanError", +] -def calculate_transmission(input_sample, input_reference, radius, radius_unit="mm", output_workspace=None): +class TransmissionErrorToleranceError(Exception): + """Exception raised when the transmission error is larger than the transmission error tolerance""" + + pass + + +class TransmissionNanError(Exception): + """Exception raised when all transmission values are NaN""" + + pass + + +def calculate_transmission( + input_sample, input_reference, radius, radius_unit="mm", transmission_error_tolerance=None, output_workspace=None +): """ Calculate the raw transmission coefficients at zero scattering angle from already prepared sample and reference data. @@ -49,10 +68,12 @@ def calculate_transmission(input_sample, input_reference, radius, radius_unit="m input_reference: str, ~mantid.api.MatrixWorkspace, ~mantid.api.IEventWorkspace Prepared direct beam workspace (possibly obtained with an attenuated beam) radius: float - Radius around the bean center for pixel integration, in millimeters. + Radius around the beam center for pixel integration, in millimeters. If None, radius will be obtained or calculated using `input_reference` workspace. radius_unit: str Either 'mm' or 'm', and only used in conjunction with option `radius`. + transmission_error_tolerance: float + Maximum relative error for transmission output_workspace: str Name of the output workspace containing the raw transmission values. If None, a hidden random name will be provided. @@ -61,6 +82,11 @@ def calculate_transmission(input_sample, input_reference, radius, radius_unit="m ------- ~mantid.api.MatrixWorkspace Workspace containing the raw transmission values + + Raises + ------ + TransmissionToleranceError + If there is insufficient statistics to calculate the transmission correction """ if output_workspace is None: output_workspace = mtd.unique_hidden_name() @@ -124,10 +150,26 @@ def calculate_transmission(input_sample, input_reference, radius, radius_unit="m # Notify of incorrect calculation of zero angle transmission # Will happen if the beam centers have been totally masked if bool(np.all(np.isnan(zero_angle_transmission_workspace.readY(0)))) is True: - raise RuntimeError("Transmission at zero-angle is NaN") + raise TransmissionNanError("Transmission at zero-angle is NaN") - # Notify of average transmission value non_gap_indexes = np.isfinite(zero_angle_transmission_workspace.readY(0)) + + if transmission_error_tolerance: + # Verify that errors are below the transmission error tolerance + transmission_relative_error = ( + zero_angle_transmission_workspace.readE(0)[non_gap_indexes] + / zero_angle_transmission_workspace.readY(0)[non_gap_indexes] + ) + if not np.all(transmission_relative_error < transmission_error_tolerance): + i_max = np.argmax(transmission_relative_error) + error_max = transmission_relative_error[i_max] + error_max_transmission = zero_angle_transmission_workspace.readY(0)[non_gap_indexes][i_max] + raise TransmissionErrorToleranceError( + f"Transmission error {error_max} > transmission error tolerance " + f"{transmission_error_tolerance} (transmission {error_max_transmission})" + ) + + # Notify of average transmission value average_zero_angle_transmission = np.mean(zero_angle_transmission_workspace.readY(0)[non_gap_indexes]) average_zero_angle_transmission_error = np.linalg.norm(zero_angle_transmission_workspace.readE(0)[non_gap_indexes]) message = "Average zero angle transmission = {0} +/- {1}".format( diff --git a/tests/integration/drtsans/mono/biosans/test_api.py b/tests/integration/drtsans/mono/biosans/test_api.py index c1d017584..96cc8ff97 100644 --- a/tests/integration/drtsans/mono/biosans/test_api.py +++ b/tests/integration/drtsans/mono/biosans/test_api.py @@ -320,7 +320,7 @@ def _load_synthetic_run(keyword: str) -> Workspace2D: @pytest.mark.datarepo def test_reduce_single_configuration_slice_transmission_false(datarepo_dir, temp_directory): reduction_input = REDUCTION_INPUT.copy() - + reduction_input = reduction_parameters(parameters_particular=reduction_input, validate=False) reduction_input["configuration"]["outputDir"] = temp_directory(prefix="trans_slice_false") prefix = "sans-backend-test" + str(threading.get_ident()) + "_" @@ -359,6 +359,7 @@ def test_reduce_single_configuration_slice_transmission_true(datarepo_dir, temp_ reduction_input = REDUCTION_INPUT.copy() reduction_input["configuration"]["useTimeSlice"] = True reduction_input["configuration"]["useTimeSliceTransmission"] = True + reduction_input["sample"]["transmission"]["errorTolerance"] = 0.05 reduction_input = reduction_parameters(parameters_particular=reduction_input, validate=False) reduction_input["configuration"]["outputDir"] = temp_directory(prefix="trans_slice_true") prefix = "sans-backend-test" + str(threading.get_ident()) + "_" @@ -366,13 +367,17 @@ def test_reduce_single_configuration_slice_transmission_true(datarepo_dir, temp_ loaded = load_all_files(reduction_input, prefix) assert len(loaded.sample) == 11 # 600.06/60 = 11 rounded up output = reduce_single_configuration(loaded, reduction_input) - assert len(output) == 11 # expect 11 out since nothing should be skipped + assert len(output) == 10 # the 11th slice is skipped due to too high transmission error # just need a couple components from reduce # but the whole thing needs to be run then a few components pulled + # The sample transmission workspace is for the last slice, which is skipped due to too large error transmission = calculate_transmission( mtd["_sample_trans"], # pull relevant transmission mtd["_empty"], # pull relevant + # The sample transmission workspace in the MDS is for the last (skipped) slice, therefore, + # tolerate a high transmission error. The transmission value is OK to compare below. + transmission_error_tolerance=3.0, ) transmission_val = transmission.extractY()[0][0] diff --git a/tests/integration/drtsans/tof/eqsans/test_transmission.py b/tests/integration/drtsans/tof/eqsans/test_transmission.py index 1f299ebb5..a31857f5e 100644 --- a/tests/integration/drtsans/tof/eqsans/test_transmission.py +++ b/tests/integration/drtsans/tof/eqsans/test_transmission.py @@ -38,6 +38,7 @@ find_beam_center, center_detector, ) +from drtsans.transmission import TransmissionNanError @pytest.fixture(scope="module") @@ -328,7 +329,7 @@ def test_masked_beam_center(datarepo_dir, transmission_fixture, temp_workspace_n with amend_config(data_dir=datarepo_dir.eqsans): sample_workspace = prepare_data("EQSANS_88975", mask=mask, output_workspace=temp_workspace_name()) reference_workspace = prepare_data("EQSANS_88973", mask=mask, output_workspace=temp_workspace_name()) - with pytest.raises(RuntimeError, match=r"Transmission at zero-angle is NaN"): + with pytest.raises(TransmissionNanError, match=r"Transmission at zero-angle is NaN"): calculate_transmission(sample_workspace, reference_workspace) [workspace.delete() for workspace in (sample_workspace, reference_workspace)] diff --git a/tests/unit/drtsans/test_transmission.py b/tests/unit/drtsans/test_transmission.py index 42a5f7e93..6043e0237 100644 --- a/tests/unit/drtsans/test_transmission.py +++ b/tests/unit/drtsans/test_transmission.py @@ -1,5 +1,5 @@ # https://code.ornl.gov/sns-hfir-scse/sans/sans-backend/blob/215_transmission_test/drtsans/transmission.py -from drtsans.transmission import calculate_transmission +from drtsans.transmission import calculate_transmission, TransmissionErrorToleranceError import numpy as np import pytest @@ -280,5 +280,37 @@ def test_transmission(generic_workspace, clean_workspace): assert result.extractE()[0][0] == pytest.approx(0.005683898) # expected transmission uncertainty +@pytest.mark.parametrize( + "generic_workspace", + [ + { + "name": "Isam", + "dx": pixel_size, + "dy": pixel_size, # data requires a square pixel + "yc": pixel_size / 2.0, # shift because the "detector" y-direction is even + "axis_values": [5.925, 6.075], + "intensities": Isam, + } + ], + indirect=True, +) +def test_transmission_error_tolerance(generic_workspace, clean_workspace): + """Test the transmission error tolerance exception""" + Isam = generic_workspace # convenient name + clean_workspace(Isam) + assert Isam.extractY().sum() == 50212 # checksum + + # generate the reference data - uncertainties are set separately + Iref = 1.8 * Isam + clean_workspace(Iref) + Iref = SetUncertainties(InputWorkspace=Iref, OutputWorkspace=Iref, SetError="sqrt") + assert Iref.extractY().sum() == 1.8 * 50212 # checksum + assert 1.8 * Isam.extractE().sum() > Iref.extractE().sum() # shouldn't match + + # run the algorithm + with pytest.raises(TransmissionErrorToleranceError): + calculate_transmission(Isam, Iref, 2.5 * pixel_size, "m", transmission_error_tolerance=0.01) + + if __name__ == "__main__": pytest.main([__file__]) From 4e98993ae7ca127f657966fe5afd3cd584a46147 Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Tue, 4 Feb 2025 14:31:20 -0500 Subject: [PATCH 2/5] address comments and add default value --- src/drtsans/mono/biosans/api.py | 20 ++++++++++++++++---- src/drtsans/mono/transmission.py | 2 +- src/drtsans/transmission.py | 8 +++++--- tests/unit/drtsans/test_transmission.py | 7 ++++++- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/drtsans/mono/biosans/api.py b/src/drtsans/mono/biosans/api.py index 97d0ecaf5..583fb76ce 100644 --- a/src/drtsans/mono/biosans/api.py +++ b/src/drtsans/mono/biosans/api.py @@ -68,6 +68,9 @@ SI_WINDOW_NOMINAL_DISTANCE_METER = 0.071 SAMPLE_SI_META_NAME = "CG3:CS:SampleToSi" +# default transmission error tolerance +DEFAULT_TRANSMISSION_ERROR_TOLERANCE = 0.01 + # setup logger # NOTE: If logging information is not showing up, please check the mantid log level. # If problem persists, please visit: @@ -1206,9 +1209,14 @@ def reduce_single_configuration( absolute_scale = reduction_config["StandardAbsoluteScale"] time_slice = reduction_config["useTimeSlice"] time_slice_transmission = reduction_config["useTimeSlice"] and reduction_config["useTimeSliceTransmission"] - sample_trans_error_tol = None + # Transmission tolerance errors are only checked when slicing the transmission run, + # because this is the only observed use-case of poor statistics for transmission calculations if time_slice_transmission: sample_trans_error_tol = reduction_input["sample"]["transmission"]["errorTolerance"] + if sample_trans_error_tol is None: + sample_trans_error_tol = DEFAULT_TRANSMISSION_ERROR_TOLERANCE + else: + sample_trans_error_tol = None output_dir = reduction_config["outputDir"] nxbins_main = reduction_config["numMainQxQyBins"] @@ -1396,10 +1404,14 @@ def _prepare_sample_transmission_ws(_sample_transmission): if len(loaded_ws.sample) > 1: output_suffix = f"_{i}" - try: - if time_slice_transmission: + if time_slice_transmission: + try: _, sample_trans_ws = _prepare_sample_transmission_ws(raw_sample_ws) + except (TransmissionErrorToleranceError, TransmissionNanError) as e: + logger.warning(f"Skipping slice {sample_name}: {e}.") + continue + try: processed_data_main, trans_main = process_single_configuration( raw_sample_ws, sample_trans_ws=sample_trans_ws, @@ -1493,7 +1505,7 @@ def _prepare_sample_transmission_ws(_sample_transmission): "background": None, }, ) - except (ZeroMonitorCountsError, TransmissionErrorToleranceError, TransmissionNanError) as e: + except ZeroMonitorCountsError as e: if time_slice: logger.warning(f"Skipping slice {sample_name}: {e}.") continue diff --git a/src/drtsans/mono/transmission.py b/src/drtsans/mono/transmission.py index cb45749ce..6a2e73100 100644 --- a/src/drtsans/mono/transmission.py +++ b/src/drtsans/mono/transmission.py @@ -31,7 +31,7 @@ def calculate_transmission( radius_unit: str Either 'mm' or 'm', and only used in conjunction with option `radius`. transmission_error_tolerance: float | None - Maximum relative error for transmission + Maximum relative error for transmission. If None, the error will not be checked. output_workspace: str Name of the output workspace containing the raw transmission values. If None, a hidden random name will be provided. diff --git a/src/drtsans/transmission.py b/src/drtsans/transmission.py index 12a374e7a..0f9128887 100644 --- a/src/drtsans/transmission.py +++ b/src/drtsans/transmission.py @@ -72,8 +72,8 @@ def calculate_transmission( If None, radius will be obtained or calculated using `input_reference` workspace. radius_unit: str Either 'mm' or 'm', and only used in conjunction with option `radius`. - transmission_error_tolerance: float - Maximum relative error for transmission + transmission_error_tolerance: float | None + Maximum relative error for transmission. If None, the error will not be checked. output_workspace: str Name of the output workspace containing the raw transmission values. If None, a hidden random name will be provided. @@ -85,8 +85,10 @@ def calculate_transmission( Raises ------ + TransmissionNanError + If all transmission values are NaN. TransmissionToleranceError - If there is insufficient statistics to calculate the transmission correction + If there are insufficient statistics to calculate the transmission correction. """ if output_workspace is None: output_workspace = mtd.unique_hidden_name() diff --git a/tests/unit/drtsans/test_transmission.py b/tests/unit/drtsans/test_transmission.py index 6043e0237..c7d47ffdd 100644 --- a/tests/unit/drtsans/test_transmission.py +++ b/tests/unit/drtsans/test_transmission.py @@ -2,6 +2,7 @@ from drtsans.transmission import calculate_transmission, TransmissionErrorToleranceError import numpy as np import pytest +import re # https://docs.mantidproject.org/nightly/algorithms/SetUncertainties-v1.html from mantid.simpleapi import SetUncertainties @@ -308,9 +309,13 @@ def test_transmission_error_tolerance(generic_workspace, clean_workspace): assert 1.8 * Isam.extractE().sum() > Iref.extractE().sum() # shouldn't match # run the algorithm - with pytest.raises(TransmissionErrorToleranceError): + with pytest.raises(TransmissionErrorToleranceError) as exc_info: calculate_transmission(Isam, Iref, 2.5 * pixel_size, "m", transmission_error_tolerance=0.01) + assert re.match( + "Transmission error 0\.01.* > transmission error tolerance 0.01 \(transmission 0\.55.*\)", str(exc_info.value) + ) + if __name__ == "__main__": pytest.main([__file__]) From ffa24d5e1fbfa61d720d2de0ab84a575fb195b81 Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Tue, 4 Feb 2025 15:19:27 -0500 Subject: [PATCH 3/5] catch also ZeroMonitorCountsError --- src/drtsans/mono/biosans/api.py | 2 +- src/drtsans/mono/normalization.py | 4 +++- src/drtsans/transmission.py | 4 ++-- tests/unit/drtsans/test_transmission.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/drtsans/mono/biosans/api.py b/src/drtsans/mono/biosans/api.py index 583fb76ce..42e6f8edb 100644 --- a/src/drtsans/mono/biosans/api.py +++ b/src/drtsans/mono/biosans/api.py @@ -1407,7 +1407,7 @@ def _prepare_sample_transmission_ws(_sample_transmission): if time_slice_transmission: try: _, sample_trans_ws = _prepare_sample_transmission_ws(raw_sample_ws) - except (TransmissionErrorToleranceError, TransmissionNanError) as e: + except (ZeroMonitorCountsError, TransmissionErrorToleranceError, TransmissionNanError) as e: logger.warning(f"Skipping slice {sample_name}: {e}.") continue diff --git a/src/drtsans/mono/normalization.py b/src/drtsans/mono/normalization.py index 5846a1a96..9e35fb3f9 100644 --- a/src/drtsans/mono/normalization.py +++ b/src/drtsans/mono/normalization.py @@ -103,8 +103,10 @@ def normalize_by_monitor(input_workspace, output_workspace=None): Raises ------ - RuntimeError + NoMonitorMetadataError No monitor metadata found in the sample logs of the input workspace + ZeroMonitorCountsError + No monitor counts in the input workspace """ metadata_entry_names = [ "monitor", # created by load_events diff --git a/src/drtsans/transmission.py b/src/drtsans/transmission.py index 0f9128887..27b6399f1 100644 --- a/src/drtsans/transmission.py +++ b/src/drtsans/transmission.py @@ -167,8 +167,8 @@ def calculate_transmission( error_max = transmission_relative_error[i_max] error_max_transmission = zero_angle_transmission_workspace.readY(0)[non_gap_indexes][i_max] raise TransmissionErrorToleranceError( - f"Transmission error {error_max} > transmission error tolerance " - f"{transmission_error_tolerance} (transmission {error_max_transmission})" + f"Transmission error {error_max:.4f} > transmission error tolerance " + f"{transmission_error_tolerance:.4f} (transmission {error_max_transmission:.4f})" ) # Notify of average transmission value diff --git a/tests/unit/drtsans/test_transmission.py b/tests/unit/drtsans/test_transmission.py index c7d47ffdd..03e632ae2 100644 --- a/tests/unit/drtsans/test_transmission.py +++ b/tests/unit/drtsans/test_transmission.py @@ -313,7 +313,7 @@ def test_transmission_error_tolerance(generic_workspace, clean_workspace): calculate_transmission(Isam, Iref, 2.5 * pixel_size, "m", transmission_error_tolerance=0.01) assert re.match( - "Transmission error 0\.01.* > transmission error tolerance 0.01 \(transmission 0\.55.*\)", str(exc_info.value) + "Transmission error 0.0102 > transmission error tolerance 0.0100 \(transmission 0.5556\)", str(exc_info.value) ) From 4b24c2984516c0d9bb1ee954157c411cc33f689f Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Wed, 5 Feb 2025 13:23:31 -0500 Subject: [PATCH 4/5] address comments on docs --- docs/user/corrections/transmission.rst | 61 ++++++++++++++------------ 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/docs/user/corrections/transmission.rst b/docs/user/corrections/transmission.rst index af61be548..1b647c857 100644 --- a/docs/user/corrections/transmission.rst +++ b/docs/user/corrections/transmission.rst @@ -7,7 +7,7 @@ The measured intensity must be corrected to account for sample transmission: .. math:: - I'_{sample}(x,y,\lambda) = \frac{I_{sample}(x,y,\lambda)}{T(\lambda,x,y)} + I'_{sample}(x,y,\lambda) = \frac{I_{sample}(x,y,\lambda)}{T(x,y,\lambda)} The transmission correction is calculated using a transmission run and an empty beam transmission run (reference). @@ -18,44 +18,46 @@ however, this is mostly used for diagnostic purposes. { "sample": { - "runNumber": None, + "runNumber": 10010, "loadOptions": {}, "thickness": 1.0, "transmission": { - "runNumber": None, - "value": None, - "errorTolerance": None + "runNumber": null, + "value": null, + "errorTolerance": null } }, "background": { - "runNumber": None, + "runNumber": null, "transmission": { - "runNumber": None, - "value": None + "runNumber": null, + "value": null } }, "emptyTransmission": { - "runNumber": None, - "value": None + "runNumber": null, + "value": null }, - "mmRadiusForTransmission": None, - "useTimeSliceTransmission": False, - "useThetaDepTransCorrection": True, + "mmRadiusForTransmission": null, + "useTimeSliceTransmission": false, + "useThetaDepTransCorrection": true } Time Slice Transmission (Bio-SANS only) --------------------------------------- -When using time slicing (``"useTimeSlice": True``), users can optionally calculate the transmission +When using time slicing (``"useTimeSlice": true``), users can optionally calculate the transmission correction using the time sliced sample run by setting the parameter -``"useTimeSliceTransmission": True``. The sample transmission run number is ignored when -``"useTimeSliceTransmission": True``. The time slice transmission option can be used when the sample +``"useTimeSliceTransmission": true``. The sample transmission run number is ignored when +``"useTimeSliceTransmission": true``. The time slice transmission option can be used when the sample transmission is expected to change over time. Time slices for which the transmission calculation fails will be skipped. The transmission calculation can fail due to all transmission values being NaN or if the transmission error is higher than the allowed relative transmission error, which is configurable in the sample -transmission parameter ``"errorTolerance"`` (default: 0.01). +transmission parameter ``"errorTolerance"`` (default: 0.01). For example, the last time slice may +be shorter and, therefore, include fewer counts, resulting in large statistical errors in the +transmission calculation. .. code-block:: json @@ -65,18 +67,18 @@ transmission parameter ``"errorTolerance"`` (default: 0.01). "loadOptions": {}, "thickness": 1.0, "transmission": { - "runNumber": None, - "value": None, + "runNumber": null, + "value": null, "errorTolerance": 0.05 } }, "emptyTransmission": { "runNumber": 10005, - "value": None + "value": null }, - "useTimeSlice": True, + "useTimeSlice": true, "timeSliceInterval": 100.0, - "useTimeSliceTransmission": True, + "useTimeSliceTransmission": true } Parameters @@ -90,20 +92,21 @@ Parameters - Description - Default * - ``"mmRadiusForTransmission"`` - - Beam radius within which the transmission will be calculated. If ``None``, then the beam + - Beam radius within which the transmission will be calculated. If ``null``, then the beam radius is calculated from the sample logs. - - ``None`` + - ``null`` * - ``"useThetaDepTransCorrection"`` - - If ``True``, a theta dependent transmission correction will be applied, which takes into + - If ``true``, a theta dependent transmission correction will be applied, which takes into account the effect of the scattering angle on the transmission. - - ``True`` + - ``true`` * - ``"useTimeSliceTransmission"`` - - (`Only for Bio-SANS and when` ``"useTimeSlice": True``.) If ``True``, the transmission + - (`Only for Bio-SANS and when` ``"useTimeSlice": true``.) If ``true``, the transmission correction will be calculated using the time sliced sample run itself instead of a separate sample transmission run. This is useful when the sample transmission is expected to change over time. Slices with relative transmission error larger than ``"transmissionErrorTolerance"`` will be skipped. - - ``False`` + - ``false`` * - ``"errorTolerance"`` - - (`Only for Bio-SANS and when` ``"useTimeSlice": True`` `and` ``"useTimeSliceTransmission": True``.) Maximum relative transmission error. + - (`Only for Bio-SANS and when` ``"useTimeSlice": true`` `and` ``"useTimeSliceTransmission": true``.) + Maximum relative transmission error. - 0.01 From 4649f214787625b25e133abc2b32cda8096ffa53 Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Wed, 5 Feb 2025 16:56:24 -0500 Subject: [PATCH 5/5] make error message more clear --- src/drtsans/transmission.py | 10 ++++++---- tests/unit/drtsans/test_transmission.py | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/drtsans/transmission.py b/src/drtsans/transmission.py index 27b6399f1..dbf1d1c82 100644 --- a/src/drtsans/transmission.py +++ b/src/drtsans/transmission.py @@ -164,11 +164,13 @@ def calculate_transmission( ) if not np.all(transmission_relative_error < transmission_error_tolerance): i_max = np.argmax(transmission_relative_error) - error_max = transmission_relative_error[i_max] - error_max_transmission = zero_angle_transmission_workspace.readY(0)[non_gap_indexes][i_max] + rel_error = transmission_relative_error[i_max] + transmission = zero_angle_transmission_workspace.readY(0)[non_gap_indexes][i_max] + abs_error = zero_angle_transmission_workspace.readE(0)[non_gap_indexes][i_max] raise TransmissionErrorToleranceError( - f"Transmission error {error_max:.4f} > transmission error tolerance " - f"{transmission_error_tolerance:.4f} (transmission {error_max_transmission:.4f})" + f"transmission_error / transmission_value ({abs_error:.4f} / {transmission:.4f} = " + f"{rel_error:.4f}) > transmission_relative_error_tolerance " + f"({transmission_error_tolerance:.4f})" ) # Notify of average transmission value diff --git a/tests/unit/drtsans/test_transmission.py b/tests/unit/drtsans/test_transmission.py index 03e632ae2..1a407f1b8 100644 --- a/tests/unit/drtsans/test_transmission.py +++ b/tests/unit/drtsans/test_transmission.py @@ -313,7 +313,9 @@ def test_transmission_error_tolerance(generic_workspace, clean_workspace): calculate_transmission(Isam, Iref, 2.5 * pixel_size, "m", transmission_error_tolerance=0.01) assert re.match( - "Transmission error 0.0102 > transmission error tolerance 0.0100 \(transmission 0.5556\)", str(exc_info.value) + "transmission_error / transmission_value \(0.0057 / 0.5556 = 0.0102\)" + " > transmission_relative_error_tolerance \(0.0100\)", + str(exc_info.value), )