Skip to content

Conversation

@searscr
Copy link
Contributor

@searscr searscr commented Oct 27, 2025

Description of work:

This adds logic to EQSANS so that the blocked beam can be processed. Either the blockedBeamRunNumber or darkFileName can be specified but not both.

Workflow overview

  • Blocked beam
    • Load events
    • Transform to wavelength
    • Beam center
    • Normalization by monitor
    • Solid angle correction
    • Sensitivity correction
  • Sample
    • Load events
    • Transform to wavelength
    • Beam center
    • Normalization by monitor
    • Solid angle correction
    • Sensitivity correction
  • Subtraction of blocked beam
  • For the BB subtracted data
    • Transmission correction
  • Subtraction of background
  • Normalization by thickness

Check all that apply:

  • updated documentation and checked that it looks correct in the pull request preview
  • Source added/refactored
  • Added unit tests
  • Added integration tests
  • Included a manual test for the reviewer
  • Verified that tests requiring the /SNS and /HFIR filesystems pass without fail

References:

  • Links to IBM EWM items: EWM 12395
  • Links to related issues or pull requests:

⚠️ Manual test for the reviewer

The following configuration can be used to test an EQSANS reduction with blocked beam.

{
    "schemaStamp": "2025-10-23T04:02:08.634003",
    "instrumentName": "EQSANS",
    "iptsNumber": "36254",
    "sample": {
        "runNumber": "167860",
        "loadOptions": null,
        "thickness": 1,
        "transmission": {
            "runNumber": "167855",
            "value": ""
        }
    },
    "background": {
        "runNumber": "167859",
        "transmission": {
            "runNumber": "167854",
            "value": ""
        }
    },
    "emptyTransmission": {
        "runNumber": "167853",
        "value": null
    },
    "beamCenter": {
        "runNumber": "167853"
    },
    "outputFileName": "porsil_final_conf1",
    "configuration": {
        "outputDir": "",
        "instrumentConfigurationDir": "/SNS/EQSANS/shared/instrument_configuration",
        "useTimeSlice": false,
        "timeSliceInterval": 300,
        "timeSliceOffset": 0.0,
        "timeSlicePeriod": null,
        "useLogSlice": false,
        "logSliceName": null,
        "logSliceInterval": 10,
        "cutTOFmin": 1000,
        "cutTOFmax": 2000,
        "wavelengthStep": 0.1,
        "wavelengthStepType": "constant Delta lambda",
        "sampleOffset": 285,
        "useDetectorOffset": true,
        "detectorOffset": 60.999582,
        "sampleApertureSize": 10,
        "sourceApertureDiameter": null,
        "usePixelCalibration": null,
        "maskFileName": "/SNS/EQSANS/IPTS-36254/shared/gn/maskWS4m10A_10mm.nxs",
        "useDefaultMask": null,
        "defaultMask": null,
        "useMaskBackTubes": null,
        "normalization": "Total charge",
        "fluxMonitorRatioFile": null,
        "beamFluxFileName": "/SNS/EQSANS/shared/NeXusFiles/EQSANS/2025B_mp/bl6_flux_2025B_Aug_rebinned_4m.txt",
        "sensitivityFileName": "/SNS/EQSANS/shared/NeXusFiles/EQSANS/2025B_mp/Sensitivity_patched_thinPMMA_4m_167517.nxs",
        "useSolidAngleCorrection": true,
        "useThetaDepTransCorrection": true,
        "mmRadiusForTransmission": 25,
        "absoluteScaleMethod": null,
        "StandardAbsoluteScale": 1.72,
        "numQxQyBins": 80,
        "1DQbinType": "scalar",
        "QbinType": "log",
        "numQBins": 100,
        "LogQBinsPerDecade": null,
        "useLogQBinsDecadeCenter": false,
        "useLogQBinsEvenDecade": false,
        "WedgeMinAngles": "-30, 60",
        "WedgeMaxAngles": "30, 120",
        "autoWedgeQmin": null,
        "autoWedgeQmax": null,
        "autoWedgeQdelta": null,
        "autoWedgeAzimuthalDelta": null,
        "autoWedgePeakWidth": null,
        "autoWedgeBackgroundWidth": null,
        "autoWedgeSignalToNoiseMin": null,
        "autoWedgePhiMin": "",
        "autoWedgePhiMax": "",
        "AnnularAngleBin": 5.0,
        "Qmin": null,
        "Qmax": null,
        "useErrorWeighting": false,
        "smearingPixelSizeX": null,
        "smearingPixelSizeY": null,
        "useSubpixels": null,
        "subpixelsX": null,
        "subpixelsY": null,
        "useSliceIDxAsSuffix": null,
        "fitInelasticIncoh": false,
        "selectMinIncoh": false,
        "incohfit_qmin": 0.08,
        "incohfit_qmax": 0.09,
        "incohfit_factor": null,
        "outputWavelengthDependentProfile": true,
        "incohfit_intensityweighted": false,
        "elasticReference": {
            "runNumber": "",
            "thickness": "1.0",
            "transmission": {
                "runNumber": null,
                "value": "0.9"
            }
        },
        "elasticReferenceBkgd": {
            "runNumber": "",
            "transmission": {
                "runNumber": null,
                "value": "0.9"
            }
        },
        "scaleComponents": {
            "detector1": [
                1.021,
                1.0204066565,
                1
            ]
        },
        "blockedBeamRunNumber": 167862,
        "darkFileName": null,
        "autoSymmetricWedges": "",
        "removeAlgorithmHistory": false
    },
    "dataDirectories": ""
}

Check list for the reviewer

  • best software practices
    • clearly named variables (better to be verbose in variable names)
    • code comments explaining the intent of code blocks
  • All the tests are passing
  • The documentation is up to date and looks correct in the pull request preview
  • code comments added when explaining intent

Execution of tests requiring the /SNS and /HFIR filesystems

It is strongly encouraged that the reviewer runs the following tests in their local machine
because these tests are not run by the GitLab CI. It is assumed that the reviewer has the /SNS and /HFIR filesystems
remotely mounted in their machine.

cd /path/to/my/local/drtsans/repo/
git fetch origin merge-requests/<MERGE_REQUEST_NUMBER>/head:mr<MERGE_REQUEST_NUMBER>
git switch mr<MERGE_REQUEST_NUMBER>
pixi shell  # activate the environment
pytest -m mount_eqsans ./tests/unit/ ./tests/integration/
exit  # exit the environment

In the above code snippet, substitute <MERGE_REQUEST_NUMBER> for the actual merge request number. Also substitute
<my_drtsans_dev_environment> with the name of the conda environment you use for development. It is critical that
you have installed the repo in this conda environment in editable mode with pip install -e . or conda develop .

@codecov-commenter
Copy link

codecov-commenter commented Oct 29, 2025

Codecov Report

❌ Patch coverage is 63.15789% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.80%. Comparing base (d7cacba) to head (e95ab3a).
⚠️ Report is 6 commits behind head on next.

Files with missing lines Patch % Lines
src/drtsans/tof/eqsans/api.py 66.66% 9 Missing ⚠️
src/drtsans/tof/eqsans/normalization.py 50.00% 3 Missing ⚠️
src/drtsans/tof/eqsans/reduction_api.py 60.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             next    #1066      +/-   ##
==========================================
- Coverage   85.89%   85.80%   -0.09%     
==========================================
  Files         101      101              
  Lines       11334    11356      +22     
==========================================
+ Hits         9735     9744       +9     
- Misses       1599     1612      +13     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jmborr jmborr requested a review from Copilot October 30, 2025 15:45
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for blocked beam run normalization in EQSANS data reduction. The blocked beam measurement allows for proper background correction when dark current files are not available.

Key changes:

  • Added blockedBeamRunNumber configuration parameter with mutual exclusivity validation against darkFileName
  • Implemented blocked beam workspace loading, processing, and subtraction from sample and background data
  • Enhanced normalization to log when using proton charge vs other duration methods for blocked beam cases

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/drtsans/configuration/schema/EQSANS.json Added blockedBeamRunNumber schema definition with validation ensuring mutual exclusivity with darkFileName
src/drtsans/tof/eqsans/api.py Added blocked beam loading, processing logic, and subtraction from sample/background workspaces
src/drtsans/tof/eqsans/reduction_api.py Added has_blocked_beam parameter to prepare_data_workspaces and workspace cleanup for blocked beam runs
src/drtsans/tof/eqsans/normalization.py Added logging for blocked beam normalization and passed has_blocked_beam flag through normalization chain

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +610 to +611
if blocked_beam.data:
bkgd_ws = subtract_background(bkgd_ws, blocked_ws)
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable blocked_ws may not be defined at line 611 if the condition at line 608 (not registered_workspace(bkgd_ws_name)) is True but the condition at line 581 (not registered_workspace(blocked_ws_name)) is False. In this scenario, blocked_ws is retrieved from mtd at line 584 but only within the scope of the if-block starting at line 579. This would cause a NameError. The check should be if blocked_beam.data and blocked_beam.data is not None: and blocked_ws should be retrieved outside the inner if-block or the entire blocked beam processing logic should be restructured to ensure blocked_ws is always available when needed.

Copilot uses AI. Check for mistakes.
Comment on lines +635 to +636
if blocked_beam.data and blocked_ws:
blocked_ws.delete()
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable blocked_ws may not be defined at line 635. If blocked_beam.data exists but the workspace was already registered (line 581-584 evaluated with the else branch), blocked_ws is only defined within the scope of the if-block starting at line 579. This will cause a NameError when attempting to delete. The blocked_ws variable should be defined in a broader scope or the deletion logic should be moved inside the block where blocked_ws is guaranteed to exist.

Copilot uses AI. Check for mistakes.
if duration.log_key == "proton_charge":
logger.notice("Workspace normalized by Time using proton charge duration.")
else:
logger.warning(f"Workspace normalized by Time using {duration.log_key}.")
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning message is unclear about why this is a warning vs. a notice. If using a non-proton-charge duration for blocked beam runs is problematic, the message should explain the potential issue or provide guidance. Consider clarifying why this situation warrants a warning, e.g., 'Workspace normalized by Time using {duration.log_key} instead of recommended proton_charge for blocked beam runs.'

Suggested change
logger.warning(f"Workspace normalized by Time using {duration.log_key}.")
logger.warning(
f"Workspace normalized by Time using {duration.log_key} instead of recommended 'proton_charge' for blocked beam runs. "
"This may lead to incorrect normalization. If possible, use 'proton_charge' for normalization of blocked beam runs."
)

Copilot uses AI. Check for mistakes.
Copy link
Member

@jmborr jmborr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The treatment of the block beam is incorrect. As I understand it, it should mimic the treatment for the dark current. We should have a meeting with Yingrui and one of the EQSANS IS to go over this.

"examples": ["Monitor", "Total charge", "Time"],
"default": "Total charge"
},
"blockedBeamRunNumber": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use validator onlyOneTrue instead of evaluateCondition. See options useLogSlice and useTimeSlice in file common.json for an example usage.
Meaning of validator onlyOneTrue is explained in docstring of function _validate_only_one_true()


# blocked beam
# (run_type, run_number, is_elastic_reference)
other_runs = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a superior choice to the infamous index scheme 😁

sample_transmission=ws_mon_pair(data=sample_transmission_ws, monitor=sample_transmission_mon_ws),
background_transmission=ws_mon_pair(data=background_transmission_ws, monitor=background_transmission_mon_ws),
dark_current=ws_mon_pair(data=dark_current_ws, monitor=dark_current_mon_ws),
dark_current=ws_mon_pair(dark_current_ws, None),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dark_current=ws_mon_pair(dark_current_ws, None),
dark_current=ws_mon_pair(dark_current_ws, monitor=None), # dark current requires no monitor data

background_transmission=ws_mon_pair(data=background_transmission_ws, monitor=background_transmission_mon_ws),
dark_current=ws_mon_pair(data=dark_current_ws, monitor=dark_current_mon_ws),
dark_current=ws_mon_pair(dark_current_ws, None),
blocked_beam=ws_mon_pair(blocked_beam_ws, None),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
blocked_beam=ws_mon_pair(blocked_beam_ws, None),
blocked_beam=ws_mon_pair(blocked_beam_ws, monitor=None), # blocked beam requires no monitor data

if not registered_workspace(ws_name):
dark_current_file = abspath(dark_current_file)
print(f"Loading filename {dark_current_file}")
print(f"Loading dark current {dark_current_file}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
print(f"Loading dark current {dark_current_file}")
logger.info(f"Loading dark current {dark_current_file}")

("blocked_beam", blocked_beam_run, False),
]

other_ws_dict = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
other_ws_dict = {}
other_ws_dict = {} # stores references to workspaces other than the sample

"mask_btp": mask_btp,
"solid_angle": solid_angle,
"sensitivity_workspace": sensitivity_workspace,
"has_blocked_beam": (blocked_beam is not None and blocked_beam.data is not None),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've looked at the effect of key "has_blocked_beam". Apparently it has none! I went down the rabbit hole:

prepare_data_workspaces(has_blocked_beam)
-->normalize_by_flux(has_blocked_beam)
   -->normalize_by_time(has_blocked_beam)

And then normalize_by_time logs a message depending on the value of has_blocked_beam. It's not worth it modifying the signature of so many functions just for the sake of one log message

# process sample
sample_ws = prepare_data_workspaces(sample_ws_raw, output_workspace=output_workspace, **prepare_data_conf)

if blocked_beam is not None and blocked_beam.data is not None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the blocked beam run does not require prepare_data_workspaces to be applied to it.

bkgd_ws_name = output_suffix + "_background"
if not registered_workspace(bkgd_ws_name):
bkgd_ws = prepare_data_workspaces(bkg_ws_raw, output_workspace=bkgd_ws_name, **prepare_data_conf)
if blocked_beam.data:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subtracting the blocked beam from a run should be part of prepare_data_workspaces. In this function:

    if dark_current is not None and dark_current.data is not None:
        subtract_dark_current(output_workspace, dark_current.data)
    elif blocked_beam is not None and blocked_beam.data is not None:
        subtract_blocked_beam(output_workspace, dark_current.data)

and a dedicated subtract_blocked_beam() should be created. This function should be a clone of subtract_dark_current and the blocked beam run subject to the same normalization as the dark current.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants