Repository: mantidproject/mantid
Pull request #39158: Add Gauge Volume to Monte Carlo Absorption
Author: login: andy-bridger
Labels: name: Diffraction
description: Issues and pull requests related to diffraction
name: ISIS: Diffraction
description: Issue and pull requests relating to Diffraction at ISIS
name: Absorption Corrections
description: Issues and pull requests related to absorption corrections
Created: 2025-04-04T09:11:40Z | Merged: 2025-04-17T12:48:55Z

### Description of work

#### Summary of work
If a gauge volume is present on a workspace and `MonteCarloAbsorption` is run, only scattering from within the gauge volume will be calculated.

Fixes #39157

#### Further detail of work
Algorithm now checks for a gauge volume and, if one is present, limits the interaction volume to this gauge volume.

### To test:

The following script contains an analytical treatment for the attenuation coefficient of a rotated cylinder illuminated with a small cylindrical gauge volume (https://doi.org/10.1107/S0567739471000652). The script then compares a range of bragg angles (theta), with a range of cylinder rotations (chi) and compares the attenuation coefficients. It seems to be working as expected, with  largest deviations at high theta, high chi, which corresponds to back scattering through a large inclination of the cylinder. The limitation in these situations is likely the number of simulated points in the monte carlo, as the thickness of material is significant, many points are required to accurately estimate the integral.

Script:

```
# import mantid algorithms, numpy and matplotlib
from mantid.simpleapi import *
import matplotlib.pyplot as plt
import numpy as np

import numpy as np
from scipy.integrate import quad

def get_q(R, theta, chi):
    sin_theta = np.sin(theta)
    sin_chi = np.sin(chi)
    # q is incoming ellipical radius - Equation (4)
    return R / np.sqrt(1 - (sin_theta**2) * (sin_chi**2))
    

def absorption_correction(theta, chi, mu, R, N_gauss=20):
    # analytical correction for vertical cylinder with small gauge vol along beam
    # https://doi.org/10.1107/S0567739471000652
    
    # q is incoming ellipical radius - Equation (4)
    q = get_q(R, theta, chi)
    
    
    # t, as func of pos z along beam path - Equation (7)
    def t(z):
        sin_theta = np.sin(theta)
        sin_chi = np.sin(chi)
        
        # find sigma^2 - Equation (5)
        A = (1 - sin_theta**2 * sin_chi**2)**2
        B = -2 * R**2 * (1 - sin_theta**2 * sin_chi**2) - 2 * (np.cos(theta)**2 - sin_theta**2 * np.cos(chi)**2) * np.sin(2*theta)**2 * sin_chi**2 * (q - z)**2
        C = R**4 + np.sin(2*theta)**4 * sin_chi**4 * (q - z)**4 + 2 * R**2 * np.sin(2*theta)**2 * sin_chi**2 * (q - z)**2 * np.cos(2*theta)
        
        root_desc = np.nan_to_num(np.sqrt(B**2 - 4*A*C)) # should be positive, if numerical accuracy fails, nan should be zero
        
        sigma_sq_roots = [(-B + root_desc) / (2*A), (-B - root_desc) / (2*A)]
        
        sigma_sq = np.max(sigma_sq_roots) if z <= q else np.min(sigma_sq_roots)
        
        
        # Solve for t(z) Equation (7)
        a = 1
        b = -2 * (q - z) * np.cos(2*theta)
        c = (q - z)**2 - sigma_sq
        t_roots = np.nan_to_num([(-b + np.sqrt(b**2 - 4*a*c)) / (2*a) , (-b - np.sqrt(b**2 - 4*a*c)) / (2*a) ])
        tz = np.max(t_roots)
        return tz # one root should be larger than 0
        
    def integral(z):
        # integral part of Equation (9)
        return np.exp(-mu * 100*(z + t(z))) #mu is in cm-1
        

    integral_eval, _ = quad(integral, 0, 2*q)
    
    #print(integral_eval)
    
    # volume integral
    volume_factor = 2*q

    return integral_eval / volume_factor

def make_cylinder(R, h, ax = (0,1,0)):
    x,y,z = -ax[0]*h/2, -ax[1]*h/2, -ax[2]*h/2
    
    return f"""
<cylinder id="A">
  <centre-of-bottom-base x="{x}" y="{y}" z="{z}" />
  <axis x="{ax[0]}" y="{ax[1]}" z="{ax[2]}" />
  <radius val="{R}" />
  <height val="{h}" />
</cylinder>
"""



def compare_mc_and_analytical(theta, chi, mu = 1, sampleR = 0.01, h = 0.1, gvR_ratio = 0.1, mc_points_per_cm = 50000):
    
    # setup ws 
    ws = CreateSampleWorkspace(OutputWorkspace='ws', Function='Powder Diffraction', 
        XMin=2000, XMax=20000, BinWidth=200, NumBanks=1, BankPixelWidth=1,
        WorkspaceType="Histogram")
        
    # set the instrument to have a detector at 2theta
    EditInstrumentGeometry(Workspace='ws', PrimaryFlightPath = 100, L2=100, Polar=2*theta)
    
    # convert to lambda
    ConvertUnits(InputWorkspace='ws', OutputWorkspace='ws', Target='Wavelength')
    
    # make a second ws to view the sample on
    ws_gv = CloneWorkspace(ws)

    # chi is defined as the rotation about an axis theta degrees from ki (0,0,1)
    rotation_ax = np.array((np.sin(np.deg2rad(theta)), 0 , np.cos(np.deg2rad(theta))))
    
    # construct the sample
    sample = make_cylinder(R = sampleR, h = h, ax = (0,1,0))
    
    
    # construct the gauge volume
    sample_q = get_q(sampleR, np.deg2rad(theta), np.deg2rad(chi))
    gv = make_cylinder(R = sampleR*gvR_ratio, h = 2*sample_q, ax = (0,0,1))
    
    # rotate the sample
    SetGoniometer(ws, Axis0 = f"{chi}, {rotation_ax[0]},0,{rotation_ax[2]},1")

    # set samples, materials and add the gauge volume
    SetSample('ws', Geometry={'Shape': 'CSG', 'Value': sample})
    SetSample('ws_gv', Geometry={'Shape': 'CSG', 'Value': gv})
    SetSampleMaterial('ws', SampleNumberDensity=1.0,
         ScatteringXSection = 1.0, AttenuationXSection=0.0, AtomicNumber = 23) 
    DefineGaugeVolume(ws, gv)

    # compute
    analytical_val = absorption_correction(np.deg2rad(theta), np.deg2rad(chi), mu, sampleR, N_gauss = 40)

    mc_val = MonteCarloAbsorption(ws, OutputWorkspace = "mc_abs",
                                    EventsPerPoint=int(sample_q*100*mc_points_per_cm),
                                    SimulateScatteringPointIn = "SampleOnly").extractY()[0,0]
    
    return 1/analytical_val, 1/mc_val

n_theta = 7
n_chi = 7

sampleR = 0.005
sampleH = 0.1
max_chi = 90 - np.rad2deg(np.arctan(2*sampleR/sampleH)) # any larger a rotation than this, the cross section is no longer elliptical

print(max_chi)

analytical_astars = np.zeros((n_theta,n_chi))
montecarlo_astars = np.zeros_like(analytical_astars)

thetas = np.round(np.linspace(0, 90, n_theta), 2)
chis = np.round(np.linspace(0, max_chi - 15, n_chi), 1)

for it, t in enumerate(thetas):
    for ic, c in enumerate(chis):
        print(t,c)
        aas, mcas = compare_mc_and_analytical(t, c, mc_points_per_cm = 1000)
        analytical_astars[ic, it] = aas 
        montecarlo_astars[ic, it] = mcas
percentage_errors = np.abs((montecarlo_astars/analytical_astars)-1)*100

print(thetas)

set_ax_lab = lambda ax, labels, x : ax.set_xticks(np.arange(len(labels)), labels) if x else ax.set_yticks(np.arange(len(labels)), labels)

fig, axs = plt.subplots(1,3)
aa = axs[0].imshow(analytical_astars)
ma = axs[1].imshow(montecarlo_astars)
da = axs[2].imshow(percentage_errors)
[set_ax_lab(ax, thetas*2, True) for ax in axs]
[set_ax_lab(ax, chis, False) for ax in axs]
fig.supxlabel(r"$2\theta$")
fig.supylabel(r"$\chi$")
axs[0].set_title("Analytical A*")
plt.colorbar(aa, ax = axs[0])
axs[1].set_title("MonteCarlo A*")
plt.colorbar(ma, ax = axs[1])
axs[2].set_title("Percentage Difference in A*")
plt.colorbar(da, ax = axs[2])

fig.show()
```

Result:

![image](https://github.com/user-attachments/assets/03f239fc-111f-4dbc-8335-473b01d7ee6c)



### Reviewer

Please comment on the points listed below ([full description](http://developer.mantidproject.org/ReviewingAPullRequest.html)).
**Your comments will be used as part of the gatekeeper process, so please comment clearly on what you have checked during your review.** If changes are made to the PR during the review process then your final comment will be the most important for gatekeepers. In this comment you should make it clear why any earlier review is still valid, or confirm that all requested changes have been addressed.

#### Code Review

- Is the code of an acceptable quality?
- Does the code conform to the [coding standards](http://developer.mantidproject.org/Standards/)?
- Are the unit tests small and test the class in isolation?
- If there is GUI work does it follow the [GUI standards](http://developer.mantidproject.org/Standards/GUIStandards.html)?
- If there are changes in the release notes then do they describe the changes appropriately?
- Do the release notes conform to the [release notes guide](https://developer.mantidproject.org/Standards/ReleaseNotesGuide.html)?

#### Functional Tests

- Do changes function as described? Add comments below that describe the tests performed?
- Do the changes handle unexpected situations, e.g. bad input?
- Has the relevant (user and developer) documentation been added/updated?

Does everything look good? Mark the review as **Approve**. A member of `@mantidproject/gatekeepers` will take care of it.

### Gatekeeper

If you need to request changes to a PR then please add a comment and set the review status to "Request changes". This will stop the PR from showing up in the list for other gatekeepers.


Linked issue #39157: Add Gauge Volume to Monte Carlo Absorption Algorithm
**Describe the problem**
When using the Monte Carlo Absorption Algorithm, if you have set a Gauge Volume with `DefineGaugeVolume` you would probably like that to be acknowledged by the algorithm.

**Describe the solution you'd like**
If a gauge volume is defined, only scattering events within this volume in the sample should be used to calculate attenuation coefficient.

**Describe alternatives you've considered**
In future, a extension of the beam profile definition might provide an alternative way of achieving this, but I think as long as `DefineGaugeVolume` exists, it would be useful for it to be acknowledged wherever possible

**Additional context**
<!--Add any other context or screenshots about the feature request here.-->


Diff:
diff --git a/Framework/Algorithms/inc/MantidAlgorithms/MonteCarloAbsorption.h b/Framework/Algorithms/inc/MantidAlgorithms/MonteCarloAbsorption.h
index 76280a6a8918..015be12a47a9 100644
--- a/Framework/Algorithms/inc/MantidAlgorithms/MonteCarloAbsorption.h
+++ b/Framework/Algorithms/inc/MantidAlgorithms/MonteCarloAbsorption.h
@@ -52,12 +52,10 @@ class MANTID_ALGORITHMS_DLL MonteCarloAbsorption : public API::Algorithm {
   }
 
 protected:
-  virtual std::shared_ptr<IMCAbsorptionStrategy>
-  createStrategy(IMCInteractionVolume &interactionVol, const IBeamProfile &beamProfile, Kernel::DeltaEMode::Type EMode,
-                 const size_t nevents, const size_t maxScatterPtAttempts, const bool regenerateTracksForEachLambda);
-  virtual std::shared_ptr<IMCInteractionVolume>
-  createInteractionVolume(const API::Sample &sample, const size_t maxScatterPtAttempts,
-                          const MCInteractionVolume::ScatteringPointVicinity pointsIn);
+  [[nodiscard]] virtual std::shared_ptr<IMCAbsorptionStrategy>
+  createStrategy(std::shared_ptr<IMCInteractionVolume> interactionVol, const IBeamProfile &beamProfile,
+                 Kernel::DeltaEMode::Type EMode, const size_t nevents, const size_t maxScatterPtAttempts,
+                 const bool regenerateTracksForEachLambda);
   virtual std::shared_ptr<SparseWorkspace> createSparseWorkspace(const API::MatrixWorkspace &modelWS,
                                                                  const size_t wavelengthPoints, const size_t rows,
                                                                  const size_t columns);
@@ -74,7 +72,7 @@ class MANTID_ALGORITHMS_DLL MonteCarloAbsorption : public API::Algorithm {
                                          const bool simulateTracksForEachWavelength, const int seed,
                                          const InterpolationOption &interpolateOpt, const bool useSparseInstrument,
                                          const size_t maxScatterPtAttempts,
-                                         const MCInteractionVolume::ScatteringPointVicinity pointsIn);
+                                         MCInteractionVolume::ScatteringPointVicinity pointsIn);
   API::MatrixWorkspace_uptr createOutputWorkspace(const API::MatrixWorkspace &inputWS) const;
   void interpolateFromSparse(API::MatrixWorkspace &targetWS, const SparseWorkspace &sparseWS,
                              const Mantid::Algorithms::InterpolationOption &interpOpt);
diff --git a/Framework/Algorithms/inc/MantidAlgorithms/SampleCorrections/IMCInteractionVolume.h b/Framework/Algorithms/inc/MantidAlgorithms/SampleCorrections/IMCInteractionVolume.h
index 67e8749d69aa..c393170dad89 100644
--- a/Framework/Algorithms/inc/MantidAlgorithms/SampleCorrections/IMCInteractionVolume.h
+++ b/Framework/Algorithms/inc/MantidAlgorithms/SampleCorrections/IMCInteractionVolume.h
@@ -35,12 +35,18 @@ using TrackPair = std::tuple<bool, std::shared_ptr<Geometry::Track>, std::shared
 */
 class MANTID_ALGORITHMS_DLL IMCInteractionVolume {
 public:
+  enum class ScatteringPointVicinity { SAMPLEANDENVIRONMENT, SAMPLEONLY, ENVIRONMENTONLY };
   virtual ~IMCInteractionVolume() = default;
   virtual TrackPair calculateBeforeAfterTrack(Kernel::PseudoRandomNumberGenerator &rng, const Kernel::V3D &startPos,
                                               const Kernel::V3D &endPos, MCInteractionStatistics &stats) const = 0;
+  virtual ComponentScatterPoint generatePoint(Kernel::PseudoRandomNumberGenerator &rng) const = 0;
   virtual const Geometry::BoundingBox getFullBoundingBox() const = 0;
   virtual void setActiveRegion(const Geometry::BoundingBox &region) = 0;
-};
+  virtual Geometry::IObject_sptr getGaugeVolume() const = 0;
+  virtual void setGaugeVolume(Geometry::IObject_sptr gaugeVolume) = 0;
 
+protected:
+  virtual void init() = 0;
+};
 } // namespace Algorithms
 } // namespace Mantid
diff --git a/Framework/Algorithms/inc/MantidAlgorithms/SampleCorrections/MCAbsorptionStrategy.h b/Framework/Algorithms/inc/MantidAlgorithms/SampleCorrections/MCAbsorptionStrategy.h
index 9168801b41f3..b119f42f0b84 100644
--- a/Framework/Algorithms/inc/MantidAlgorithms/SampleCorrections/MCAbsorptionStrategy.h
+++ b/Framework/Algorithms/inc/MantidAlgorithms/SampleCorrections/MCAbsorptionStrategy.h
@@ -40,7 +40,7 @@ class MonteCarloAbsorption;
 */
 class MANTID_ALGORITHMS_DLL MCAbsorptionStrategy : public IMCAbsorptionStrategy {
 public:
-  MCAbsorptionStrategy(IMCInteractionVolume &interactionVolume, const IBeamProfile &beamProfile,
+  MCAbsorptionStrategy(std::shared_ptr<IMCInteractionVolume> interactionVolume, const IBeamProfile &beamProfile,
                        Kernel::DeltaEMode::Type EMode, const size_t nevents, const size_t maxScatterPtAttempts,
                        const bool regenerateTracksForEachLambda);
   virtual void calculate(Kernel::PseudoRandomNumberGenerator &rng, const Kernel::V3D &finalPos,
@@ -49,13 +49,13 @@ class MANTID_ALGORITHMS_DLL MCAbsorptionStrategy : public IMCAbsorptionStrategy
                          MCInteractionStatistics &stats) override;
 
 private:
+  std::shared_ptr<IMCInteractionVolume> m_scatterVol;
   const IBeamProfile &m_beamProfile;
-  const IMCInteractionVolume &m_scatterVol;
   const size_t m_nevents;
   const size_t m_maxScatterAttempts;
   const Kernel::DeltaEMode::Type m_EMode;
   const bool m_regenerateTracksForEachLambda;
-  IMCInteractionVolume &setActiveRegion(IMCInteractionVolume &interactionVolume, const IBeamProfile &beamProfile);
+  void setActiveRegion();
 };
 
 } // namespace Algorithms
diff --git a/Framework/Algorithms/inc/MantidAlgorithms/SampleCorrections/MCInteractionVolume.h b/Framework/Algorithms/inc/MantidAlgorithms/SampleCorrections/MCInteractionVolume.h
index fab0e8ef60b0..3a3bb51e83c1 100644
--- a/Framework/Algorithms/inc/MantidAlgorithms/SampleCorrections/MCInteractionVolume.h
+++ b/Framework/Algorithms/inc/MantidAlgorithms/SampleCorrections/MCInteractionVolume.h
@@ -28,16 +28,24 @@ class IBeamProfile;
 class MANTID_ALGORITHMS_DLL MCInteractionVolume : public IMCInteractionVolume {
 public:
   enum class ScatteringPointVicinity { SAMPLEANDENVIRONMENT, SAMPLEONLY, ENVIRONMENTONLY };
-  MCInteractionVolume(const API::Sample &sample, const size_t maxScatterAttempts = 5000,
-                      const ScatteringPointVicinity pointsIn = ScatteringPointVicinity::SAMPLEANDENVIRONMENT);
-
+  [[nodiscard]] static std::shared_ptr<IMCInteractionVolume>
+  create(const API::Sample &sample, const size_t maxScatterAttempts = 5000,
+         const ScatteringPointVicinity pointsIn = ScatteringPointVicinity::SAMPLEANDENVIRONMENT,
+         Geometry::IObject_sptr gaugeVolume = nullptr);
   const Geometry::BoundingBox getFullBoundingBox() const override;
   virtual TrackPair calculateBeforeAfterTrack(Kernel::PseudoRandomNumberGenerator &rng, const Kernel::V3D &startPos,
                                               const Kernel::V3D &endPos, MCInteractionStatistics &stats) const override;
-  ComponentScatterPoint generatePoint(Kernel::PseudoRandomNumberGenerator &rng) const;
+  ComponentScatterPoint generatePoint(Kernel::PseudoRandomNumberGenerator &rng) const override;
   void setActiveRegion(const Geometry::BoundingBox &region) override;
+  Geometry::IObject_sptr getGaugeVolume() const override;
+  void setGaugeVolume(Geometry::IObject_sptr gaugeVolume) override;
 
 private:
+  // init required to be called separately to constructor, so here we prevent public access to direct creation of
+  // instance
+  explicit MCInteractionVolume(const API::Sample &sample, const size_t maxScatterAttempts = 5000,
+                               const ScatteringPointVicinity pointsIn = ScatteringPointVicinity::SAMPLEANDENVIRONMENT,
+                               Geometry::IObject_sptr gaugeVolume = nullptr);
   int getComponentIndex(Kernel::PseudoRandomNumberGenerator &rng) const;
   std::optional<Kernel::V3D> generatePointInObjectByIndex(int componentIndex,
                                                           Kernel::PseudoRandomNumberGenerator &rng) const;
@@ -46,6 +54,10 @@ class MANTID_ALGORITHMS_DLL MCInteractionVolume : public IMCInteractionVolume {
   Geometry::BoundingBox m_activeRegion;
   const size_t m_maxScatterAttempts;
   const ScatteringPointVicinity m_pointsIn;
+  Geometry::IObject_sptr m_gaugeVolume;
+
+protected:
+  void init() override;
 };
 
 } // namespace Algorithms
diff --git a/Framework/Algorithms/src/AddAbsorptionWeightedPathLengths.cpp b/Framework/Algorithms/src/AddAbsorptionWeightedPathLengths.cpp
index 2ff5529e33af..a9d0f38be14e 100644
--- a/Framework/Algorithms/src/AddAbsorptionWeightedPathLengths.cpp
+++ b/Framework/Algorithms/src/AddAbsorptionWeightedPathLengths.cpp
@@ -118,7 +118,8 @@ void AddAbsorptionWeightedPathLengths::exec() {
 
   auto beamProfile = BeamProfileFactory::createBeamProfile(*instrument, inputWS->sample());
   // Configure strategy
-  MCInteractionVolume interactionVol(inputWS->sample(), maxScatterPtAttempts);
+  std::shared_ptr<IMCInteractionVolume> interactionVol =
+      MCInteractionVolume::create(inputWS->sample(), maxScatterPtAttempts);
   MCAbsorptionStrategy strategy(interactionVol, *beamProfile, DeltaEMode::Elastic, nevents, maxScatterPtAttempts, true);
 
   bool useSinglePath = getProperty("UseSinglePath");
diff --git a/Framework/Algorithms/src/MonteCarloAbsorption.cpp b/Framework/Algorithms/src/MonteCarloAbsorption.cpp
index e1be07da17fb..2011629397d8 100644
--- a/Framework/Algorithms/src/MonteCarloAbsorption.cpp
+++ b/Framework/Algorithms/src/MonteCarloAbsorption.cpp
@@ -6,6 +6,7 @@
 // SPDX - License - Identifier: GPL - 3.0 +
 #include "MantidAlgorithms/MonteCarloAbsorption.h"
 #include "MantidAPI/InstrumentValidator.h"
+#include "MantidAPI/Run.h"
 #include "MantidAPI/Sample.h"
 #include "MantidAPI/SpectrumInfo.h"
 #include "MantidAPI/WorkspaceUnitValidator.h"
@@ -18,6 +19,7 @@
 #include "MantidGeometry/Instrument.h"
 #include "MantidGeometry/Instrument/ReferenceFrame.h"
 #include "MantidGeometry/Instrument/SampleEnvironment.h"
+#include "MantidGeometry/Objects/ShapeFactory.h"
 #include "MantidKernel/BoundedValidator.h"
 #include "MantidKernel/CompositeValidator.h"
 #include "MantidKernel/DeltaEMode.h"
@@ -192,22 +194,6 @@ std::map<std::string, std::string> MonteCarloAbsorption::validateInputs() {
   return issues;
 }
 
-/**
- * Factory method to return an instance of the required interaction volume
- * class
- * @param sample A reference to the object defining details of the sample
- * @param maxScatterPtAttempts The maximum number of tries to generate a random
- * point within the object
- * @param pointsIn Where to generate the scattering point in
- * @return a pointer to an MCAbsorptionStrategy object
- */
-std::shared_ptr<IMCInteractionVolume>
-MonteCarloAbsorption::createInteractionVolume(const API::Sample &sample, const size_t maxScatterPtAttempts,
-                                              const MCInteractionVolume::ScatteringPointVicinity pointsIn) {
-  auto interactionVol = std::make_shared<MCInteractionVolume>(sample, maxScatterPtAttempts, pointsIn);
-  return interactionVol;
-}
-
 /**
  * Factory method to return an instance of the required absorption strategy
  * class
@@ -223,12 +209,12 @@ MonteCarloAbsorption::createInteractionVolume(const API::Sample &sample, const s
  * @return a pointer to an MCAbsorptionStrategy object
  */
 std::shared_ptr<IMCAbsorptionStrategy>
-MonteCarloAbsorption::createStrategy(IMCInteractionVolume &interactionVol, const IBeamProfile &beamProfile,
-                                     Kernel::DeltaEMode::Type EMode, const size_t nevents,
-                                     const size_t maxScatterPtAttempts, const bool regenerateTracksForEachLambda) {
-  auto MCAbs = std::make_shared<MCAbsorptionStrategy>(interactionVol, beamProfile, EMode, nevents, maxScatterPtAttempts,
-                                                      regenerateTracksForEachLambda);
-  return MCAbs;
+MonteCarloAbsorption::createStrategy(std::shared_ptr<IMCInteractionVolume> interactionVol,
+                                     const IBeamProfile &beamProfile, Kernel::DeltaEMode::Type EMode,
+                                     const size_t nevents, const size_t maxScatterPtAttempts,
+                                     const bool regenerateTracksForEachLambda) {
+  return std::make_shared<MCAbsorptionStrategy>(interactionVol, beamProfile, EMode, nevents, maxScatterPtAttempts,
+                                                regenerateTracksForEachLambda);
 }
 
 /**
@@ -276,7 +262,7 @@ MatrixWorkspace_uptr MonteCarloAbsorption::doSimulation(const MatrixWorkspace &i
                                                         const InterpolationOption &interpolateOpt,
                                                         const bool useSparseInstrument,
                                                         const size_t maxScatterPtAttempts,
-                                                        const MCInteractionVolume::ScatteringPointVicinity pointsIn) {
+                                                        MCInteractionVolume::ScatteringPointVicinity pointsIn) {
   auto outputWS = createOutputWorkspace(inputWS);
   const auto inputNbins = static_cast<int>(inputWS.blocksize());
 
@@ -314,10 +300,35 @@ MatrixWorkspace_uptr MonteCarloAbsorption::doSimulation(const MatrixWorkspace &i
   prog.setNotifyStep(0.01);
   const std::string reportMsg = "Computing corrections";
 
-  // Configure strategy
-  auto interactionVolume = createInteractionVolume(inputWS.sample(), maxScatterPtAttempts, pointsIn);
-  auto strategy = createStrategy(*interactionVolume, *beamProfile, efixed.emode(), nevents, maxScatterPtAttempts,
-                                 resimulateTracksForDiffWavelengths);
+  // Configure interaction volume
+
+  bool hasGaugeVol = false;
+  Geometry::IObject_sptr gaugeVolume = nullptr;
+
+  try {
+    inputWS.run().getProperty("GaugeVolume");
+    hasGaugeVol = true;
+  } catch (const std::exception &) {
+  }
+
+  if (hasGaugeVol) {
+    std::string xmlString = inputWS.run().getProperty("GaugeVolume")->value();
+    gaugeVolume = ShapeFactory().createShape(xmlString);
+    if (pointsIn != MCInteractionVolume::ScatteringPointVicinity::SAMPLEONLY) {
+      g_log.warning("Gauge Volume found. Scattering Points limited to Sample Only.");
+    }
+    pointsIn = MCInteractionVolume::ScatteringPointVicinity::SAMPLEONLY; // if gauge volume present, only generate
+                                                                         // points from the sample
+  }
+
+  std::shared_ptr<IMCInteractionVolume> interactionVolume =
+      MCInteractionVolume::create(inputWS.sample(), maxScatterPtAttempts, pointsIn, gaugeVolume);
+
+  Geometry::IObject_sptr gv = interactionVolume->getGaugeVolume();
+
+  std::shared_ptr<IMCAbsorptionStrategy> strategy =
+      createStrategy(interactionVolume, *beamProfile, efixed.emode(), nevents, maxScatterPtAttempts,
+                     resimulateTracksForDiffWavelengths);
 
   const auto &spectrumInfo = simulationWS.spectrumInfo();
 
diff --git a/Framework/Algorithms/src/SampleCorrections/MCAbsorptionStrategy.cpp b/Framework/Algorithms/src/SampleCorrections/MCAbsorptionStrategy.cpp
index f10f109f4b42..30c9548bd0dc 100644
--- a/Framework/Algorithms/src/SampleCorrections/MCAbsorptionStrategy.cpp
+++ b/Framework/Algorithms/src/SampleCorrections/MCAbsorptionStrategy.cpp
@@ -28,25 +28,24 @@ namespace Algorithms {
  * @param regenerateTracksForEachLambda Whether to resimulate tracks for each
  * wavelength point or not
  */
-MCAbsorptionStrategy::MCAbsorptionStrategy(IMCInteractionVolume &interactionVolume, const IBeamProfile &beamProfile,
-                                           DeltaEMode::Type EMode, const size_t nevents,
-                                           const size_t maxScatterPtAttempts, const bool regenerateTracksForEachLambda)
-    : m_beamProfile(beamProfile), m_scatterVol(setActiveRegion(interactionVolume, beamProfile)), m_nevents(nevents),
+MCAbsorptionStrategy::MCAbsorptionStrategy(std::shared_ptr<IMCInteractionVolume> interactionVolume,
+                                           const IBeamProfile &beamProfile, DeltaEMode::Type EMode,
+                                           const size_t nevents, const size_t maxScatterPtAttempts,
+                                           const bool regenerateTracksForEachLambda)
+    : m_scatterVol(std::move(interactionVolume)), m_beamProfile(beamProfile), m_nevents(nevents),
       m_maxScatterAttempts(maxScatterPtAttempts), m_EMode(EMode),
-      m_regenerateTracksForEachLambda(regenerateTracksForEachLambda) {}
+      m_regenerateTracksForEachLambda(regenerateTracksForEachLambda) {
+
+  setActiveRegion();
+}
 
 /**
  * Set the active region on the interaction volume as smaller of the sample
  * bounding box and the beam cross section. Trying to keep the beam details
  * outside the interaction volume class
- * @param interactionVolume The interaction volume object
- * @param beamProfile The beam profile
- * @return The interaction volume object with active region set
  */
-IMCInteractionVolume &MCAbsorptionStrategy::setActiveRegion(IMCInteractionVolume &interactionVolume,
-                                                            const IBeamProfile &beamProfile) {
-  interactionVolume.setActiveRegion(beamProfile.defineActiveRegion(interactionVolume.getFullBoundingBox()));
-  return interactionVolume;
+void MCAbsorptionStrategy::setActiveRegion() {
+  m_scatterVol->setActiveRegion(m_beamProfile.defineActiveRegion(m_scatterVol->getFullBoundingBox()));
 }
 
 /**
@@ -70,8 +69,9 @@ void MCAbsorptionStrategy::calculate(Kernel::PseudoRandomNumberGenerator &rng, c
                                      const std::vector<double> &lambdas, const double lambdaFixed,
                                      std::vector<double> &attenuationFactors, std::vector<double> &attFactorErrors,
                                      MCInteractionStatistics &stats) {
-  const auto scatterBounds = m_scatterVol.getFullBoundingBox();
+  const auto scatterBounds = m_scatterVol->getFullBoundingBox();
   const auto nbins = static_cast<int>(lambdas.size());
+  Geometry::IObject_sptr gv = m_scatterVol->getGaugeVolume();
 
   std::vector<double> wgtMean(attenuationFactors.size()), wgtM2(attenuationFactors.size());
 
@@ -85,7 +85,7 @@ void MCAbsorptionStrategy::calculate(Kernel::PseudoRandomNumberGenerator &rng, c
         if (m_regenerateTracksForEachLambda || j == 0) {
           const auto neutron = m_beamProfile.generatePoint(rng, scatterBounds);
           std::tie(success, beforeScatter, afterScatter) =
-              m_scatterVol.calculateBeforeAfterTrack(rng, neutron.startPos, finalPos, stats);
+              m_scatterVol->calculateBeforeAfterTrack(rng, neutron.startPos, finalPos, stats);
         } else {
           success = true;
         }
@@ -120,7 +120,9 @@ void MCAbsorptionStrategy::calculate(Kernel::PseudoRandomNumberGenerator &rng, c
                                    std::to_string(m_maxScatterAttempts) +
                                    " attempts. Try increasing the maximum "
                                    "threshold or if this does not help then "
-                                   "please check the defined shape.");
+                                   "please check the defined shape and, "
+                                   "if defined, the gauge volume (both its shape "
+                                   "and its intersection with the defined sample shape).");
         }
       } while (true);
     }
diff --git a/Framework/Algorithms/src/SampleCorrections/MCInteractionVolume.cpp b/Framework/Algorithms/src/SampleCorrections/MCInteractionVolume.cpp
index 7f51bc2ef8c2..c7eb80a3a4e6 100644
--- a/Framework/Algorithms/src/SampleCorrections/MCInteractionVolume.cpp
+++ b/Framework/Algorithms/src/SampleCorrections/MCInteractionVolume.cpp
@@ -13,6 +13,7 @@
 #include <iomanip>
 
 namespace Mantid {
+using Geometry::IObject_sptr;
 using Geometry::Track;
 using Kernel::V3D;
 
@@ -22,24 +23,17 @@ namespace Algorithms {
  * Construct the volume encompassing the sample + any environment kit. The
  * active region defines a bounding region for the sampling of the scattering
  * position.
- * @param sample A reference to a sample object that defines a valid shape
- * & material
- * @param maxScatterAttempts The maximum number of tries to generate a random
- * point within the object. [Default=5000]
- * @param pointsIn Where to generate the scattering point in
  */
-MCInteractionVolume::MCInteractionVolume(const API::Sample &sample, const size_t maxScatterAttempts,
-                                         const MCInteractionVolume::ScatteringPointVicinity pointsIn)
-    : m_sample(sample.getShape().clone()), m_env(nullptr), m_activeRegion(getFullBoundingBox()),
-      m_maxScatterAttempts(maxScatterAttempts), m_pointsIn(pointsIn) {
-  try {
-    m_env = &sample.getEnvironment();
-    assert(m_env);
-    if (m_env->nelements() == 0) {
-      throw std::invalid_argument("MCInteractionVolume() - Sample enviroment has zero components.");
+void MCInteractionVolume::init() {
+  m_activeRegion = getFullBoundingBox();
+  if (m_env) {
+    try {
+      if (m_env->nelements() == 0) {
+        throw std::invalid_argument("MCInteractionVolume() - Sample enviroment has zero components.");
+      }
+    } catch (std::runtime_error &) {
+      // swallow this as no defined environment from getEnvironment
     }
-  } catch (std::runtime_error &) {
-    // swallow this as no defined environment from getEnvironment
   }
 
   bool atLeastOneValidShape = m_sample->hasValidShape();
@@ -58,19 +52,77 @@ MCInteractionVolume::MCInteractionVolume(const API::Sample &sample, const size_t
 }
 
 /**
- * Returns the axis-aligned bounding box for the volume including env if
- * m_pointsIn != SampleOnly
- * @return The bounding box
+ * Factory Method for constructing the volume encompassing the sample + any environment kit. The
+ * active region defines a bounding region for the sampling of the scattering
+ * position.
+ * @param sample A reference to a sample object that defines a valid shape
+ * & material
+ * @param maxScatterAttempts The maximum number of tries to generate a random
+ * point within the object. [Default=5000]
+ * @param pointsIn Where to generate the scattering point in
+ * @param gaugeVolume Pointer to any gauge volume object defined for the interaction
+ * @return returns a shared pointer to interaction volume object
+ */
+std::shared_ptr<IMCInteractionVolume>
+MCInteractionVolume::create(const API::Sample &sample, const size_t maxScatterAttempts,
+                            const MCInteractionVolume::ScatteringPointVicinity pointsIn, IObject_sptr gaugeVolume) {
+  auto interactionVol =
+      std::shared_ptr<MCInteractionVolume>(new MCInteractionVolume(sample, maxScatterAttempts, pointsIn, gaugeVolume));
+  // init calls member functions that require instantiation of virtual functions so deferring this until after
+  // construction through a factory is preferable.
+  interactionVol->init();
+  return interactionVol;
+}
+
+/**
+ * Construct the volume encompassing the sample + any environment kit. The
+ * active region defines a bounding region for the sampling of the scattering
+ * position.
+ * @param sample A reference to a sample object that defines a valid shape
+ * & material
+ * @param maxScatterAttempts The maximum number of tries to generate a random
+ * point within the object. [Default=5000]
+ * @param pointsIn Where to generate the scattering point in
+ * @param gaugeVolume Pointer to any gauge volume object defined for the interaction
+ */
+MCInteractionVolume::MCInteractionVolume(const API::Sample &sample, const size_t maxScatterAttempts,
+                                         const MCInteractionVolume::ScatteringPointVicinity pointsIn,
+                                         IObject_sptr gaugeVolume)
+    : m_sample(sample.getShape().clone()), m_env(sample.hasEnvironment() ? &sample.getEnvironment() : nullptr),
+      m_maxScatterAttempts(maxScatterAttempts), m_pointsIn(pointsIn), m_gaugeVolume(gaugeVolume) {}
+
+/**
+ * Returns the defined gauge volume if one is present, otherwise returns nullptr
+ * @return Shared pointer to gauge volume object
+ */
+Geometry::IObject_sptr MCInteractionVolume::getGaugeVolume() const { return m_gaugeVolume; }
+
+/**
+ * Sets the gauge volume on the InteractionVolume
+ * @param gaugeVolume A shared pointer to the gauge volume shape object
+ */
+void MCInteractionVolume::setGaugeVolume(Geometry::IObject_sptr gaugeVolume) { m_gaugeVolume = gaugeVolume; }
+
+/**
+ * Returns the defined gauge volume if one is present, otherwise returns axis-aligned bounding box
+ * for the volume including env if m_pointsIn != SampleOnly
+ * @return The bounding box of the interaction volume
  */
 const Geometry::BoundingBox MCInteractionVolume::getFullBoundingBox() const {
   auto sampleBox = m_sample->getBoundingBox();
-  if (m_pointsIn != ScatteringPointVicinity::SAMPLEONLY && m_env) {
+  if (m_gaugeVolume != nullptr) {
+    sampleBox = m_gaugeVolume->getBoundingBox();
+  } else if (m_pointsIn != ScatteringPointVicinity::SAMPLEONLY && m_env) {
     const auto &envBox = m_env->boundingBox();
     sampleBox.grow(envBox);
   }
   return sampleBox;
 }
 
+/**
+ * Sets the active region volume on the InteractionVolume
+ * @param region A reference to a Bounding Box for the active region
+ */
 void MCInteractionVolume::setActiveRegion(const Geometry::BoundingBox &region) { m_activeRegion = region; }
 
 /**
@@ -108,8 +160,18 @@ int MCInteractionVolume::getComponentIndex(Kernel::PseudoRandomNumberGenerator &
 std::optional<Kernel::V3D>
 MCInteractionVolume::generatePointInObjectByIndex(int componentIndex, Kernel::PseudoRandomNumberGenerator &rng) const {
   std::optional<Kernel::V3D> pointGenerated{std::nullopt};
+  std::optional<Kernel::V3D> tmpPoint{std::nullopt};
   if (componentIndex == -1) {
-    pointGenerated = m_sample->generatePointInObject(rng, m_activeRegion, 1);
+    if (m_gaugeVolume != nullptr) {
+      tmpPoint = m_gaugeVolume->generatePointInObject(rng, m_activeRegion, 1);
+      if (tmpPoint) {
+        if (m_sample->isValid(tmpPoint.value())) {
+          pointGenerated = tmpPoint;
+        }
+      }
+    } else {
+      pointGenerated = m_sample->generatePointInObject(rng, m_activeRegion, 1);
+    }
   } else {
     pointGenerated = m_env->getComponent(componentIndex).generatePointInObject(rng, m_activeRegion, 1);
   }
diff --git a/Framework/Algorithms/test/MCAbsorptionStrategyTest.h b/Framework/Algorithms/test/MCAbsorptionStrategyTest.h
index e6fb9e63fe65..787c09ce8ff6 100644
--- a/Framework/Algorithms/test/MCAbsorptionStrategyTest.h
+++ b/Framework/Algorithms/test/MCAbsorptionStrategyTest.h
@@ -46,8 +46,8 @@ class MCAbsorptionStrategyTest : public CxxTest::TestSuite {
     MockBeamProfile testBeamProfile;
     EXPECT_CALL(testBeamProfile, defineActiveRegion(_)).WillOnce(Return(testSampleSphere.getShape().getBoundingBox()));
     const size_t nevents(10), maxTries(100);
-    MCInteractionVolume interactionVolume(testSampleSphere);
-    MCAbsorptionStrategy mcabsorb(interactionVolume, testBeamProfile, Mantid::Kernel::DeltaEMode::Type::Direct, nevents,
+    std::shared_ptr<IMCInteractionVolume> interactionVol = MCInteractionVolume::create(testSampleSphere);
+    MCAbsorptionStrategy mcabsorb(interactionVol, testBeamProfile, Mantid::Kernel::DeltaEMode::Type::Direct, nevents,
                                   maxTries, false);
     // 3 random numbers per event expected
     MockRNG rng;
@@ -86,8 +86,8 @@ class MCAbsorptionStrategyTest : public CxxTest::TestSuite {
     MockBeamProfile testBeamProfile;
     EXPECT_CALL(testBeamProfile, defineActiveRegion(_)).WillOnce(Return(testSampleSphere.getShape().getBoundingBox()));
     const size_t nevents(3), maxTries(100);
-    MCInteractionVolume interactionVolume(testSampleSphere);
-    MCAbsorptionStrategy mcabsorb(interactionVolume, testBeamProfile, Mantid::Kernel::DeltaEMode::Type::Direct, nevents,
+    std::shared_ptr<IMCInteractionVolume> interactionVol = MCInteractionVolume::create(testSampleSphere);
+    MCAbsorptionStrategy mcabsorb(interactionVol, testBeamProfile, Mantid::Kernel::DeltaEMode::Type::Direct, nevents,
                                   maxTries, false);
     // 3 random numbers per event expected to generate x,y,z of each scatter pt
     MockRNG rng;
@@ -138,7 +138,7 @@ class MCAbsorptionStrategyTest : public CxxTest::TestSuite {
     using namespace MonteCarloTesting;
     using namespace ::testing;
     MockBeamProfile testBeamProfile;
-    MockMCInteractionVolume testInteractionVolume;
+    std::shared_ptr<MockMCInteractionVolume> testInteractionVolume = MockMCInteractionVolume::createMock();
     MCAbsorptionStrategy testStrategy(testInteractionVolume, testBeamProfile, Mantid::Kernel::DeltaEMode::Elastic, 5, 2,
                                       true);
     MockRNG rng;
@@ -147,7 +147,7 @@ class MCAbsorptionStrategyTest : public CxxTest::TestSuite {
     MCInteractionStatistics trackStatistics(-1, Mantid::API::Sample{});
     Mantid::Geometry::BoundingBox emptyBoundingBox;
     EXPECT_CALL(testBeamProfile, generatePoint(_, _)).Times(Exactly(static_cast<int>(6)));
-    EXPECT_CALL(testInteractionVolume, getFullBoundingBox()).Times(1).WillOnce(Return(emptyBoundingBox));
+    EXPECT_CALL(*testInteractionVolume, getFullBoundingBox()).Times(1).WillOnce(Return(emptyBoundingBox));
 
     auto beforeScatter = std::make_shared<MockTrack>();
     auto afterScatter = std::make_shared<MockTrack>();
@@ -157,7 +157,7 @@ class MCAbsorptionStrategyTest : public CxxTest::TestSuite {
     auto ReturnNullTracksAndFalse = [beforeScatter, afterScatter](auto &, auto &, auto &, auto &) {
       return std::tuple{false, nullptr, nullptr};
     };
-    EXPECT_CALL(testInteractionVolume, calculateBeforeAfterTrack(_, _, _, _))
+    EXPECT_CALL(*testInteractionVolume, calculateBeforeAfterTrack(_, _, _, _))
         .Times(Exactly(6))
         .WillOnce(Invoke(ReturnTracksAndTrue))
         .WillOnce(Invoke(ReturnNullTracksAndFalse))
@@ -192,8 +192,8 @@ class MCAbsorptionStrategyTest : public CxxTest::TestSuite {
     auto testThinAnnulus = MonteCarloTesting::createTestSample(MonteCarloTesting::TestSampleType::ThinAnnulus);
     RectangularBeamProfile testBeamProfile(ReferenceFrame(Y, Z, Right, "source"), V3D(), 1, 1);
     const size_t nevents(10), maxTries(1);
-    MCInteractionVolume interactionVolume(testThinAnnulus);
-    MCAbsorptionStrategy mcabs(interactionVolume, testBeamProfile, Mantid::Kernel::DeltaEMode::Type::Direct, nevents,
+    std::shared_ptr<IMCInteractionVolume> interactionVol = MCInteractionVolume::create(testThinAnnulus);
+    MCAbsorptionStrategy mcabs(interactionVol, testBeamProfile, Mantid::Kernel::DeltaEMode::Type::Direct, nevents,
                                maxTries, false);
     MockRNG rng;
     EXPECT_CALL(rng, nextValue()).WillRepeatedly(Return(0.5));
@@ -220,16 +220,35 @@ class MCAbsorptionStrategyTest : public CxxTest::TestSuite {
     MOCK_CONST_METHOD1(defineActiveRegion, Mantid::Geometry::BoundingBox(const Mantid::Geometry::BoundingBox &));
     GNU_DIAG_ON_SUGGEST_OVERRIDE
   };
+
   class MockMCInteractionVolume final : public IMCInteractionVolume {
   public:
     GNU_DIAG_OFF_SUGGEST_OVERRIDE
+    // Mock Factory
+    static std::shared_ptr<MockMCInteractionVolume> createMock() {
+      auto mock = std::make_shared<MockMCInteractionVolume>();
+      mock->init();
+      return mock;
+    }
+
+    // Factory method mock (static, not part of IMCInteractionVolume)
+    static std::shared_ptr<IMCInteractionVolume> create(const Mantid::API::Sample &, size_t, ScatteringPointVicinity,
+                                                        Mantid::Geometry::IObject_sptr) {
+      return createMock();
+    }
+
     MOCK_CONST_METHOD4(calculateBeforeAfterTrack,
                        Mantid::Algorithms::TrackPair(Mantid::Kernel::PseudoRandomNumberGenerator &rng,
                                                      const Mantid::Kernel::V3D &startPos,
                                                      const Mantid::Kernel::V3D &endPos,
                                                      MCInteractionStatistics &stats));
     MOCK_CONST_METHOD0(getFullBoundingBox, const Mantid::Geometry::BoundingBox());
+    MOCK_CONST_METHOD1(generatePoint,
+                       Mantid::Algorithms::ComponentScatterPoint(Mantid::Kernel::PseudoRandomNumberGenerator &rng));
     MOCK_METHOD1(setActiveRegion, void(const Mantid::Geometry::BoundingBox &));
+    MOCK_METHOD1(setGaugeVolume, void(Mantid::Geometry::IObject_sptr gaugeVolume));
+    MOCK_CONST_METHOD0(getGaugeVolume, Mantid::Geometry::IObject_sptr());
+    MOCK_METHOD0(init, void());
     GNU_DIAG_ON_SUGGEST_OVERRIDE
   };
   class MockTrack final : public Mantid::Geometry::Track {
diff --git a/Framework/Algorithms/test/MCInteractionVolumeTest.h b/Framework/Algorithms/test/MCInteractionVolumeTest.h
index cd192b94bb60..56e0f66b89c4 100644
--- a/Framework/Algorithms/test/MCInteractionVolumeTest.h
+++ b/Framework/Algorithms/test/MCInteractionVolumeTest.h
@@ -18,6 +18,7 @@
 #include <gmock/gmock.h>
 
 using Mantid::Algorithms::ComponentScatterPoint;
+using Mantid::Algorithms::IMCInteractionVolume;
 using Mantid::Algorithms::MCInteractionStatistics;
 using Mantid::Algorithms::MCInteractionVolume;
 using namespace ::testing;
@@ -38,10 +39,10 @@ class MCInteractionVolumeTest : public CxxTest::TestSuite {
   void test_Bounding_Volume_Matches_Sample() {
     auto sample = createTestSample(TestSampleType::SolidSphere);
     auto sampleBox = sample.getShape().getBoundingBox();
-    MCInteractionVolume interactor(sample);
-    interactor.setActiveRegion(sampleBox);
+    std::shared_ptr<IMCInteractionVolume> interactor = MCInteractionVolume::create(sample);
+    interactor->setActiveRegion(sampleBox);
 
-    const auto interactionBox = interactor.getFullBoundingBox();
+    const auto interactionBox = interactor->getFullBoundingBox();
     TS_ASSERT_EQUALS(sampleBox.minPoint(), interactionBox.minPoint());
     TS_ASSERT_EQUALS(sampleBox.maxPoint(), interactionBox.maxPoint());
   }
@@ -55,10 +56,10 @@ class MCInteractionVolumeTest : public CxxTest::TestSuite {
     EXPECT_CALL(rng, nextValue()).Times(Exactly(3)).WillRepeatedly(Return(0.25));
 
     auto sample = createTestSample(TestSampleType::SolidSphere);
-    MCInteractionVolume interactor(sample);
+    std::shared_ptr<IMCInteractionVolume> interactor = MCInteractionVolume::create(sample);
     MCInteractionStatistics trackStatistics(-1, sample);
     auto [success, beforeScatter, afterScatter] =
-        interactor.calculateBeforeAfterTrack(rng, startPos, endPos, trackStatistics);
+        interactor->calculateBeforeAfterTrack(rng, startPos, endPos, trackStatistics);
     TS_ASSERT(success);
     TestGeneratedTracks(startPos, endPos, beforeScatter, afterScatter, sample.getShape());
   }
@@ -74,17 +75,17 @@ class MCInteractionVolumeTest : public CxxTest::TestSuite {
     // force scatter near front
     EXPECT_CALL(rng, nextValue()).Times(Exactly(3)).WillRepeatedly(Return(0.25));
 
-    MCInteractionVolume interactor(sample);
+    std::shared_ptr<IMCInteractionVolume> interactor = MCInteractionVolume::create(sample);
     MCInteractionStatistics trackStatistics(-1, sample);
     auto [success, beforeScatter, afterScatter] =
-        interactor.calculateBeforeAfterTrack(rng, startPos, endPos, trackStatistics);
+        interactor->calculateBeforeAfterTrack(rng, startPos, endPos, trackStatistics);
     TestGeneratedTracks(startPos, endPos, beforeScatter, afterScatter, sample.getShape());
     TS_ASSERT(Mock::VerifyAndClearExpectations(&rng));
 
     // force scatter near back (longer distance traversed with before lambda)
     EXPECT_CALL(rng, nextValue()).Times(Exactly(3)).WillRepeatedly(Return(0.75));
     std::tie(success, beforeScatter, afterScatter) =
-        interactor.calculateBeforeAfterTrack(rng, startPos, endPos, trackStatistics);
+        interactor->calculateBeforeAfterTrack(rng, startPos, endPos, trackStatistics);
     TestGeneratedTracks(startPos, endPos, beforeScatter, afterScatter, sample.getShape());
   }
 
@@ -104,11 +105,11 @@ class MCInteractionVolumeTest : public CxxTest::TestSuite {
         .WillOnce(Return(0.5))  // r2
         .WillOnce(Return(0.5)); // r3
 
-    MCInteractionVolume interactor(sample);
-    interactor.setActiveRegion(sample.getEnvironment().boundingBox());
+    std::shared_ptr<IMCInteractionVolume> interactor = MCInteractionVolume::create(sample);
+    interactor->setActiveRegion(sample.getEnvironment().boundingBox());
     MCInteractionStatistics trackStatistics(-1, sample);
     auto [success, beforeScatter, afterScatter] =
-        interactor.calculateBeforeAfterTrack(rng, startPos, endPos, trackStatistics);
+        interactor->calculateBeforeAfterTrack(rng, startPos, endPos, trackStatistics);
     TestGeneratedTracks(startPos, endPos, beforeScatter, afterScatter, sample.getEnvironment().getContainer());
     TS_ASSERT(Mock::VerifyAndClearExpectations(&rng));
 
@@ -116,7 +117,7 @@ class MCInteractionVolumeTest : public CxxTest::TestSuite {
     EXPECT_CALL(rng, nextInt(-1, 0)).Times(Exactly(1)).WillOnce(Return(-1));      // MCInteractionVolume::generatePoint
     EXPECT_CALL(rng, nextValue()).Times(Exactly(3)).WillRepeatedly(Return(0.25)); // RandomPoint::bounded r1, r2, r3
     std::tie(success, beforeScatter, afterScatter) =
-        interactor.calculateBeforeAfterTrack(rng, startPos, endPos, trackStatistics);
+        interactor->calculateBeforeAfterTrack(rng, startPos, endPos, trackStatistics);
     TestGeneratedTracks(startPos, endPos, beforeScatter, afterScatter, sample.getShape());
     TS_ASSERT(Mock::VerifyAndClearExpectations(&rng));
   }
@@ -126,7 +127,7 @@ class MCInteractionVolumeTest : public CxxTest::TestSuite {
     auto kit = createTestKit();
     sample.setEnvironment(std::make_unique<Mantid::Geometry::SampleEnvironment>(*kit));
 
-    TS_ASSERT_THROWS_NOTHING(MCInteractionVolume mcv(sample));
+    TS_ASSERT_THROWS_NOTHING(MCInteractionVolume::create(sample));
   }
 
   //----------------------------------------------------------------------------
@@ -135,10 +136,10 @@ class MCInteractionVolumeTest : public CxxTest::TestSuite {
   void test_Construction_With_Invalid_Sample_Shape_Throws_Error() {
     Mantid::API::Sample sample;
     // nothing
-    TS_ASSERT_THROWS(MCInteractionVolume mcv(sample), const std::invalid_argument &);
+    TS_ASSERT_THROWS(MCInteractionVolume::create(sample), const std::invalid_argument &);
     // valid shape
     sample.setShape(ComponentCreationHelper::createSphere(1));
-    TS_ASSERT_THROWS_NOTHING(MCInteractionVolume mcv(sample));
+    TS_ASSERT_THROWS_NOTHING(MCInteractionVolume::create(sample));
   }
 
   void test_Throws_If_Point_Cannot_Be_Generated() {
@@ -150,9 +151,9 @@ class MCInteractionVolumeTest : public CxxTest::TestSuite {
     MersenneTwister rng;
     rng.setSeed(1);
     const size_t maxTries(1);
-    MCInteractionVolume interactor(sample, maxTries);
+    std::shared_ptr<IMCInteractionVolume> interactor = MCInteractionVolume::create(sample, maxTries);
     MCInteractionStatistics trackStatistics(-1, sample);
-    TS_ASSERT_THROWS(interactor.calculateBeforeAfterTrack(rng, startPos, endPos, trackStatistics),
+    TS_ASSERT_THROWS(interactor->calculateBeforeAfterTrack(rng, startPos, endPos, trackStatistics),
                      const std::runtime_error &);
   }
 
@@ -165,8 +166,8 @@ class MCInteractionVolumeTest : public CxxTest::TestSuite {
     sample.setShape(ComponentCreationHelper::createSphere(1));
     sample.setEnvironment(std::make_unique<Mantid::Geometry::SampleEnvironment>(*kit));
 
-    MCInteractionVolume interactor(sample, maxAttempts);
-    interactor.setActiveRegion(kit->getComponent(0).getBoundingBox());
+    std::shared_ptr<IMCInteractionVolume> interactor = MCInteractionVolume::create(sample, maxAttempts);
+    interactor->setActiveRegion(kit->getComponent(0).getBoundingBox());
 
     // Generate "random" sequence
     MockRNG rng;
@@ -174,31 +175,31 @@ class MCInteractionVolumeTest : public CxxTest::TestSuite {
     EXPECT_CALL(rng, nextInt(_, _)).Times(Exactly(1)).WillOnce(Return(0));
     EXPECT_CALL(rng, nextValue()).Times(3).WillOnce(Return(0.55)).WillOnce(Return(0.65)).WillOnce(Return(0.70));
     ComponentScatterPoint comp1Point;
-    TS_ASSERT_THROWS_NOTHING(comp1Point = interactor.generatePoint(rng));
+    TS_ASSERT_THROWS_NOTHING(comp1Point = interactor->generatePoint(rng));
     TS_ASSERT(kit->isValid(comp1Point.scatterPoint));
     TS_ASSERT_EQUALS(comp1Point.componentIndex, 0);
     TS_ASSERT(Mock::VerifyAndClearExpectations(&rng));
 
     // Selects second component
-    MCInteractionVolume interactor2(sample, maxAttempts);
-    interactor2.setActiveRegion(kit->getComponent(1).getBoundingBox());
+    std::shared_ptr<IMCInteractionVolume> interactor2 = MCInteractionVolume::create(sample, maxAttempts);
+    interactor2->setActiveRegion(kit->getComponent(1).getBoundingBox());
 
     EXPECT_CALL(rng, nextInt(_, _)).Times(Exactly(1)).WillOnce(Return(1));
     EXPECT_CALL(rng, nextValue()).Times(3).WillOnce(Return(0.55)).WillOnce(Return(0.65)).WillOnce(Return(0.70));
     ComponentScatterPoint comp2Point;
-    TS_ASSERT_THROWS_NOTHING(comp2Point = interactor2.generatePoint(rng));
+    TS_ASSERT_THROWS_NOTHING(comp2Point = interactor2->generatePoint(rng));
     TS_ASSERT(comp2Point.scatterPoint != comp1Point.scatterPoint);
     TS_ASSERT(kit->isValid(comp2Point.scatterPoint));
     TS_ASSERT_EQUALS(comp2Point.componentIndex, 1);
     TS_ASSERT(Mock::VerifyAndClearExpectations(&rng));
 
     // Selects third component
-    MCInteractionVolume interactor3(sample, maxAttempts);
-    interactor3.setActiveRegion(kit->getComponent(2).getBoundingBox());
+    std::shared_ptr<IMCInteractionVolume> interactor3 = MCInteractionVolume::create(sample, maxAttempts);
+    interactor3->setActiveRegion(kit->getComponent(2).getBoundingBox());
     EXPECT_CALL(rng, nextInt(_, _)).Times(Exactly(1)).WillOnce(Return(2));
     EXPECT_CALL(rng, nextValue()).Times(3).WillOnce(Return(0.55)).WillOnce(Return(0.65)).WillOnce(Return(0.70));
     ComponentScatterPoint comp3Point;
-    TS_ASSERT_THROWS_NOTHING(comp3Point = interactor3.generatePoint(rng));
+    TS_ASSERT_THROWS_NOTHING(comp3Point = interactor3->generatePoint(rng));
     TS_ASSERT(comp3Point.scatterPoint != comp2Point.scatterPoint);
     TS_ASSERT(comp3Point.scatterPoint != comp1Point.scatterPoint);
     TS_ASSERT(kit->isValid(comp3Point.scatterPoint));
@@ -222,9 +223,9 @@ class MCInteractionVolumeTest : public CxxTest::TestSuite {
     sample.setShape(ComponentCreationHelper::createSphere(1));
     sample.setEnvironment(std::make_unique<Mantid::Geometry::SampleEnvironment>(*kit));
 
-    MCInteractionVolume interactor(sample, maxAttempts);
+    std::shared_ptr<IMCInteractionVolume> interactor = MCInteractionVolume::create(sample, maxAttempts);
     // Restrict region to can
-    TS_ASSERT_THROWS(interactor.generatePoint(rng), const std::runtime_error &);
+    TS_ASSERT_THROWS(interactor->generatePoint(rng), const std::runtime_error &);
     TS_ASSERT(Mock::VerifyAndClearExpectations(&rng));
   }
 
diff --git a/Framework/Algorithms/test/MonteCarloAbsorptionTest.h b/Framework/Algorithms/test/MonteCarloAbsorptionTest.h
index 3324b9efde72..e5c861ec757c 100644
--- a/Framework/Algorithms/test/MonteCarloAbsorptionTest.h
+++ b/Framework/Algorithms/test/MonteCarloAbsorptionTest.h
@@ -39,7 +39,9 @@ enum class Environment {
   CubeSamplePlusContainer,
   CylinderSampleOnly,
   CylinderSamplePlusContainer,
-  MeshSamplePlusContainer
+  CylinderSamplePlusGV,
+  RotatedCylinderSamplePlusGV,
+  MeshSamplePlusContainer,
 };
 
 struct TestWorkspaceDescriptor {
@@ -49,6 +51,7 @@ struct TestWorkspaceDescriptor {
   Environment sampleEnviron;
   unsigned int emode;
   double efixed;
+  bool straightThrough = true;
 };
 
 void addSample(const Mantid::API::MatrixWorkspace_sptr &ws, const Environment environment) {
@@ -127,21 +130,65 @@ void addSample(const Mantid::API::MatrixWorkspace_sptr &ws, const Environment en
                                        Mantid::PhysicalConstants::NeutronAtom::ReferenceLambda /*absorption xs*/),
                                    1 /*number density*/)));
       ws->mutableSample().setShape(shape);
-      if (environment == Environment::CubeSamplePlusContainer) {
-        auto xmlShapeStreamFront = ComponentCreationHelper::cuboidXML(0.005, 0.005, 0.0025, {0., 0., -0.0125}, "front");
-        auto xmlShapeStreamBack = ComponentCreationHelper::cuboidXML(0.005, 0.005, 0.0025, {0., 0., 0.0025}, "back");
-        std::string combinedXML = xmlShapeStreamFront + xmlShapeStreamBack + "<algebra val=\"back:front\"/>";
-        ShapeFactory shapeMaker;
-        auto holderShape = shapeMaker.createShape(combinedXML);
-        auto shape = std::shared_ptr<IObject>(holderShape->cloneWithMaterial(
-            Mantid::Kernel::Material("Test",
-                                     Mantid::PhysicalConstants::NeutronAtom(
-                                         0, 0, 0, 0, 0, 0.5 /*total scattering xs*/,
-                                         Mantid::PhysicalConstants::NeutronAtom::ReferenceLambda /*absorption xs*/),
-                                     1 /*number density*/)));
-        auto can = std::make_shared<Container>(shape);
-        ws->mutableSample().setEnvironment(std::make_unique<SampleEnvironment>("can", can));
-      }
+    } else if (environment == Environment::CylinderSamplePlusGV) {
+      // Define a cylindrical sample shape orientated with axis perpedicular to beam
+      constexpr double sampleRadius{0.01};
+      constexpr double sampleHeight{0.1};
+      const V3D sampleBaseCentre{0., -sampleHeight / 2., 0.};
+      const V3D yAxis{0., 1., 0.};
+      auto cylinderShape = ComponentCreationHelper::createCappedCylinder(sampleRadius, sampleHeight, sampleBaseCentre,
+                                                                         yAxis, "sample-cylinder");
+      // create test material with mu=1
+      auto shape = std::shared_ptr<IObject>(cylinderShape->cloneWithMaterial(Mantid::Kernel::Material(
+          "Test",
+          Mantid::PhysicalConstants::NeutronAtom(0, 0, 0, 0, 0, 1.0 /*total scattering xs*/, 0.0 /*absorption xs*/),
+          1 /*number density*/)));
+      ws->mutableSample().setShape(shape);
+      // create 1mm radius cylinder gauge volume
+      constexpr double gvRadius{0.001};
+      constexpr double gvHeight{2 * sampleRadius};
+      const V3D gvBaseCentre{0., 0., -sampleRadius};
+      const V3D gvYAxis{0., 0., 1.};
+      auto gaugeVolumeXML =
+          ComponentCreationHelper::cappedCylinderXML(gvRadius, gvHeight, gvBaseCentre, gvYAxis, "gauge-volume");
+      ws->mutableRun().addProperty<std::string>("GaugeVolume", gaugeVolumeXML);
+    } else if (environment == Environment::RotatedCylinderSamplePlusGV) {
+      // Define a cylindrical sample shape which has been rotated 45 degrees about (1,0,1)
+      constexpr double sampleRadius{0.01};
+      constexpr double sampleHeight{0.1};
+      const V3D sampleBaseCentre{-sampleHeight / (4.0), -sampleHeight / (2.0 * std::sqrt(2)), -sampleHeight / (4.0)};
+      const V3D yAxis = -sampleBaseCentre;
+      auto cylinderShape = ComponentCreationHelper::createCappedCylinder(sampleRadius, sampleHeight, sampleBaseCentre,
+                                                                         yAxis, "sample-cylinder");
+      // create test material with mu=1
+      auto shape = std::shared_ptr<IObject>(cylinderShape->cloneWithMaterial(Mantid::Kernel::Material(
+          "Test",
+          Mantid::PhysicalConstants::NeutronAtom(0, 0, 0, 0, 0, 1.0 /*total scattering xs*/, 0.0 /*absorption xs*/),
+          1 /*number density*/)));
+      ws->mutableSample().setShape(shape);
+      // create 0.1mm radius cylinder gauge volume
+      constexpr double gvRadius{0.0001};       //
+      constexpr double gvHeight{2 * 0.011547}; // beam path length within ellipse of r = 0.01 at 45 degrees
+      const V3D gvBaseCentre{0., 0., -gvHeight / 2.};
+      const V3D gvYAxis{0., 0., 1.};
+      auto gaugeVolumeXML =
+          ComponentCreationHelper::cappedCylinderXML(gvRadius, gvHeight, gvBaseCentre, gvYAxis, "gauge-volume");
+      ws->mutableRun().addProperty<std::string>("GaugeVolume", gaugeVolumeXML);
+    }
+    if (environment == Environment::CubeSamplePlusContainer) {
+      auto xmlShapeStreamFront = ComponentCreationHelper::cuboidXML(0.005, 0.005, 0.0025, {0., 0., -0.0125}, "front");
+      auto xmlShapeStreamBack = ComponentCreationHelper::cuboidXML(0.005, 0.005, 0.0025, {0., 0., 0.0025}, "back");
+      std::string combinedXML = xmlShapeStreamFront + xmlShapeStreamBack + "<algebra val=\"back:front\"/>";
+      ShapeFactory shapeMaker;
+      auto holderShape = shapeMaker.createShape(combinedXML);
+      auto shape = std::shared_ptr<IObject>(holderShape->cloneWithMaterial(
+          Mantid::Kernel::Material("Test",
+                                   Mantid::PhysicalConstants::NeutronAtom(
+                                       0, 0, 0, 0, 0, 0.5 /*total scattering xs*/,
+                                       Mantid::PhysicalConstants::NeutronAtom::ReferenceLambda /*absorption xs*/),
+                                   1 /*number density*/)));
+      auto can = std::make_shared<Container>(shape);
+      ws->mutableSample().setEnvironment(std::make_unique<SampleEnvironment>("can", can));
     }
   }
 }
@@ -165,12 +212,18 @@ Mantid::API::MatrixWorkspace_sptr setUpWS(const TestWorkspaceDescriptor &wsProps
     auto pixelShape = ComponentCreationHelper::createCappedCylinder(
         cylRadius, cylHeight, V3D(0.0, -cylHeight / 2.0, 0.0), V3D(0., 1.0, 0.), "pixel-shape");
 
-    // source and detector are at +/-100m so tracks approx parallel to beam
+    // source and detector are at +/-100m so tracks approx parallel to beam for straight case
     constexpr double distance = 100.0;
     Detector *det = new Detector("det", 1, pixelShape, nullptr);
-    det->setPos(0, 0, distance);
-    testInst->add(det);
-    testInst->markAsDetector(det);
+    if (wsProps.straightThrough) {
+      det->setPos(0, 0, distance);
+      testInst->add(det);
+      testInst->markAsDetector(det);
+    } else {
+      det->setPos(-distance, 0, 0);
+      testInst->add(det);
+      testInst->markAsDetector(det);
+    }
 
     ComponentCreationHelper::addSourceToInstrument(testInst, V3D(0.0, 0.0, -distance));
     ComponentCreationHelper::addSampleToInstrument(testInst, V3D(0.0, 0.0, 0.0));
@@ -349,6 +402,56 @@ class MonteCarloAbsorptionTest : public CxxTest::TestSuite {
     TS_ASSERT_DELTA(calculatedAttFactor, yData[0], delta);
   }
 
+  void test_Workspace_With_Cylindrical_Sample_And_Gauge_Volume() {
+    using namespace Mantid::Geometry;
+
+    using Mantid::Kernel::DeltaEMode;
+    TestWorkspaceDescriptor wsProps = {1, 2, false, Environment::CylinderSamplePlusGV, DeltaEMode::Elastic, 1};
+    auto testWS = setUpWS(wsProps);
+
+    auto mcAbsorb = createAlgorithm();
+    constexpr int NEVENTS = 1000;
+    mcAbsorb->setProperty("EventsPerPoint", NEVENTS);
+
+    TS_ASSERT_THROWS_NOTHING(mcAbsorb->setProperty("InputWorkspace", testWS));
+    TS_ASSERT_THROWS_NOTHING(mcAbsorb->execute());
+    auto outputWS = getOutputWorkspace(mcAbsorb);
+
+    verifyDimensions(wsProps, outputWS);
+    auto yData = outputWS->getSpectrum(0).dataY();
+
+    constexpr double delta(1e-03);
+    // pass straight through - all path lengths are exactly 2cm
+    const double calculatedAttFactor = exp(-2);
+    TS_ASSERT_DELTA(calculatedAttFactor, yData[0], delta);
+  }
+
+  void test_Workspace_With_Rotated_Cylindrical_Sample_And_Gauge_Volume() {
+    using namespace Mantid::Geometry;
+
+    using Mantid::Kernel::DeltaEMode;
+    // set detector at 90 degrees to beam
+    TestWorkspaceDescriptor wsProps = {1,  2,    false, Environment::RotatedCylinderSamplePlusGV, DeltaEMode::Elastic,
+                                       -1, false};
+    auto testWS = setUpWS(wsProps);
+
+    auto mcAbsorb = createAlgorithm();
+    constexpr int NEVENTS = 5000;
+    mcAbsorb->setProperty("EventsPerPoint", NEVENTS);
+
+    TS_ASSERT_THROWS_NOTHING(mcAbsorb->setProperty("InputWorkspace", testWS));
+    TS_ASSERT_THROWS_NOTHING(mcAbsorb->execute());
+    auto outputWS = getOutputWorkspace(mcAbsorb);
+
+    verifyDimensions(wsProps, outputWS);
+    auto yData = outputWS->getSpectrum(0).dataY();
+
+    constexpr double delta(1e-02);
+    // calculated theoretical absoprtion for this cylinder, using https://doi.org/10.1107/S0567739471000652
+    const double calculatedAttFactor = 0.14047;
+    TS_ASSERT_DELTA(calculatedAttFactor / yData[0], 1.0, delta); // 'within 1%' tolerance
+  }
+
   void test_Workspace_Slit_Beam_Size_Set() {
     using namespace Mantid::Geometry;
 
@@ -698,7 +801,7 @@ class MonteCarloAbsorptionTest : public CxxTest::TestSuite {
 
   protected:
     std::shared_ptr<Mantid::Algorithms::IMCAbsorptionStrategy>
-    createStrategy(Mantid::Algorithms::IMCInteractionVolume &interactionVol,
+    createStrategy(std::shared_ptr<Mantid::Algorithms::IMCInteractionVolume> interactionVol,
                    const Mantid::Algorithms::IBeamProfile &beamProfile, Mantid::Kernel::DeltaEMode::Type EMode,
                    const size_t nevents, const size_t maxScatterPtAttempts,
                    const bool regenerateTracksForEachLambda) override {
diff --git a/docs/source/algorithms/MonteCarloAbsorption-v1.rst b/docs/source/algorithms/MonteCarloAbsorption-v1.rst
index f1242e561a8c..a9ccf8599ca0 100644
--- a/docs/source/algorithms/MonteCarloAbsorption-v1.rst
+++ b/docs/source/algorithms/MonteCarloAbsorption-v1.rst
@@ -153,6 +153,13 @@ After the simulation has been run and the spatial interpolation done, the interp
 
 .. note:: If the input workspace contains varying bin widths then the output is always interpolated.
 
+Interaction Region
+##################
+
+Within the algorithm, the ``SimulateScatteringPointIn`` parameter can be used to control whether scattering points are simulate from within the sample, within the environment, or both. Additionally, the volume from which scattered points are generated can be controlled by altering the beam profile (:ref:`SetSample <algm-SetSample>`) or by defining a gauge volume (:ref:`algm-DefineGaugeVolume`).
+
+.. note:: If a gauge volume is set, any definition of ``SimulateScatteringPointIn`` will be overridden to ``SampleOnly``.
+
 Usage
 -----
 
diff --git a/docs/source/release/v6.13.0/Framework/Algorithms/New_features/39157.rst b/docs/source/release/v6.13.0/Framework/Algorithms/New_features/39157.rst
new file mode 100644
index 000000000000..5d0482af9f5b
--- /dev/null
+++ b/docs/source/release/v6.13.0/Framework/Algorithms/New_features/39157.rst
@@ -0,0 +1 @@
+- In :ref:`algm-MonteCarloAbsorption`, when a gauge volume has been defined with :ref:`algm-DefineGaugeVolume`, the algorithm now uses this gauge volume to generate the scattering points for calculating attenuation within the sample.


Reviews:
user: login: warunawickramasingha
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T13:46:07Z
user: login: warunawickramasingha
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T14:34:14Z
user: login: warunawickramasingha
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T09:00:24Z
user: login: andy-bridger
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T09:05:56Z
user: login: warunawickramasingha
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T09:50:20Z
user: login: warunawickramasingha
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T09:59:21Z
user: login: warunawickramasingha
body: Thanks for the updates. I have added some comments for you to have a look, some are just a few optimizations if you could include. 

The script given provided the output as below, hope that looks all good 
![image](https://github.com/user-attachments/assets/8bef1a40-692e-4b73-acc0-785306c44007)

state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T16:23:46Z
user: login: andy-bridger
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T07:35:09Z
user: login: warunawickramasingha
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T09:13:21Z
user: login: warunawickramasingha
body: Thanks for the changes. I have followed the given test steps and code looks great! Happy to approve!
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T12:20:19Z
user: login: warunawickramasingha
body: Thanks for updating the keywords order. My VS2022 was happy with both orders despite CI only excepted `[[nodiscard]]` before `virtual`. 
Code looks better now and happy to approve!
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T07:17:42Z
user: login: RichardWaiteSTFC
body: Looks good, some complicated C++ stuff in here (at least to my eyes)!
Just a quick question about if the gauge volume doesn't intersect the sample, maybe we can assume this won't happen.
Otherwise looks great, thanks for your work on this, and for adding the tests - I note none of the other tests (e.g. sample only scattering etc.) required any changes.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T08:18:55Z
user: login: andy-bridger
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T13:06:54Z
user: login: RichardWaiteSTFC
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:14:46Z
user: login: RichardWaiteSTFC
body: Great thanks!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T12:48:45Z

Review comments:
diff_hunk: @@ -43,10 +47,9 @@ MCAbsorptionStrategy::MCAbsorptionStrategy(IMCInteractionVolume &interactionVolu
  * @param beamProfile The beam profile
  * @return The interaction volume object with active region set
path: Framework/Algorithms/src/SampleCorrections/MCAbsorptionStrategy.cpp
user: login: warunawickramasingha
body: Since this method now return `void`, need to remove the doxygen line starting with `@return`
created_at: 2025-04-04T13:46:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,9 +30,11 @@ namespace Algorithms {
  * @param pointsIn Where to generate the scattering point in
  */
 MCInteractionVolume::MCInteractionVolume(const API::Sample &sample, const size_t maxScatterAttempts,
-                                         const MCInteractionVolume::ScatteringPointVicinity pointsIn)
-    : m_sample(sample.getShape().clone()), m_env(nullptr), m_activeRegion(getFullBoundingBox()),
-      m_maxScatterAttempts(maxScatterAttempts), m_pointsIn(pointsIn) {
+                                         const MCInteractionVolume::ScatteringPointVicinity pointsIn,
+                                         IObject_sptr gaugeVolume)
+    : m_sample(sample.getShape().clone()), m_env(nullptr), m_maxScatterAttempts(maxScatterAttempts),
+      m_pointsIn(pointsIn), m_gaugeVolume(gaugeVolume) {
+  m_activeRegion = this->MCInteractionVolume::getFullBoundingBox();
path: Framework/Algorithms/src/SampleCorrections/MCInteractionVolume.cpp
user: login: warunawickramasingha
body: This line contains 3 issues,
1. this->MCInteractionVolume::getFullBoundingBox() is redundant and simply call getFullBoundingBox()
2. In general, it is not a good idea to call `virtual` functions inside a constructor, even in the constructor initializer list, since it can lead to undefined behaviours if there will be another derived class from `MCInteractionVolume` later at some point. A [good reading](https://isocpp.org/wiki/faq/strange-inheritance#calling-virtuals-from-ctors). 
3. In addition, as I can see when `getFullBoundingBox()` is called, `m_env` is still `null` which gets accessed inside that method - hope that is not what is expected?

The safer way to handle point 2 above would be to introduce a `virtual void init()` method in the base class and override that in the derived class, and move the logic in the constructor to that method. The `init()` method needs to be called as a must by the party who instantiates that object. 

created_at: 2025-04-04T14:34:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,19 +52,55 @@ MCInteractionVolume::MCInteractionVolume(const API::Sample &sample, const size_t
 }
 
 /**
- * Returns the axis-aligned bounding box for the volume including env if
- * m_pointsIn != SampleOnly
- * @return The bounding box
+ * Construct the volume encompassing the sample + any environment kit. The
+ * active region defines a bounding region for the sampling of the scattering
+ * position.
+ * @param sample A reference to a sample object that defines a valid shape
+ * & material
+ * @param maxScatterAttempts The maximum number of tries to generate a random
+ * point within the object. [Default=5000]
+ * @param pointsIn Where to generate the scattering point in
+ */
+MCInteractionVolume::MCInteractionVolume(const API::Sample &sample, const size_t maxScatterAttempts,
+                                         const MCInteractionVolume::ScatteringPointVicinity pointsIn,
+                                         IObject_sptr gaugeVolume)
+    : m_sample(sample.getShape().clone()), m_env(sample.hasEnvironment() ? &sample.getEnvironment() : nullptr),
+      m_maxScatterAttempts(maxScatterAttempts), m_pointsIn(pointsIn), m_gaugeVolume(gaugeVolume) {
+  init();
path: Framework/Algorithms/src/SampleCorrections/MCInteractionVolume.cpp
user: login: warunawickramasingha
body: The `init()` method is still a virtual function and hence don't call it inside the constructor. As I have mentioned before -> The `init()` method needs to be called **as a must** by the party who instantiates that object before using the obj.
ex:- 
```
auto interactionVol = std::make_shared<MCInteractionVolume>(<the args..>);
interactionVol->init();
```


created_at: 2025-04-08T09:00:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,19 +52,55 @@ MCInteractionVolume::MCInteractionVolume(const API::Sample &sample, const size_t
 }
 
 /**
- * Returns the axis-aligned bounding box for the volume including env if
- * m_pointsIn != SampleOnly
- * @return The bounding box
+ * Construct the volume encompassing the sample + any environment kit. The
+ * active region defines a bounding region for the sampling of the scattering
+ * position.
+ * @param sample A reference to a sample object that defines a valid shape
+ * & material
+ * @param maxScatterAttempts The maximum number of tries to generate a random
+ * point within the object. [Default=5000]
+ * @param pointsIn Where to generate the scattering point in
+ */
+MCInteractionVolume::MCInteractionVolume(const API::Sample &sample, const size_t maxScatterAttempts,
+                                         const MCInteractionVolume::ScatteringPointVicinity pointsIn,
+                                         IObject_sptr gaugeVolume)
+    : m_sample(sample.getShape().clone()), m_env(sample.hasEnvironment() ? &sample.getEnvironment() : nullptr),
+      m_maxScatterAttempts(maxScatterAttempts), m_pointsIn(pointsIn), m_gaugeVolume(gaugeVolume) {
+  init();
path: Framework/Algorithms/src/SampleCorrections/MCInteractionVolume.cpp
user: login: andy-bridger
body: Is it ok if I make init non-virtual? Beacuse I don't think anything is further derived from MCInteractionVolume and I don't want to have to chase calling init in all the instantiations?
created_at: 2025-04-08T09:05:55Z
author_association: COLLABORATOR
diff_hunk: @@ -58,19 +52,55 @@ MCInteractionVolume::MCInteractionVolume(const API::Sample &sample, const size_t
 }
 
 /**
- * Returns the axis-aligned bounding box for the volume including env if
- * m_pointsIn != SampleOnly
- * @return The bounding box
+ * Construct the volume encompassing the sample + any environment kit. The
+ * active region defines a bounding region for the sampling of the scattering
+ * position.
+ * @param sample A reference to a sample object that defines a valid shape
+ * & material
+ * @param maxScatterAttempts The maximum number of tries to generate a random
+ * point within the object. [Default=5000]
+ * @param pointsIn Where to generate the scattering point in
+ */
+MCInteractionVolume::MCInteractionVolume(const API::Sample &sample, const size_t maxScatterAttempts,
+                                         const MCInteractionVolume::ScatteringPointVicinity pointsIn,
+                                         IObject_sptr gaugeVolume)
+    : m_sample(sample.getShape().clone()), m_env(sample.hasEnvironment() ? &sample.getEnvironment() : nullptr),
+      m_maxScatterAttempts(maxScatterAttempts), m_pointsIn(pointsIn), m_gaugeVolume(gaugeVolume) {
+  init();
path: Framework/Algorithms/src/SampleCorrections/MCInteractionVolume.cpp
user: login: warunawickramasingha
body: Even if you make the `init` non virtual, it will call the `getFullBoundingBox()` virtual function within it, and it would have the risks explained [here](https://isocpp.org/wiki/faq/strange-inheritance#calling-virtuals-from-ctors). 

Although it is a bit of trouble to search for the cases where objects of MCInteractionVolume are created, a separate call to `init()` is the ideal way to avoid those risks of undefined behaviours or possible crashes. 

And If you think MCInteractionVolume won't be further dervied, better to mark it as final. 
i.e `class MANTID_ALGORITHMS_DLL MCInteractionVolume final`
created_at: 2025-04-08T09:50:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,15 +29,19 @@ class MANTID_ALGORITHMS_DLL MCInteractionVolume : public IMCInteractionVolume {
 public:
   enum class ScatteringPointVicinity { SAMPLEANDENVIRONMENT, SAMPLEONLY, ENVIRONMENTONLY };
   MCInteractionVolume(const API::Sample &sample, const size_t maxScatterAttempts = 5000,
path: Framework/Algorithms/inc/MantidAlgorithms/SampleCorrections/MCInteractionVolume.h
user: login: warunawickramasingha
body: Because all the params except for the `sample` have default values, it would better to define this constructor as `explicit` to prevent unintended or silent type conversions.
```
explicit MCInteractionVolume(const API::Sample &sample, const size_t maxScatterAttempts = 5000,
                      const ScatteringPointVicinity pointsIn = ScatteringPointVicinity::SAMPLEANDENVIRONMENT,
                      Geometry::IObject_sptr gaugeVolume = nullptr);
```
created_at: 2025-04-08T09:59:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,19 +52,77 @@ MCInteractionVolume::MCInteractionVolume(const API::Sample &sample, const size_t
 }
 
 /**
- * Returns the axis-aligned bounding box for the volume including env if
- * m_pointsIn != SampleOnly
- * @return The bounding box
+ * Factory Method for constructing the volume encompassing the sample + any environment kit. The
+ * active region defines a bounding region for the sampling of the scattering
+ * position.
+ * @param sample A reference to a sample object that defines a valid shape
+ * & material
+ * @param maxScatterAttempts The maximum number of tries to generate a random
+ * point within the object. [Default=5000]
+ * @param pointsIn Where to generate the scattering point in
+ * @param gaugeVolume Pointer to any gauge volume object defined for the interaction
+ * @return returns a shared pointer to interaction volume object
+ */
+std::shared_ptr<IMCInteractionVolume>
+MCInteractionVolume::create(const API::Sample &sample, const size_t maxScatterAttempts,
+                            const MCInteractionVolume::ScatteringPointVicinity pointsIn, IObject_sptr gaugeVolume) {
+  auto interactionVol =
path: Framework/Algorithms/src/SampleCorrections/MCInteractionVolume.cpp
user: login: warunawickramasingha
body: This manual memory management inside a smart pointer is not a best practice and can cause issues if there would be any exceptions thrown between the `new` and `shared_ptr`. A better way here would be to have,

```
auto interactionVol =
      std::make_shared<MCInteractionVolume>(sample, maxScatterAttempts, pointsIn, gaugeVolume);
```
created_at: 2025-04-08T17:29:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,25 +28,27 @@ namespace Algorithms {
  * @param regenerateTracksForEachLambda Whether to resimulate tracks for each
  * wavelength point or not
  */
-MCAbsorptionStrategy::MCAbsorptionStrategy(IMCInteractionVolume &interactionVolume, const IBeamProfile &beamProfile,
-                                           DeltaEMode::Type EMode, const size_t nevents,
-                                           const size_t maxScatterPtAttempts, const bool regenerateTracksForEachLambda)
-    : m_beamProfile(beamProfile), m_scatterVol(setActiveRegion(interactionVolume, beamProfile)), m_nevents(nevents),
+MCAbsorptionStrategy::MCAbsorptionStrategy(std::shared_ptr<IMCInteractionVolume> interactionVolume,
+                                           const IBeamProfile &beamProfile, DeltaEMode::Type EMode,
+                                           const size_t nevents, const size_t maxScatterPtAttempts,
+                                           const bool regenerateTracksForEachLambda)
+    : m_scatterVol(std::move(interactionVolume)), m_beamProfile(beamProfile), m_nevents(nevents),
       m_maxScatterAttempts(maxScatterPtAttempts), m_EMode(EMode),
-      m_regenerateTracksForEachLambda(regenerateTracksForEachLambda) {}
+      m_regenerateTracksForEachLambda(regenerateTracksForEachLambda) {
+
+  setActiveRegion(m_scatterVol, beamProfile);
path: Framework/Algorithms/src/SampleCorrections/MCAbsorptionStrategy.cpp
user: login: warunawickramasingha
body: I think this `setActiveRegion()` private method is redundant and can be removed as it just operates on two member variables `m_scatterVol` and `m_beamProfile` to do this
`interactionVolume->setActiveRegion(beamProfile.defineActiveRegion(interactionVolume->getFullBoundingBox()));`

But if you think it's better to keep the nice docstrings given via `setActiveRegion` method, the method signature can be minimised like below

```
void MCAbsorptionStrategy::setActiveRegion() {
    m_scatterVol->setActiveRegion(m_beamProfile.defineActiveRegion(m_scatterVol->getFullBoundingBox()));
}
```
created_at: 2025-04-09T10:01:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,16 +28,24 @@ class IBeamProfile;
 class MANTID_ALGORITHMS_DLL MCInteractionVolume : public IMCInteractionVolume {
 public:
   enum class ScatteringPointVicinity { SAMPLEANDENVIRONMENT, SAMPLEONLY, ENVIRONMENTONLY };
-  MCInteractionVolume(const API::Sample &sample, const size_t maxScatterAttempts = 5000,
-                      const ScatteringPointVicinity pointsIn = ScatteringPointVicinity::SAMPLEANDENVIRONMENT);
-
+  static std::shared_ptr<IMCInteractionVolume>
path: Framework/Algorithms/inc/MantidAlgorithms/SampleCorrections/MCInteractionVolume.h
user: login: warunawickramasingha
body: Not a must, but it would be better if `[[nodiscard]]` attribute is added here to make sure the value returned from `create` is always consumed i.e:
`[[nodiscard]] static std::shared_ptr<IMCInteractionVolume>create(...`
created_at: 2025-04-09T15:40:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,12 +52,11 @@ class MANTID_ALGORITHMS_DLL MonteCarloAbsorption : public API::Algorithm {
   }
 
 protected:
-  virtual std::shared_ptr<IMCAbsorptionStrategy>
-  createStrategy(IMCInteractionVolume &interactionVol, const IBeamProfile &beamProfile, Kernel::DeltaEMode::Type EMode,
-                 const size_t nevents, const size_t maxScatterPtAttempts, const bool regenerateTracksForEachLambda);
-  virtual std::shared_ptr<IMCInteractionVolume>
-  createInteractionVolume(const API::Sample &sample, const size_t maxScatterPtAttempts,
-                          const MCInteractionVolume::ScatteringPointVicinity pointsIn);
+  virtual std::shared_ptr<IMCAbsorptionStrategy> createStrategy(std::shared_ptr<IMCInteractionVolume> interactionVol,
path: Framework/Algorithms/inc/MantidAlgorithms/MonteCarloAbsorption.h
user: login: warunawickramasingha
body: Better to use `[[nodiscard]]` attribute here as well. i.e:
`virtual [[nodiscard]] std::shared_ptr<IMCAbsorptionStrategy> createStrategy(...`
created_at: 2025-04-09T15:44:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,19 +52,77 @@ MCInteractionVolume::MCInteractionVolume(const API::Sample &sample, const size_t
 }
 
 /**
- * Returns the axis-aligned bounding box for the volume including env if
- * m_pointsIn != SampleOnly
- * @return The bounding box
+ * Factory Method for constructing the volume encompassing the sample + any environment kit. The
+ * active region defines a bounding region for the sampling of the scattering
+ * position.
+ * @param sample A reference to a sample object that defines a valid shape
+ * & material
+ * @param maxScatterAttempts The maximum number of tries to generate a random
+ * point within the object. [Default=5000]
+ * @param pointsIn Where to generate the scattering point in
+ * @param gaugeVolume Pointer to any gauge volume object defined for the interaction
+ * @return returns a shared pointer to interaction volume object
+ */
+std::shared_ptr<IMCInteractionVolume>
+MCInteractionVolume::create(const API::Sample &sample, const size_t maxScatterAttempts,
+                            const MCInteractionVolume::ScatteringPointVicinity pointsIn, IObject_sptr gaugeVolume) {
+  auto interactionVol =
path: Framework/Algorithms/src/SampleCorrections/MCInteractionVolume.cpp
user: login: andy-bridger
body: From what I gather, since c++17 there is no longer an exception safety issue, so this is relatively ok? : https://stackoverflow.com/questions/20895648/difference-in-make-shared-and-normal-shared-ptr-in-c
https://learn.microsoft.com/en-us/cpp/cpp/how-to-create-and-use-shared-ptr-instances?view=msvc-170&viewFallbackFrom=vs-2019

I initially had `make_shared` but I get errors, I think around the private `MCInteractionVolume` being in place to ensure init is called?
created_at: 2025-04-10T07:35:09Z
author_association: COLLABORATOR
diff_hunk: @@ -58,19 +52,77 @@ MCInteractionVolume::MCInteractionVolume(const API::Sample &sample, const size_t
 }
 
 /**
- * Returns the axis-aligned bounding box for the volume including env if
- * m_pointsIn != SampleOnly
- * @return The bounding box
+ * Factory Method for constructing the volume encompassing the sample + any environment kit. The
+ * active region defines a bounding region for the sampling of the scattering
+ * position.
+ * @param sample A reference to a sample object that defines a valid shape
+ * & material
+ * @param maxScatterAttempts The maximum number of tries to generate a random
+ * point within the object. [Default=5000]
+ * @param pointsIn Where to generate the scattering point in
+ * @param gaugeVolume Pointer to any gauge volume object defined for the interaction
+ * @return returns a shared pointer to interaction volume object
+ */
+std::shared_ptr<IMCInteractionVolume>
+MCInteractionVolume::create(const API::Sample &sample, const size_t maxScatterAttempts,
+                            const MCInteractionVolume::ScatteringPointVicinity pointsIn, IObject_sptr gaugeVolume) {
+  auto interactionVol =
path: Framework/Algorithms/src/SampleCorrections/MCInteractionVolume.cpp
user: login: warunawickramasingha
body: Ok got it, the compiler issues were there because `make_shared` still needed to access the constructor of `MCInteractionVolume` which is private. In that case, this way of doing is fine I guess. Thanks!
created_at: 2025-04-10T09:13:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,8 +160,18 @@ int MCInteractionVolume::getComponentIndex(Kernel::PseudoRandomNumberGenerator &
 std::optional<Kernel::V3D>
 MCInteractionVolume::generatePointInObjectByIndex(int componentIndex, Kernel::PseudoRandomNumberGenerator &rng) const {
   std::optional<Kernel::V3D> pointGenerated{std::nullopt};
+  std::optional<Kernel::V3D> tmpPoint{std::nullopt};
   if (componentIndex == -1) {
-    pointGenerated = m_sample->generatePointInObject(rng, m_activeRegion, 1);
+    if (m_gaugeVolume != nullptr) {
+      tmpPoint = m_gaugeVolume->generatePointInObject(rng, m_activeRegion, 1);
+      if (tmpPoint) {
+        if (m_sample->isValid(tmpPoint.value())) {
path: Framework/Algorithms/src/SampleCorrections/MCInteractionVolume.cpp
user: login: RichardWaiteSTFC
body: What happens if the gauge volume is incorrectly defined and there is no intersection with the sample?
I don't know the best way of checking there's an intersection? You can fairly easily check at least one vertex of the bounding box of the gauge volume is in the sample, but I don't think that's quite enough (though maybe enough to throw a warning?)
created_at: 2025-04-15T08:12:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,8 +160,18 @@ int MCInteractionVolume::getComponentIndex(Kernel::PseudoRandomNumberGenerator &
 std::optional<Kernel::V3D>
 MCInteractionVolume::generatePointInObjectByIndex(int componentIndex, Kernel::PseudoRandomNumberGenerator &rng) const {
   std::optional<Kernel::V3D> pointGenerated{std::nullopt};
+  std::optional<Kernel::V3D> tmpPoint{std::nullopt};
   if (componentIndex == -1) {
-    pointGenerated = m_sample->generatePointInObject(rng, m_activeRegion, 1);
+    if (m_gaugeVolume != nullptr) {
+      tmpPoint = m_gaugeVolume->generatePointInObject(rng, m_activeRegion, 1);
+      if (tmpPoint) {
+        if (m_sample->isValid(tmpPoint.value())) {
path: Framework/Algorithms/src/SampleCorrections/MCInteractionVolume.cpp
user: login: andy-bridger
body: If there is no intersection with the sample all attempts to generate points will fail and it will hit the following error:
https://github.com/mantidproject/mantid/blob/67deb90983f01d3324b44d520d460d75981879fd/Framework/Algorithms/src/SampleCorrections/MCAbsorptionStrategy.cpp#L117-L123

I think this is pretty much expected behaviour, if I had a "gauge volume" which only partially intersected the sample I would expect to have to increase max attempts
created_at: 2025-04-15T13:06:54Z
author_association: COLLABORATOR
diff_hunk: @@ -108,8 +160,18 @@ int MCInteractionVolume::getComponentIndex(Kernel::PseudoRandomNumberGenerator &
 std::optional<Kernel::V3D>
 MCInteractionVolume::generatePointInObjectByIndex(int componentIndex, Kernel::PseudoRandomNumberGenerator &rng) const {
   std::optional<Kernel::V3D> pointGenerated{std::nullopt};
+  std::optional<Kernel::V3D> tmpPoint{std::nullopt};
   if (componentIndex == -1) {
-    pointGenerated = m_sample->generatePointInObject(rng, m_activeRegion, 1);
+    if (m_gaugeVolume != nullptr) {
+      tmpPoint = m_gaugeVolume->generatePointInObject(rng, m_activeRegion, 1);
+      if (tmpPoint) {
+        if (m_sample->isValid(tmpPoint.value())) {
path: Framework/Algorithms/src/SampleCorrections/MCInteractionVolume.cpp
user: login: RichardWaiteSTFC
body: OK sounds good, maybe add `check defined sample and gauge volume` to the warning?
created_at: 2025-04-16T09:14:46Z
author_association: CONTRIBUTOR

PR comments:
user: login: sf1919
created_at: 2025-04-10T13:05:03Z
body: There appears to be some genuine failures (see OSX in particular) so I won't re-run anymore tests
author_association: CONTRIBUTOR
user: login: warunawickramasingha
created_at: 2025-04-10T13:18:41Z
body: > There appears to be some genuine failures (see OSX in particular) so I won't re-run anymore tests

It seems the compiler is expecting `[[nodiscard]]` before `virtual` keyword
author_association: CONTRIBUTOR